diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 4215a388ac8..a929b9907a0 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -826,13 +826,6 @@ function createHoverText(hoverData, opts, gd) { } } - // used by other modules (initially just ternary) that - // manage their own hoverinfo independent of cleanPoint - // the rest of this will still apply, so such modules - // can still put things in (x|y|z)Label, text, and name - // and hoverinfo will still determine their visibility - if(d.extraText !== undefined) text += d.extraText; - if(d.zLabel !== undefined) { if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; @@ -851,6 +844,13 @@ function createHoverText(hoverData, opts, gd) { text += (text ? '
' : '') + d.text; } + // used by other modules (initially just ternary) that + // manage their own hoverinfo independent of cleanPoint + // the rest of this will still apply, so such modules + // can still put things in (x|y|z)Label, text, and name + // and hoverinfo will still determine their visibility + if(d.extraText !== undefined) text += (text ? '
' : '') + d.extraText; + // if 'text' is empty at this point, // put 'name' in main label and don't show secondary label if(text === '') { diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 98c1e766652..fbedb04763f 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -384,20 +384,10 @@ function drawTexts(g, gd) { if(!this.text()) text = ' \u0020\u0020 '; - var transforms, direction; var fullInput = legendItem.trace._fullInput || {}; var update = {}; - // N.B. this block isn't super clean, - // is unfortunately untested at the moment, - // and only works for for 'ohlc' and 'candlestick', - // but should be generalized for other one-to-many transforms - if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) { - transforms = legendItem.trace.transforms; - direction = transforms[transforms.length - 1].direction; - - update[direction + '.name'] = text; - } else if(Registry.hasTransform(fullInput, 'groupby')) { + if(Registry.hasTransform(fullInput, 'groupby')) { var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby'); var index = groupbyIndices[groupbyIndices.length - 1]; diff --git a/src/components/legend/style.js b/src/components/legend/style.js index b5a4878b9f7..fd022a9eb41 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -52,7 +52,9 @@ module.exports = function style(s, gd) { .each(styleBoxes) .each(stylePies) .each(styleLines) - .each(stylePoints); + .each(stylePoints) + .each(styleCandles) + .each(styleOHLC); function styleLines(d) { var trace = d[0].trace; @@ -207,7 +209,61 @@ module.exports = function style(s, gd) { .call(Color.fill, trace.fillcolor); if(w) { - p.call(Color.stroke, trace.line.color); + Color.stroke(p, trace.line.color); + } + }); + } + + function styleCandles(d) { + var trace = d[0].trace, + pts = d3.select(this).select('g.legendpoints') + .selectAll('path.legendcandle') + .data(trace.type === 'candlestick' && trace.visible ? [d, d] : []); + pts.enter().append('path').classed('legendcandle', true) + .attr('d', function(_, i) { + if(i) return 'M-15,0H-8M-8,6V-6H8Z'; // increasing + return 'M15,0H8M8,-6V6H-8Z'; // decreasing + }) + .attr('transform', 'translate(20,0)') + .style('stroke-miterlimit', 1); + pts.exit().remove(); + pts.each(function(_, i) { + var container = trace[i ? 'increasing' : 'decreasing']; + var w = container.line.width, + p = d3.select(this); + + p.style('stroke-width', w + 'px') + .call(Color.fill, container.fillcolor); + + if(w) { + Color.stroke(p, container.line.color); + } + }); + } + + function styleOHLC(d) { + var trace = d[0].trace, + pts = d3.select(this).select('g.legendpoints') + .selectAll('path.legendohlc') + .data(trace.type === 'ohlc' && trace.visible ? [d, d] : []); + pts.enter().append('path').classed('legendohlc', true) + .attr('d', function(_, i) { + if(i) return 'M-15,0H0M-8,-6V0'; // increasing + return 'M15,0H0M8,6V0'; // decreasing + }) + .attr('transform', 'translate(20,0)') + .style('stroke-miterlimit', 1); + pts.exit().remove(); + pts.each(function(_, i) { + var container = trace[i ? 'increasing' : 'decreasing']; + var w = container.line.width, + p = d3.select(this); + + p.style('fill', 'none') + .call(Drawing.dashLine, container.line.dash, w); + + if(w) { + Color.stroke(p, container.line.color); } }); } diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 5202aa2a7a6..651bfc72e7f 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -14,16 +14,18 @@ var oppAxisAttrs = require('./oppaxis_attributes'); var axisIds = require('../../plots/cartesian/axis_ids'); module.exports = function handleDefaults(layoutIn, layoutOut, axName) { - if(!layoutIn[axName].rangeslider) return; + var axIn = layoutIn[axName]; + var axOut = layoutOut[axName]; + + if(!(axIn.rangeslider || layoutOut._requestRangeslider[axOut._id])) return; // not super proud of this (maybe store _ in axis object instead - if(!Lib.isPlainObject(layoutIn[axName].rangeslider)) { - layoutIn[axName].rangeslider = {}; + if(!Lib.isPlainObject(axIn.rangeslider)) { + axIn.rangeslider = {}; } - var containerIn = layoutIn[axName].rangeslider, - axOut = layoutOut[axName], - containerOut = axOut.rangeslider = {}; + var containerIn = axIn.rangeslider; + var containerOut = axOut.rangeslider = {}; function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 8aff445e6a0..ea673113a2f 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -455,7 +455,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { id: id, plotgroup: plotgroup, xaxis: xa, - yaxis: ya + yaxis: ya, + isRangePlot: true }; if(isMainPlot) mainplotinfo = plotinfo; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index bb025e3f2c5..bab4077e28f 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -434,9 +434,7 @@ exports.coerceFont = function(coerce, attr, dfltObj) { */ exports.coerceHoverinfo = function(traceIn, traceOut, layoutOut) { var moduleAttrs = traceOut._module.attributes; - var attrs = moduleAttrs.hoverinfo ? - {hoverinfo: moduleAttrs.hoverinfo} : - baseTraceAttrs; + var attrs = moduleAttrs.hoverinfo ? moduleAttrs : baseTraceAttrs; var valObj = attrs.hoverinfo; var dflt; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index d4f44af785a..b0912b31020 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -330,6 +330,32 @@ exports.cleanData = function(data, existingData) { } } + // fixes from converting finance from transforms to real trace types + if(trace.type === 'candlestick' || trace.type === 'ohlc') { + var increasingShowlegend = (trace.increasing || {}).showlegend !== false; + var decreasingShowlegend = (trace.decreasing || {}).showlegend !== false; + var increasingName = cleanFinanceDir(trace.increasing); + var decreasingName = cleanFinanceDir(trace.decreasing); + + // now figure out something smart to do with the separate direction + // names we removed + if((increasingName !== false) && (decreasingName !== false)) { + // both sub-names existed: base name previously had no effect + // so ignore it and try to find a shared part of the sub-names + + var newName = commonPrefix( + increasingName, decreasingName, + increasingShowlegend, decreasingShowlegend + ); + // if no common part, leave whatever name was (or wasn't) there + if(newName) trace.name = newName; + } + else if((increasingName || decreasingName) && !trace.name) { + // one sub-name existed but not the base name - just use the sub-name + trace.name = increasingName || decreasingName; + } + } + // transforms backward compatibility fixes if(Array.isArray(trace.transforms)) { var transforms = trace.transforms; @@ -388,6 +414,38 @@ exports.cleanData = function(data, existingData) { } }; +function cleanFinanceDir(dirContainer) { + if(!Lib.isPlainObject(dirContainer)) return false; + + var dirName = dirContainer.name; + + delete dirContainer.name; + delete dirContainer.showlegend; + + return (typeof dirName === 'string' || typeof dirName === 'number') && String(dirName); +} + +function commonPrefix(name1, name2, show1, show2) { + // if only one is shown in the legend, use that + if(show1 && !show2) return name1; + if(show2 && !show1) return name2; + + // if both or neither are in the legend, check if one is blank (or whitespace) + // and use the other one + // note that hover labels can still use the name even if the legend doesn't + if(!name1.trim()) return name2; + if(!name2.trim()) return name1; + + var minLen = Math.min(name1.length, name2.length); + var i; + for(i = 0; i < minLen; i++) { + if(name1.charAt(i) !== name2.charAt(i)) break; + } + + var out = name1.substr(0, i); + return out.trim(); +} + // textposition - support partial attributes (ie just 'top') // and incorrect use of middle / center etc. function cleanTextPosition(textposition) { diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 94638f8ff50..f5681c44a3a 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -256,12 +256,13 @@ exports.getTraceValObject = function(trace, parts) { var moduleAttrs, valObject; if(head === 'transforms') { - if(!Array.isArray(trace.transforms)) return false; + var transforms = trace.transforms; + if(!Array.isArray(transforms) || !transforms.length) return false; var tNum = parts[1]; - if(!isIndex(tNum) || tNum >= trace.transforms.length) { + if(!isIndex(tNum) || tNum >= transforms.length) { return false; } - moduleAttrs = (Registry.transformsRegistry[trace.transforms[tNum].type] || {}).attributes; + moduleAttrs = (Registry.transformsRegistry[transforms[tNum].type] || {}).attributes; valObject = moduleAttrs && moduleAttrs[parts[2]]; i = 3; // start recursing only inside the transform } diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index a134bd71cf5..59ac6f1b87f 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -68,6 +68,7 @@ module.exports = { 'carpetlayer', 'violinlayer', 'boxlayer', + 'ohlclayer', 'scatterlayer' ], diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 3304737bfb6..8ffa10cdc7b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -192,7 +192,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback // remaining plot traces should also be able to do this. Once implemented, // we won't need this - which should sometimes be a big speedup. if(plotinfo.plot) { - plotinfo.plot.selectAll('g:not(.scatterlayer)').selectAll('g.trace').remove(); + plotinfo.plot.selectAll('g:not(.scatterlayer):not(.ohlclayer)').selectAll('g.trace').remove(); } // plot all traces for each module at once @@ -200,12 +200,16 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback var _module = modules[j]; // skip over non-cartesian trace modules - if(_module.basePlotModule.name !== 'cartesian') continue; + if(!_module.plot || _module.basePlotModule.name !== 'cartesian') continue; // plot all traces of this type on this subplot at once - var cdModule = getModuleCalcData(cdSubplot, _module); + var cdModuleAndOthers = getModuleCalcData(cdSubplot, _module); + var cdModule = cdModuleAndOthers[0]; + // don't need to search the found traces again - in fact we need to NOT + // so that if two modules share the same plotter we don't double-plot + cdSubplot = cdModuleAndOthers[1]; - if(_module.plot) _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); + _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); } } @@ -215,6 +219,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) var oldPlots = oldFullLayout._plots || {}; var hadScatter, hasScatter; + var hadOHLC, hasOHLC; var hadGl, hasGl; var i, k, subplotInfo, moduleName; @@ -232,28 +237,36 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) moduleName = oldModules[i].name; if(moduleName === 'scatter') hadScatter = true; else if(moduleName === 'scattergl') hadGl = true; + else if(moduleName === 'ohlc') hadOHLC = true; } for(i = 0; i < newModules.length; i++) { moduleName = newModules[i].name; if(moduleName === 'scatter') hasScatter = true; else if(moduleName === 'scattergl') hasGl = true; + else if(moduleName === 'ohlc') hasOHLC = true; } - if(hadScatter && !hasScatter) { - for(k in oldPlots) { - subplotInfo = oldPlots[k]; - if(subplotInfo.plot) { - subplotInfo.plot.select('g.scatterlayer') - .selectAll('g.trace') - .remove(); + var layersToEmpty = []; + if(hadScatter && !hasScatter) layersToEmpty.push('g.scatterlayer'); + if(hadOHLC && !hasOHLC) layersToEmpty.push('g.ohlclayer'); + + if(layersToEmpty.length) { + for(var layeri = 0; layeri < layersToEmpty.length; layeri++) { + for(k in oldPlots) { + subplotInfo = oldPlots[k]; + if(subplotInfo.plot) { + subplotInfo.plot.select(layersToEmpty[layeri]) + .selectAll('g.trace') + .remove(); + } } - } - oldFullLayout._infolayer.selectAll('g.rangeslider-container') - .select('g.scatterlayer') - .selectAll('g.trace') - .remove(); + oldFullLayout._infolayer.selectAll('g.rangeslider-container') + .select(layersToEmpty[layeri]) + .selectAll('g.trace') + .remove(); + } } if(hadGl && !hasGl) { diff --git a/src/plots/get_data.js b/src/plots/get_data.js index 763fb255d30..e06ca4608c3 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -12,11 +12,11 @@ var Registry = require('../registry'); var SUBPLOT_PATTERN = require('./cartesian/constants').SUBPLOT_PATTERN; /** - * Get calcdata traces(s) associated with a given subplot + * Get calcdata trace(s) associated with a given subplot * - * @param {array} calcData (as in gd.calcdata) - * @param {string} type subplot type - * @param {string} subplotId subplot id to look for + * @param {array} calcData: as in gd.calcdata + * @param {string} type: subplot type + * @param {string} subplotId: subplot id to look for * * @return {array} array of calcdata traces */ @@ -36,20 +36,40 @@ exports.getSubplotCalcData = function(calcData, type, subplotId) { return subplotCalcData; }; - +/** + * Get calcdata trace(s) that can be plotted with a given module + * NOTE: this isn't necessarily just exactly matching trace type, + * if multiple trace types use the same plotting routine, they will be + * collected here. + * In order to not plot the same thing multiple times, we return two arrays, + * the calcdata we *will* plot with this module, and the ones we *won't* + * + * @param {array} calcdata: as in gd.calcdata + * @param {object|string} typeOrModule: the plotting module, or its name + * + * @return {array[array]} [foundCalcdata, remainingCalcdata] + */ exports.getModuleCalcData = function(calcdata, typeOrModule) { var moduleCalcData = []; + var remainingCalcData = []; var _module = typeof typeOrModule === 'string' ? Registry.getModule(typeOrModule) : typeOrModule; - if(!_module) return moduleCalcData; + if(!_module) return [moduleCalcData, calcdata]; for(var i = 0; i < calcdata.length; i++) { var cd = calcdata[i]; var trace = cd[0].trace; + if(trace.visible !== true) continue; - if((trace._module === _module) && (trace.visible === true)) moduleCalcData.push(cd); + // we use this to find data to plot - so if there's a .plot + if(trace._module.plot === _module.plot) { + moduleCalcData.push(cd); + } + else { + remainingCalcData.push(cd); + } } - return moduleCalcData; + return [moduleCalcData, remainingCalcData]; }; /** diff --git a/src/plots/plots.js b/src/plots/plots.js index 80af2b79bc5..05aa915e073 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -373,6 +373,10 @@ plots.supplyDefaults = function(gd) { var splomAxes = newFullLayout._splomAxes = {x: {}, y: {}}; var splomSubplots = newFullLayout._splomSubplots = {}; + // for traces to request a default rangeslider on their x axes + // eg set `_requestRangeslider.x2 = true` for xaxis2 + newFullLayout._requestRangeslider = {}; + // then do the data newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 30914fecb14..28f81a985d3 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -42,9 +42,11 @@ module.exports = function plot(gd, plotinfo, cdbar) { bartraces.enter().append('g') .attr('class', 'trace bars'); - bartraces.each(function(d) { - d[0].node3 = d3.select(this); - }); + if(!plotinfo.isRangePlot) { + bartraces.each(function(d) { + d[0].node3 = d3.select(this); + }); + } bartraces.append('g') .attr('class', 'points') diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 9452bc3390a..0c4c75dc58a 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -143,11 +143,6 @@ module.exports = function calc(gd, trace) { } }; - // don't show labels in candlestick hover labels - if(trace._fullInput && trace._fullInput.type === 'candlestick') { - delete cd[0].t.labels; - } - fullLayout[numKey]++; return cd; } else { diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 3ff3fe024ef..79e24509360 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -58,25 +58,26 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { hoverPseudoDistance, spikePseudoDistance; var boxDelta = t.bdPos; + var posAcceptance = t.wHover; var shiftPos = function(di) { return di.pos + t.bPos - pVal; }; if(isViolin && trace.side !== 'both') { if(trace.side === 'positive') { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos, pos + boxDelta, hoverPseudoDistance); + return Fx.inbox(pos, pos + posAcceptance, hoverPseudoDistance); }; } if(trace.side === 'negative') { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos - boxDelta, pos, hoverPseudoDistance); + return Fx.inbox(pos - posAcceptance, pos, hoverPseudoDistance); }; } } else { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos - boxDelta, pos + boxDelta, hoverPseudoDistance); + return Fx.inbox(pos - posAcceptance, pos + posAcceptance, hoverPseudoDistance); }; } @@ -133,10 +134,9 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc; else pointData.color = trace.fillcolor; - pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - t.bdPos, true); - pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + t.bdPos, true); + pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - boxDelta, true); + pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + boxDelta, true); - Axes.tickText(pAxis, pAxis.c2l(di.pos), 'hover').text; pointData[pLetter + 'LabelVal'] = di.pos; var spikePosAttr = pLetter + 'Spike'; diff --git a/src/traces/box/index.js b/src/traces/box/index.js index ad32d7000aa..621f825f813 100644 --- a/src/traces/box/index.js +++ b/src/traces/box/index.js @@ -24,7 +24,7 @@ Box.selectPoints = require('./select'); Box.moduleType = 'trace'; Box.name = 'box'; Box.basePlotModule = require('../../plots/cartesian'); -Box.categories = ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts']; +Box.categories = ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts', 'boxLayout']; Box.meta = { description: [ 'In vertical (horizontal) box plots,', diff --git a/src/traces/box/layout_defaults.js b/src/traces/box/layout_defaults.js index fd136614907..68af8851d0b 100644 --- a/src/traces/box/layout_defaults.js +++ b/src/traces/box/layout_defaults.js @@ -8,13 +8,15 @@ 'use strict'; +var Registry = require('../../registry'); var Lib = require('../../lib'); var layoutAttributes = require('./layout_attributes'); function _supply(layoutIn, layoutOut, fullData, coerce, traceType) { var hasTraceType; + var category = traceType + 'Layout'; for(var i = 0; i < fullData.length; i++) { - if(fullData[i].type === traceType) { + if(Registry.traceIs(fullData[i], category)) { hasTraceType = true; break; } diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 8f8bea84b5c..042c7363d10 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -32,19 +32,22 @@ function plot(gd, plotinfo, cdbox) { var cd0 = d[0]; var t = cd0.t; var trace = cd0.trace; - var sel = cd0.node3 = d3.select(this); + var sel = d3.select(this); + if(!plotinfo.isRangePlot) cd0.node3 = sel; var numBoxes = fullLayout._numBoxes; + var groupFraction = (1 - fullLayout.boxgap); + var group = (fullLayout.boxmode === 'group' && numBoxes > 1); // box half width - var bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1); + var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1); // box center offset - var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * (1 - fullLayout.boxgap) : 0; + var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0; // whisker width var wdPos = bdPos * trace.whiskerwidth; if(trace.visible !== true || t.empty) { - d3.select(this).remove(); + sel.remove(); return; } @@ -62,6 +65,9 @@ function plot(gd, plotinfo, cdbox) { t.bPos = bPos; t.bdPos = bdPos; t.wdPos = wdPos; + // half-width within which to accept hover for this box + // always split the distance to the closest box + t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1); // boxes and whiskers plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t); @@ -121,8 +127,16 @@ function plotBoxAndWhiskers(sel, axes, trace, t) { valAxis.c2p(d.med, true), Math.min(q1, q3) + 1, Math.max(q1, q3) - 1 ); - var lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true); - var uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true); + + // for compatibility with box, violin, and candlestick + // perhaps we should put this into cd0.t instead so it's more explicit, + // but what we have now is: + // - box always has d.lf, but boxpoints can be anything + // - violin has d.lf and should always use it (boxpoints is undefined) + // - candlestick has only min/max + var useExtremes = (d.lf === undefined) || (trace.boxpoints === false); + var lf = valAxis.c2p(useExtremes ? d.min : d.lf, true); + var uf = valAxis.c2p(useExtremes ? d.max : d.uf, true); var ln = valAxis.c2p(d.ln, true); var un = valAxis.c2p(d.un, true); diff --git a/src/traces/box/set_positions.js b/src/traces/box/set_positions.js index 21437c39a25..ca460fd8297 100644 --- a/src/traces/box/set_positions.js +++ b/src/traces/box/set_positions.js @@ -25,21 +25,23 @@ function setPositions(gd, plotinfo) { var minPad = 0; var maxPad = 0; - // make list of boxes + // make list of boxes / candlesticks + // For backward compatibility, candlesticks are treated as if they *are* box traces here for(var j = 0; j < calcdata.length; j++) { var cd = calcdata[j]; var t = cd[0].t; var trace = cd[0].trace; - if(trace.visible === true && trace.type === 'box' && + if(trace.visible === true && + (trace.type === 'box' || trace.type === 'candlestick') && !t.empty && - trace.orientation === orientation && + (trace.orientation || 'v') === orientation && trace.xaxis === xa._id && trace.yaxis === ya._id ) { boxList.push(j); - if(trace.boxpoints !== false) { + if(trace.boxpoints) { minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1); maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1); } diff --git a/src/traces/box/style.js b/src/traces/box/style.js index 265692eedb2..aef404372b7 100644 --- a/src/traces/box/style.js +++ b/src/traces/box/style.js @@ -22,20 +22,35 @@ module.exports = function style(gd, cd) { var trace = d[0].trace; var lineWidth = trace.line.width; - el.selectAll('path.box') - .style('stroke-width', lineWidth + 'px') - .call(Color.stroke, trace.line.color) - .call(Color.fill, trace.fillcolor); - - el.selectAll('path.mean') - .style({ - 'stroke-width': lineWidth, - 'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px' - }) - .call(Color.stroke, trace.line.color); - - var pts = el.selectAll('path.point'); - Drawing.pointStyle(pts, trace, gd); - Drawing.selectedPointStyle(pts, trace); + function styleBox(boxSel, lineWidth, lineColor, fillColor) { + boxSel.style('stroke-width', lineWidth + 'px') + .call(Color.stroke, lineColor) + .call(Color.fill, fillColor); + } + + var allBoxes = el.selectAll('path.box'); + + if(trace.type === 'candlestick') { + allBoxes.each(function(boxData) { + var thisBox = d3.select(this); + var container = trace[boxData.dir]; // dir = 'increasing' or 'decreasing' + styleBox(thisBox, container.line.width, container.line.color, container.fillcolor); + // TODO: custom selection style for candlesticks + thisBox.style('opacity', trace.selectedpoints && !boxData.selected ? 0.3 : 1); + }); + } + else { + styleBox(allBoxes, lineWidth, trace.line.color, trace.fillcolor); + el.selectAll('path.mean') + .style({ + 'stroke-width': lineWidth, + 'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px' + }) + .call(Color.stroke, trace.line.color); + + var pts = el.selectAll('path.point'); + Drawing.pointStyle(pts, trace, gd); + Drawing.selectedPointStyle(pts, trace); + } }); }; diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index f4a6c8c9394..ac007ffc9af 100644 --- a/src/traces/candlestick/attributes.js +++ b/src/traces/candlestick/attributes.js @@ -15,9 +15,6 @@ var boxAttrs = require('../box/attributes'); function directionAttrs(lineColorDefault) { return { - name: OHLCattrs.increasing.name, - showlegend: OHLCattrs.increasing.showlegend, - line: { color: extendFlat({}, boxAttrs.line.color, {dflt: lineColorDefault}), width: boxAttrs.line.width, diff --git a/src/traces/candlestick/calc.js b/src/traces/candlestick/calc.js new file mode 100644 index 00000000000..72933f236a3 --- /dev/null +++ b/src/traces/candlestick/calc.js @@ -0,0 +1,48 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); + +var calcCommon = require('../ohlc/calc').calcCommon; + +module.exports = function(gd, trace) { + var fullLayout = gd._fullLayout; + var xa = Axes.getFromId(gd, trace.xaxis); + var ya = Axes.getFromId(gd, trace.yaxis); + + var x = xa.makeCalcdata(trace, 'x'); + + var cd = calcCommon(gd, trace, x, ya, ptFunc); + + if(cd.length) { + Lib.extendFlat(cd[0].t, { + num: fullLayout._numBoxes, + dPos: Lib.distinctVals(x).minDiff / 2, + posLetter: 'x', + valLetter: 'y', + }); + + fullLayout._numBoxes++; + return cd; + } else { + return [{t: {empty: true}}]; + } +}; + +function ptFunc(o, h, l, c) { + return { + min: l, + q1: Math.min(o, c), + med: c, + q3: Math.max(o, c), + max: h, + }; +} diff --git a/src/traces/candlestick/defaults.js b/src/traces/candlestick/defaults.js index ee5dcdf7682..e00862ca43a 100644 --- a/src/traces/candlestick/defaults.js +++ b/src/traces/candlestick/defaults.js @@ -10,14 +10,11 @@ 'use strict'; var Lib = require('../../lib'); +var Color = require('../../components/color'); var handleOHLC = require('../ohlc/ohlc_defaults'); -var handleDirectionDefaults = require('../ohlc/direction_defaults'); -var helpers = require('../ohlc/helpers'); var attributes = require('./attributes'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - helpers.pushDummyTransformOpts(traceIn, traceOut); - function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } @@ -35,12 +32,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('whiskerwidth'); + + layout._requestRangeslider[traceOut.xaxis] = true; }; function handleDirection(traceIn, traceOut, coerce, direction) { - handleDirectionDefaults(traceIn, traceOut, coerce, direction); - - coerce(direction + '.line.color'); + var lineColor = coerce(direction + '.line.color'); coerce(direction + '.line.width', traceOut.line.width); - coerce(direction + '.fillcolor'); + coerce(direction + '.fillcolor', Color.addOpacity(lineColor, 0.5)); } diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js index ef94d47bc17..4a6372585b7 100644 --- a/src/traces/candlestick/index.js +++ b/src/traces/candlestick/index.js @@ -8,13 +8,11 @@ 'use strict'; -var Registry = require('../../registry'); - module.exports = { moduleType: 'trace', name: 'candlestick', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'svg', 'showLegend', 'candlestick'], + categories: ['cartesian', 'svg', 'showLegend', 'candlestick', 'boxLayout'], meta: { description: [ 'The candlestick is a style of financial chart describing', @@ -32,8 +30,13 @@ module.exports = { }, attributes: require('./attributes'), + layoutAttributes: require('../box/layout_attributes'), + supplyLayoutDefaults: require('../box/layout_defaults').supplyLayoutDefaults, + setPositions: require('../box/set_positions').setPositions, supplyDefaults: require('./defaults'), + calc: require('./calc'), + plot: require('../box/plot').plot, + style: require('../box/style'), + hoverPoints: require('../ohlc/hover'), + selectPoints: require('../ohlc/select') }; - -Registry.register(require('../box')); -Registry.register(require('./transform')); diff --git a/src/traces/candlestick/transform.js b/src/traces/candlestick/transform.js deleted file mode 100644 index 36b1e5de6c7..00000000000 --- a/src/traces/candlestick/transform.js +++ /dev/null @@ -1,129 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var helpers = require('../ohlc/helpers'); - -exports.moduleType = 'transform'; - -exports.name = 'candlestick'; - -exports.attributes = {}; - -exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { - helpers.clearEphemeralTransformOpts(traceIn); - helpers.copyOHLC(transformIn, traceOut); - - return transformIn; -}; - -exports.transform = function transform(dataIn, state) { - var dataOut = []; - - for(var i = 0; i < dataIn.length; i++) { - var traceIn = dataIn[i]; - - if(traceIn.type !== 'candlestick') { - dataOut.push(traceIn); - continue; - } - - dataOut.push( - makeTrace(traceIn, state, 'increasing'), - makeTrace(traceIn, state, 'decreasing') - ); - } - - helpers.addRangeSlider(dataOut, state.layout); - - return dataOut; -}; - -function makeTrace(traceIn, state, direction) { - var traceOut = { - type: 'box', - boxpoints: false, - - visible: traceIn.visible, - hoverinfo: traceIn.hoverinfo, - opacity: traceIn.opacity, - xaxis: traceIn.xaxis, - yaxis: traceIn.yaxis, - - transforms: helpers.makeTransform(traceIn, state, direction), - _inputLength: traceIn._inputLength - }; - - // the rest of below may not have been coerced - - var directionOpts = traceIn[direction]; - - if(directionOpts) { - Lib.extendFlat(traceOut, { - - // to make autotype catch date axes soon!! - x: traceIn.x || [0], - xcalendar: traceIn.xcalendar, - - // concat low and high to get correct autorange - y: [].concat(traceIn.low).concat(traceIn.high), - - whiskerwidth: traceIn.whiskerwidth, - text: traceIn.text, - - name: directionOpts.name, - showlegend: directionOpts.showlegend, - line: directionOpts.line, - fillcolor: directionOpts.fillcolor - }); - } - - return traceOut; -} - -exports.calcTransform = function calcTransform(gd, trace, opts) { - var direction = opts.direction, - filterFn = helpers.getFilterFn(direction); - - var open = trace.open, - high = trace.high, - low = trace.low, - close = trace.close; - - var len = trace._inputLength, - x = [], - y = []; - - var appendX = trace._fullInput.x ? - function(i) { - var v = trace.x[i]; - x.push(v, v, v, v, v, v); - } : - function(i) { - x.push(i, i, i, i, i, i); - }; - - var appendY = function(o, h, l, c) { - y.push(l, o, c, c, c, h); - }; - - for(var i = 0; i < len; i++) { - if(filterFn(open[i], close[i]) && isNumeric(high[i]) && isNumeric(low[i])) { - appendX(i); - appendY(open[i], high[i], low[i], close[i]); - } - } - - trace.x = x; - trace.y = y; -}; diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index 6cb5b7a77e5..e37213cbdc0 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -20,27 +20,6 @@ var lineAttrs = scatterAttrs.line; function directionAttrs(lineColorDefault) { return { - name: { - valType: 'string', - role: 'info', - editType: 'style', - description: [ - 'Sets the segment name.', - 'The segment name appear as the legend item and on hover.' - ].join(' ') - }, - - showlegend: { - valType: 'boolean', - role: 'info', - dflt: true, - editType: 'style', - description: [ - 'Determines whether or not an item corresponding to this', - 'segment is shown in the legend.' - ].join(' ') - }, - line: { color: extendFlat({}, lineAttrs.color, {dflt: lineColorDefault}), width: lineAttrs.width, diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js new file mode 100644 index 00000000000..42de0e1a086 --- /dev/null +++ b/src/traces/ohlc/calc.js @@ -0,0 +1,164 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); +var _ = Lib._; +var Axes = require('../../plots/cartesian/axes'); +var BADNUM = require('../../constants/numerical').BADNUM; + +function calc(gd, trace) { + var xa = Axes.getFromId(gd, trace.xaxis); + var ya = Axes.getFromId(gd, trace.yaxis); + + var tickLen = convertTickWidth(gd, xa, trace); + var minDiff = trace._minDiff; + trace._minDiff = null; + var x = trace._xcalc; + trace._xcalc = null; + + var cd = calcCommon(gd, trace, x, ya, ptFunc); + + Axes.expand(xa, x, {vpad: minDiff / 2}); + + if(cd.length) { + Lib.extendFlat(cd[0].t, { + wHover: minDiff / 2, + tickLen: tickLen + }); + return cd; + } else { + return [{t: {empty: true}}]; + } +} + +function ptFunc(o, h, l, c) { + return { + o: o, + h: h, + l: l, + c: c + }; +} + + +// shared between OHLC and candlestick +// ptFunc makes a calcdata point specific to each trace type, from oi, hi, li, ci +function calcCommon(gd, trace, x, ya, ptFunc) { + var o = ya.makeCalcdata(trace, 'open'); + var h = ya.makeCalcdata(trace, 'high'); + var l = ya.makeCalcdata(trace, 'low'); + var c = ya.makeCalcdata(trace, 'close'); + + var hasTextArray = Array.isArray(trace.text); + + // we're optimists - before we have any changing data, assume increasing + var increasing = true; + var cPrev = null; + + var cd = []; + for(var i = 0; i < x.length; i++) { + var xi = x[i]; + var oi = o[i]; + var hi = h[i]; + var li = l[i]; + var ci = c[i]; + + if(xi !== BADNUM && oi !== BADNUM && hi !== BADNUM && li !== BADNUM && ci !== BADNUM) { + if(ci === oi) { + // if open == close, look for a change from the previous close + if(cPrev !== null && ci !== cPrev) increasing = ci > cPrev; + // else (c === cPrev or cPrev is null) no change + } + else increasing = ci > oi; + + cPrev = ci; + + var pt = ptFunc(oi, hi, li, ci); + + pt.pos = xi; + pt.yc = (oi + ci) / 2; + pt.i = i; + pt.dir = increasing ? 'increasing' : 'decreasing'; + + if(hasTextArray) pt.tx = trace.text[i]; + + cd.push(pt); + } + } + + Axes.expand(ya, l.concat(h), {padded: true}); + + if(cd.length) { + cd[0].t = { + labels: { + open: _(gd, 'open:') + ' ', + high: _(gd, 'high:') + ' ', + low: _(gd, 'low:') + ' ', + close: _(gd, 'close:') + ' ' + } + }; + } + + return cd; +} + +/* + * find min x-coordinates difference of all traces + * attached to this x-axis and stash the result in _minDiff + * in all traces; when a trace uses this in its + * calc step it deletes _minDiff, so that next calc this is + * done again in case the data changed. + * also since we need it here, stash _xcalc on the trace + */ +function convertTickWidth(gd, xa, trace) { + var minDiff = trace._minDiff; + + if(!minDiff) { + var fullData = gd._fullData, + ohlcTracesOnThisXaxis = []; + + minDiff = Infinity; + + var i; + + for(i = 0; i < fullData.length; i++) { + var tracei = fullData[i]; + + if(tracei.type === 'ohlc' && + tracei.visible === true && + tracei.xaxis === xa._id + ) { + ohlcTracesOnThisXaxis.push(tracei); + + var xcalc = xa.makeCalcdata(tracei, 'x'); + tracei._xcalc = xcalc; + + var _minDiff = Lib.distinctVals(xcalc).minDiff; + if(_minDiff && isFinite(_minDiff)) { + minDiff = Math.min(minDiff, _minDiff); + } + } + } + + // if minDiff is still Infinity here, set it to 1 + if(minDiff === Infinity) minDiff = 1; + + for(i = 0; i < ohlcTracesOnThisXaxis.length; i++) { + ohlcTracesOnThisXaxis[i]._minDiff = minDiff; + } + } + + return minDiff * trace.tickwidth; +} + +module.exports = { + calc: calc, + calcCommon: calcCommon +}; diff --git a/src/traces/ohlc/defaults.js b/src/traces/ohlc/defaults.js index ee9a9948ce4..d8ddf1b03ce 100644 --- a/src/traces/ohlc/defaults.js +++ b/src/traces/ohlc/defaults.js @@ -11,13 +11,9 @@ var Lib = require('../../lib'); var handleOHLC = require('./ohlc_defaults'); -var handleDirectionDefaults = require('./direction_defaults'); var attributes = require('./attributes'); -var helpers = require('./helpers'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - helpers.pushDummyTransformOpts(traceIn, traceOut); - function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } @@ -36,11 +32,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('tickwidth'); + + layout._requestRangeslider[traceOut.xaxis] = true; }; function handleDirection(traceIn, traceOut, coerce, direction) { - handleDirectionDefaults(traceIn, traceOut, coerce, direction); - coerce(direction + '.line.color'); coerce(direction + '.line.width', traceOut.line.width); coerce(direction + '.line.dash', traceOut.line.dash); diff --git a/src/traces/ohlc/direction_defaults.js b/src/traces/ohlc/direction_defaults.js deleted file mode 100644 index 4bed650bb69..00000000000 --- a/src/traces/ohlc/direction_defaults.js +++ /dev/null @@ -1,24 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - - -module.exports = function handleDirectionDefaults(traceIn, traceOut, coerce, direction) { - coerce(direction + '.showlegend'); - - // trace-wide *showlegend* overrides direction *showlegend* - if(traceIn.showlegend === false) { - traceOut[direction].showlegend = false; - } - - var nameDflt = traceOut.name + ' - ' + direction; - - coerce(direction + '.name', nameDflt); -}; diff --git a/src/traces/ohlc/helpers.js b/src/traces/ohlc/helpers.js deleted file mode 100644 index eff64c4f05c..00000000000 --- a/src/traces/ohlc/helpers.js +++ /dev/null @@ -1,147 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); - -// This routine gets called during the trace supply-defaults step. -// -// This is a hacky way to make 'ohlc' and 'candlestick' trace types -// go through the transform machinery. -// -// Note that, we must mutate user data (here traceIn) as opposed -// to full data (here traceOut) as - at the moment - transform -// defaults (which are called after trace defaults) start -// from a clear transforms container. The mutations inflicted are -// cleared in exports.clearEphemeralTransformOpts. -exports.pushDummyTransformOpts = function(traceIn, traceOut) { - var transformOpts = { - - // give dummy transform the same type as trace - type: traceOut.type, - - // track ephemeral transforms in user data - _ephemeral: true - }; - - if(Array.isArray(traceIn.transforms)) { - traceIn.transforms.push(transformOpts); - } - else { - traceIn.transforms = [transformOpts]; - } -}; - -// This routine gets called during the transform supply-defaults step -// where it clears ephemeral transform opts in user data -// and effectively put back user date in its pre-supplyDefaults state. -exports.clearEphemeralTransformOpts = function(traceIn) { - var transformsIn = traceIn.transforms; - - if(!Array.isArray(transformsIn)) return; - - for(var i = 0; i < transformsIn.length; i++) { - if(transformsIn[i]._ephemeral) transformsIn.splice(i, 1); - } - - if(transformsIn.length === 0) delete traceIn.transforms; -}; - -// This routine gets called during the transform supply-defaults step -// where it passes 'ohlc' and 'candlestick' attributes -// (found the transform container via exports.makeTransform) -// to the traceOut container such that they can -// be compatible with filter and groupby transforms. -// -// Note that this routine only has an effect during the -// second round of transform defaults done on generated traces -exports.copyOHLC = function(container, traceOut) { - if(container.open) traceOut.open = container.open; - if(container.high) traceOut.high = container.high; - if(container.low) traceOut.low = container.low; - if(container.close) traceOut.close = container.close; -}; - -// This routine gets called during the applyTransform step. -// -// We need to track trace attributes and which direction -// ('increasing' or 'decreasing') -// the generated correspond to for the calcTransform step. -// -// To make sure that the attributes reach the calcTransform, -// store it in the transform opts object. -exports.makeTransform = function(traceIn, state, direction) { - var out = Lib.extendFlat([], traceIn.transforms); - - out[state.transformIndex] = { - type: traceIn.type, - direction: direction, - - // these are copied to traceOut during exports.copyOHLC - open: traceIn.open, - high: traceIn.high, - low: traceIn.low, - close: traceIn.close - }; - - return out; -}; - -exports.getFilterFn = function(direction) { - return new _getFilterFn(direction); -}; - -function _getFilterFn(direction) { - // we're optimists - before we have any changing data, assume increasing - var isPrevIncreasing = true; - var cPrev = null; - - function _isIncreasing(o, c) { - if(o === c) { - if(c > cPrev) { - isPrevIncreasing = true; // increasing - } else if(c < cPrev) { - isPrevIncreasing = false; // decreasing - } - // else isPrevIncreasing is not changed - } - else isPrevIncreasing = (o < c); - cPrev = c; - return isPrevIncreasing; - } - - function isIncreasing(o, c) { - return isNumeric(o) && isNumeric(c) && _isIncreasing(+o, +c); - } - - function isDecreasing(o, c) { - return isNumeric(o) && isNumeric(c) && !_isIncreasing(+o, +c); - } - - return direction === 'increasing' ? isIncreasing : isDecreasing; -} - -exports.addRangeSlider = function(data, layout) { - var hasOneVisibleTrace = false; - - for(var i = 0; i < data.length; i++) { - if(data[i].visible === true) { - hasOneVisibleTrace = true; - break; - } - } - - if(hasOneVisibleTrace) { - if(!layout.xaxis) layout.xaxis = {}; - if(!layout.xaxis.rangeslider) layout.xaxis.rangeslider = {}; - } -}; diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js new file mode 100644 index 00000000000..ef4ff08d7ac --- /dev/null +++ b/src/traces/ohlc/hover.js @@ -0,0 +1,109 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Axes = require('../../plots/cartesian/axes'); +var Fx = require('../../components/fx'); +var Color = require('../../components/color'); +var fillHoverText = require('../scatter/fill_hover_text'); + +var DIRSYMBOL = { + increasing: '▲', + decreasing: '▼' +}; + +module.exports = function hoverPoints(pointData, xval, yval, hovermode) { + var cd = pointData.cd; + var xa = pointData.xa; + var ya = pointData.ya; + var trace = cd[0].trace; + var t = cd[0].t; + + var type = trace.type; + var minAttr = type === 'ohlc' ? 'l' : 'min'; + var maxAttr = type === 'ohlc' ? 'h' : 'max'; + + // potentially shift xval for grouped candlesticks + var centerShift = t.bPos || 0; + var x0 = xval - centerShift; + + // ohlc and candlestick call displayHalfWidth different things... + var displayHalfWidth = t.bdPos || t.tickLen; + var hoverHalfWidth = t.wHover; + + // if two items are overlaying, let the narrowest one win + var pseudoDistance = Math.min(1, displayHalfWidth / Math.abs(xa.r2c(xa.range[1]) - xa.r2c(xa.range[0]))); + var hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance; + var spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance; + + function dx(di) { + var pos = di.pos - x0; + return Fx.inbox(pos - hoverHalfWidth, pos + hoverHalfWidth, hoverPseudoDistance); + } + + function dy(di) { + return Fx.inbox(di[minAttr] - yval, di[maxAttr] - yval, hoverPseudoDistance); + } + + function dxy(di) { return (dx(di) + dy(di)) / 2; } + var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); + Fx.getClosest(cd, distfn, pointData); + + // skip the rest (for this trace) if we didn't find a close point + if(pointData.index === false) return []; + + // we don't make a calcdata point if we're missing any piece (x/o/h/l/c) + // so we need to fix the index here to point to the data arrays + var cdIndex = pointData.index; + var di = cd[cdIndex]; + var i = pointData.index = di.i; + + var dir = di.dir; + var container = trace[dir]; + var lc = container.line.color; + + if(Color.opacity(lc) && container.line.width) pointData.color = lc; + else pointData.color = container.fillcolor; + + pointData.x0 = xa.c2p(di.pos + centerShift - displayHalfWidth, true); + pointData.x1 = xa.c2p(di.pos + centerShift + displayHalfWidth, true); + + pointData.xLabelVal = di.pos; + + pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance; + pointData.xSpike = xa.c2p(di.pos, true); + + function getLabelLine(attr) { + return t.labels[attr] + Axes.hoverLabelText(ya, trace[attr][i]); + } + + var hoverinfo = trace.hoverinfo; + var hoverParts = hoverinfo.split('+'); + var isAll = hoverinfo === 'all'; + var hasY = isAll || hoverParts.indexOf('y') !== -1; + var hasText = isAll || hoverParts.indexOf('text') !== -1; + + var textParts = hasY ? [ + getLabelLine('open'), + getLabelLine('high'), + getLabelLine('low'), + getLabelLine('close') + ' ' + DIRSYMBOL[dir] + ] : []; + if(hasText) fillHoverText(di, trace, textParts); + + // don't make .yLabelVal or .text, since we're managing hoverinfo + // put it all in .extraText + pointData.extraText = textParts.join('
'); + + // this puts the label *and the spike* at the midpoint of the box, ie + // halfway between open and close, not between high and low. + pointData.y0 = pointData.y1 = ya.c2p(di.yc, true); + + return [pointData]; +}; diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js index cabac0d8568..8d116b066a8 100644 --- a/src/traces/ohlc/index.js +++ b/src/traces/ohlc/index.js @@ -8,8 +8,6 @@ 'use strict'; -var Registry = require('../../registry'); - module.exports = { moduleType: 'trace', name: 'ohlc', @@ -26,14 +24,16 @@ module.exports = { 'Sample points where the close value is higher (lower) then the open', 'value are called increasing (decreasing).', - 'By default, increasing candles are drawn in green whereas', + 'By default, increasing items are drawn in green whereas', 'decreasing are drawn in red.' ].join(' ') }, attributes: require('./attributes'), supplyDefaults: require('./defaults'), + calc: require('./calc').calc, + plot: require('./plot'), + style: require('./style'), + hoverPoints: require('./hover'), + selectPoints: require('./select') }; - -Registry.register(require('../scatter')); -Registry.register(require('./transform')); diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js index 261e92b7922..65c9fe14e0d 100644 --- a/src/traces/ohlc/ohlc_defaults.js +++ b/src/traces/ohlc/ohlc_defaults.js @@ -13,11 +13,11 @@ var Registry = require('../../registry'); module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { - var x = coerce('x'), - open = coerce('open'), - high = coerce('high'), - low = coerce('low'), - close = coerce('close'); + var x = coerce('x'); + var open = coerce('open'); + var high = coerce('high'); + var low = coerce('low'); + var close = coerce('close'); var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x'], layout); @@ -28,7 +28,7 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { if(x) len = Math.min(len, x.length); - traceOut._inputLength = len; + traceOut._length = len; return len; }; diff --git a/src/traces/ohlc/plot.js b/src/traces/ohlc/plot.js new file mode 100644 index 00000000000..6a54b48a347 --- /dev/null +++ b/src/traces/ohlc/plot.js @@ -0,0 +1,66 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); + +var Lib = require('../../lib'); + +module.exports = function plot(gd, plotinfo, cdOHLC) { + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + + var ohlcLayer = plotinfo.plot.select('g.ohlclayer'); + + var traces = ohlcLayer.selectAll('g.trace') + .data(cdOHLC, function(d) { return d[0].trace.uid; }); + + traces.enter().append('g') + .attr('class', 'trace ohlc'); + + traces.exit().remove(); + + traces.order(); + + traces.each(function(d) { + var cd0 = d[0]; + var t = cd0.t; + var trace = cd0.trace; + var sel = d3.select(this); + if(!plotinfo.isRangePlot) cd0.node3 = sel; + + if(trace.visible !== true || t.empty) { + sel.remove(); + return; + } + + var tickLen = t.tickLen; + + var paths = sel.selectAll('path').data(Lib.identity); + + paths.enter().append('path'); + + paths.exit().remove(); + + paths.attr('d', function(d) { + var x = xa.c2p(d.pos, true); + var xo = xa.c2p(d.pos - tickLen, true); + var xc = xa.c2p(d.pos + tickLen, true); + + var yo = ya.c2p(d.o, true); + var yh = ya.c2p(d.h, true); + var yl = ya.c2p(d.l, true); + var yc = ya.c2p(d.c, true); + + return 'M' + xo + ',' + yo + 'H' + x + + 'M' + x + ',' + yh + 'V' + yl + + 'M' + xc + ',' + yc + 'H' + x; + }); + }); +}; diff --git a/src/traces/ohlc/select.js b/src/traces/ohlc/select.js new file mode 100644 index 00000000000..29bed35028f --- /dev/null +++ b/src/traces/ohlc/select.js @@ -0,0 +1,43 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = function selectPoints(searchInfo, polygon) { + var cd = searchInfo.cd; + var xa = searchInfo.xaxis; + var ya = searchInfo.yaxis; + var selection = []; + var i; + // for (potentially grouped) candlesticks + var posOffset = cd[0].t.bPos || 0; + + if(polygon === false) { + // clear selection + for(i = 0; i < cd.length; i++) { + cd[i].selected = 0; + } + } else { + for(i = 0; i < cd.length; i++) { + var di = cd[i]; + + if(polygon.contains([xa.c2p(di.pos + posOffset), ya.c2p(di.yc)])) { + selection.push({ + pointNumber: di.i, + x: xa.c2d(di.pos), + y: ya.c2d(di.yc) + }); + di.selected = 1; + } else { + di.selected = 0; + } + } + } + + return selection; +}; diff --git a/src/traces/ohlc/style.js b/src/traces/ohlc/style.js new file mode 100644 index 00000000000..db85aea96bb --- /dev/null +++ b/src/traces/ohlc/style.js @@ -0,0 +1,35 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); +var Drawing = require('../../components/drawing'); +var Color = require('../../components/color'); + +module.exports = function style(gd, cd) { + var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.ohlclayer').selectAll('g.trace'); + + s.style('opacity', function(d) { + return d[0].trace.opacity; + }); + + s.each(function(d) { + var trace = d[0].trace; + + d3.select(this).selectAll('path').each(function(di) { + var dirLine = trace[di.dir].line; + d3.select(this) + .style('fill', 'none') + .call(Color.stroke, dirLine.color) + .call(Drawing.dashLine, dirLine.dash, dirLine.width) + // TODO: custom selection style for OHLC + .style('opacity', trace.selectedpoints && !di.selected ? 0.3 : 1); + }); + }); +}; diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js deleted file mode 100644 index af93b1d5946..00000000000 --- a/src/traces/ohlc/transform.js +++ /dev/null @@ -1,268 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var _ = Lib._; -var helpers = require('./helpers'); -var Axes = require('../../plots/cartesian/axes'); -var axisIds = require('../../plots/cartesian/axis_ids'); - -exports.moduleType = 'transform'; - -exports.name = 'ohlc'; - -exports.attributes = {}; - -exports.supplyDefaults = function(transformIn, traceOut, layout, traceIn) { - helpers.clearEphemeralTransformOpts(traceIn); - helpers.copyOHLC(transformIn, traceOut); - - return transformIn; -}; - -exports.transform = function transform(dataIn, state) { - var dataOut = []; - - for(var i = 0; i < dataIn.length; i++) { - var traceIn = dataIn[i]; - - if(traceIn.type !== 'ohlc') { - dataOut.push(traceIn); - continue; - } - - dataOut.push( - makeTrace(traceIn, state, 'increasing'), - makeTrace(traceIn, state, 'decreasing') - ); - } - - helpers.addRangeSlider(dataOut, state.layout); - - return dataOut; -}; - -function makeTrace(traceIn, state, direction) { - var len = traceIn._inputLength; - var traceOut = { - type: 'scatter', - mode: 'lines', - connectgaps: false, - - visible: traceIn.visible, - opacity: traceIn.opacity, - xaxis: traceIn.xaxis, - yaxis: traceIn.yaxis, - - hoverinfo: makeHoverInfo(traceIn), - transforms: helpers.makeTransform(traceIn, state, direction), - _inputLength: len - }; - - // the rest of below may not have been coerced - - var directionOpts = traceIn[direction]; - - if(directionOpts) { - Lib.extendFlat(traceOut, { - - // to make autotype catch date axes soon!! - x: traceIn.x || [0], - xcalendar: traceIn.xcalendar, - - // concat low and high to get correct autorange - y: traceIn.low.slice(0, len).concat(traceIn.high.slice(0, len)), - - text: traceIn.text, - - name: directionOpts.name, - showlegend: directionOpts.showlegend, - line: directionOpts.line - }); - } - - return traceOut; -} - -// Let scatter hoverPoint format 'x' coordinates, if desired. -// -// Note that, this solution isn't perfect: it shows open and close -// values at slightly different 'x' coordinates then the rest of the -// segments, but is for more robust than calling `Axes.tickText` during -// calcTransform. -// -// A future iteration should perhaps try to add a hook for transforms in -// the hoverPoints handlers. -function makeHoverInfo(traceIn) { - var hoverinfo = traceIn.hoverinfo; - - if(hoverinfo === 'all') return 'x+text+name'; - - var parts = hoverinfo.split('+'), - indexOfY = parts.indexOf('y'), - indexOfText = parts.indexOf('text'); - - if(indexOfY !== -1) { - parts.splice(indexOfY, 1); - - if(indexOfText === -1) parts.push('text'); - } - - return parts.join('+'); -} - -exports.calcTransform = function calcTransform(gd, trace, opts) { - var direction = opts.direction, - filterFn = helpers.getFilterFn(direction); - - var xa = axisIds.getFromTrace(gd, trace, 'x'), - ya = axisIds.getFromTrace(gd, trace, 'y'), - tickWidth = convertTickWidth(gd, xa, trace); - - var open = trace.open, - high = trace.high, - low = trace.low, - close = trace.close, - textIn = trace.text; - - var openName = _(gd, 'open:') + ' '; - var highName = _(gd, 'high:') + ' '; - var lowName = _(gd, 'low:') + ' '; - var closeName = _(gd, 'close:') + ' '; - - var len = trace._inputLength, - x = [], - y = [], - textOut = []; - - var appendX; - if(trace._fullInput.x) { - appendX = function(i) { - var xi = trace.x[i], - xcalendar = trace.xcalendar, - xcalc = xa.d2c(xi, 0, xcalendar); - - x.push( - xa.c2d(xcalc - tickWidth, 0, xcalendar), - xi, xi, xi, xi, - xa.c2d(xcalc + tickWidth, 0, xcalendar), - null); - }; - } - else { - appendX = function(i) { - x.push( - i - tickWidth, - i, i, i, i, - i + tickWidth, - null); - }; - } - - var appendY = function(o, h, l, c) { - y.push(o, o, h, l, c, c, null); - }; - - var format = function(ax, val) { - return Axes.tickText(ax, ax.c2l(val), 'hover').text; - }; - - var hoverinfo = trace._fullInput.hoverinfo, - hoverParts = hoverinfo.split('+'), - hasAll = hoverinfo === 'all', - hasY = hasAll || hoverParts.indexOf('y') !== -1, - hasText = hasAll || hoverParts.indexOf('text') !== -1; - - var getTextItem = Array.isArray(textIn) ? - function(i) { return textIn[i] || ''; } : - function() { return textIn; }; - - var appendText = function(i, o, h, l, c) { - var t = []; - - if(hasY) { - t.push(openName + format(ya, o)); - t.push(highName + format(ya, h)); - t.push(lowName + format(ya, l)); - t.push(closeName + format(ya, c)); - } - - if(hasText) t.push(getTextItem(i)); - - var _t = t.join('
'); - - textOut.push(_t, _t, _t, _t, _t, _t, null); - }; - - for(var i = 0; i < len; i++) { - if(filterFn(open[i], close[i]) && isNumeric(high[i]) && isNumeric(low[i])) { - appendX(i); - appendY(open[i], high[i], low[i], close[i]); - appendText(i, open[i], high[i], low[i], close[i]); - } - } - - trace.x = x; - trace.y = y; - trace.text = textOut; - trace._length = x.length; -}; - -function convertTickWidth(gd, xa, trace) { - var fullInput = trace._fullInput, - tickWidth = fullInput.tickwidth, - minDiff = fullInput._minDiff; - - if(!minDiff) { - var fullData = gd._fullData, - ohlcTracesOnThisXaxis = []; - - minDiff = Infinity; - - // find min x-coordinates difference of all traces - // attached to this x-axis and stash the result - - var i; - - for(i = 0; i < fullData.length; i++) { - var _trace = fullData[i]._fullInput; - - if(_trace.type === 'ohlc' && - _trace.visible === true && - _trace.xaxis === xa._id - ) { - ohlcTracesOnThisXaxis.push(_trace); - - // - _trace.x may be undefined here, - // it is filled later in calcTransform - // - // - handle trace of length 1 separately. - - if(_trace.x && _trace.x.length > 1) { - var xcalc = Lib.simpleMap(_trace.x, xa.d2c, 0, trace.xcalendar), - _minDiff = Lib.distinctVals(xcalc).minDiff; - minDiff = Math.min(minDiff, _minDiff); - } - } - } - - // if minDiff is still Infinity here, set it to 1 - if(minDiff === Infinity) minDiff = 1; - - for(i = 0; i < ohlcTracesOnThisXaxis.length; i++) { - ohlcTracesOnThisXaxis[i]._minDiff = minDiff; - } - } - - return minDiff * tickWidth; -} diff --git a/src/traces/parcoords/base_plot.js b/src/traces/parcoords/base_plot.js index 25fbfd48b29..f50461e77c3 100644 --- a/src/traces/parcoords/base_plot.js +++ b/src/traces/parcoords/base_plot.js @@ -18,7 +18,7 @@ var PARCOORDS = 'parcoords'; exports.name = PARCOORDS; exports.plot = function(gd) { - var calcData = getModuleCalcData(gd.calcdata, PARCOORDS); + var calcData = getModuleCalcData(gd.calcdata, PARCOORDS)[0]; if(calcData.length) parcoordsPlot(gd, calcData); }; diff --git a/src/traces/pie/base_plot.js b/src/traces/pie/base_plot.js index b6dc4549031..7bcb8e4462c 100644 --- a/src/traces/pie/base_plot.js +++ b/src/traces/pie/base_plot.js @@ -15,7 +15,7 @@ exports.name = 'pie'; exports.plot = function(gd) { var Pie = Registry.getModule('pie'); - var cdPie = getModuleCalcData(gd.calcdata, Pie); + var cdPie = getModuleCalcData(gd.calcdata, Pie)[0]; if(cdPie.length) Pie.plot(gd, cdPie); }; diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index 5c1afc1dbb0..5aaac032417 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -22,7 +22,7 @@ exports.baseLayoutAttrOverrides = overrideAll({ }, 'plot', 'nested'); exports.plot = function(gd) { - var calcData = getModuleCalcData(gd.calcdata, SANKEY); + var calcData = getModuleCalcData(gd.calcdata, SANKEY)[0]; plot(gd, calcData); }; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index db0456edf8a..c9e92ee72e0 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -21,7 +21,7 @@ var linkTraces = require('./link_traces'); var polygonTester = require('../../lib/polygon').tester; module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCompleteCallback) { - var i, uids, selection, join, onComplete; + var i, uids, join, onComplete; var scatterlayer = plotinfo.plot.select('g.scatterlayer'); @@ -30,9 +30,8 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionOpts, makeOnCo var isFullReplot = !transitionOpts; var hasTransition = !!transitionOpts && transitionOpts.duration > 0; - selection = scatterlayer.selectAll('g.trace'); - - join = selection.data(cdscatter, function(d) { return d[0].trace.uid; }); + join = scatterlayer.selectAll('g.trace') + .data(cdscatter, function(d) { return d[0].trace.uid; }); // Append new traces: join.enter().append('g') @@ -177,7 +176,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = ''; // store node for tweaking by selectPoints - cdscatter[0].node3 = tr; + if(!plotinfo.isRangePlot) cdscatter[0].node3 = tr; var prevRevpath = ''; var prevPolygons = []; diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index 73fbc59d11f..10b062e2c38 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -22,7 +22,7 @@ var SPLOM = 'splom'; function plot(gd) { var fullLayout = gd._fullLayout; var _module = Registry.getModule(SPLOM); - var splomCalcData = getModuleCalcData(gd.calcdata, _module); + var splomCalcData = getModuleCalcData(gd.calcdata, _module)[0]; prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); diff --git a/src/traces/table/base_plot.js b/src/traces/table/base_plot.js index 9004d109f5f..6e074e8cc44 100644 --- a/src/traces/table/base_plot.js +++ b/src/traces/table/base_plot.js @@ -16,7 +16,7 @@ var TABLE = 'table'; exports.name = TABLE; exports.plot = function(gd) { - var calcData = getModuleCalcData(gd.calcdata, TABLE); + var calcData = getModuleCalcData(gd.calcdata, TABLE)[0]; if(calcData.length) tablePlot(gd, calcData); }; diff --git a/src/traces/violin/index.js b/src/traces/violin/index.js index 26e39f644f6..ac0d06ac031 100644 --- a/src/traces/violin/index.js +++ b/src/traces/violin/index.js @@ -23,7 +23,7 @@ module.exports = { moduleType: 'trace', name: 'violin', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts'], + categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts', 'violinLayout'], meta: { description: [ 'In vertical (horizontal) violin plots,', diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js index a0c670c3467..4b89227cad6 100644 --- a/src/traces/violin/plot.js +++ b/src/traces/violin/plot.js @@ -42,13 +42,18 @@ module.exports = function plot(gd, plotinfo, cd) { var cd0 = d[0]; var t = cd0.t; var trace = cd0.trace; - var sel = cd0.node3 = d3.select(this); + var sel = d3.select(this); + if(!plotinfo.isRangePlot) cd0.node3 = sel; var numViolins = fullLayout._numViolins; var group = (fullLayout.violinmode === 'group' && numViolins > 1); + var groupFraction = 1 - fullLayout.violingap; // violin max half width - var bdPos = t.bdPos = t.dPos * (1 - fullLayout.violingap) * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1); + var bdPos = t.bdPos = t.dPos * groupFraction * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1); // violin center offset - var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * (1 - fullLayout.violingap) : 0; + var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * groupFraction : 0; + // half-width within which to accept hover for this violin + // always split the distance to the closest violin + t.wHover = t.dPos * (group ? groupFraction / numViolins : 1); if(trace.visible !== true || t.empty) { d3.select(this).remove(); diff --git a/test/image/baselines/candlestick_double-y-axis.png b/test/image/baselines/candlestick_double-y-axis.png index f423ad6fd0f..7db85e0a0c8 100644 Binary files a/test/image/baselines/candlestick_double-y-axis.png and b/test/image/baselines/candlestick_double-y-axis.png differ diff --git a/test/image/baselines/candlestick_rangeslider_thai.png b/test/image/baselines/candlestick_rangeslider_thai.png index 76894bfb9de..901883613cd 100644 Binary files a/test/image/baselines/candlestick_rangeslider_thai.png and b/test/image/baselines/candlestick_rangeslider_thai.png differ diff --git a/test/image/baselines/finance_style.png b/test/image/baselines/finance_style.png index ac72e76f013..a969a040c7e 100644 Binary files a/test/image/baselines/finance_style.png and b/test/image/baselines/finance_style.png differ diff --git a/test/image/baselines/finance_subplots_categories.png b/test/image/baselines/finance_subplots_categories.png new file mode 100644 index 00000000000..737202f3241 Binary files /dev/null and b/test/image/baselines/finance_subplots_categories.png differ diff --git a/test/image/baselines/ohlc_first.png b/test/image/baselines/ohlc_first.png index ddac6bf2b50..8615422ad20 100644 Binary files a/test/image/baselines/ohlc_first.png and b/test/image/baselines/ohlc_first.png differ diff --git a/test/image/mocks/finance_style.json b/test/image/mocks/finance_style.json index 7adc4f2bdd6..b0f274298a3 100644 --- a/test/image/mocks/finance_style.json +++ b/test/image/mocks/finance_style.json @@ -121,7 +121,7 @@ "decreasing": { "line": { "color": "rgb(128, 128, 128)", - "dash": "dash" + "dash": "dot" } }, "line": { @@ -268,7 +268,7 @@ "calendar": "islamic", "title": "Islamic dates" }, - "showlegend": false, + "showlegend": true, "height": 450, "width": 1100, "autosize": true diff --git a/test/image/mocks/finance_subplots_categories.json b/test/image/mocks/finance_subplots_categories.json new file mode 100644 index 00000000000..5a12d9facb2 --- /dev/null +++ b/test/image/mocks/finance_subplots_categories.json @@ -0,0 +1,73 @@ +{ + "data": [{ + "type": "ohlc", + "x": [ + "2018-04-17", "2018-04-18", "2018-04-19", "2018-04-20", + "2018-04-23", "2018-04-24", "2018-04-25", "2018-04-26", "2018-04-27" + ], + "open": [10, 11, 12, 13, 12, 13, 14, 15, 16], + "high": [15, 16, 17, 18, 17, 18, 19, 20, 21], + "low": [7, 8, 9, 10, 9, 10, 11, 12, 13], + "close": [9, 10, 12, 13, 13, 12, 14, 14, 17], + "name": "Date OHLC" + }, { + "type": "candlestick", + "x": [ + "2018-04-17", "2018-04-18", "2018-04-19", "2018-04-20", + "2018-04-23", "2018-04-24", "2018-04-25", "2018-04-26", "2018-04-27" + ], + "open": [20, 21, 22, 23, 22, 23, 24, 25, 26], + "high": [25, 26, 27, 28, 27, 28, 29, 30, 31], + "low": [17, 18, 19, 20, 19, 20, 21, 22, 23], + "close": [19, 20, 22, 23, 23, 22, 24, 24, 27], + "name": "Date Candlestick" + }, { + "type": "ohlc", + "xaxis": "x2", + "x": [ + "2018-04-17", "2018-04-18", "2018-04-19", "2018-04-20", + "2018-04-23", "2018-04-24", "2018-04-25", "2018-04-26", "2018-04-27" + ], + "open": [10, 11, 12, 13, 12, 13, 14, 15, 16], + "high": [15, 16, 17, 18, 17, 18, 19, 20, 21], + "low": [7, 8, 9, 10, 9, 10, 11, 12, 13], + "close": [9, 10, 12, 13, 13, 12, 14, 14, 17], + "increasing": {"line": {"color": "orange"}}, + "decreasing": {"line": {"color": "blue"}}, + "name": "Category OHLC" + }, { + "type": "candlestick", + "xaxis": "x2", + "x": [ + "2018-04-17", "2018-04-18", "2018-04-19", "2018-04-20", + "2018-04-23", "2018-04-24", "2018-04-25", "2018-04-26", "2018-04-27" + ], + "open": [20, 21, 22, 23, 22, 23, 24, 25, 26], + "high": [25, 26, 27, 28, 27, 28, 29, 30, 31], + "low": [17, 18, 19, 20, 19, 20, 21, 22, 23], + "close": [19, 20, 22, 23, 23, 22, 24, 24, 27], + "increasing": {"line": {"color": "orange"}}, + "decreasing": {"line": {"color": "blue"}}, + "name": "Category Candlestick" + }, { + "type": "box", + "x": [ + "2018-04-23", "2018-04-23", "2018-04-23", "2018-04-23", "2018-04-23", + "2018-04-23", "2018-04-23", "2018-04-23", "2018-04-23", "2018-04-23", + "2018-04-24", "2018-04-24", "2018-04-24", "2018-04-24", "2018-04-24", + "2018-04-24", "2018-04-24", "2018-04-24", "2018-04-24", "2018-04-24" + ], + "y": [ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 + ], + "name": "Box" + }], + "layout": { + "xaxis": {"domain": [0, 0.45], "title": "Date"}, + "xaxis2": {"domain": [0.55, 1], "title": "Category", "type": "category"}, + "width": 800, + "height": 500, + "boxgroupgap": 0 + } +} diff --git a/test/jasmine/bundle_tests/finance_test.js b/test/jasmine/bundle_tests/finance_test.js index b56e10e14b6..33f26511b92 100644 --- a/test/jasmine/bundle_tests/finance_test.js +++ b/test/jasmine/bundle_tests/finance_test.js @@ -13,16 +13,18 @@ describe('Bundle with finance trace type', function() { var mock = require('@mocks/finance_style.json'); - it('should register the correct trace modules for the generated traces', function() { + it('should not register transforms anymore', function() { var transformModules = Object.keys(Plotly.Plots.transformsRegistry); - expect(transformModules).toEqual(['ohlc', 'candlestick']); + expect(transformModules).toEqual([]); }); it('should register the correct trace modules for the generated traces', function() { var traceModules = Object.keys(Plotly.Plots.modules); - expect(traceModules).toEqual(['scatter', 'box', 'ohlc', 'candlestick']); + // scatter is registered no matter what + // ohlc uses some parts of box by direct require but does not need to register it. + expect(traceModules).toEqual(['scatter', 'ohlc', 'candlestick']); }); it('should graph ohlc and candlestick traces', function(done) { @@ -30,8 +32,8 @@ describe('Bundle with finance trace type', function() { Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { var gSubplot = d3.select('g.cartesianlayer'); - expect(gSubplot.selectAll('g.trace.scatter').size()).toEqual(2); - expect(gSubplot.selectAll('g.trace.boxes').size()).toEqual(2); + expect(gSubplot.selectAll('g.trace.ohlc').size()).toEqual(1); + expect(gSubplot.selectAll('g.trace.boxes').size()).toEqual(1); destroyGraphDiv(); done(); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index dd95ea474b6..22c99aac4bc 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -183,7 +183,8 @@ describe('Test axes', function() { _has: Plots._hasPlotType, _basePlotModules: [], _dfltTitle: {x: 'x', y: 'y'}, - _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _requestRangeslider: {} }; fullData = []; }); diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index a50e578ad1c..d2d76b5ac58 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -172,7 +172,7 @@ describe('Test box hover:', function() { return Plotly.plot(gd, fig).then(function() { mouseEvent('mousemove', pos[0], pos[1]); - assertHoverLabelContent(specs); + assertHoverLabelContent(specs, specs.desc); }); } diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index 770fe352dba..7d0f4c85a76 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -6,6 +6,7 @@ var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var supplyAllDefaults = require('../assets/supply_defaults'); +var failTest = require('../assets/fail_test'); var mock0 = { open: [33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50], @@ -47,13 +48,8 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); - - var directions = out._fullData.map(function(fullTrace) { - return fullTrace.transforms[0].direction; - }); - - expect(directions).toEqual(['increasing', 'decreasing', 'increasing', 'decreasing']); + // not sure this test is really necessary anymore, since these are real traces... + expect(out._fullData.length).toEqual(2); }); it('should not mutate user data', function() { @@ -98,7 +94,7 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); + expect(out._fullData.length).toEqual(2); var transformTypesIn = out.data.map(function(trace) { return trace.transforms.map(function(opts) { @@ -114,21 +110,20 @@ describe('finance charts defaults:', function() { }); }); - // dummy 'ohlc' and 'candlestick' transforms are pushed at the end - // of the 'transforms' array container - - expect(transformTypesOut).toEqual([ - ['filter', 'ohlc'], ['filter', 'ohlc'], - ['filter', 'candlestick'], ['filter', 'candlestick'] - ]); + expect(transformTypesOut).toEqual([ ['filter'], ['filter'] ]); }); - it('should slice data array according to minimum supplied length', function() { + it('should not slice data arrays but record minimum supplied length', function() { - function assertDataLength(fullTrace, len) { + function assertDataLength(trace, fullTrace, len) { expect(fullTrace.visible).toBe(true); - expect(fullTrace._inputLength).toBe(len); + expect(fullTrace._length).toBe(len); + + expect(fullTrace.open).toBe(trace.open); + expect(fullTrace.close).toBe(trace.close); + expect(fullTrace.high).toBe(trace.high); + expect(fullTrace.low).toBe(trace.low); } var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); @@ -139,15 +134,11 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); - assertDataLength(out._fullData[0], 5); - assertDataLength(out._fullData[1], 5); - assertDataLength(out._fullData[2], 4); - assertDataLength(out._fullData[3], 4); + assertDataLength(trace0, out._fullData[0], 5); + assertDataLength(trace1, out._fullData[1], 4); expect(out._fullData[0]._fullInput.x).toBeUndefined(); - expect(out._fullData[1]._fullInput.x).toBeUndefined(); - expect(out._fullData[2]._fullInput.x).toBeDefined(); - expect(out._fullData[3]._fullInput.x).toBeDefined(); + expect(out._fullData[1]._fullInput.x).toBeDefined(); }); it('should set visible to *false* when minimum supplied length is 0', function() { @@ -160,13 +151,13 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); expect(out.data.length).toEqual(2); - expect(out._fullData.length).toEqual(4); + expect(out._fullData.length).toEqual(2); var visibilities = out._fullData.map(function(fullTrace) { return fullTrace.visible; }); - expect(visibilities).toEqual([false, false, false, false]); + expect(visibilities).toEqual([false, false]); }); it('direction *showlegend* should be inherited from trace-wide *showlegend*', function() { @@ -188,10 +179,10 @@ describe('finance charts defaults:', function() { return fullTrace.showlegend; }); - expect(visibilities).toEqual([false, false, false, false]); + expect(visibilities).toEqual([false, false]); }); - it('direction *name* should be inherited from trace-wide *name*', function() { + it('direction *name* should be ignored if there\'s a trace-wide *name*', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', name: 'Company A' @@ -211,10 +202,8 @@ describe('finance charts defaults:', function() { }); expect(names).toEqual([ - 'Company A - increasing', - 'Company A - decreasing', - 'B - UP', - 'B - DOWN' + 'Company A', + 'Company B' ]); }); @@ -238,11 +227,9 @@ describe('finance charts defaults:', function() { }); expect(names).toEqual([ - 'trace 0 - increasing', - 'trace 0 - decreasing', + 'trace 0', 'trace 1', - 'trace 2 - increasing', - 'trace 2 - decreasing', + 'trace 2', 'trace 3' ]); }); @@ -267,22 +254,14 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); - var fullData = out._fullData; - var fullInput = fullData.map(function(fullTrace) { return fullTrace._fullInput; }); - - assertLine(fullInput[0].increasing, 1, 'dash'); - assertLine(fullInput[0].decreasing, 1, 'dot'); - assertLine(fullInput[2].increasing, 0); - assertLine(fullInput[2].decreasing, 3); - - assertLine(fullData[0], 1, 'dash'); - assertLine(fullData[1], 1, 'dot'); - assertLine(fullData[2], 0); - assertLine(fullData[3], 3); + assertLine(fullData[0].increasing, 1, 'dash'); + assertLine(fullData[0].decreasing, 1, 'dot'); + assertLine(fullData[1].increasing, 0); + assertLine(fullData[1].decreasing, 3); }); - it('trace-wide *visible* should be passed to generated traces', function() { + it('trace-wide *visible* should work', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', visible: 'legendonly' @@ -301,7 +280,7 @@ describe('finance charts defaults:', function() { // only three items here as visible: false traces are not transformed - expect(visibilities).toEqual(['legendonly', 'legendonly', false]); + expect(visibilities).toEqual(['legendonly', false]); }); it('should add a few layout settings by default', function() { @@ -362,7 +341,7 @@ describe('finance charts defaults:', function() { out._fullData.forEach(function(fullTrace, i) { - expect(fullTrace.xcalendar).toBe(i < 2 ? 'hebrew' : 'julian'); + expect(fullTrace.xcalendar).toBe(i < 1 ? 'hebrew' : 'julian'); }); }); @@ -374,14 +353,14 @@ describe('finance charts defaults:', function() { }); }); -describe('finance charts calc transforms:', function() { +describe('finance charts calc', function() { 'use strict'; function calcDatatoTrace(calcTrace) { return calcTrace[0].trace; } - function _calcRaw(data, layout) { + function _calcGd(data, layout) { var gd = { data: data, layout: layout || {} @@ -389,7 +368,19 @@ describe('finance charts calc transforms:', function() { supplyAllDefaults(gd); Plots.doCalcdata(gd); - return gd.calcdata; + gd.calcdata.forEach(function(cd) { + // fill in some stuff that happens during setPositions or plot + if(cd[0].trace.type === 'candlestick') { + var diff = cd[1].pos - cd[0].pos; + cd[0].t.wHover = diff / 2; + cd[0].t.bdPos = diff / 4; + } + }); + return gd; + } + + function _calcRaw(data, layout) { + return _calcGd(data, layout).calcdata; } function _calc(data, layout) { @@ -408,6 +399,10 @@ describe('finance charts calc transforms:', function() { trace.close.push(1, 1, 1, 'close'); } + function mapGet(array, attr) { + return array.map(function(di) { return di[attr]; }); + } + it('should fill when *x* is not present', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', @@ -419,84 +414,16 @@ describe('finance charts calc transforms:', function() { }); addJunk(trace1); - var out = _calc([trace0, trace1]); - - expect(out[0].x).toEqual([ - -0.3, 0, 0, 0, 0, 0.3, null, - 2.7, 3, 3, 3, 3, 3.3, null, - 4.7, 5, 5, 5, 5, 5.3, null, - 6.7, 7, 7, 7, 7, 7.3, null - ]); - expect(out[1].x).toEqual([ - 0.7, 1, 1, 1, 1, 1.3, null, - 1.7, 2, 2, 2, 2, 2.3, null, - 3.7, 4, 4, 4, 4, 4.3, null, - 5.7, 6, 6, 6, 6, 6.3, null - ]); - expect(out[2].x).toEqual([ - 0, 0, 0, 0, 0, 0, - 3, 3, 3, 3, 3, 3, - 5, 5, 5, 5, 5, 5, - 7, 7, 7, 7, 7, 7 - ]); - expect(out[3].x).toEqual([ - 1, 1, 1, 1, 1, 1, - 2, 2, 2, 2, 2, 2, - 4, 4, 4, 4, 4, 4, - 6, 6, 6, 6, 6, 6 - ]); - }); - - it('should fill *text* for OHLC hover labels', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - text: ['A', 'B', 'C', 'D'] - }); - - var trace1 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - text: 'IMPORTANT', - hoverinfo: 'x+text', - xaxis: 'x2' - }); - - var trace2 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - hoverinfo: 'y', - xaxis: 'x2' - }); - - var trace3 = Lib.extendDeep({}, mock0, { - type: 'ohlc', - hoverinfo: 'x', - }); - - var out = _calc([trace0, trace1, trace2, trace3]); - - expect(out[0].hoverinfo).toEqual('x+text+name'); - expect(out[0].text[0]) - .toEqual('open: 33.01
high: 34.2
low: 31.7
close: 34.1
A'); - expect(out[0].hoverinfo).toEqual('x+text+name'); - expect(out[1].text[0]) - .toEqual('open: 33.31
high: 34.37
low: 30.75
close: 31.93
B'); + var out = _calcRaw([trace0, trace1]); + var indices = [0, 1, 2, 3, 4, 5, 6, 7]; + var i = 'increasing'; + var d = 'decreasing'; + var directions = [i, d, d, i, d, i, d, i]; - expect(out[2].hoverinfo).toEqual('x+text'); - expect(out[2].text[0]).toEqual('IMPORTANT'); - - expect(out[3].hoverinfo).toEqual('x+text'); - expect(out[3].text[0]).toEqual('IMPORTANT'); - - expect(out[4].hoverinfo).toEqual('text'); - expect(out[4].text[0]) - .toEqual('open: 33.01
high: 34.2
low: 31.7
close: 34.1'); - expect(out[5].hoverinfo).toEqual('text'); - expect(out[5].text[0]) - .toEqual('open: 33.31
high: 34.37
low: 30.75
close: 31.93'); - - expect(out[6].hoverinfo).toEqual('x'); - expect(out[6].text[0]).toEqual(''); - expect(out[7].hoverinfo).toEqual('x'); - expect(out[7].text[0]).toEqual(''); + expect(mapGet(out[0], 'pos')).toEqual(indices); + expect(mapGet(out[0], 'dir')).toEqual(directions); + expect(mapGet(out[1], 'pos')).toEqual(indices); + expect(mapGet(out[1], 'dir')).toEqual(directions); }); it('should work with *filter* transforms', function() { @@ -523,42 +450,21 @@ describe('finance charts calc transforms:', function() { var out = _calc([trace0, trace1]); - expect(out.length).toEqual(4); + expect(out.length).toEqual(2); expect(out[0].x).toEqual([ - '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null, - '2016-09-05 22:48', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06 01:12', null, - '2016-09-09 22:48', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10 01:12', null + '2016-09-01', '2016-09-02', '2016-09-03', '2016-09-05', '2016-09-06', '2016-09-07', '2016-09-10' ]); - expect(out[0].y).toEqual([ - 33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null, - 33.05, 33.05, 33.25, 32.75, 33.1, 33.1, null, - 33.5, 33.5, 34.62, 32.87, 33.7, 33.7, null - ]); - expect(out[1].x).toEqual([ - '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, - '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null, - '2016-09-04 22:48', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05 01:12', null, - '2016-09-06 22:48', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07 01:12', null - ]); - expect(out[1].y).toEqual([ - 33.31, 33.31, 34.37, 30.75, 31.93, 31.93, null, - 33.5, 33.5, 33.62, 32.87, 33.37, 33.37, null, - 34.12, 34.12, 35.18, 30.81, 31.18, 31.18, null, - 33.31, 33.31, 35.37, 32.75, 32.93, 32.93, null + expect(out[0].open).toEqual([ + 33.01, 33.31, 33.50, 34.12, 33.05, 33.31, 33.50 ]); - expect(out[2].x).toEqual([ - '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', - '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10' + expect(out[1].x).toEqual([ + '2016-09-01', '2016-09-10' ]); - expect(out[2].y).toEqual([ - 31.7, 33.01, 34.1, 34.1, 34.1, 34.2, - 32.87, 33.5, 33.7, 33.7, 33.7, 34.62 + expect(out[1].close).toEqual([ + 34.10, 33.70 ]); - - expect(out[3].x).toEqual([]); - expect(out[3].y).toEqual([]); }); it('should work with *groupby* transforms (ohlc)', function() { @@ -575,35 +481,23 @@ describe('finance charts calc transforms:', function() { var out = _calc([trace0]); - expect(out[0].name).toEqual('trace 0 - increasing'); + expect(out.length).toBe(2); + + expect(out[0].name).toBe('b'); expect(out[0].x).toEqual([ - '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null + '2016-09-01', '2016-09-02', '2016-09-03' ]); - expect(out[0].y).toEqual([ - 33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null, + expect(out[0].open).toEqual([ + 33.01, 33.31, 33.5 ]); - expect(out[1].name).toEqual('trace 0 - decreasing'); + expect(out[1].name).toBe('a'); expect(out[1].x).toEqual([ - '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, - '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null + '2016-09-04' ]); - expect(out[1].y).toEqual([ - 33.31, 33.31, 34.37, 30.75, 31.93, 31.93, null, - 33.5, 33.5, 33.62, 32.87, 33.37, 33.37, null + expect(out[1].open).toEqual([ + 32.06 ]); - - expect(out[2].name).toEqual('trace 0 - increasing'); - expect(out[2].x).toEqual([ - '2016-09-03 22:48', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04 01:12', null - ]); - expect(out[2].y).toEqual([ - 32.06, 32.06, 34.25, 31.62, 33.18, 33.18, null - ]); - - expect(out[3].name).toEqual('trace 0 - decreasing'); - expect(out[3].x).toEqual([]); - expect(out[3].y).toEqual([]); }); it('should work with *groupby* transforms (candlestick)', function() { @@ -619,109 +513,78 @@ describe('finance charts calc transforms:', function() { var out = _calc([trace0]); - expect(out[0].name).toEqual('trace 0 - increasing'); + expect(out[0].name).toEqual('a'); expect(out[0].x).toEqual([ - '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', - '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04' + '2016-09-01', '2016-09-04' ]); - expect(out[0].y).toEqual([ - 31.7, 33.01, 34.1, 34.1, 34.1, 34.2, - 31.62, 32.06, 33.18, 33.18, 33.18, 34.25 + expect(out[0].open).toEqual([ + 33.01, 32.06 ]); - expect(out[1].name).toEqual('trace 0 - decreasing'); - expect(out[1].x).toEqual([]); - expect(out[1].y).toEqual([]); - - expect(out[2].name).toEqual('trace 0 - increasing'); - expect(out[2].x).toEqual([]); - expect(out[2].y).toEqual([]); - - expect(out[3].name).toEqual('trace 0 - decreasing'); - expect(out[3].x).toEqual([ - '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', - '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03' + expect(out[1].name).toEqual('b'); + expect(out[1].x).toEqual([ + '2016-09-02', '2016-09-03' ]); - expect(out[3].y).toEqual([ - 30.75, 33.31, 31.93, 31.93, 31.93, 34.37, - 32.87, 33.5, 33.37, 33.37, 33.37, 33.62 + expect(out[1].open).toEqual([ + 33.31, 33.5 ]); }); it('should use the smallest trace minimum x difference to convert *tickwidth* to data coords for all traces attached to a given x-axis', function() { var trace0 = Lib.extendDeep({}, mock1, { - type: 'ohlc', - tickwidth: 0.5 + type: 'ohlc' }); var trace1 = Lib.extendDeep({}, mock1, { type: 'ohlc', - tickwidth: 0.5 + // shift time coordinates by 10 hours + x: mock1.x.map(function(d) { return d + ' 10:00'; }) }); - // shift time coordinates by 10 hours - trace1.x = trace1.x.map(function(d) { - return d + ' 10:00'; - }); - - var out = _calc([trace0, trace1]); + var out = _calcRaw([trace0, trace1]); - expect(out[0].x).toEqual([ - '2016-08-31 12:00', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 12:00', null, - '2016-09-03 12:00', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04 12:00', null, - '2016-09-05 12:00', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06 12:00', null, - '2016-09-09 12:00', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10 12:00', null - ]); + var oneDay = 1000 * 3600 * 24; + expect(out[0][0].t.tickLen).toBeCloseTo(oneDay * 0.3, 0); + expect(out[0][0].t.wHover).toBeCloseTo(oneDay * 0.5, 0); + expect(out[1][0].t.tickLen).toBe(out[0][0].t.tickLen); + expect(out[1][0].t.wHover).toBe(out[0][0].t.wHover); + }); - expect(out[1].x).toEqual([ - '2016-09-01 12:00', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 12:00', null, - '2016-09-02 12:00', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 12:00', null, - '2016-09-04 12:00', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05 12:00', null, - '2016-09-06 12:00', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07', '2016-09-07 12:00', null - ]); + it('works with category x data', function() { + // see https://github.com/plotly/plotly.js/issues/2004 + // fixed automatically as part of the refactor to a non-transform trace + var trace0 = Lib.extendDeep({}, mock0, { + type: 'ohlc', + x: ['a', 'b', 'c', 'd', 'e'] + }); - expect(out[2].x).toEqual([ - '2016-08-31 22:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 10:00', '2016-09-01 22:00', null, - '2016-09-03 22:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 10:00', '2016-09-04 22:00', null, - '2016-09-05 22:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 10:00', '2016-09-06 22:00', null, - '2016-09-09 22:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 10:00', '2016-09-10 22:00', null - ]); + var out = _calcRaw([trace0]); - expect(out[3].x).toEqual([ - '2016-09-01 22:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 10:00', '2016-09-02 22:00', null, - '2016-09-02 22:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 10:00', '2016-09-03 22:00', null, - '2016-09-04 22:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 10:00', '2016-09-05 22:00', null, - '2016-09-06 22:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 10:00', '2016-09-07 22:00', null - ]); + expect(out[0][0].t.tickLen).toBeCloseTo(0.3, 5); + expect(out[0][0].t.wHover).toBeCloseTo(0.5, 5); }); - it('should fallback to a minimum x difference of 0.5 in one-item traces', function() { - var trace0 = Lib.extendDeep({}, mock1, { + it('should fallback to a spacing of 1 in one-item traces', function() { + var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', - tickwidth: 0.5 + x: ['2016-01-01'] }); - trace0.x = [ '2016-01-01' ]; var trace1 = Lib.extendDeep({}, mock0, { type: 'ohlc', - tickwidth: 0.5 + x: [10], + xaxis: 'x2' }); - trace1.x = [ 10 ]; - - var out = _calc([trace0, trace1]); - - var x0 = Lib.simpleMap(out[0].x, Lib.dateTime2ms); - expect(x0[x0.length - 2] - x0[0]).toEqual(1); - - var x2 = Lib.simpleMap(out[2].x, Lib.dateTime2ms); - expect(x2[x2.length - 2] - x2[0]).toEqual(1); - expect(out[1].x).toEqual([]); - expect(out[3].x).toEqual([]); + var out = _calcRaw([trace0, trace1]); + expect(out[0][0].t.tickLen).toBeCloseTo(0.3, 5); + expect(out[0][0].t.wHover).toBeCloseTo(0.5, 5); + expect(out[1][0].t.tickLen).toBeCloseTo(0.3, 5); + expect(out[1][0].t.wHover).toBeCloseTo(0.5, 5); }); it('should handle cases where \'open\' and \'close\' entries are equal', function() { - var out = _calc([{ + var out = _calcRaw([{ type: 'ohlc', open: [0, 1, 0, 2, 1, 1, 2, 2], high: [3, 3, 3, 3, 3, 3, 3, 3], @@ -736,36 +599,30 @@ describe('finance charts calc transforms:', function() { close: [0, 1, 0, 2] }]); - expect(out[0].x).toEqual([ - 0, 0, 0, 0, 0, 0, null, - 1, 1, 1, 1, 1, 1, null, - 6, 6, 6, 6, 6, 6, null, - 7, 7, 7, 7, 7, 7, null - ]); - expect(out[1].x).toEqual([ - 2, 2, 2, 2, 2, 2, null, - 3, 3, 3, 3, 3, 3, null, - 4, 4, 4, 4, 4, 4, null, - 5, 5, 5, 5, 5, 5, null + expect(mapGet(out[0], 'dir')).toEqual([ + 'increasing', 'increasing', 'decreasing', 'decreasing', + 'decreasing', 'decreasing', 'increasing', 'increasing' ]); - expect(out[2].x).toEqual([ - 0, 0, 0, 0, 0, 0, - 3, 3, 3, 3, 3, 3 - ]); - expect(out[3].x).toEqual([ - 1, 1, 1, 1, 1, 1, - 2, 2, 2, 2, 2, 2 + expect(mapGet(out[1], 'dir')).toEqual([ + 'increasing', 'decreasing', 'decreasing', 'increasing' ]); }); - it('should not include box hover labels prefix in candlestick calcdata', function() { - var trace0 = Lib.extendDeep({}, mock0, { - type: 'candlestick', - }); - var out = _calcRaw([trace0]); + it('should include finance hover labels prefix in calcdata', function() { + ['candlestick', 'ohlc'].forEach(function(type) { + var trace0 = Lib.extendDeep({}, mock0, { + type: type, + }); + var out = _calcRaw([trace0]); - expect(out[0][0].t.labels).toBeUndefined(); + expect(out[0][0].t.labels).toEqual({ + open: 'open: ', + high: 'high: ', + low: 'low: ', + close: 'close: ' + }); + }); }); }); @@ -783,8 +640,8 @@ describe('finance charts updates:', function() { destroyGraphDiv(); }); - function countScatterTraces() { - return d3.select('g.cartesianlayer').selectAll('g.trace.scatter').size(); + function countOHLCTraces() { + return d3.select('g.cartesianlayer').selectAll('g.trace.ohlc').size(); } function countBoxTraces() { @@ -801,18 +658,18 @@ describe('finance charts updates:', function() { var path0; Plotly.plot(gd, [trace0]).then(function() { - expect(gd.calcdata[0][0].x).toEqual(-0.3); - expect(gd.calcdata[0][0].y).toEqual(33.01); + expect(gd.calcdata[0][0].t.tickLen).toBeCloseTo(0.3, 5); + expect(gd.calcdata[0][0].o).toEqual(33.01); return Plotly.restyle(gd, 'tickwidth', 0.5); }) .then(function() { - expect(gd.calcdata[0][0].x).toEqual(-0.5); + expect(gd.calcdata[0][0].t.tickLen).toBeCloseTo(0.5, 5); return Plotly.restyle(gd, 'open', [[0, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87]]); }) .then(function() { - expect(gd.calcdata[0][0].y).toEqual(0); + expect(gd.calcdata[0][0].o).toEqual(0); return Plotly.restyle(gd, { type: 'candlestick', @@ -821,15 +678,15 @@ describe('finance charts updates:', function() { }) .then(function() { path0 = d3.select('path.box').attr('d'); + expect(path0).toBeDefined(); return Plotly.restyle(gd, 'whiskerwidth', 0.2); }) .then(function() { expect(d3.select('path.box').attr('d')).not.toEqual(path0); - - done(); - }); - + }) + .catch(failTest) + .then(done); }); it('should be able to toggle visibility', function(done) { @@ -839,47 +696,47 @@ describe('finance charts updates:', function() { ]; Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(countScatterTraces()).toEqual(0); + expect(countOHLCTraces()).toEqual(0); expect(countBoxTraces()).toEqual(0); return Plotly.restyle(gd, 'visible', 'legendonly', [1]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); + expect(countOHLCTraces()).toEqual(0); expect(countBoxTraces()).toEqual(0); return Plotly.restyle(gd, 'visible', true, [1]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); return Plotly.restyle(gd, 'visible', true, [0]); }) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); return Plotly.restyle(gd, 'visible', 'legendonly', [0]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); return Plotly.restyle(gd, 'visible', true); }) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); - - done(); - }); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); + }) + .catch(failTest) + .then(done); }); it('Plotly.relayout should work', function(done) { @@ -892,9 +749,9 @@ describe('finance charts updates:', function() { }) .then(function() { expect(countRangeSliders()).toEqual(0); - - done(); - }); + }) + .catch(failTest) + .then(done); }); @@ -904,13 +761,9 @@ describe('finance charts updates:', function() { Lib.extendDeep({}, mock0, { type: 'candlestick' }), ]; - // ohlc have 7 calc pts per 'x' coords - Plotly.plot(gd, data).then(function() { - expect(gd.calcdata[0].length).toEqual(28); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(4); - expect(gd.calcdata[3].length).toEqual(4); + expect(gd.calcdata[0].length).toEqual(8); + expect(gd.calcdata[1].length).toEqual(8); return Plotly.extendTraces(gd, { open: [[ 34, 35 ]], @@ -920,10 +773,8 @@ describe('finance charts updates:', function() { }, [1]); }) .then(function() { - expect(gd.calcdata[0].length).toEqual(28); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(6); - expect(gd.calcdata[3].length).toEqual(4); + expect(gd.calcdata[0].length).toEqual(8); + expect(gd.calcdata[1].length).toEqual(10); return Plotly.extendTraces(gd, { open: [[ 34, 35 ]], @@ -933,13 +784,11 @@ describe('finance charts updates:', function() { }, [0]); }) .then(function() { - expect(gd.calcdata[0].length).toEqual(42); - expect(gd.calcdata[1].length).toEqual(28); - expect(gd.calcdata[2].length).toEqual(6); - expect(gd.calcdata[3].length).toEqual(4); - - done(); - }); + expect(gd.calcdata[0].length).toEqual(10); + expect(gd.calcdata[1].length).toEqual(10); + }) + .catch(failTest) + .then(done); }); it('Plotly.deleteTraces / addTraces should work', function(done) { @@ -949,19 +798,19 @@ describe('finance charts updates:', function() { ]; Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); return Plotly.deleteTraces(gd, [1]); }) .then(function() { - expect(countScatterTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(1); expect(countBoxTraces()).toEqual(0); return Plotly.deleteTraces(gd, [0]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); + expect(countOHLCTraces()).toEqual(0); expect(countBoxTraces()).toEqual(0); var trace = Lib.extendDeep({}, mock0, { type: 'candlestick' }); @@ -969,68 +818,66 @@ describe('finance charts updates:', function() { return Plotly.addTraces(gd, [trace]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); var trace = Lib.extendDeep({}, mock0, { type: 'ohlc' }); return Plotly.addTraces(gd, [trace]); }) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); - - done(); - }); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); + }) + .catch(failTest) + .then(done); }); it('Plotly.addTraces + Plotly.relayout should update candlestick box position values', function(done) { - function assertBoxPosFields(dPos) { - expect(gd.calcdata.length).toEqual(dPos.length); + function assertBoxPosFields(bPos) { + expect(gd.calcdata.length).toEqual(bPos.length); gd.calcdata.forEach(function(calcTrace, i) { - if(dPos[i] === undefined) { - expect(calcTrace[0].t.dPos).toBeUndefined(); - } - else { - expect(calcTrace[0].t.dPos).toEqual(dPos[i]); - } + expect(calcTrace[0].t.bPos).toBeCloseTo(bPos[i], 0); }); } var trace0 = { type: 'candlestick', - x: ['2011-01-01'], - open: [0], - high: [3], - low: [1], - close: [3] + x: ['2011-01-01', '2011-01-02'], + open: [1, 2], + high: [3, 4], + low: [0, 1], + close: [2, 3] }; - Plotly.plot(gd, [trace0]).then(function() { - assertBoxPosFields([0.5, undefined]); + Plotly.plot(gd, [trace0], {boxmode: 'group'}) + .then(function() { + assertBoxPosFields([0]); - return Plotly.addTraces(gd, {}); + return Plotly.addTraces(gd, [Lib.extendDeep({}, trace0)]); }) .then(function() { + assertBoxPosFields([-15120000, 15120000]); + var update = { type: 'candlestick', - x: [['2011-02-02']], - open: [[0]], - high: [[3]], - low: [[1]], - close: [[3]] + x: [['2011-01-01', '2011-01-05'], ['2011-01-01', '2011-01-03']], + open: [[1, 0]], + high: [[3, 2]], + low: [[0, -1]], + close: [[2, 1]] }; return Plotly.restyle(gd, update); }) .then(function() { - assertBoxPosFields([0.5, undefined, 0.5, undefined]); - - done(); - }); + assertBoxPosFields([-30240000, 30240000]); + }) + .catch(failTest) + .then(done); }); it('Plotly.plot with data-less trace and adding with Plotly.restyle', function(done) { @@ -1041,7 +888,7 @@ describe('finance charts updates:', function() { ]; Plotly.plot(gd, data).then(function() { - expect(countScatterTraces()).toEqual(0); + expect(countOHLCTraces()).toEqual(0); expect(countBoxTraces()).toEqual(0); expect(countRangeSliders()).toEqual(0); @@ -1053,8 +900,8 @@ describe('finance charts updates:', function() { }, [0]); }) .then(function() { - expect(countScatterTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); expect(countRangeSliders()).toEqual(1); return Plotly.restyle(gd, { @@ -1065,16 +912,18 @@ describe('finance charts updates:', function() { }, [1]); }) .then(function() { - expect(countScatterTraces()).toEqual(2); - expect(countBoxTraces()).toEqual(2); + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); expect(countRangeSliders()).toEqual(1); }) + .catch(failTest) .then(done); }); }); describe('finance charts *special* handlers:', function() { + // not special anymore - just test that they work as normal afterEach(destroyGraphDiv); @@ -1112,7 +961,7 @@ describe('finance charts *special* handlers:', function() { .then(function(gd) { return new Promise(function(resolve) { gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['increasing.name']).toEqual('0'); + expect(eventData[0].name).toEqual('0'); expect(eventData[1]).toEqual([0]); delayedResolve(resolve); }); @@ -1123,36 +972,15 @@ describe('finance charts *special* handlers:', function() { .then(function(gd) { return new Promise(function(resolve) { gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['decreasing.name']).toEqual('1'); - expect(eventData[1]).toEqual([0]); - delayedResolve(resolve); - }); - - editText(1, '1'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['decreasing.name']).toEqual('2'); - expect(eventData[1]).toEqual([1]); - delayedResolve(resolve); - }); - - editText(3, '2'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0]['increasing.name']).toEqual('3'); + expect(eventData[0].name).toEqual('1'); expect(eventData[1]).toEqual([1]); delayedResolve(resolve); }); - editText(2, '3'); + editText(1, '1'); }); }) + .catch(failTest) .then(done); }); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 1ad54ea3b6c..87907aea7bd 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -12,7 +12,7 @@ var mouseEvent = require('../assets/mouse_event'); var click = require('../assets/click'); var delay = require('../assets/delay'); var doubleClick = require('../assets/double_click'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; @@ -512,7 +512,7 @@ describe('hover info', function() { name: 'one' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -548,7 +548,7 @@ describe('hover info', function() { name: 'one' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -587,7 +587,7 @@ describe('hover info', function() { }); }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -623,7 +623,7 @@ describe('hover info', function() { name: 'one' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -693,7 +693,7 @@ describe('hover info', function() { assertHoverLabelStyle(d3.select(this), styles[i]); }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -731,7 +731,7 @@ describe('hover info', function() { name: 'one' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -769,7 +769,7 @@ describe('hover info', function() { name: 'one' }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -791,7 +791,7 @@ describe('hover info', function() { axis: '2' }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -833,7 +833,7 @@ describe('hover info', function() { expect(pt.xaxis).toBe(gd._fullLayout.xaxis); expect(pt.yaxis).toBe(gd._fullLayout.yaxis); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -858,7 +858,7 @@ describe('hover info', function() { axis: '3.3' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -886,7 +886,7 @@ describe('hover info', function() { axis: 'soup - nuts' }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -912,7 +912,7 @@ describe('hover info', function() { nums: 'x: 3 - 5\ny: 4 - 6\nz: 3' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -936,11 +936,114 @@ describe('hover info', function() { nums: 'x: 3.3\ny: 4.2\nz: 3' }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); + ['candlestick', 'ohlc'].forEach(function(type) { + describe(type + ' hoverinfo', function() { + var gd; + + function financeMock(traceUpdates, layoutUpdates) { + return { + data: [Lib.extendFlat({}, { + type: type, + x: ['2011-01-01', '2011-01-02', '2011-01-03'], + open: [1, 2, 3], + high: [3, 4, 5], + low: [0, 1, 2], + close: [0, 3, 2] + }, traceUpdates || {})], + layout: Lib.extendDeep({}, { + width: 400, + height: 400, + margin: {l: 50, r: 50, t: 50, b: 50} + }, layoutUpdates || {}) + }; + } + + beforeEach(function() { + gd = createGraphDiv(); + }); + + it('has the right basic and event behavior', function(done) { + var pts; + Plotly.plot(gd, financeMock({ + customdata: [11, 22, 33] + })) + .then(function() { + gd.on('plotly_hover', function(e) { pts = e.points; }); + + _hoverNatural(gd, 150, 150); + assertHoverLabelContent({ + nums: 'open: 2\nhigh: 4\nlow: 1\nclose: 3 ▲', + axis: 'Jan 2, 2011' + }); + }) + .then(function() { + expect(pts).toBeDefined(); + expect(pts.length).toBe(1); + expect(pts[0]).toEqual(jasmine.objectContaining({ + x: '2011-01-02', + open: 2, + high: 4, + low: 1, + close: 3, + customdata: 22, + curveNumber: 0, + pointNumber: 1, + data: gd.data[0], + fullData: gd._fullData[0], + xaxis: gd._fullLayout.xaxis, + yaxis: gd._fullLayout.yaxis + })); + + return Plotly.relayout(gd, {hovermode: 'closest'}); + }) + .then(function() { + _hoverNatural(gd, 150, 150); + assertHoverLabelContent({ + nums: 'Jan 2, 2011\nopen: 2\nhigh: 4\nlow: 1\nclose: 3 ▲' + }); + }) + .catch(failTest) + .then(done); + }); + + it('shows text iff text is in hoverinfo', function(done) { + Plotly.plot(gd, financeMock({text: ['A', 'B', 'C']})) + .then(function() { + _hover(gd, 150, 150); + assertHoverLabelContent({ + nums: 'open: 2\nhigh: 4\nlow: 1\nclose: 3 ▲\nB', + axis: 'Jan 2, 2011' + }); + + return Plotly.restyle(gd, {hoverinfo: 'x+text'}); + }) + .then(function() { + _hover(gd, 150, 150); + assertHoverLabelContent({ + nums: 'B', + axis: 'Jan 2, 2011' + }); + + return Plotly.restyle(gd, {hoverinfo: 'x+y'}); + }) + .then(function() { + _hover(gd, 150, 150); + assertHoverLabelContent({ + nums: 'open: 2\nhigh: 4\nlow: 1\nclose: 3 ▲', + axis: 'Jan 2, 2011' + }); + }) + .catch(failTest) + .then(done); + }); + }); + }); + describe('hoverformat', function() { var data = [{ x: [1, 2, 3], @@ -1043,7 +1146,7 @@ describe('hover info', function() { .then(function() { expect(hoverHandler).not.toHaveBeenCalled(); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1070,7 +1173,7 @@ describe('hover info', function() { .then(function() { expect(hoverHandler).toHaveBeenCalledTimes(1); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1107,7 +1210,7 @@ describe('hover info', function() { expect(labelCount()).toBe(7); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1410,7 +1513,7 @@ describe('hover on many lines+bars', function() { expect(d3.select(gd).selectAll('g.hovertext').size()).toBe(2); expect(d3.select(gd).selectAll('g.axistext').size()).toBe(1); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1559,7 +1662,7 @@ describe('hover on fill', function() { // gives same results w/o closing point assertLabelsCorrect([200, 200], [73.75, 250], 'trace 0'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1596,7 +1699,7 @@ describe('hover on fill', function() { // then make sure we can still select a *different* item afterward assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1622,7 +1725,7 @@ describe('hover on fill', function() { // hover on the cartesian trace in the corner assertLabelsCorrect([363, 122], [363, 122], 'trace 38'); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1696,7 +1799,9 @@ describe('hover updates', function() { }).then(function() { // Assert label restored: assertLabelsCorrect(null, [103, 100], 'trace 10.5', 'animation/update 3'); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); it('should not trigger infinite loop of plotly_unhover events', function(done) { @@ -1951,7 +2056,7 @@ describe('Test hover label custom styling:', function() { text: [13, 'Arial', 'rgb(255, 255, 255)'] }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -2007,36 +2112,11 @@ describe('Test hover label custom styling:', function() { text: [11, 'Gravitas', 'rgb(255, 0, 0)'] }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); -describe('ohlc hover interactions', function() { - var data = [{ - type: 'candlestick', - x: ['2011-01-01', '2012-01-01'], - open: [2, 2], - high: [3, 3], - low: [0, 0], - close: [3, 3], - }]; - - beforeEach(function() { - this.gd = createGraphDiv(); - }); - - afterEach(destroyGraphDiv); - - // See: https://github.com/plotly/plotly.js/issues/1807 - it('should not fail in appendArrayPointValue', function() { - Plotly.plot(this.gd, data); - mouseEvent('mousemove', 203, 213); - - expect(d3.select('.hovertext').size()).toBe(1); - }); -}); - describe('hover distance', function() { 'use strict'; diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index c5177d04480..3302fe9a137 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -2319,6 +2319,90 @@ describe('Test plot api', function() { expect(gd.layout.shapes[2].yref).toEqual('y'); }); + + it('removes direction names and showlegend from finance traces', function() { + var data = [{ + type: 'ohlc', open: [1], high: [3], low: [0], close: [2], + increasing: { + showlegend: true, + name: 'Yeti goes up' + }, + decreasing: { + showlegend: 'legendonly', + name: 'Yeti goes down' + }, + name: 'Snowman' + }, { + type: 'candlestick', open: [1], high: [3], low: [0], close: [2], + increasing: { + name: 'Bigfoot' + }, + decreasing: { + showlegend: false, + name: 'Biggerfoot' + }, + name: 'Nobody' + }, { + type: 'ohlc', open: [1], high: [3], low: [0], close: [2], + increasing: { + name: 'Batman' + }, + decreasing: { + showlegend: true + }, + name: 'Robin' + }, { + type: 'candlestick', open: [1], high: [3], low: [0], close: [2], + increasing: { + showlegend: false, + }, + decreasing: { + name: 'Fred' + } + }, { + type: 'ohlc', open: [1], high: [3], low: [0], close: [2], + increasing: { + showlegend: false, + name: 'Gruyere heating up' + }, + decreasing: { + showlegend: false, + name: 'Gruyere cooling off' + }, + name: 'Emmenthaler' + }]; + + Plotly.plot(gd, data); + + // Even if both showlegends are false, leave trace.showlegend out + // My rationale for this is that legends are sufficiently different + // now that it's worthwhile resetting their existence to default + gd.data.forEach(function(trace) { + expect(trace.increasing.name).toBeUndefined(); + expect(trace.increasing.showlegend).toBeUndefined(); + expect(trace.decreasing.name).toBeUndefined(); + expect(trace.decreasing.showlegend).toBeUndefined(); + }); + + // Both directions have names: ignore trace.name, as it + // had no effect on the output previously + // Ideally 'Yeti goes' would be smart enough to truncate + // at 'Yeti' but I don't see how to do that... + expect(gd.data[0].name).toBe('Yeti goes'); + // One direction has empty or hidden name so use the other + // Note that even '' in both names would render trace.name impact-less + expect(gd.data[1].name).toBe('Bigfoot'); + + // One direction has a name but trace.name is there too: + // just use trace.name + expect(gd.data[2].name).toBe('Robin'); + + // No trace.name, only one direction name: use the direction name + expect(gd.data[3].name).toBe('Fred'); + + // both names exist but hidden from the legend: still look for common prefix + expect(gd.data[4].name).toBe('Gruyere'); + }); }); describe('Plotly.newPlot', function() { @@ -2824,6 +2908,53 @@ describe('Test plot api', function() { .then(done); }); + it('can change data in candlesticks multiple times', function(done) { + // test that we've fixed the original issue in + // https://github.com/plotly/plotly.js/issues/2510 + + function assertCalc(open, high, low, close) { + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({ + min: low, + max: high, + med: close, + q1: Math.min(open, close), + q3: Math.max(open, close), + dir: close >= open ? 'increasing' : 'decreasing' + })); + } + var trace = { + type: 'candlestick', + low: [1], + open: [2], + close: [3], + high: [4] + }; + Plotly.newPlot(gd, [trace]) + .then(function() { + assertCalc(2, 4, 1, 3); + + trace.low = [0]; + return Plotly.react(gd, [trace]); + }) + .then(function() { + assertCalc(2, 4, 0, 3); + + trace.low = [-1]; + return Plotly.react(gd, [trace]); + }) + .then(function() { + assertCalc(2, 4, -1, 3); + + trace.close = [1]; + return Plotly.react(gd, [trace]); + }) + .then(function() { + assertCalc(2, 4, -1, 1); + }) + .catch(failTest) + .then(done); + }); + it('can change frames without redrawing', function(done) { var data = [{y: [1, 2, 3]}]; var layout = {}; @@ -2954,10 +3085,7 @@ describe('Test plot api', function() { return Plotly.react(gd, mock); }) .then(function() { - // TODO: remove this exemption once we fix finance - if(mockSpec[0] !== 'finance_style') { - expect(fullJson()).toEqual(initialJson); - } + expect(fullJson()).toEqual(initialJson); countCalls({}); }) .catch(failTest) diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js index ac5304d72a7..32ca0d61b6b 100644 --- a/test/jasmine/tests/polar_test.js +++ b/test/jasmine/tests/polar_test.js @@ -798,7 +798,7 @@ describe('Test polar interactions:', function() { .then(done); }); - it('should response to drag interactions on plot area', function(done) { + it('@flaky should respond to drag interactions on plot area', function(done) { var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json')); // to avoid dragging on hover labels diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 1b4fc6cd83d..abbad0f08f1 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); var setConvert = require('@src/plots/cartesian/set_convert'); +var name2id = require('@src/plots/cartesian/axis_ids').name2id; var RangeSlider = require('@src/components/rangeslider'); var constants = require('@src/components/rangeslider/constants'); @@ -509,7 +510,11 @@ describe('Rangeslider handleDefaults function', function() { function _supply(layoutIn, layoutOut, axName) { setConvert(layoutOut[axName]); + layoutOut[axName]._id = name2id(axName); + if(!layoutOut._requestRangeslider) layoutOut._requestRangeslider = {}; RangeSlider.handleDefaults(layoutIn, layoutOut, axName); + // we don't care about this after it's done its job + delete layoutOut._requestRangeslider; } it('should not coerce anything if rangeslider isn\'t set', function() { @@ -519,6 +524,7 @@ describe('Rangeslider handleDefaults function', function() { _supply(layoutIn, layoutOut, 'xaxis'); expect(layoutIn).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toBeUndefined(); }); it('should not mutate layoutIn', function() { @@ -547,6 +553,27 @@ describe('Rangeslider handleDefaults function', function() { expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); + it('should set defaults if rangeslider is requested', function() { + var layoutIn = { xaxis: {}}, + layoutOut = { xaxis: {}, _requestRangeslider: {x: true} }, + expected = { + visible: true, + autorange: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: {} + }; + + _supply(layoutIn, layoutOut, 'xaxis'); + // in fact we DO mutate layoutIn - which we should probably try not to do, + // but that's a problem for another time. + // see https://github.com/plotly/plotly.js/issues/1473 + expect(layoutIn).toEqual({xaxis: {rangeslider: {}}}); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); + }); + it('should set defaults if rangeslider.visible is true', function() { var layoutIn = { xaxis: { rangeslider: { visible: true }} }, layoutOut = { xaxis: { rangeslider: {}} }, diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index aef6ca104b3..992a54b5f3a 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -6,7 +6,7 @@ var doubleClick = require('../assets/double_click'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var touchEvent = require('../assets/touch_event'); @@ -190,7 +190,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -282,7 +282,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -336,7 +336,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -355,7 +355,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(gd.data[0].selectedpoints).toBeUndefined(); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -374,7 +374,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(gd._fullData[0].selectedpoints).toBeUndefined(); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -408,7 +408,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -488,7 +488,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { checkPointCount(0, '(multiple invisible traces lasso)'); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -527,7 +527,7 @@ describe('@flaky Test select box and lasso in general:', function() { expect(selectedData.points[0].x).toBe(0); expect(selectedData.points[0].y).toBe(0); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -549,7 +549,7 @@ describe('@flaky Test select box and lasso in general:', function() { .then(function() { assertSelectionNodes(0, 0); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -730,7 +730,7 @@ describe('@flaky Test select box and lasso per trace:', function() { LASSOEVENTS, 'scatterternary lasso after relayout' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -766,7 +766,7 @@ describe('@flaky Test select box and lasso per trace:', function() { null, LASSOEVENTS, 'scattercarpet lasso' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -823,7 +823,7 @@ describe('@flaky Test select box and lasso per trace:', function() { [[370, 120], [500, 200]], null, null, NOEVENTS, 'scattermapbox pan' ); }) - .catch(fail) + .catch(failTest) .then(done); }, LONG_TIMEOUT_INTERVAL); @@ -889,7 +889,7 @@ describe('@flaky Test select box and lasso per trace:', function() { [[370, 120], [500, 200]], null, null, NOEVENTS, 'scattergeo pan' ); }) - .catch(fail) + .catch(failTest) .then(done); }, LONG_TIMEOUT_INTERVAL); @@ -927,7 +927,7 @@ describe('@flaky Test select box and lasso per trace:', function() { LASSOEVENTS, 'scatterpolar lasso' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -990,7 +990,7 @@ describe('@flaky Test select box and lasso per trace:', function() { [[370, 120], [500, 200]], null, [280, 190], NOEVENTS, 'choropleth pan' ); }) - .catch(fail) + .catch(failTest) .then(done); }, LONG_TIMEOUT_INTERVAL); @@ -1063,7 +1063,7 @@ describe('@flaky Test select box and lasso per trace:', function() { null, BOXEVENTS, 'bar select' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1123,7 +1123,7 @@ describe('@flaky Test select box and lasso per trace:', function() { null, BOXEVENTS, 'date/category select' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1172,7 +1172,7 @@ describe('@flaky Test select box and lasso per trace:', function() { null, BOXEVENTS, 'histogram select' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1236,7 +1236,7 @@ describe('@flaky Test select box and lasso per trace:', function() { null, BOXEVENTS, 'box select' ); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1299,10 +1299,87 @@ describe('@flaky Test select box and lasso per trace:', function() { null, BOXEVENTS, 'violin select' ); }) - .catch(fail) + .catch(failTest) .then(done); }); + ['ohlc', 'candlestick'].forEach(function(type) { + it('should work for ' + type + ' traces', function(done) { + var assertPoints = makeAssertPoints(['curveNumber', 'x', 'open', 'high', 'low', 'close']); + var assertSelectedPoints = makeAssertSelectedPoints(); + var assertRanges = makeAssertRanges(); + var assertLassoPoints = makeAssertLassoPoints(); + var l0 = 275; + var lv0 = '2011-01-03 18:00'; + var r0 = 325; + var rv0 = '2011-01-04 06:00'; + var l1 = 75; + var lv1 = '2011-01-01 18:00'; + var r1 = 125; + var rv1 = '2011-01-02 06:00'; + var t = 75; + var tv = 7.565; + var b = 225; + var bv = -1.048; + + function countUnSelectedPaths(selector) { + var unselected = 0; + d3.select(gd).selectAll(selector).each(function() { + var opacity = this.style.opacity; + if(opacity < 1) unselected++; + }); + return unselected; + } + + Plotly.newPlot(gd, [{ + type: type, + x: ['2011-01-02', '2011-01-03', '2011-01-04'], + open: [1, 2, 3], + high: [3, 4, 5], + low: [0, 1, 2], + close: [0, 3, 2] + }], { + width: 400, + height: 400, + margin: {l: 50, r: 50, t: 50, b: 50}, + yaxis: {range: [-3, 9]}, + dragmode: 'lasso' + }) + .then(function() { + return _run( + [[l0, t], [l0, b], [r0, b], [r0, t], [l0, t]], + function() { + assertPoints([[0, '2011-01-04', 3, 5, 2, 2]]); + assertSelectedPoints([[2]]); + assertLassoPoints([ + [lv0, lv0, rv0, rv0, lv0], + [tv, bv, bv, tv, tv] + ]); + expect(countUnSelectedPaths('.cartesianlayer .trace path')).toBe(2); + expect(countUnSelectedPaths('.rangeslider-rangeplot .trace path')).toBe(0); + }, + null, LASSOEVENTS, type + ' lasso' + ); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'select'); + }) + .then(function() { + return _run( + [[l1, t], [r1, b]], + function() { + assertPoints([[0, '2011-01-02', 1, 3, 0, 0]]); + assertSelectedPoints([[0]]); + assertRanges([[lv1, rv1], [bv, tv]]); + }, + null, BOXEVENTS, type + ' select' + ); + }) + .catch(failTest) + .then(done); + }); + }); + it('should work on traces with enabled transforms', function(done) { var assertSelectedPoints = makeAssertSelectedPoints(); @@ -1342,7 +1419,7 @@ describe('@flaky Test select box and lasso per trace:', function() { BOXEVENTS, 'transformed trace select (all points selected)' ); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -1399,7 +1476,7 @@ describe('Test that selections persist:', function() { style: [0.2, 1, 0.2] }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1445,7 +1522,7 @@ describe('Test that selections persist:', function() { style: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 1, 1, 1], }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -1487,7 +1564,7 @@ describe('Test that selections persist:', function() { style: [0.2, 1, 0.2, 0.2, 0.2], }); }) - .catch(fail) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index d322679b8a4..9ad0c331c87 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -17,7 +17,8 @@ var mockFullLayout = { _modules: [], _basePlotModules: [], _has: function() {}, - _dfltTitle: {x: 'xxx', y: 'yyy'} + _dfltTitle: {x: 'xxx', y: 'yyy'}, + _requestRangeslider: {} };