Skip to content

Commit e930009

Browse files
authored
Merge pull request #3275 from plotly/tickson-boundary
Add `tickson: 'boundaries'` for category cartesian axes
2 parents ed9ad69 + eeb9055 commit e930009

File tree

8 files changed

+332
-26
lines changed

8 files changed

+332
-26
lines changed

src/components/colorbar/draw.js

+1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ module.exports = function draw(gd, id) {
198198
letter: 'y',
199199
font: fullLayout.font,
200200
noHover: true,
201+
noTickson: true,
201202
calendar: fullLayout.calendar // not really necessary (yet?)
202203
};
203204

src/plots/cartesian/axes.js

+81-26
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,20 @@ function formatCategory(ax, out) {
11631163
var tt = ax._categories[Math.round(out.x)];
11641164
if(tt === undefined) tt = '';
11651165
out.text = String(tt);
1166+
1167+
// Setup ticks and grid lines boundaries
1168+
// at 1/2 a 'category' to the left/bottom
1169+
if(ax.tickson === 'boundaries') {
1170+
var inbounds = function(v) {
1171+
var p = ax.l2p(v);
1172+
return p >= 0 && p <= ax._length ? v : null;
1173+
};
1174+
1175+
out.xbnd = [
1176+
inbounds(out.x - 0.5),
1177+
inbounds(out.x + ax.dtick - 0.5)
1178+
];
1179+
}
11661180
}
11671181

11681182
function formatLinear(ax, out, hover, extraPrecision, hideexp) {
@@ -1610,14 +1624,41 @@ axes.drawOne = function(gd, ax, opts) {
16101624
var subplotsWithAx = axes.getSubplots(gd, ax);
16111625

16121626
var vals = ax._vals = axes.calcTicks(ax);
1613-
// We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
1614-
// The key case here is removing zero lines when the axis bound is zero
1615-
var valsClipped = ax._valsClipped = axes.clipEnds(ax, vals);
16161627

16171628
if(!ax.visible) return;
16181629

16191630
var transFn = axes.makeTransFn(ax);
16201631

1632+
// We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
1633+
// The key case here is removing zero lines when the axis bound is zero
1634+
var valsClipped;
1635+
var tickVals;
1636+
var gridVals;
1637+
1638+
if(ax.tickson === 'boundaries' && vals.length) {
1639+
// valsBoundaries is not used for labels;
1640+
// no need to worry about the other tickTextObj keys
1641+
var valsBoundaries = [];
1642+
var _push = function(d, bndIndex) {
1643+
var xb = d.xbnd[bndIndex];
1644+
if(xb !== null) {
1645+
valsBoundaries.push(Lib.extendFlat({}, d, {x: xb}));
1646+
}
1647+
};
1648+
for(i = 0; i < vals.length; i++) _push(vals[i], 0);
1649+
_push(vals[i - 1], 1);
1650+
1651+
valsClipped = axes.clipEnds(ax, valsBoundaries);
1652+
tickVals = ax.ticks === 'inside' ? valsClipped : valsBoundaries;
1653+
gridVals = valsClipped;
1654+
} else {
1655+
valsClipped = axes.clipEnds(ax, vals);
1656+
tickVals = ax.ticks === 'inside' ? valsClipped : vals;
1657+
gridVals = valsClipped;
1658+
}
1659+
1660+
ax._valsClipped = valsClipped;
1661+
16211662
if(!fullLayout._hasOnlyLargeSploms) {
16221663
// keep track of which subplots (by main conteraxis) we've already
16231664
// drawn grids for, so we don't overdraw overlaying subplots
@@ -1637,7 +1678,7 @@ axes.drawOne = function(gd, ax, opts) {
16371678
'M' + counterAxis._offset + ',0h' + counterAxis._length;
16381679

16391680
axes.drawGrid(gd, ax, {
1640-
vals: valsClipped,
1681+
vals: gridVals,
16411682
layer: plotinfo.gridlayer.select('.' + axId),
16421683
path: gridPath,
16431684
transFn: transFn
@@ -1652,7 +1693,6 @@ axes.drawOne = function(gd, ax, opts) {
16521693
}
16531694

16541695
var tickSigns = axes.getTickSigns(ax);
1655-
var tickVals = ax.ticks === 'inside' ? valsClipped : vals;
16561696
var tickSubplots = [];
16571697

16581698
if(ax.ticks) {
@@ -1920,8 +1960,9 @@ axes.makeTickPath = function(ax, shift, sgn) {
19201960
axes.makeLabelFns = function(ax, shift, angle) {
19211961
var axLetter = ax._id.charAt(0);
19221962
var pad = (ax.linewidth || 1) / 2;
1963+
var ticksOnOutsideLabels = ax.tickson !== 'boundaries' && ax.ticks === 'outside';
19231964

1924-
var labelStandoff = ax.ticks === 'outside' ? ax.ticklen : 0;
1965+
var labelStandoff = ticksOnOutsideLabels ? ax.ticklen : 0;
19251966
var labelShift = 0;
19261967

19271968
if(angle && ax.ticks === 'outside') {
@@ -1930,7 +1971,7 @@ axes.makeLabelFns = function(ax, shift, angle) {
19301971
labelShift = ax.ticklen * Math.sin(rad);
19311972
}
19321973

1933-
if(ax.showticklabels && (ax.ticks === 'outside' || ax.showline)) {
1974+
if(ax.showticklabels && (ticksOnOutsideLabels || ax.showline)) {
19341975
labelStandoff += 0.2 * ax.tickfont.size;
19351976
}
19361977

@@ -2018,7 +2059,6 @@ axes.drawTicks = function(gd, ax, opts) {
20182059
ticks.attr('transform', opts.transFn);
20192060
};
20202061

2021-
20222062
/**
20232063
* Draw axis grid
20242064
*
@@ -2151,8 +2191,6 @@ axes.drawLabels = function(gd, ax, opts) {
21512191
var tickLabels = opts.layer.selectAll('g.' + cls)
21522192
.data(ax.showticklabels ? vals : [], makeDataFn(ax));
21532193

2154-
var maxFontSize = 0;
2155-
var autoangle = 0;
21562194
var labelsReady = [];
21572195

21582196
tickLabels.enter().append('g')
@@ -2187,10 +2225,6 @@ axes.drawLabels = function(gd, ax, opts) {
21872225

21882226
tickLabels.exit().remove();
21892227

2190-
tickLabels.each(function(d) {
2191-
maxFontSize = Math.max(maxFontSize, d.fontSize);
2192-
});
2193-
21942228
ax._tickLabels = tickLabels;
21952229

21962230
// TODO ??
@@ -2273,16 +2307,20 @@ axes.drawLabels = function(gd, ax, opts) {
22732307
// check for auto-angling if x labels overlap
22742308
// don't auto-angle at all for log axes with
22752309
// base and digit format
2276-
if(axLetter === 'x' && !isNumeric(ax.tickangle) &&
2310+
if(vals.length && axLetter === 'x' && !isNumeric(ax.tickangle) &&
22772311
(ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')
22782312
) {
2313+
var maxFontSize = 0;
22792314
var lbbArray = [];
2315+
var i;
22802316

22812317
tickLabels.each(function(d) {
22822318
var s = d3.select(this);
22832319
var thisLabel = s.select('.text-math-group');
22842320
if(thisLabel.empty()) thisLabel = s.select('text');
22852321

2322+
maxFontSize = Math.max(maxFontSize, d.fontSize);
2323+
22862324
var x = ax.l2p(d.x);
22872325
var bb = Drawing.bBox(thisLabel.node());
22882326

@@ -2298,21 +2336,38 @@ axes.drawLabels = function(gd, ax, opts) {
22982336
});
22992337
});
23002338

2301-
for(var i = 0; i < lbbArray.length - 1; i++) {
2302-
if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) {
2303-
// any overlap at all - set 30 degrees
2304-
autoangle = 30;
2305-
break;
2339+
var autoangle = 0;
2340+
2341+
if(ax.tickson === 'boundaries') {
2342+
var gap = 2;
2343+
if(ax.ticks) gap += ax.tickwidth / 2;
2344+
2345+
for(i = 0; i < lbbArray.length; i++) {
2346+
var xbnd = vals[i].xbnd;
2347+
var lbb = lbbArray[i];
2348+
if(
2349+
(xbnd[0] !== null && (lbb.left - ax.l2p(xbnd[0])) < gap) ||
2350+
(xbnd[1] !== null && (ax.l2p(xbnd[1]) - lbb.right) < gap)
2351+
) {
2352+
autoangle = 90;
2353+
break;
2354+
}
2355+
}
2356+
} else {
2357+
var vLen = vals.length;
2358+
var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1);
2359+
var fitBetweenTicks = tickSpacing < maxFontSize * 2.5;
2360+
2361+
// any overlap at all - set 30 degrees or 90 degrees
2362+
for(i = 0; i < lbbArray.length - 1; i++) {
2363+
if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) {
2364+
autoangle = fitBetweenTicks ? 90 : 30;
2365+
break;
2366+
}
23062367
}
23072368
}
23082369

23092370
if(autoangle) {
2310-
var tickspacing = Math.abs(
2311-
(vals[vals.length - 1].x - vals[0].x) * ax._m
2312-
) / (vals.length - 1);
2313-
if(tickspacing < maxFontSize * 2.5) {
2314-
autoangle = 90;
2315-
}
23162371
positionLabels(tickLabels, autoangle);
23172372
}
23182373
ax._lastangle = autoangle;

src/plots/cartesian/axis_defaults.js

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var setConvert = require('./set_convert');
2828
* outerTicks: boolean, should ticks default to outside?
2929
* showGrid: boolean, should gridlines be shown by default?
3030
* noHover: boolean, this axis doesn't support hover effects?
31+
* noTickson: boolean, this axis doesn't support 'tickson'
3132
* data: the plot data, used to manage categories
3233
* bgColor: the plot background color, to calculate default gridline colors
3334
*/
@@ -89,5 +90,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
8990

9091
if(options.automargin) coerce('automargin');
9192

93+
if(!options.noTickson &&
94+
containerOut.type === 'category' && (containerOut.ticks || containerOut.showgrid)) {
95+
coerce('tickson');
96+
}
97+
9298
return containerOut;
9399
};

src/plots/cartesian/layout_attributes.js

+14
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,20 @@ module.exports = {
304304
'the axis lines.'
305305
].join(' ')
306306
},
307+
tickson: {
308+
valType: 'enumerated',
309+
values: ['labels', 'boundaries'],
310+
role: 'info',
311+
dflt: 'labels',
312+
editType: 'ticks',
313+
description: [
314+
'Determines where ticks and grid lines are drawn with respect to their',
315+
'corresponding tick labels.',
316+
'Only has an effect for axes of `type` *category*.',
317+
'When set to *boundaries*, ticks and grid lines are drawn half a category',
318+
'to the left/bottom of labels.'
319+
].join(' ')
320+
},
307321
mirror: {
308322
valType: 'enumerated',
309323
values: [true, 'ticks', false, 'all', 'allticks'],

src/plots/gl3d/layout/axis_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) {
5050
letter: axName[0],
5151
data: options.data,
5252
showGrid: true,
53+
noTickson: true,
5354
bgColor: options.bgColor,
5455
calendar: options.calendar
5556
},
31 KB
Loading
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"data": [
3+
{
4+
"type": "box",
5+
"x": ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"],
6+
"y": [0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3]
7+
},
8+
{
9+
"type": "box",
10+
"x": ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"],
11+
"y": [0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5]
12+
},
13+
{
14+
"type": "box",
15+
"x": ["day 1", "day 1", "day 1", "day 1", "day 1", "day 1", "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"],
16+
"y": [0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2]
17+
},
18+
19+
{
20+
"type": "bar",
21+
"x": [1, 2, 1],
22+
"y": ["apples", "bananas", "clementines"],
23+
"orientation": "h",
24+
"xaxis": "x2",
25+
"yaxis": "y2"
26+
},
27+
{
28+
"type": "bar",
29+
"x": [1.3, 2.2, 0.8],
30+
"y": ["apples", "bananas", "clementines"],
31+
"orientation": "h",
32+
"xaxis": "x2",
33+
"yaxis": "y2"
34+
},
35+
{
36+
"type": "bar",
37+
"x": [3, 3.2, 1.8],
38+
"y": ["apples", "bananas", "clementines"],
39+
"orientation": "h",
40+
"xaxis": "x2",
41+
"yaxis": "y2"
42+
},
43+
44+
{
45+
"type": "bar",
46+
"name": "with dtick !== 1",
47+
"x": ["a", "b", "c", "d", "e", "f", "g", "h"],
48+
"y": [1, 2, 1, 2, 1, 3, 4, 1],
49+
"xaxis": "x3",
50+
"yaxis": "y3"
51+
},
52+
53+
{
54+
"mode": "markers",
55+
"marker": {"symbol": "square"},
56+
"name": "with overlapping tick labels",
57+
"x": ["A very long title", "short", "Another very long title"],
58+
"y": [1, 4, 2],
59+
"xaxis": "x4",
60+
"yaxis": "y4"
61+
}
62+
],
63+
"layout": {
64+
"boxmode": "group",
65+
"grid": {
66+
"rows": 4,
67+
"columns": 1,
68+
"pattern": "independent",
69+
"ygap": 0.2
70+
},
71+
"xaxis": {
72+
"ticks": "outside",
73+
"tickson": "boundaries",
74+
"gridcolor": "white",
75+
"gridwidth": 4
76+
},
77+
"yaxis2": {
78+
"ticks": "inside",
79+
"tickson": "boundaries",
80+
"gridcolor": "white",
81+
"gridwidth": 4
82+
},
83+
"xaxis3": {
84+
"ticks": "inside",
85+
"tickson": "boundaries",
86+
"gridcolor": "white",
87+
"gridwidth": 4,
88+
"dtick": 2
89+
},
90+
"xaxis4": {
91+
"domain": [0.22, 0.78],
92+
"ticks": "outside",
93+
"ticklen": 20,
94+
"tickson": "boundaries",
95+
"gridcolor": "white",
96+
"gridwidth": 4
97+
},
98+
"plot_bgcolor": "lightgrey",
99+
"showlegend": false,
100+
"width": 500,
101+
"height": 800,
102+
"margin": {"b": 140}
103+
}
104+
}

0 commit comments

Comments
 (0)