8000 add support for axis dividers · lzhice/plotly.js@93323b8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 93323b8

Browse files
committed
add support for axis dividers
1 parent f404d9f commit 93323b8

File tree

3 files changed

+193
-66
lines changed

3 files changed

+193
-66
lines changed

src/plots/cartesian/axes.js

Lines changed: 151 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ axes.ti E7F5 ckText = function(ax, x, hover) {
979979

980980
// Setup ticks and grid lines boundaries
981981
// at 1/2 a 'category' to the left/bottom
982-
if(ax.tickson === 'boundaries') {
982+
if(ax.tickson === 'boundaries' || ax.showdividers) {
983983
var inbounds = function(v) {
984984
var p = ax.l2p(v);
985985
return p >= 0 && p <= ax._length ? v : null;
@@ -1583,6 +1583,7 @@ axes.draw = function(gd, arg, opts) {
15831583
plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove();
15841584
if(xa.type === 'multicategory') {
15851585
plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick2').remove();
1586+
plotinfo.xaxislayer.selectAll('.' + xa._id + 'divider').remove();
15861587
}
15871588
if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove();
15881589
if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove();
@@ -1642,36 +1643,21 @@ axes.drawOne = function(gd, ax, opts) {
16421643
ax._selections = {};
16431644

16441645
var transFn = axes.makeTransFn(ax);
1645-
1646+
var tickVals;
16461647
// We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
16471648
// The key case here is removing zero lines when the axis bound is zero
16481649
var valsClipped;
1649-
var tickVals;
1650-
var gridVals;
16511650

16521651
if(ax.tickson === 'boundaries' && vals.length) {
1653-
// valsBoundaries is not used for labels;
1654-
// no need to worry about the other tickTextObj keys
1655-
var valsBoundaries = [];
1656-
var _push = function(d, bndIndex) {
1657-
var xb = d.xbnd[bndIndex];
1658-
if(xb !== null) {
1659-
valsBoundaries.push(Lib.extendFlat({}, d, {x: xb}));
1660-
}
1661-
};
1662-
for(i = 0; i < vals.length; i++) _push(vals[i], 0);
1663-
_push(vals[i - 1], 1);
1664-
1665-
valsClipped = axes.clipEnds(ax, valsBoundaries);
1666-
tickVals = ax.ticks === 'inside' ? valsClipped : valsBoundaries;
1667-
gridVals = valsClipped;
1652+
var boundaryVals = getBoundaryVals(ax, vals);
1653+
valsClipped = axes.clipEnds(ax, boundaryVals);
1654+
tickVals = ax.ticks === 'inside' ? valsClipped : boundaryVals;
16681655
} else {
16691656
valsClipped = axes.clipEnds(ax, vals);
16701657
tickVals = ax.ticks === 'inside' ? valsClipped : vals;
1671-
gridVals = valsClipped;
16721658
}
16731659

1674-
ax._valsClipped = valsClipped;
1660+
var gridVals = ax._gridVals = valsClipped;
16751661

16761662
if(!fullLayout._hasOnlyLargeSploms) {
16771663
// keep track of which subplots (by main conteraxis) we've already
@@ -1759,56 +1745,35 @@ axes.drawOne = function(gd, ax, opts) {
17591745
});
17601746

17611747
if(ax.type === 'multicategory') {
1762-
seq.push(function() {
1763-
// TODO?
1764-
// drawDividers()
1765-
1766-
var secondaryLabelVals = [];
1767-
var lookup = {};
1768-
for(i = 0; i < vals.length; i++) {
1769-
var d = vals[i];
1770-
if(lookup[d.text2]) {
1771-
lookup[d.text2].push(d.x);
1772-
} else {
1773-
lookup[d.text2] = [d.x];
1774-
}
1775-
}
1776-
for(var k in lookup) {
1777-
secondaryLabelVals.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k));
1778-
}
1748+
var labelLength = 0;
17791749

1780-
var labelHeight = 0;
1781-
ax._selections[ax._id + 'tick'].each(function() {
1782-
var thisLabel = selectTickLabel(this);
1783-
1784-
// TODO Drawing.bBox doesn't work when labels are rotated
1785-
// var bb = Drawing.bBox(thisLabel.node());
1786-
var bb = thisLabel.node().getBoundingClientRect();
1787-
labelHeight = Math.max(labelHeight, bb.height);
1788-
});
1789-
1790-
var secondarayPosition;
1791-
if(ax.side === 'bottom') {
1792-
secondarayPosition = mainLinePosition + labelHeight + 2;
1793-
} else {
1794-
secondarayPosition = mainLinePosition - labelHeight - 2;
1795-
if(ax.tickfont) {
1796-
secondarayPosition -= (ax.tickfont.size * LINE_SPACING);
1797-
}
1798-
}
1799-
1800-
var secondaryLabelFns = axes.makeLabelFns(ax, secondarayPosition);
1750+
seq.push(function() {
1751+
labelLength += getLabelLevelSpan(ax._selections[axId + 'tick']) + 2;
1752+
labelLength += ax._lastangle ? ax.tickfont.size * LINE_SPACING : 0;
1753+
var secondaryPosition = mainLinePosition + labelLength * tickSigns[2];
1754+
var secondaryLabelFns = axes.makeLabelFns(ax, secondaryPosition);
18011755

18021756
return axes.drawLabels(gd, ax, {
1803-
vals: secondaryLabelVals,
1757+
vals: getSecondaryLabelVals(ax, vals),
18041758
layer: mainAxLayer,
1805-
cls: ax._id + 'tick2',
1759+
cls: axId + 'tick2',
18061760
transFn: transFn,
18071761
labelXFn: secondaryLabelFns.labelXFn,
18081762
labelYFn: secondaryLabelFns.labelYFn,
18091763
labelAnchorFn: secondaryLabelFns.labelAnchorFn,
18101764
});
18111765
});
1766+
1767+
seq.push(function() {
1768+
labelLength += getLabelLevelSpan(ax._selections[axId + 'tick2']) + 2;
1769+
1770+
return drawDividers(gd, ax, {
1771+
vals: getDividerVals(ax, vals),
1772+
layer: mainAxLayer,
1773+
path: axes.makeTickPath(ax, mainLinePosition, tickSigns[2], labelLength),
1774+
transFn: transFn
1775+
});
1776+
});
18121777
}
18131778

18141779
function extendRange(range, newRange) {
@@ -1937,6 +1902,87 @@ axes.drawOne = function(gd, ax, opts) {
19371902
return Lib.syncOrAsync(seq);
19381903
};
19391904

1905+
function getBoundaryVals(ax, vals) {
1906+
var out = [];
1907+
var i;
1908+
1909+
// boundaryVals are never used for labels;
1910+
// no need to worry about the other tickTextObj keys
1911+
var _push = function(d, bndIndex) {
1912+
var xb = d.xbnd[bndIndex];
1913+
if(xb !== null) {
1914+
out.push(Lib.extendFlat({}, d, {x: xb}));
1915+
}
1916+
};
1917+
1918+
for(i = 0; i < vals.length; i++) {
1919+
_push(vals[i], 0);
1920+
}
1921+
_push(vals[i - 1], 1);
1922+
1923+
return out;
1924+
}
1925+
1926+
function getSecondaryLabelVals(ax, vals) {
1927+
var out = [];
1928+
var lookup = {};
1929+
1930+
for(var i = 0; i < vals.length; i++) {
1931+
var d = vals[i];
1932+
if(lookup[d.text2]) {
1933+
lookup[d.text2].push(d.x);
1934+
} else {
1935+
lookup[d.text2] = [d.x];
1936+
}
1937+
}
1938+
1939+
for(var k in lookup) {
1940+
out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k));
1941+
}
1942+
1943+
return out;
1944+
}
1945+
1946+
function getDividerVals(ax, vals) {
1947+
var out = [];
1948+
var i, current;
1949+
1950+
// never used for labels;
1951+
// no need to worry about the other tickTextObj keys
1952+
var _push = function(d, bndIndex) {
1953+
var xb = d.xbnd[bndIndex];
1954+
if(xb !== null) {
1955+
out.push(Lib.extendFlat({}, d, {x: xb}));
1956+
}
1957+
};
1958+
1959+
for(i = 0; i < vals.length; i++) {
1960+
var d = vals[i];
1961+
if(d.text2 !== current) {
1962+
_push(d, 0);
1963+
}
1964+
current = d.text2;
1965+
}
1966+
_push(vals[i - 1], 1);
1967+
1968+
return out;
1969+
}
1970+
1971+
function getLabelLevelSpan(tickLabels) {
1972+
var out = 2;
1973+
1974+
tickLabels.each(function() {
1975+
var thisLabel = selectTickLabel(this);
1976+
1977+
// TODO Drawing.bBox doesn't work when labels are rotated
1978+
// var bb = Drawing.bBox(thisLabel.node());
1979+
var bb = thisLabel.node().getBoundingClientRect();
1980+
out = Math.max(out, bb.height);
1981+
});
1982+
1983+
return out;
1984+
}
1985+
19401986
/**
19411987
* Which direction do the 'ax.side' values, and free ticks go?
19421988
*
@@ -1988,12 +2034,15 @@ axes.makeTransFn = function(ax) {
19882034
* - {number} linewidth
19892035
* @param {number} shift along direction of ticklen
19902036
* @param {1 or -1} sng tick sign
2037+
* @param {number (optional)} len tick length
19912038
* @return {string}
19922039
*/
1993-
axes.makeTickPath = function(ax, shift, sgn) {
2040+
axes.makeTickPath = function(ax, shift, sgn, len) {
2041+
len = len !== undefined ? len : ax.ticklen;
2042+
19942043
var axLetter = ax._id.charAt(0);
19952044
var pad = (ax.linewidth || 1) / 2;
1996-
var len = ax.ticklen;
2045+
19972046
return axLetter === 'x' ?
19982047
'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) :
19992048
'M' + (shift + pad * sgn) + ',0h' + (len * sgn);
@@ -2181,7 +2230,6 @@ axes.drawGrid = function(gd, ax, opts) {
21812230
* - {string} zerolinecolor
21822231
* - {number (optional)} _gridWidthCrispRound
21832232
* @param {object} opts
2184-
* - {array of object} vals (calcTicks output-like)
21852233
* - {d3 selection} layer
21862234
* - {object} counterAxis (full axis object corresponding to counter axis)
21872235
* - {string or fn} path
@@ -2392,10 +2440,12 @@ axes.drawLabels = function(gd, ax, opts) {
23922440

23932441
var autoangle = 0;
23942442

2395-
if(ax.tickson === 'boundaries' && cls === ax._id + 'tick') {
2443+
if((ax.tickson === 'boundaries' || ax.showdividers) && cls === ax._id + 'tick') {
23962444
var gap = 2;
23972445
if(ax.ticks) gap += ax.tickwidth / 2;
23982446

2447+
// TODO should secondary labels also fall into this fix-overlap regime?
2448+
23992449
for(i = 0; i < lbbArray.length; i++) {
24002450
var xbnd = vals[i].xbnd;
24012451
var lbb = lbbArray[i];
@@ -2437,6 +2487,41 @@ axes.drawLabels = function(gd, ax, opts) {
24372487
return done;
24382488
};
24392489

2490+
/**
2491+
* Draw axis dividers
2492+
*
2493+
* @param {DOM element} gd
2494+
* @param {object} ax (full) axis object
2495+
* - {string} _id
2496+
* - {string} showdividers
2497+
* - {number} dividerwidth
2498+
* - {string} dividercolor
2499+
* @param {object} opts
2500+
* - {array of object} vals (calcTicks output-like)
2501+
* - {d3 selection} layer
2502+
* - {fn} path
2503+
* - {fn} transFn
2504+
*/
2505+
function drawDividers(gd, ax, opts) {
2506+
var cls = ax._id + 'divider';
2507+
var vals = opts.vals;
2508+
2509+
var dividers = opts.layer.selectAll('path.' + cls)
2510+
.data(ax.showdividers ? vals : [], makeDataFn(ax));
2511+
2512+
dividers.exit().remove();
2513+
2514+
dividers.enter().insert('path', ':first-child')
2515+
.classed(cls, 1)
2516+
.classed('crisp', 1)
2517+
.call(Color.stroke, ax.dividercolor)
2518+
.style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px');
2519+
2520+
dividers
2521+
.attr('transform', opts.transFn)
2522+
.attr('d', opts.path);
2523+
}
2524+
24402525
function drawTitle(gd, ax) {
24412526
var fullLayout = gd._fullLayout;
24422527
var axId = ax._id;
@@ -2519,7 +2604,7 @@ axes.shouldShowZeroLine = function(gd, ax, counterAxis) {
25192604
(rng[0] * rng[1] <= 0) &&
25202605
ax.zeroline &&
25212606
(ax.type === 'linear' || ax.type === '-') &&
2522-
ax._valsClipped.length &&
2607+
ax._gridVals.length &&
25232608
(
25242609
clipEnds(ax, 0) ||
25252610
!anyCounterAxLineAtZero(gd, ax, counterAxis, rng) ||

src/plots/cartesian/axis_defaults.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,13 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
9797
coerce('tickson');
9898
}
9999

100+
if(containerOut.type === 'multicategory') {
101+
var showDividers = coerce('showdividers');
102+
if(showDividers) {
103+
coerce('dividercolor');
104+
coerce('dividerwidth');
105+
}
106+
}
107+
100108
return containerOut;
101109
};

src/plots/cartesian/layout_attributes.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,40 @@ module.exports = {
666666
editType: 'ticks',
667667
description: 'Sets the width (in px) of the zero line.'
668668
},
669+
670+
showdividers: {
671+
valType: 'boolean',
672+
dflt: true,
673+
role: 'style',
674+
editType: 'ticks',
675+
description: [
676+
'Determines whether or not a dividers are drawn',
677+
'between the category levels of this axis.',
678+
'Only has an effect on *multicategory* axes.'
679+
].join(' ')
680+
},
681+
dividercolor: {
682+
valType: 'color',
683+
dflt: colorAttrs.defaultLine,
684+
role: 'style',
685+
editType: 'ticks',
686+
description: [
687+
'Sets the color of the dividers',
688+
'Only has an effect on *multicategory* axes.'
689+
].join(' ')
690+
},
691+
dividerwidth: {
692+
valType: 'number',
693+
dflt: 1,
694+
role: 'style',
695+
editType: 'ticks',
696+
description: [
697+
'Sets the width (in px) of the dividers',
698+
'Only has an effect on *multicategory* axes.'
699+
].join(' ')
700+
},
701+
// TODO dividerlen: that would override "to label base" length?
702+
669703
// positioning attributes
670704
// anchor: not used directly, just put here for reference
671705
// values are any opposite-letter axis id

0 commit comments

Comments
 (0)
0