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
is properly // cleared after each mocked text edits. function delayedResolve(resolve) { - setTimeout(function() { return resolve(gd); }, 0); + setTimeout(function () { + return resolve(gd); + }, 0); } - Plotly.newPlot(gd, [ - Lib.extendDeep({}, mock0, { type: 'ohlc' }), - Lib.extendDeep({}, mock0, { type: 'candlestick' }) - ], {}, { - editable: true - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0].name).toEqual('0'); - expect(eventData[1]).toEqual([0]); - delayedResolve(resolve); + Plotly.newPlot( + gd, + [Lib.extendDeep({}, mock0, { type: 'ohlc' }), Lib.extendDeep({}, mock0, { type: 'candlestick' })], + {}, + { + editable: true + } + ) + .then(function (gd) { + return new Promise(function (resolve) { + gd.once('plotly_restyle', function (eventData) { + expect(eventData[0].name).toEqual('0'); + expect(eventData[1]).toEqual([0]); + delayedResolve(resolve); + }); + + editText(0, '0'); }); - - editText(0, '0'); - }); - }) - .then(function(gd) { - return new Promise(function(resolve) { - gd.once('plotly_restyle', function(eventData) { - expect(eventData[0].name).toEqual('1'); - expect(eventData[1]).toEqual([1]); - delayedResolve(resolve); + }) + .then(function (gd) { + return new Promise(function (resolve) { + gd.once('plotly_restyle', function (eventData) { + expect(eventData[0].name).toEqual('1'); + expect(eventData[1]).toEqual([1]); + delayedResolve(resolve); + }); + + editText(1, '1'); }); - - editText(1, '1'); - }); - }) - .then(done, done.fail); + }) + .then(done, done.fail); }); }); -describe('finance trace hover:', function() { +describe('finance trace hover:', function () { var gd; afterEach(destroyGraphDiv); @@ -995,29 +1028,35 @@ describe('finance trace hover:', function() { function run(specs) { gd = createGraphDiv(); - var data = specs.traces.map(function(t) { - return Lib.extendFlat({ - type: specs.type, - open: [1, 2], - close: [2, 3], - high: [3, 4], - low: [0, 5] - }, t); + var data = specs.traces.map(function (t) { + return Lib.extendFlat( + { + type: specs.type, + open: [1, 2], + close: [2, 3], + high: [3, 4], + low: [0, 5] + }, + t + ); }); - var layout = Lib.extendFlat({ - showlegend: false, - width: 400, - height: 400, - margin: {t: 0, b: 0, l: 0, r: 0, pad: 0} - }, specs.layout || {}); + var layout = Lib.extendFlat( + { + showlegend: false, + width: 400, + height: 400, + margin: { t: 0, b: 0, l: 0, r: 0, pad: 0 } + }, + specs.layout || {} + ); var xval = 'xval' in specs ? specs.xval : 0; var yval = 'yval' in specs ? specs.yval : 1; var hovermode = layout.hovermode || 'x'; - return Plotly.newPlot(gd, data, layout).then(function() { - var results = gd.calcdata.map(function(cd) { + return Plotly.newPlot(gd, data, layout).then(function () { + var results = gd.calcdata.map(function (cd) { var trace = cd[0].trace; var pointData = { index: false, @@ -1029,196 +1068,299 @@ describe('finance trace hover:', function() { maxHoverDistance: 20 }; var pts = trace._module.hoverPoints(pointData, xval, yval, hovermode); - return pts ? pts[0] : {distance: Infinity}; + return pts ? pts[0] : { distance: Infinity }; }); var actual = results[0]; var exp = specs.exp; - for(var k in exp) { + for (var k in exp) { var msg = '- key ' + k; expect(actual[k]).toBe(exp[k], msg); } }); } - ['ohlc', 'candlestick'].forEach(function(type) { - [{ - type: type, - desc: 'basic', - traces: [{}], - exp: { - extraText: 'open: 1
high: 3
low: 0
close: 2 ▲' - } - }, { - type: type, - desc: 'with scalar text', - traces: [{text: 'SCALAR'}], - exp: { - extraText: 'open: 1
high: 3
low: 0
close: 2 ▲
SCALAR' - } - }, { - type: type, - desc: 'with array text', - traces: [{text: ['A', 'B']}], - exp: { - extraText: 'open: 1
high: 3
low: 0
close: 2 ▲
A' - } - }, { - type: type, - desc: 'just scalar text', - traces: [{hoverinfo: 'text', text: 'SCALAR'}], - exp: { - extraText: 'SCALAR' - } - }, { - type: type, - desc: 'just array text', - traces: [{hoverinfo: 'text', text: ['A', 'B']}], - exp: { - extraText: 'A' - } - }, { - type: type, - desc: 'just scalar hovertext', - traces: [{hoverinfo: 'text', hovertext: 'SCALAR', text: 'NOP'}], - exp: { - extraText: 'SCALAR' - } - }, { - type: type, - desc: 'just array hovertext', - traces: [{hoverinfo: 'text', hovertext: ['A', 'B'], text: ['N', 'O', 'P']}], - exp: { - extraText: 'A' - } - }, { - type: type, - desc: 'just array text with array hoverinfo', - traces: [{hoverinfo: ['text', 'text'], text: ['A', 'B']}], - exp: { - extraText: 'A' - } - }, { - type: type, - desc: 'when high === low in *closest* mode', - traces: [{ - high: [6, null, 7, 8], - close: [4, null, 7, 8], - low: [5, null, 7, 8], - open: [3, null, 7, 8] - }], - layout: {hovermode: 'closest'}, - xval: 2, - yval: 6.9, - exp: { - extraText: 'open: 7
high: 7
low: 7
close: 7 ▲' + ['ohlc', 'candlestick'].forEach(function (type) { + [ + { + type: type, + desc: 'basic', + traces: [{}], + exp: { + extraText: 'open: 1
high: 3
low: 0
close: 2 ▲' + } + }, + { + type: type, + desc: 'with scalar text', + traces: [{ text: 'SCALAR' }], + exp: { + extraText: 'open: 1
high: 3
low: 0
close: 2 ▲
SCALAR' + } + }, + { + type: type, + desc: 'with array text', + traces: [{ text: ['A', 'B'] }], + exp: { + extraText: 'open: 1
high: 3
low: 0
close: 2 ▲
A' + } + }, + { + type: type, + desc: 'just scalar text', + traces: [{ hoverinfo: 'text', text: 'SCALAR' }], + exp: { + extraText: 'SCALAR' + } + }, + { + type: type, + desc: 'just array text', + traces: [{ hoverinfo: 'text', text: ['A', 'B'] }], + exp: { + extraText: 'A' + } + }, + { + type: type, + desc: 'just scalar hovertext', + traces: [{ hoverinfo: 'text', hovertext: 'SCALAR', text: 'NOP' }], + exp: { + extraText: 'SCALAR' + } + }, + { + type: type, + desc: 'just array hovertext', + traces: [{ hoverinfo: 'text', hovertext: ['A', 'B'], text: ['N', 'O', 'P'] }], + exp: { + extraText: 'A' + } + }, + { + type: type, + desc: 'just array text with array hoverinfo', + traces: [{ hoverinfo: ['text', 'text'], text: ['A', 'B'] }], + exp: { + extraText: 'A' + } + }, + { + type: type, + desc: 'when high === low in *closest* mode', + traces: [ + { + high: [6, null, 7, 8], + close: [4, null, 7, 8], + low: [5, null, 7, 8], + open: [3, null, 7, 8] + } + ], + layout: { hovermode: 'closest' }, + xval: 2, + yval: 6.9, + exp: { + extraText: 'open: 7
high: 7
low: 7
close: 7 ▲' + } } - }] - .forEach(function(specs) { - it('should generate correct hover labels ' + type + ' - ' + specs.desc, function(done) { + ].forEach(function (specs) { + it('should generate correct hover labels ' + type + ' - ' + specs.desc, function (done) { run(specs).then(done, done.fail); }); }); }); }); -describe('finance trace hover via Fx.hover():', function() { +describe('finance trace hover via Fx.hover():', function () { var gd; - beforeEach(function() { gd = createGraphDiv(); }); + beforeEach(function () { + gd = createGraphDiv(); + }); afterEach(destroyGraphDiv); - ['candlestick', 'ohlc'].forEach(function(type) { - it('should pick correct ' + type + ' item', function(done) { + ['candlestick', 'ohlc'].forEach(function (type) { + it('should pick correct ' + type + ' item', function (done) { var x = ['hover ok!', 'time2', 'hover off by 1', 'time4']; - Plotly.newPlot(gd, [{ - x: x, - high: [6, null, 7, 8], - close: [4, null, 7, 8], - low: [5, null, 7, 8], - open: [3, null, 7, 8], - type: type - }, { - x: x, - y: [1, null, 2, 3], - type: 'bar' - }], { - xaxis: { rangeslider: {visible: false} }, - width: 500, - height: 500 - }) - .then(function() { - gd.on('plotly_hover', function(d) { - Plotly.Fx.hover(gd, [ - {curveNumber: 0, pointNumber: d.points[0].pointNumber}, - {curveNumber: 1, pointNumber: d.points[0].pointNumber} - ]); - }); - }) - .then(function() { hover(281, 252); }) - .then(function() { - assertHoverLabelContent({ - nums: [ - 'hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', - '(hover off by 1, 2)' - ], - name: ['trace 0', 'trace 1'] - }, 'hover over 3rd items (aka 2nd visible items)'); - }) - .then(function() { - Lib.clearThrottle(); - return Plotly.react(gd, [gd.data[0]], gd.layout); - }) - .then(function() { hover(281, 252); }) - .then(function() { - assertHoverLabelContent({ - nums: 'hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', - name: '' - }, 'after removing 2nd trace'); - }) - .then(done, done.fail); + Plotly.newPlot( + gd, + [ + { + x: x, + high: [6, null, 7, 8], + close: [4, null, 7, 8], + low: [5, null, 7, 8], + open: [3, null, 7, 8], + type: type + }, + { + x: x, + y: [1, null, 2, 3], + type: 'bar' + } + ], + { + xaxis: { rangeslider: { visible: false } }, + width: 500, + height: 500 + } + ) + .then(function () { + gd.on('plotly_hover', function (d) { + Plotly.Fx.hover(gd, [ + { curveNumber: 0, pointNumber: d.points[0].pointNumber }, + { curveNumber: 1, pointNumber: d.points[0].pointNumber } + ]); + }); + }) + .then(function () { + hover(281, 252); + }) + .then(function () { + assertHoverLabelContent( + { + nums: ['hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', '(hover off by 1, 2)'], + name: ['trace 0', 'trace 1'] + }, + 'hover over 3rd items (aka 2nd visible items)' + ); + }) + .then(function () { + Lib.clearThrottle(); + return Plotly.react(gd, [gd.data[0]], gd.layout); + }) + .then(function () { + hover(281, 252); + }) + .then(function () { + assertHoverLabelContent( + { + nums: 'hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', + name: '' + }, + 'after removing 2nd trace' + ); + }) + .then(done, done.fail); }); - it('should ignore empty ' + type + ' item', function(done) { + it('should ignore empty ' + type + ' item', function (done) { // only the bar chart's hover will be displayed when hovering over the 3rd items var x = ['time1', 'time2', 'time3', 'time4']; - Plotly.newPlot(gd, [{ - x: x, - high: [6, 3, null, 8], - close: [4, 3, null, 8], - low: [5, 3, null, 8], - open: [3, 3, null, 8], - type: type - }, { - x: x, - y: [1, 2, 3, 4], - type: 'bar' - }], { - hovermode: 'x', - xaxis: { rangeslider: {visible: false} }, - width: 500, - height: 500 - }) - .then(function() { - gd.on('plotly_hover', function(d) { - Plotly.Fx.hover(gd, [ - {curveNumber: 0, pointNumber: d.points[0].pointNumber}, - {curveNumber: 1, pointNumber: d.points[0].pointNumber} - ]); - }); - }) - .then(function() { hover(281, 252); }) - .then(function() { - assertHoverLabelContent({ - nums: '(time3, 3)', - name: 'trace 1' - }, 'hover over 3rd items'); - }) - .then(done, done.fail); + Plotly.newPlot( + gd, + [ + { + x: x, + high: [6, 3, null, 8], + close: [4, 3, null, 8], + low: [5, 3, null, 8], + open: [3, 3, null, 8], + type: type + }, + { + x: x, + y: [1, 2, 3, 4], + type: 'bar' + } + ], + { + hovermode: 'x', + xaxis: { rangeslider: { visible: false } }, + width: 500, + height: 500 + } + ) + .then(function () { + gd.on('plotly_hover', function (d) { + Plotly.Fx.hover(gd, [ + { curveNumber: 0, pointNumber: d.points[0].pointNumber }, + { curveNumber: 1, pointNumber: d.points[0].pointNumber } + ]); + }); + }) + .then(function () { + hover(281, 252); + }) + .then(function () { + assertHoverLabelContent( + { + nums: '(time3, 3)', + name: 'trace 1' + }, + 'hover over 3rd items' + ); + }) + .then(done, done.fail); + }); + + it('should use hovertemplate (when provided) for ' + type, function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3], + open: [10, 20, 15], + high: [15, 25, 20], + low: [8, 18, 13], + close: [12, 22, 17], + type: type, + hovertemplate: 'O:%{open} H:%{high}
L:%{low} C:%{close}' + } + ], + { + xaxis: { rangeslider: { visible: false } }, + width: 500, + height: 500 + } + ) + .then(function () { + hover(251, 178); + }) + .then(function () { + assertHoverLabelContent({ + nums: 'O:20 H:25\nL:18 C:22', + name: '' + }); + }) + .then(done, done.fail); + }); + it('should ignore hovertemplate in split mode for ' + type, function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3], + open: [10, 20, 15], + high: [15, 25, 20], + low: [8, 18, 13], + close: [12, 22, 17], + type: type, + hovertemplate: 'O:%{open} H:%{high}
L:%{low} C:%{close}', + hoverlabel: { split: true } + } + ], + { + xaxis: { rangeslider: { visible: false } }, + width: 500, + height: 500 + } + ) + .then(function () { + hover(251, 178); + }) + .then(function () { + assertHoverLabelContent({ + nums: ['(2, high: 25)', '(2, close: 22)', '(2, open: 20)', '(2, low: 18)'], + name: ['', '', '', ''] + }); + }) + .then(done, done.fail); }); }); }); diff --git a/test/plot-schema.json b/test/plot-schema.json index fa9a2fff787..9d41ec19435 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -22186,12 +22186,30 @@ "valType": "boolean" }, "split": { - "description": "Show hover information (open, close, high, low) in separate labels.", + "description": "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.", "dflt": false, "editType": "style", "valType": "boolean" } }, + "hovertemplate": { + "arrayOk": true, + "description": "Template string used for rendering the information that appear on hover box. Note that this will override `hoverinfo`. Variables are inserted using %{variable}, for example \"y: %{y}\" as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}. When showing info for several points, *xother* will be added to those with different x positions from the first point. An underscore before or after *(x|y)other* will add a space on that side, only when this field is shown. Numbers are formatted using d3-format's syntax %{variable:d3-format}, for example \"Price: %{y:$.2f}\". https://github.com/d3/d3-format/tree/v1.4.5#d3-format for details on the formatting syntax. Dates are formatted using d3-time-format's syntax %{variable|d3-time-format}, for example \"Day: %{2019-01-01|%A}\". https://github.com/d3/d3-time-format/tree/v2.2.3#locale_format for details on the date formatting syntax. Variables that can't be found will be replaced with the specifier. For example, a template of \"data: %{x}, %{y}\" will result in a value of \"data: 1, %{y}\" if x is 1 and y is missing. Variables with an undefined value will be replaced with the fallback value. The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plotly.com/javascript/plotlyjs-events/#event-data. Additionally, all attributes that can be specified per-point (the ones that are `arrayOk: true`) are available. Finally, the template string has access to variables `open`, `high`, `low` and `close`. Anything contained in tag `` is displayed in the secondary box, for example `%{fullData.name}`. To hide the secondary box completely, use an empty tag ``.", + "dflt": "", + "editType": "none", + "valType": "string" + }, + "hovertemplatefallback": { + "description": "Fallback string that's displayed when a variable referenced in a template is missing. If the boolean value 'false' is passed in, the specifier with the missing variable will be displayed.", + "dflt": "-", + "editType": "none", + "valType": "any" + }, + "hovertemplatesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `hovertemplate`.", + "editType": "none", + "valType": "string" + }, "hovertext": { "arrayOk": true, "description": "Same as `text`.", @@ -52888,12 +52906,30 @@ "valType": "boolean" }, "split": { - "description": "Show hover information (open, close, high, low) in separate labels.", + "description": "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.", "dflt": false, "editType": "style", "valType": "boolean" } }, + "hovertemplate": { + "arrayOk": true, + "description": "Template string used for rendering the information that appear on hover box. Note that this will override `hoverinfo`. Variables are inserted using %{variable}, for example \"y: %{y}\" as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}. When showing info for several points, *xother* will be added to those with different x positions from the first point. An underscore before or after *(x|y)other* will add a space on that side, only when this field is shown. Numbers are formatted using d3-format's syntax %{variable:d3-format}, for example \"Price: %{y:$.2f}\". https://github.com/d3/d3-format/tree/v1.4.5#d3-format for details on the formatting syntax. Dates are formatted using d3-time-format's syntax %{variable|d3-time-format}, for example \"Day: %{2019-01-01|%A}\". https://github.com/d3/d3-time-format/tree/v2.2.3#locale_format for details on the date formatting syntax. Variables that can't be found will be replaced with the specifier. For example, a template of \"data: %{x}, %{y}\" will result in a value of \"data: 1, %{y}\" if x is 1 and y is missing. Variables with an undefined value will be replaced with the fallback value. The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plotly.com/javascript/plotlyjs-events/#event-data. Additionally, all attributes that can be specified per-point (the ones that are `arrayOk: true`) are available. Finally, the template string has access to variables `open`, `high`, `low` and `close`. Anything contained in tag `` is displayed in the secondary box, for example `%{fullData.name}`. To hide the secondary box completely, use an empty tag ``.", + "dflt": "", + "editType": "none", + "valType": "string" + }, + "hovertemplatefallback": { + "description": "Fallback string that's displayed when a variable referenced in a template is missing. If the boolean value 'false' is passed in, the specifier with the missing variable will be displayed.", + "dflt": "-", + "editType": "none", + "valType": "any" + }, + "hovertemplatesrc": { + "description": "Sets the source reference on Chart Studio Cloud for `hovertemplate`.", + "editType": "none", + "valType": "string" + }, "hovertext": { "arrayOk": true, "description": "Same as `text`.",