diff --git a/draftlogs/7619_add.md b/draftlogs/7619_add.md new file mode 100644 index 00000000000..f97c8f34fa1 --- /dev/null +++ b/draftlogs/7619_add.md @@ -0,0 +1 @@ +- Add `hovertemplate` for `candlestick` and `ohlc` traces [[#7619](https://github.com/plotly/plotly.js/pull/7619)] diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 1b4a2b46d27..4cd374b40c8 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1662,7 +1662,11 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { text = name; } - // hovertemplate + // Ignore hovertemplate if hoverlabel.split is set + // This ensures correct behavior of hoverlabel.split for candlestick and OHLC traces + // Not very elegant but it works + if (d.trace?.hoverlabel?.split) d.hovertemplate = ''; + const { hovertemplate = false } = d; if (hovertemplate) { const labels = d.hovertemplateLabels || d; diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index 4cfde82c30c..b56302e8b5f 100644 --- a/src/traces/candlestick/attributes.js +++ b/src/traces/candlestick/attributes.js @@ -8,7 +8,7 @@ var boxAttrs = require('../box/attributes'); function directionAttrs(lineColorDefault) { return { line: { - color: extendFlat({}, boxAttrs.line.color, {dflt: lineColorDefault}), + color: extendFlat({}, boxAttrs.line.color, { dflt: lineColorDefault }), width: boxAttrs.line.width, editType: 'style' }, @@ -49,6 +49,8 @@ module.exports = { text: OHLCattrs.text, hovertext: OHLCattrs.hovertext, + hovertemplate: OHLCattrs.hovertemplate, + hovertemplatefallback: OHLCattrs.hovertemplatefallback, whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }), diff --git a/src/traces/candlestick/defaults.js b/src/traces/candlestick/defaults.js index e8e44b4ed5b..779b883091d 100644 --- a/src/traces/candlestick/defaults.js +++ b/src/traces/candlestick/defaults.js @@ -12,12 +12,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var len = handleOHLC(traceIn, traceOut, coerce, layout); - if(!len) { + if (!len) { traceOut.visible = false; return; } - handlePeriodDefaults(traceIn, traceOut, layout, coerce, {x: true}); + handlePeriodDefaults(traceIn, traceOut, layout, coerce, { x: true }); coerce('xhoverformat'); coerce('yhoverformat'); @@ -28,6 +28,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('hovertext'); + coerce('hovertemplate'); + coerce('hovertemplatefallback'); + coerce('whiskerwidth'); layout._requestRangeslider[traceOut.xaxis] = true; diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index e5305791d4f..df0f3e100ee 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -3,6 +3,7 @@ var extendFlat = require('../../lib').extendFlat; var scatterAttrs = require('../scatter/attributes'); var axisHoverFormat = require('../../plots/cartesian/axis_format_attributes').axisHoverFormat; +const { hovertemplateAttrs, templatefallbackAttrs } = require('../../plots/template_attributes'); var dash = require('../../components/drawing/attributes').dash; var fxAttrs = require('../../components/fx/attributes'); var delta = require('../../constants/delta.js'); @@ -15,7 +16,7 @@ var lineAttrs = scatterAttrs.line; function directionAttrs(lineColorDefault) { return { line: { - color: extendFlat({}, lineAttrs.color, {dflt: lineColorDefault}), + color: extendFlat({}, lineAttrs.color, { dflt: lineColorDefault }), width: lineAttrs.width, dash: dash, editType: 'style' @@ -25,7 +26,6 @@ function directionAttrs(lineColorDefault) { } module.exports = { - xperiod: scatterAttrs.xperiod, xperiod0: scatterAttrs.xperiod0, xperiodalignment: scatterAttrs.xperiodalignment, @@ -35,10 +35,7 @@ module.exports = { x: { valType: 'data_array', editType: 'calc+clearAxisTypes', - description: [ - 'Sets the x coordinates.', - 'If absent, linear coordinate will be generated.' - ].join(' ') + description: 'Sets the x coordinates. If absent, linear coordinate will be generated.' }, open: { @@ -99,7 +96,7 @@ module.exports = { 'If a single string, the same string appears over', 'all the data points.', 'If an array of string, the items are mapped in order to', - 'this trace\'s sample points.' + "this trace's sample points." ].join(' ') }, hovertext: { @@ -109,17 +106,20 @@ module.exports = { editType: 'calc', description: 'Same as `text`.' }, - + hovertemplate: hovertemplateAttrs( + {}, + { + keys: ['open', 'high', 'low', 'close'] + } + ), + hovertemplatefallback: templatefallbackAttrs(), tickwidth: { valType: 'number', min: 0, max: 0.5, dflt: 0.3, editType: 'calc', - description: [ - 'Sets the width of the open/close tick marks', - 'relative to the *x* minimal interval.' - ].join(' ') + description: 'Sets the width of the open/close tick marks relative to the *x* minimal interval.' }, hoverlabel: extendFlat({}, fxAttrs.hoverlabel, { @@ -128,8 +128,8 @@ module.exports = { dflt: false, editType: 'style', description: [ - 'Show hover information (open, close, high, low) in', - 'separate labels.' + 'Show hover information (open, close, high, low) in separate labels, rather than a single unified label.', + 'Default: *false*. When set to *true*, `hovertemplate` is ignored.' ].join(' ') } }), diff --git a/src/traces/ohlc/defaults.js b/src/traces/ohlc/defaults.js index 2557ecea8ee..0b7afe9f2eb 100644 --- a/src/traces/ohlc/defaults.js +++ b/src/traces/ohlc/defaults.js @@ -11,12 +11,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var len = handleOHLC(traceIn, traceOut, coerce, layout); - if(!len) { + if (!len) { traceOut.visible = false; return; } - handlePeriodDefaults(traceIn, traceOut, layout, coerce, {x: true}); + handlePeriodDefaults(traceIn, traceOut, layout, coerce, { x: true }); coerce('xhoverformat'); coerce('yhoverformat'); @@ -28,6 +28,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('hovertext'); + coerce('hovertemplate'); + coerce('hovertemplatefallback'); + coerce('tickwidth'); layout._requestRangeslider[traceOut.xaxis] = true; diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js index 5800303db40..e82aef39f9d 100644 --- a/src/traces/ohlc/hover.js +++ b/src/traces/ohlc/hover.js @@ -99,16 +99,11 @@ function hoverSplit(pointData, xval, yval, hovermode) { // skip the rest (for this trace) if we didn't find a close point if(!closestPoint) return []; - var cdIndex = closestPoint.index; - var di = cd[cdIndex]; - var hoverinfo = di.hi || trace.hoverinfo; - var hoverParts = hoverinfo.split('+'); - var isAll = hoverinfo === 'all'; - var hasY = isAll || hoverParts.indexOf('y') !== -1; + var di = cd[closestPoint.index]; + var hoverinfo = di.hi || trace.hoverinfo || ''; - // similar to hoverOnPoints, we return nothing - // if all or y is not present. - if(!hasY) return []; + // If hoverinfo is 'none' or 'skip', we don't show any hover labels + if (hoverinfo === 'none' || hoverinfo === 'skip') return []; var attrs = ['high', 'open', 'close', 'low']; @@ -165,7 +160,7 @@ function hoverOnPoints(pointData, xval, yval, hovermode) { return t.labels[attr] + Axes.hoverLabelText(ya, trace[attr][i], trace.yhoverformat); } - var hoverinfo = di.hi || trace.hoverinfo; + var hoverinfo = di.hi || trace.hoverinfo || ''; var hoverParts = hoverinfo.split('+'); var isAll = hoverinfo === 'all'; var hasY = isAll || hoverParts.indexOf('y') !== -1; diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index bf0bc8ef3fd..ac158cf4c96 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -10,22 +10,18 @@ var supplyAllDefaults = require('../assets/supply_defaults'); var hover = require('../assets/hover'); var assertHoverLabelContent = require('../assets/custom_assertions').assertHoverLabelContent; - var mock0 = { - open: [33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50], - high: [34.20, 34.37, 33.62, 34.25, 35.18, 33.25, 35.37, 34.62], - low: [31.70, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87], - close: [34.10, 31.93, 33.37, 33.18, 31.18, 33.10, 32.93, 33.70] + open: [33.01, 33.31, 33.5, 32.06, 34.12, 33.05, 33.31, 33.5], + high: [34.2, 34.37, 33.62, 34.25, 35.18, 33.25, 35.37, 34.62], + low: [31.7, 30.75, 32.87, 31.62, 30.81, 32.75, 32.75, 32.87], + close: [34.1, 31.93, 33.37, 33.18, 31.18, 33.1, 32.93, 33.7] }; var mock1 = Lib.extendDeep({}, mock0, { - x: [ - '2016-09-01', '2016-09-02', '2016-09-03', '2016-09-04', - '2016-09-05', '2016-09-06', '2016-09-07', '2016-09-10' - ] + x: ['2016-09-01', '2016-09-02', '2016-09-03', '2016-09-04', '2016-09-05', '2016-09-06', '2016-09-07', '2016-09-10'] }); -describe('finance charts defaults:', function() { +describe('finance charts defaults:', function () { 'use strict'; function _supply(data, layout) { @@ -39,7 +35,7 @@ describe('finance charts defaults:', function() { return gd; } - it('should generated the correct number of full traces', function() { + it('should generated the correct number of full traces', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); @@ -55,7 +51,7 @@ describe('finance charts defaults:', function() { expect(out._fullData.length).toEqual(2); }); - it('should not slice data arrays but record minimum supplied length', function() { + it('should not slice data arrays but record minimum supplied length', function () { function assertDataLength(trace, fullTrace, len) { expect(fullTrace.visible).toBe(true); @@ -68,7 +64,7 @@ describe('finance charts defaults:', function() { } var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - trace0.open = [33.01, 33.31, 33.50, 32.06, 34.12]; + trace0.open = [33.01, 33.31, 33.5, 32.06, 34.12]; var trace1 = Lib.extendDeep({}, mock1, { type: 'candlestick' }); trace1.x = ['2016-09-01', '2016-09-02', '2016-09-03', '2016-09-04']; @@ -82,7 +78,7 @@ describe('finance charts defaults:', function() { expect(out._fullData[1]._fullInput.x).toBeDefined(); }); - it('should set visible to *false* when a component (other than x) is missing', function() { + it('should set visible to *false* when a component (other than x) is missing', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); trace0.close = undefined; @@ -94,17 +90,17 @@ describe('finance charts defaults:', function() { expect(out.data.length).toEqual(2); expect(out._fullData.length).toEqual(2); - var visibilities = out._fullData.map(function(fullTrace) { + var visibilities = out._fullData.map(function (fullTrace) { return fullTrace.visible; }); expect(visibilities).toEqual([false, false]); }); - it('should return visible: false if any data component is empty', function() { - ['ohlc', 'candlestick'].forEach(function(type) { - ['open', 'high', 'low', 'close', 'x'].forEach(function(attr) { - var trace = Lib.extendDeep({}, mock1, {type: type}); + it('should return visible: false if any data component is empty', function () { + ['ohlc', 'candlestick'].forEach(function (type) { + ['open', 'high', 'low', 'close', 'x'].forEach(function (attr) { + var trace = Lib.extendDeep({}, mock1, { type: type }); trace[attr] = []; var out = _supply([trace]); expect(out._fullData[0].visible).toBe(false, type + ' - ' + attr); @@ -112,10 +108,10 @@ describe('finance charts defaults:', function() { }); }); - it('direction *showlegend* should be inherited from trace-wide *showlegend*', function() { + it('direction *showlegend* should be inherited from trace-wide *showlegend*', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', - showlegend: false, + showlegend: false }); var trace1 = Lib.extendDeep({}, mock1, { @@ -127,14 +123,14 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); - var visibilities = out._fullData.map(function(fullTrace) { + var visibilities = out._fullData.map(function (fullTrace) { return fullTrace.showlegend; }); expect(visibilities).toEqual([false, false]); }); - it('direction *name* should be ignored if there\'s a 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' @@ -149,17 +145,14 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); - var names = out._fullData.map(function(fullTrace) { + var names = out._fullData.map(function (fullTrace) { return fullTrace.name; }); - expect(names).toEqual([ - 'Company A', - 'Company B' - ]); + expect(names).toEqual(['Company A', 'Company B']); }); - it('trace *name* default should make reference to user data trace indices', function() { + it('trace *name* default should make reference to user data trace indices', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); @@ -167,29 +160,24 @@ describe('finance charts defaults:', function() { var trace1 = { type: 'scatter' }; var trace2 = Lib.extendDeep({}, mock1, { - type: 'candlestick', + type: 'candlestick' }); var trace3 = { type: 'bar' }; var out = _supply([trace0, trace1, trace2, trace3]); - var names = out._fullData.map(function(fullTrace) { + var names = out._fullData.map(function (fullTrace) { return fullTrace.name; }); - expect(names).toEqual([ - 'trace 0', - 'trace 1', - 'trace 2', - 'trace 3' - ]); + expect(names).toEqual(['trace 0', 'trace 1', 'trace 2', 'trace 3']); }); - it('trace-wide styling should set default for corresponding per-direction styling', function() { + it('trace-wide styling should set default for corresponding per-direction styling', function () { function assertLine(cont, width, dash) { expect(cont.line.width).toEqual(width); - if(dash) expect(cont.line.dash).toEqual(dash); + if (dash) expect(cont.line.dash).toEqual(dash); } var trace0 = Lib.extendDeep({}, mock0, { @@ -213,7 +201,7 @@ describe('finance charts defaults:', function() { assertLine(fullData[1].decreasing, 3); }); - it('trace-wide *visible* should work', function() { + it('trace-wide *visible* should work', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', visible: 'legendonly' @@ -226,7 +214,7 @@ describe('finance charts defaults:', function() { var out = _supply([trace0, trace1]); - var visibilities = out._fullData.map(function(fullTrace) { + var visibilities = out._fullData.map(function (fullTrace) { return fullTrace.visible; }); @@ -235,7 +223,7 @@ describe('finance charts defaults:', function() { expect(visibilities).toEqual(['legendonly', false]); }); - it('should add a few layout settings by default', function() { + it('should add a few layout settings by default', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); @@ -252,7 +240,7 @@ describe('finance charts defaults:', function() { }); var layout1 = { - xaxis: { rangeslider: { visible: false }} + xaxis: { rangeslider: { visible: false } } }; var out1 = _supply([trace1], layout1); @@ -261,7 +249,7 @@ describe('finance charts defaults:', function() { expect(out1._fullLayout.xaxis.rangeslider.visible).toBe(false); }); - it('pushes layout.calendar to all output traces', function() { + it('pushes layout.calendar to all output traces', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); @@ -270,15 +258,14 @@ describe('finance charts defaults:', function() { type: 'candlestick' }); - var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); + var out = _supply([trace0, trace1], { calendar: 'nanakshahi' }); - - out._fullData.forEach(function(fullTrace) { + out._fullData.forEach(function (fullTrace) { expect(fullTrace.xcalendar).toBe('nanakshahi'); }); }); - it('accepts a calendar per input trace', function() { + it('accepts a calendar per input trace', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', xcalendar: 'hebrew' @@ -289,15 +276,14 @@ describe('finance charts defaults:', function() { xcalendar: 'julian' }); - var out = _supply([trace0, trace1], {calendar: 'nanakshahi'}); - + var out = _supply([trace0, trace1], { calendar: 'nanakshahi' }); - out._fullData.forEach(function(fullTrace, i) { + out._fullData.forEach(function (fullTrace, i) { expect(fullTrace.xcalendar).toBe(i < 1 ? 'hebrew' : 'julian'); }); }); - it('should make empty candlestick traces autotype to *linear* (as opposed to real box traces)', function() { + it('should make empty candlestick traces autotype to *linear* (as opposed to real box traces)', function () { var trace0 = { type: 'candlestick' }; var out = _supply([trace0], { xaxis: {} }); @@ -305,7 +291,7 @@ describe('finance charts defaults:', function() { }); }); -describe('finance charts calc', function() { +describe('finance charts calc', function () { 'use strict'; function calcDatatoTrace(calcTrace) { @@ -320,9 +306,9 @@ describe('finance charts calc', function() { supplyAllDefaults(gd); Plots.doCalcdata(gd); - gd.calcdata.forEach(function(cd) { + gd.calcdata.forEach(function (cd) { // fill in some stuff that happens during crossTraceCalc or plot - if(cd[0].trace.type === 'candlestick') { + 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; @@ -343,7 +329,7 @@ describe('finance charts calc', function() { // one of o, h, l, c is not numeric function addJunk(trace) { // x filtering happens in other ways - if(trace.x) trace.x.push(1, 1, 1, 1); + if (trace.x) trace.x.push(1, 1, 1, 1); trace.open.push('', 1, 1, 1); trace.high.push(1, null, 1, 1); @@ -352,17 +338,19 @@ describe('finance charts calc', function() { } function mapGet(array, attr) { - return array.map(function(di) { return di[attr]; }); + return array.map(function (di) { + return di[attr]; + }); } - it('should fill when *x* is not present', function() { + it('should fill when *x* is not present', function () { var trace0 = Lib.extendDeep({}, mock0, { - type: 'ohlc', + type: 'ohlc' }); addJunk(trace0); var trace1 = Lib.extendDeep({}, mock0, { - type: 'candlestick', + type: 'candlestick' }); addJunk(trace1); @@ -372,9 +360,18 @@ describe('finance charts calc', function() { var d = 'decreasing'; var directions = [i, d, d, i, d, i, d, i, undefined, undefined, undefined, undefined]; var empties = [ - undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, - true, true, true, true + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + true, + true, + true ]; expect(mapGet(out[0], 'pos')).toEqual(indices); @@ -385,7 +382,7 @@ describe('finance charts calc', function() { expect(mapGet(out[1], 'empty')).toEqual(empties); }); - 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() { + 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' }); @@ -393,7 +390,9 @@ describe('finance charts calc', function() { var trace1 = Lib.extendDeep({}, mock1, { type: 'ohlc', // shift time coordinates by 10 hours - x: mock1.x.map(function(d) { return d + ' 10:00'; }) + x: mock1.x.map(function (d) { + return d + ' 10:00'; + }) }); var out = _calcRaw([trace0, trace1]); @@ -405,7 +404,7 @@ describe('finance charts calc', function() { expect(out[1][0].t.wHover).toBe(out[0][0].t.wHover); }); - it('works with category x data', function() { + 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, { @@ -419,7 +418,7 @@ describe('finance charts calc', function() { expect(out[0][0].t.wHover).toBeCloseTo(0.5, 5); }); - it('should fallback to a spacing of 1 in one-item traces', function() { + it('should fallback to a spacing of 1 in one-item traces', function () { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', x: ['2016-01-01'] @@ -438,36 +437,43 @@ describe('finance charts calc', function() { expect(out[1][0].t.wHover).toBeCloseTo(0.5, 5); }); - it('should handle cases where \'open\' and \'close\' entries are equal', function() { - var out = _calcRaw([{ - type: 'ohlc', - open: [0, 1, 0, 2, 1, 1, 2, 2], - high: [3, 3, 3, 3, 3, 3, 3, 3], - low: [-1, -1, -1, -1, -1, -1, -1, -1], - close: [0, 2, 0, 1, 1, 1, 2, 2], - tickwidth: 0 - }, { - type: 'candlestick', - open: [0, 2, 0, 1], - high: [3, 3, 3, 3], - low: [-1, -1, -1, -1], - close: [0, 1, 0, 2] - }]); + it("should handle cases where 'open' and 'close' entries are equal", function () { + var out = _calcRaw([ + { + type: 'ohlc', + open: [0, 1, 0, 2, 1, 1, 2, 2], + high: [3, 3, 3, 3, 3, 3, 3, 3], + low: [-1, -1, -1, -1, -1, -1, -1, -1], + close: [0, 2, 0, 1, 1, 1, 2, 2], + tickwidth: 0 + }, + { + type: 'candlestick', + open: [0, 2, 0, 1], + high: [3, 3, 3, 3], + low: [-1, -1, -1, -1], + close: [0, 1, 0, 2] + } + ]); expect(mapGet(out[0], 'dir')).toEqual([ - 'increasing', 'increasing', 'decreasing', 'decreasing', - 'decreasing', 'decreasing', 'increasing', 'increasing' + 'increasing', + 'increasing', + 'decreasing', + 'decreasing', + 'decreasing', + 'decreasing', + 'increasing', + 'increasing' ]); - expect(mapGet(out[1], 'dir')).toEqual([ - 'increasing', 'decreasing', 'decreasing', 'increasing' - ]); + expect(mapGet(out[1], 'dir')).toEqual(['increasing', 'decreasing', 'decreasing', 'increasing']); }); - it('should include finance hover labels prefix in calcdata', function() { - ['candlestick', 'ohlc'].forEach(function(type) { + it('should include finance hover labels prefix in calcdata', function () { + ['candlestick', 'ohlc'].forEach(function (type) { var trace0 = Lib.extendDeep({}, mock0, { - type: type, + type: type }); var out = _calcRaw([trace0]); @@ -481,14 +487,16 @@ describe('finance charts calc', function() { }); }); -describe('finance charts auto-range', function() { +describe('finance charts auto-range', function () { var gd; - beforeEach(function() { gd = createGraphDiv(); }); + beforeEach(function () { + gd = createGraphDiv(); + }); afterEach(destroyGraphDiv); - describe('should give correct results with trailing nulls', function() { + describe('should give correct results with trailing nulls', function () { var base = { x: ['time1', 'time2', 'time3'], high: [10, 11, null], @@ -497,36 +505,38 @@ describe('finance charts auto-range', function() { open: [4, 4, null] }; - it('- ohlc case', function(done) { - var trace = Lib.extendDeep({}, base, {type: 'ohlc'}); + it('- ohlc case', function (done) { + var trace = Lib.extendDeep({}, base, { type: 'ohlc' }); - Plotly.newPlot(gd, [trace]).then(function() { - expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.5, 2.5], 1); - }) - .then(done, done.fail); + Plotly.newPlot(gd, [trace]) + .then(function () { + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.5, 2.5], 1); + }) + .then(done, done.fail); }); - it('- candlestick case', function(done) { - var trace = Lib.extendDeep({}, base, {type: 'candlestick'}); + it('- candlestick case', function (done) { + var trace = Lib.extendDeep({}, base, { type: 'candlestick' }); - Plotly.newPlot(gd, [trace]).then(function() { - expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.5, 2.5], 1); - }) - .then(done, done.fail); + Plotly.newPlot(gd, [trace]) + .then(function () { + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.5, 2.5], 1); + }) + .then(done, done.fail); }); }); }); -describe('finance charts updates:', function() { +describe('finance charts updates:', function () { 'use strict'; var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); - afterEach(function() { + afterEach(function () { Plotly.purge(gd); destroyGraphDiv(); }); @@ -543,186 +553,190 @@ describe('finance charts updates:', function() { return d3Select('g.rangeslider-rangeplot').size(); } - it('Plotly.restyle should work', function(done) { + it('Plotly.restyle should work', function (done) { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); var path0; - Plotly.newPlot(gd, [trace0]).then(function() { - expect(gd.calcdata[0][0].t.tickLen).toBeCloseTo(0.3, 5); - expect(gd.calcdata[0][0].o).toEqual(33.01); + Plotly.newPlot(gd, [trace0]) + .then(function () { + 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].t.tickLen).toBeCloseTo(0.5, 5); + return Plotly.restyle(gd, 'tickwidth', 0.5); + }) + .then(function () { + 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].o).toEqual(0); + 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].o).toEqual(0); - return Plotly.restyle(gd, { - type: 'candlestick', - open: [[33.01, 33.31, 33.50, 32.06, 34.12, 33.05, 33.31, 33.50]] - }); - }) - .then(function() { - path0 = d3Select('path.box').attr('d'); - expect(path0).toBeDefined(); + return Plotly.restyle(gd, { + type: 'candlestick', + open: [[33.01, 33.31, 33.5, 32.06, 34.12, 33.05, 33.31, 33.5]] + }); + }) + .then(function () { + path0 = d3Select('path.box').attr('d'); + expect(path0).toBeDefined(); - return Plotly.restyle(gd, 'whiskerwidth', 0.2); - }) - .then(function() { - expect(d3Select('path.box').attr('d')).not.toEqual(path0); - }) - .then(done, done.fail); + return Plotly.restyle(gd, 'whiskerwidth', 0.2); + }) + .then(function () { + expect(d3Select('path.box').attr('d')).not.toEqual(path0); + }) + .then(done, done.fail); }); - it('should be able to toggle visibility', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; + it('should be able to toggle visibility', function (done) { + var data = [Lib.extendDeep({}, mock0, { type: 'ohlc' }), Lib.extendDeep({}, mock0, { type: 'candlestick' })]; - Plotly.newPlot(gd, data).then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); + Plotly.newPlot(gd, data) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); + return Plotly.restyle(gd, 'visible', false); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); - return Plotly.restyle(gd, 'visible', true, [1]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(1); + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); - return Plotly.restyle(gd, 'visible', true, [0]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); - return Plotly.restyle(gd, 'visible', 'legendonly', [0]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(1); + return Plotly.restyle(gd, 'visible', 'legendonly', [0]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); - }) - .then(done, done.fail); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); + }) + .then(done, done.fail); }); - it('Plotly.relayout should work', function(done) { + it('Plotly.relayout should work', function (done) { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - Plotly.newPlot(gd, [trace0]).then(function() { - expect(countRangeSliders()).toEqual(1); + Plotly.newPlot(gd, [trace0]) + .then(function () { + expect(countRangeSliders()).toEqual(1); - return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); - }) - .then(function() { - expect(countRangeSliders()).toEqual(0); - }) - .then(done, done.fail); + return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); + }) + .then(function () { + expect(countRangeSliders()).toEqual(0); + }) + .then(done, done.fail); }); - it('Plotly.extendTraces should work', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; - - Plotly.newPlot(gd, data).then(function() { - expect(gd.calcdata[0].length).toEqual(8); - expect(gd.calcdata[1].length).toEqual(8); - - return Plotly.extendTraces(gd, { - open: [[ 34, 35 ]], - high: [[ 40, 41 ]], - low: [[ 32, 33 ]], - close: [[ 38, 39 ]] - }, [1]); - }) - .then(function() { - expect(gd.calcdata[0].length).toEqual(8); - expect(gd.calcdata[1].length).toEqual(10); - - return Plotly.extendTraces(gd, { - open: [[ 34, 35 ]], - high: [[ 40, 41 ]], - low: [[ 32, 33 ]], - close: [[ 38, 39 ]] - }, [0]); - }) - .then(function() { - expect(gd.calcdata[0].length).toEqual(10); - expect(gd.calcdata[1].length).toEqual(10); - }) - .then(done, done.fail); + it('Plotly.extendTraces should work', function (done) { + var data = [Lib.extendDeep({}, mock0, { type: 'ohlc' }), Lib.extendDeep({}, mock0, { type: 'candlestick' })]; + + Plotly.newPlot(gd, data) + .then(function () { + expect(gd.calcdata[0].length).toEqual(8); + expect(gd.calcdata[1].length).toEqual(8); + + return Plotly.extendTraces( + gd, + { + open: [[34, 35]], + high: [[40, 41]], + low: [[32, 33]], + close: [[38, 39]] + }, + [1] + ); + }) + .then(function () { + expect(gd.calcdata[0].length).toEqual(8); + expect(gd.calcdata[1].length).toEqual(10); + + return Plotly.extendTraces( + gd, + { + open: [[34, 35]], + high: [[40, 41]], + low: [[32, 33]], + close: [[38, 39]] + }, + [0] + ); + }) + .then(function () { + expect(gd.calcdata[0].length).toEqual(10); + expect(gd.calcdata[1].length).toEqual(10); + }) + .then(done, done.fail); }); - it('Plotly.deleteTraces / addTraces should work', function(done) { - var data = [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }), - ]; + it('Plotly.deleteTraces / addTraces should work', function (done) { + var data = [Lib.extendDeep({}, mock0, { type: 'ohlc' }), Lib.extendDeep({}, mock0, { type: 'candlestick' })]; - Plotly.newPlot(gd, data).then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); + Plotly.newPlot(gd, data) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); - return Plotly.deleteTraces(gd, [1]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(0); + return Plotly.deleteTraces(gd, [1]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(0); - return Plotly.deleteTraces(gd, [0]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); + return Plotly.deleteTraces(gd, [0]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); - var trace = Lib.extendDeep({}, mock0, { type: 'candlestick' }); + var trace = Lib.extendDeep({}, mock0, { type: 'candlestick' }); - return Plotly.addTraces(gd, [trace]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(1); + return Plotly.addTraces(gd, [trace]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); - var trace = Lib.extendDeep({}, mock0, { type: 'ohlc' }); + var trace = Lib.extendDeep({}, mock0, { type: 'ohlc' }); - return Plotly.addTraces(gd, [trace]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); - }) - .then(done, done.fail); + return Plotly.addTraces(gd, [trace]); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); + }) + .then(done, done.fail); }); - it('Plotly.addTraces + Plotly.relayout should update candlestick box position values', function(done) { + it('Plotly.addTraces + Plotly.relayout should update candlestick box position values', function (done) { function assertBoxPosFields(bPos) { expect(gd.calcdata.length).toEqual(bPos.length); - gd.calcdata.forEach(function(calcTrace, i) { + gd.calcdata.forEach(function (calcTrace, i) { expect(calcTrace[0].t.bPos).toBeCloseTo(bPos[i], 0); }); } @@ -736,123 +750,133 @@ describe('finance charts updates:', function() { close: [2, 3] }; - Plotly.newPlot(gd, [trace0], {boxmode: 'group'}) - .then(function() { - assertBoxPosFields([0]); + Plotly.newPlot(gd, [trace0], { boxmode: 'group' }) + .then(function () { + assertBoxPosFields([0]); - return Plotly.addTraces(gd, [Lib.extendDeep({}, trace0)]); - }) - .then(function() { - assertBoxPosFields([-15120000, 15120000]); + return Plotly.addTraces(gd, [Lib.extendDeep({}, trace0)]); + }) + .then(function () { + assertBoxPosFields([-15120000, 15120000]); + + var update = { + type: 'candlestick', + 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]] + }; - var update = { - type: 'candlestick', - 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([-30240000, 30240000]); - }) - .then(done, done.fail); + return Plotly.restyle(gd, update); + }) + .then(function () { + assertBoxPosFields([-30240000, 30240000]); + }) + .then(done, done.fail); }); - it('Plotly.newPlot with data-less trace and adding with Plotly.restyle', function(done) { - var data = [ - { type: 'candlestick' }, - { type: 'ohlc' }, - { type: 'bar', y: [2, 1, 2] } - ]; - - Plotly.newPlot(gd, data).then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(0); - expect(countRangeSliders()).toEqual(0); - - return Plotly.restyle(gd, { - open: [mock0.open], - high: [mock0.high], - low: [mock0.low], - close: [mock0.close] - }, [0]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(0); - expect(countBoxTraces()).toEqual(1); - expect(countRangeSliders()).toEqual(1); - - return Plotly.restyle(gd, { - open: [mock0.open], - high: [mock0.high], - low: [mock0.low], - close: [mock0.close] - }, [1]); - }) - .then(function() { - expect(countOHLCTraces()).toEqual(1); - expect(countBoxTraces()).toEqual(1); - expect(countRangeSliders()).toEqual(1); - }) - .then(done, done.fail); + it('Plotly.newPlot with data-less trace and adding with Plotly.restyle', function (done) { + var data = [{ type: 'candlestick' }, { type: 'ohlc' }, { type: 'bar', y: [2, 1, 2] }]; + + Plotly.newPlot(gd, data) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(0); + expect(countRangeSliders()).toEqual(0); + + return Plotly.restyle( + gd, + { + open: [mock0.open], + high: [mock0.high], + low: [mock0.low], + close: [mock0.close] + }, + [0] + ); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(0); + expect(countBoxTraces()).toEqual(1); + expect(countRangeSliders()).toEqual(1); + + return Plotly.restyle( + gd, + { + open: [mock0.open], + high: [mock0.high], + low: [mock0.low], + close: [mock0.close] + }, + [1] + ); + }) + .then(function () { + expect(countOHLCTraces()).toEqual(1); + expect(countBoxTraces()).toEqual(1); + expect(countRangeSliders()).toEqual(1); + }) + .then(done, done.fail); }); - it('should be able to update ohlc tickwidth', function(done) { - var trace0 = Lib.extendDeep({}, mock0, {type: 'ohlc'}); + it('should be able to update ohlc tickwidth', function (done) { + var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); function _assert(msg, exp) { var tickLen = gd.calcdata[0][0].t.tickLen; - expect(tickLen) - .toBe(exp.tickLen, 'tickLen val in calcdata - ' + msg); + expect(tickLen).toBe(exp.tickLen, 'tickLen val in calcdata - ' + msg); var pathd = d3Select(gd).select('.ohlc > path').attr('d'); - expect(pathd) - .toBe(exp.pathd, 'path d attr - ' + msg); + expect(pathd).toBe(exp.pathd, 'path d attr - ' + msg); } Plotly.newPlot(gd, [trace0], { - xaxis: { rangeslider: {visible: false} } - }) - .then(function() { - _assert('auto rng / base tickwidth', { - tickLen: 0.3, - pathd: 'M13.5,137.63H33.75M33.75,75.04V206.53M54,80.3H33.75' - }); - return Plotly.restyle(gd, 'tickwidth', 0); + xaxis: { rangeslider: { visible: false } } }) - .then(function() { - _assert('auto rng / no tickwidth', { - tickLen: 0, - pathd: 'M33.75,137.63H33.75M33.75,75.04V206.53M33.75,80.3H33.75' - }); + .then(function () { + _assert('auto rng / base tickwidth', { + tickLen: 0.3, + pathd: 'M13.5,137.63H33.75M33.75,75.04V206.53M54,80.3H33.75' + }); + return Plotly.restyle(gd, 'tickwidth', 0); + }) + .then(function () { + _assert('auto rng / no tickwidth', { + tickLen: 0, + pathd: 'M33.75,137.63H33.75M33.75,75.04V206.53M33.75,80.3H33.75' + }); - return Plotly.update(gd, { - tickwidth: null - }, { - 'xaxis.range': [0, 8], - 'yaxis.range': [30, 36] - }); - }) - .then(function() { - _assert('set rng / base tickwidth', { - tickLen: 0.3, - pathd: 'M-20.25,134.55H0M0,81V193.5M20.25,85.5H0' - }); - return Plotly.restyle(gd, 'tickwidth', 0); - }) - .then(function() { - _assert('set rng / no tickwidth', { - tickLen: 0, - pathd: 'M0,134.55H0M0,81V193.5M0,85.5H0' - }); - }) - .then(done, done.fail); + return Plotly.update( + gd, + { + tickwidth: null + }, + { + 'xaxis.range': [0, 8], + 'yaxis.range': [30, 36] + } + ); + }) + .then(function () { + _assert('set rng / base tickwidth', { + tickLen: 0.3, + pathd: 'M-20.25,134.55H0M0,81V193.5M20.25,85.5H0' + }); + return Plotly.restyle(gd, 'tickwidth', 0); + }) + .then(function () { + _assert('set rng / no tickwidth', { + tickLen: 0, + pathd: 'M0,134.55H0M0,81V193.5M0,85.5H0' + }); + }) + .then(done, done.fail); }); - it('should work with typed array', function(done) { + it('should work with typed array', function (done) { var mockTA = { open: new Float32Array(mock0.open), high: new Float32Array(mock0.high), @@ -861,82 +885,87 @@ describe('finance charts updates:', function() { }; var dataTA = [ - Lib.extendDeep({}, mockTA, {type: 'ohlc'}), - Lib.extendDeep({}, mockTA, {type: 'candlestick'}), + Lib.extendDeep({}, mockTA, { type: 'ohlc' }), + Lib.extendDeep({}, mockTA, { type: 'candlestick' }) ]; - var data0 = [ - Lib.extendDeep({}, mock0, {type: 'ohlc'}), - Lib.extendDeep({}, mock0, {type: 'candlestick'}), - ]; + var data0 = [Lib.extendDeep({}, mock0, { type: 'ohlc' }), Lib.extendDeep({}, mock0, { type: 'candlestick' })]; Plotly.newPlot(gd, dataTA) - .then(function() { - expect(countOHLCTraces()).toBe(1, '# of ohlc traces'); - expect(countBoxTraces()).toBe(1, '# of candlestick traces'); - }) - .then(function() { return Plotly.react(gd, data0); }) - .then(function() { - expect(countOHLCTraces()).toBe(1, '# of ohlc traces'); - expect(countBoxTraces()).toBe(1, '# of candlestick traces'); - }) - .then(done, done.fail); + .then(function () { + expect(countOHLCTraces()).toBe(1, '# of ohlc traces'); + expect(countBoxTraces()).toBe(1, '# of candlestick traces'); + }) + .then(function () { + return Plotly.react(gd, data0); + }) + .then(function () { + expect(countOHLCTraces()).toBe(1, '# of ohlc traces'); + expect(countBoxTraces()).toBe(1, '# of candlestick traces'); + }) + .then(done, done.fail); }); - it('should clear empty candlestick boxes using react', function(done) { + it('should clear empty candlestick boxes using react', function (done) { var type = 'candlestick'; var x = [0, 1]; - var steps = [{ - data: [{ - close: [132, null], - high: [204, 20], - low: [30, 193], - open: [78, 79], - type: type, - x: x - }], - layout: {} - }, - { - data: [{ - close: [140, 78], - high: [91, 117], - low: [115, 78], - open: [null, 97], - type: type, - x: x - }], - layout: {} - }]; + var steps = [ + { + data: [ + { + close: [132, null], + high: [204, 20], + low: [30, 193], + open: [78, 79], + type: type, + x: x + } + ], + layout: {} + }, + { + data: [ + { + close: [140, 78], + high: [91, 117], + low: [115, 78], + open: [null, 97], + type: type, + x: x + } + ], + layout: {} + } + ]; Plotly.newPlot(gd, steps[0]) - .then(function() { - return Plotly.react(gd, steps[1]); - }).then(function() { - expect( - d3Select('g.cartesianlayer') - .selectAll('g.trace.boxes') - .selectAll('path') - .node() - .getAttribute('d') - ).toEqual('M0,0Z'); - }) - .then(done, done.fail); + .then(function () { + return Plotly.react(gd, steps[1]); + }) + .then(function () { + expect( + d3Select('g.cartesianlayer').selectAll('g.trace.boxes').selectAll('path').node().getAttribute('d') + ).toEqual('M0,0Z'); + }) + .then(done, done.fail); }); }); -describe('finance charts *special* handlers:', function() { +describe('finance charts *special* handlers:', function () { // not special anymore - just test that they work as normal afterEach(destroyGraphDiv); - it('`editable: true` handlers should work', function(done) { + it('`editable: true` handlers should work', function (done) { var gd = createGraphDiv(); function editText(itemNumber, newText) { var textNode = d3SelectAll('text.legendtext') - .filter(function(_, i) { return i === itemNumber; }).node(); + .filter(function (_, i) { + return i === itemNumber; + }) + .node(); textNode.dispatchEvent(new window.MouseEvent('click')); var editNode = d3Select('.plugin-editable.editable').node(); @@ -952,42 +981,46 @@ describe('finance charts *special* handlers:', function() { // of the rendering queue to make sure the edit