Skip to content

Commit e979cf0

Browse files
committed
refactor candlestick into a first-class trace, also box/violin hover expanded acceptance
1 parent 18e19a8 commit e979cf0

24 files changed

+460
-199
lines changed

src/components/legend/draw.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,9 @@ function drawTexts(g, gd) {
390390

391391
// N.B. this block isn't super clean,
392392
// is unfortunately untested at the moment,
393-
// and only works for for 'ohlc' and 'candlestick',
393+
// and only works for for 'ohlc',
394394
// but should be generalized for other one-to-many transforms
395-
if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) {
395+
if(fullInput.type === 'ohlc') {
396396
transforms = legendItem.trace.transforms;
397397
direction = transforms[transforms.length - 1].direction;
398398

src/components/legend/style.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ module.exports = function style(s, gd) {
5252
.each(styleBoxes)
5353
.each(stylePies)
5454
.each(styleLines)
55-
.each(stylePoints);
55+
.each(stylePoints)
56+
.each(styleCandles);
5657

5758
function styleLines(d) {
5859
var trace = d[0].trace;
@@ -207,7 +208,35 @@ module.exports = function style(s, gd) {
207208
.call(Color.fill, trace.fillcolor);
208209

209210
if(w) {
210-
p.call(Color.stroke, trace.line.color);
211+
Color.stroke(p, trace.line.color);
212+
}
213+
});
214+
}
215+
216+
function styleCandles(d) {
217+
var trace = d[0].trace,
218+
pts = d3.select(this).select('g.legendpoints')
219+
.selectAll('path.legendcandle')
220+
.data(Registry.traceIs(trace, 'candlestick') && trace.visible ? [d, d] : []);
221+
pts.enter().append('path').classed('legendcandle', true)
222+
// if we want the median bar, prepend M6,0H-6
223+
.attr('d', function(_, i) {
224+
if(i) return 'M-15,0H-8M-8,6V-6H8Z'; // increasing
225+
return 'M15,0H8M8,-6V6H-8Z'; // decreasing
226+
})
227+
.attr('transform', 'translate(20,0)')
228+
.style('stroke-miterlimit', 1);
229+
pts.exit().remove();
230+
pts.each(function(_, i) {
231+
var container = trace[i ? 'increasing' : 'decreasing'];
232+
var w = container.line.width,
233+
p = d3.select(this);
234+
235+
p.style('stroke-width', w + 'px')
236+
.call(Color.fill, container.fillcolor);
237+
238+
if(w) {
239+
Color.stroke(p, container.line.color);
211240
}
212241
});
213242
}

src/components/rangeslider/defaults.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ var oppAxisAttrs = require('./oppaxis_attributes');
1414
var axisIds = require('../../plots/cartesian/axis_ids');
1515

1616
module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
17-
if(!layoutIn[axName].rangeslider) return;
17+
var axIn = layoutIn[axName];
18+
var axOut = layoutOut[axName];
19+
20+
if(!(axIn.rangeslider || layoutOut._requestRangeslider[axOut._id])) return;
1821

1922
// not super proud of this (maybe store _ in axis object instead
20-
if(!Lib.isPlainObject(layoutIn[axName].rangeslider)) {
21-
layoutIn[axName].rangeslider = {};
23+
if(!Lib.isPlainObject(axIn.rangeslider)) {
24+
axIn.rangeslider = {};
2225
}
2326

24-
var containerIn = layoutIn[axName].rangeslider,
25-
axOut = layoutOut[axName],
26-
containerOut = axOut.rangeslider = {};
27+
var containerIn = axIn.rangeslider;
28+
var containerOut = axOut.rangeslider = {};
2729

2830
function coerce(attr, dflt) {
2931
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);

src/plot_api/helpers.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,26 @@ exports.cleanData = function(data, existingData) {
330330
}
331331
}
332332

333+
// fixes from converting finance from transforms to real trace types
334+
if(trace.type === 'candlestick') {
335+
var increasingName = cleanFinanceDir(trace.increasing);
336+
var decreasingName = cleanFinanceDir(trace.decreasing);
337+
338+
// now figure out something smart to do with the separate direction
339+
// names we removed
340+
if(increasingName && decreasingName) {
341+
// both sub-names existed: base name previously had no effect
342+
// so ignore it and try to find a shared part of the sub-names
343+
var newName = commonPrefix(increasingName, decreasingName);
344+
// if no common part, leave whatever name was (or wasn't) there
345+
if(newName) trace.name = newName;
346+
}
347+
else if((increasingName || decreasingName) && !trace.name) {
348+
// one sub-name existed but not the base name - just use the sub-name
349+
trace.name = increasingName || decreasingName;
350+
}
351+
}
352+
333353
// transforms backward compatibility fixes
334354
if(Array.isArray(trace.transforms)) {
335355
var transforms = trace.transforms;
@@ -388,6 +408,31 @@ exports.cleanData = function(data, existingData) {
388408
}
389409
};
390410

411+
function cleanFinanceDir(dirContainer) {
412+
if(!Lib.isPlainObject(dirContainer)) return '';
413+
414+
var dirName = (dirContainer.showlegend === false) ? '' : dirContainer.name;
415+
416+
delete dirContainer.name;
417+
delete dirContainer.showlegend;
418+
419+
return dirName;
420+
}
421+
422+
function commonPrefix(name1, name2) {
423+
if(!name1.trim()) return name2;
424+
if(!name2.trim()) return name1;
425+
426+
var minLen = Math.min(name1.length, name2.length);
427+
var i;
428+
for(i = 0; i < minLen; i++) {
429+
if(name1.charAt(i) !== name2.charAt(i)) break;
430+
}
431+
432+
var out = name1.substr(0, i);
433+
return out.trim();
434+
}
435+
391436
// textposition - support partial attributes (ie just 'top')
392437
// and incorrect use of middle / center etc.
393438
function cleanTextPosition(textposition) {

src/plot_api/plot_schema.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,17 @@ exports.findArrayAttributes = function(trace) {
252252
exports.getTraceValObject = function(trace, parts) {
253253
var head = parts[0];
254254
var i = 1; // index to start recursing from
255+
var transforms = trace.transforms;
256+
if(!Array.isArray(transforms) || !transforms.length) transforms = false;
255257
var moduleAttrs, valObject;
256258

257259
if(head === 'transforms') {
258-
if(!Array.isArray(trace.transforms)) return false;
260+
if(!transforms) return false;
259261
var tNum = parts[1];
260-
if(!isIndex(tNum) || tNum >= trace.transforms.length) {
262+
if(!isIndex(tNum) || tNum >= transforms.length) {
261263
return false;
262264
}
263-
moduleAttrs = (Registry.transformsRegistry[trace.transforms[tNum].type] || {}).attributes;
265+
moduleAttrs = (Registry.transformsRegistry[transforms[tNum].type] || {}).attributes;
264266
valObject = moduleAttrs && moduleAttrs[parts[2]];
265267
i = 3; // start recursing only inside the transform
266268
}
@@ -285,8 +287,22 @@ exports.getTraceValObject = function(trace, parts) {
285287
}
286288
}
287289

288-
// finally look in the global attributes
290+
// next look in the global attributes
289291
if(!valObject) valObject = baseAttributes[head];
292+
293+
// finally check if we have a transform matching the original trace type
294+
if(!valObject && transforms) {
295+
var inputType = trace._input.type;
296+
if(inputType && inputType !== trace.type) {
297+
for(var j = 0; j < transforms.length; j++) {
298+
if(transforms[j].type === inputType) {
299+
moduleAttrs = ((Registry.modules[inputType] || {})._module || {}).attributes;
300+
valObject = moduleAttrs && moduleAttrs[head];
301+
if(valObject) break;
302+
}
303+
}
304+
}
305+
}
290306
}
291307

292308
return recurseIntoValObject(valObject, parts, i);

src/plots/plots.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,10 @@ plots.supplyDefaults = function(gd) {
366366
newFullLayout._basePlotModules = [];
367367
newFullLayout._subplots = emptySubplotLists();
368368

369+
// for traces to request a default rangeslider on their x axes
370+
// eg set `_requestRangeslider.x2 = true` for xaxis2
371+
newFullLayout._requestRangeslider = {};
372+
369373
// then do the data
370374
newFullLayout._globalTransforms = (gd._context || {}).globalTransforms;
371375
plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout);

src/traces/box/calc.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,6 @@ module.exports = function calc(gd, trace) {
143143
}
144144
};
145145

146-
// don't show labels in candlestick hover labels
147-
if(trace._fullInput && trace._fullInput.type === 'candlestick') {
148-
delete cd[0].t.labels;
149-
}
150-
151146
fullLayout[numKey]++;
152147
return cd;
153148
} else {

src/traces/box/hover.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,26 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) {
5858
hoverPseudoDistance, spikePseudoDistance;
5959

6060
var boxDelta = t.bdPos;
61+
var posAcceptance = t.wHover;
6162
var shiftPos = function(di) { return di.pos + t.bPos - pVal; };
6263

6364
if(isViolin && trace.side !== 'both') {
6465
if(trace.side === 'positive') {
6566
dPos = function(di) {
6667
var pos = shiftPos(di);
67-
return Fx.inbox(pos, pos + boxDelta, hoverPseudoDistance);
68+
return Fx.inbox(pos, pos + posAcceptance, hoverPseudoDistance);
6869
};
6970
}
7071
if(trace.side === 'negative') {
7172
dPos = function(di) {
7273
var pos = shiftPos(di);
73-
return Fx.inbox(pos - boxDelta, pos, hoverPseudoDistance);
74+
return Fx.inbox(pos - posAcceptance, pos, hoverPseudoDistance);
7475
};
7576
}
7677
} else {
7778
dPos = function(di) {
7879
var pos = shiftPos(di);
79-
return Fx.inbox(pos - boxDelta, pos + boxDelta, hoverPseudoDistance);
80+
return Fx.inbox(pos - posAcceptance, pos + posAcceptance, hoverPseudoDistance);
8081
};
8182
}
8283

@@ -133,10 +134,9 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) {
133134
else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
134135
else pointData.color = trace.fillcolor;
135136

136-
pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - t.bdPos, true);
137-
pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + t.bdPos, true);
137+
pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - boxDelta, true);
138+
pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + boxDelta, true);
138139

139-
Axes.tickText(pAxis, pAxis.c2l(di.pos), 'hover').text;
140140
pointData[pLetter + 'LabelVal'] = di.pos;
141141

142142
var spikePosAttr = pLetter + 'Spike';

src/traces/box/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Box.selectPoints = require('./select');
2424
Box.moduleType = 'trace';
2525
Box.name = 'box';
2626
Box.basePlotModule = require('../../plots/cartesian');
27-
Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'];
27+
Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend', 'boxLayout'];
2828
Box.meta = {
2929
description: [
3030
'In vertical (horizontal) box plots,',

src/traces/box/layout_defaults.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
'use strict';
1010

11+
var Registry = require('../../registry');
1112
var Lib = require('../../lib');
1213
var layoutAttributes = require('./layout_attributes');
1314

1415
function _supply(layoutIn, layoutOut, fullData, coerce, traceType) {
1516
var hasTraceType;
17+
var category = traceType + 'Layout';
1618
for(var i = 0; i < fullData.length; i++) {
17-
if(fullData[i].type === traceType) {
19+
if(Registry.traceIs(fullData[i], category)) {
1820
hasTraceType = true;
1921
break;
2022
}

src/traces/box/plot.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ function plot(gd, plotinfo, cdbox) {
3535
var sel = cd0.node3 = d3.select(this);
3636
var numBoxes = fullLayout._numBoxes;
3737

38+
var groupFraction = (1 - fullLayout.boxgap);
39+
3840
var group = (fullLayout.boxmode === 'group' && numBoxes > 1);
3941
// box half width
40-
var bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1);
42+
var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1);
4143
// box center offset
42-
var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * (1 - fullLayout.boxgap) : 0;
44+
var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0;
4345
// whisker width
4446
var wdPos = bdPos * trace.whiskerwidth;
4547

@@ -62,6 +64,9 @@ function plot(gd, plotinfo, cdbox) {
6264
t.bPos = bPos;
6365
t.bdPos = bdPos;
6466
t.wdPos = wdPos;
67+
// half-width within which to accept hover for this box
68+
// always split the distance to the closest box
69+
t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1);
6570

6671
// boxes and whiskers
6772
plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t);
@@ -121,8 +126,8 @@ function plotBoxAndWhiskers(sel, axes, trace, t) {
121126
valAxis.c2p(d.med, true),
122127
Math.min(q1, q3) + 1, Math.max(q1, q3) - 1
123128
);
124-
var lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true);
125-
var uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true);
129+
var lf = valAxis.c2p(trace.boxpoints ? d.lf : d.min, true);
130+
var uf = valAxis.c2p(trace.boxpoints ? d.uf : d.max, true);
126131
var ln = valAxis.c2p(d.ln, true);
127132
var un = valAxis.c2p(d.un, true);
128133

src/traces/box/set_positions.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,23 @@ function setPositions(gd, plotinfo) {
2525
var minPad = 0;
2626
var maxPad = 0;
2727

28-
// make list of boxes
28+
// make list of boxes / candlesticks
29+
// For backward compatibility, candlesticks are treated as if they *are* box traces here
2930
for(var j = 0; j < calcdata.length; j++) {
3031
var cd = calcdata[j];
3132
var t = cd[0].t;
3233
var trace = cd[0].trace;
3334

34-
if(trace.visible === true && trace.type === 'box' &&
35+
if(trace.visible === true &&
36+
(trace.type === 'box' || trace.type === 'candlestick') &&
3537
!t.empty &&
36-
trace.orientation === orientation &&
38+
(trace.orientation || 'v') === orientation &&
3739
trace.xaxis === xa._id &&
3840
trace.yaxis === ya._id
3941
) {
4042
boxList.push(j);
4143

42-
if(trace.boxpoints !== false) {
44+
if(trace.boxpoints) {
4345
minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1);
4446
maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1);
4547
}

src/traces/box/style.js

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,35 @@ module.exports = function style(gd, cd) {
2222
var trace = d[0].trace;
2323
var lineWidth = trace.line.width;
2424

25-
el.selectAll('path.box')
26-
.style('stroke-width', lineWidth + 'px')
27-
.call(Color.stroke, trace.line.color)
28-
.call(Color.fill, trace.fillcolor);
29-
30-
el.selectAll('path.mean')
31-
.style({
32-
'stroke-width': lineWidth,
33-
'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px'
34-
})
35-
.call(Color.stroke, trace.line.color);
36-
37-
var pts = el.selectAll('path.point');
38-
Drawing.pointStyle(pts, trace, gd);
39-
Drawing.selectedPointStyle(pts, trace);
25+
function styleBox(boxSel, lineWidth, lineColor, fillColor) {
26+
boxSel.style('stroke-width', lineWidth + 'px')
27+
.call(Color.stroke, lineColor)
28+
.call(Color.fill, fillColor);
29+
}
30+
31+
var allBoxes = el.selectAll('path.box');
32+
33+
if(trace.type === 'candlestick') {
34+
allBoxes.each(function(boxData) {
35+
var thisBox = d3.select(this);
36+
var container = trace[boxData.candle]; // candle = 'increasing' or 'decreasing'
37+
styleBox(thisBox, container.line.width, container.line.color, container.fillcolor);
38+
// TODO: custom selection style for candlesticks
39+
thisBox.style('opacity', trace.selectedpoints && !boxData.selected ? 0.3 : 1);
40+
});
41+
}
42+
else {
43+
styleBox(allBoxes, lineWidth, trace.line.color, trace.fillcolor);
44+
el.selectAll('path.mean')
45+
.style({
46+
'stroke-width': lineWidth,
47+
'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px'
48+
})
49+
.call(Color.stroke, trace.line.color);
50+
51+
var pts = el.selectAll('path.point');
52+
Drawing.pointStyle(pts, trace, gd);
53+
Drawing.selectedPointStyle(pts, trace);
54+
}
4055
});
4156
};

0 commit comments

Comments
 (0)