diff --git a/lib/index-finance.js b/lib/index-finance.js index 4a590fde2bd..fe34318733d 100644 --- a/lib/index-finance.js +++ b/lib/index-finance.js @@ -18,7 +18,8 @@ Plotly.register([ require('./ohlc'), require('./candlestick'), require('./funnel'), - require('./waterfall') + require('./waterfall'), + require('./indicator') ]); module.exports = Plotly; diff --git a/lib/index.js b/lib/index.js index c3f2ab67bdd..325492841b2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,6 +49,7 @@ Plotly.register([ require('./scattermapbox'), require('./sankey'), + require('./indicator'), require('./table'), diff --git a/lib/indicator.js b/lib/indicator.js new file mode 100644 index 00000000000..d9d92eab655 --- /dev/null +++ b/lib/indicator.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = require('../src/traces/indicator'); diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 89e9ce3a6c1..6c8b1884a7b 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -146,6 +146,9 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) { if(hasCartesian) { hoverGroup = ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']; } + if(hasNoHover(fullData)) { + hoverGroup = []; + } if((hasCartesian || hasGL2D) && !allAxesFixed) { zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d']; @@ -216,6 +219,14 @@ function isSelectable(fullData) { return selectable; } +// check whether all trace are 'noHover' +function hasNoHover(fullData) { + for(var i = 0; i < fullData.length; i++) { + if(!Registry.traceIs(fullData[i], 'noHover')) return false; + } + return true; +} + function appendButtonsToGroups(groups, buttons) { if(buttons.length) { if(Array.isArray(buttons[0])) { diff --git a/src/constants/delta.js b/src/constants/delta.js new file mode 100644 index 00000000000..bc94eedd93e --- /dev/null +++ b/src/constants/delta.js @@ -0,0 +1,20 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = { + INCREASING: { + COLOR: '#3D9970', + SYMBOL: '▲' + }, + DECREASING: { + COLOR: '#FF4136', + SYMBOL: '▼' + } +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 33a1a000cb7..60cbf512bd3 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -3779,6 +3779,9 @@ function makePlotFramework(gd) { // single sunburst layer for the whole plot fullLayout._sunburstlayer = fullLayout._paper.append('g').classed('sunburstlayer', true); + // single indicator layer for the whole plot + fullLayout._indicatorlayer = fullLayout._toppaper.append('g').classed('indicatorlayer', true); + // fill in image server scrape-svg fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true); diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 507bab751fd..9249a871a45 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -88,7 +88,7 @@ module.exports = function toSVG(gd, format, scale) { // fill whatever container it's displayed in regardless of plot size. svg.node().style.background = ''; - svg.selectAll('text') + svg.selectAll('text,tspan') .attr({'data-unformatted': null, 'data-math': null}) .each(function() { var txt = d3.select(this); diff --git a/src/traces/indicator/attributes.js b/src/traces/indicator/attributes.js new file mode 100644 index 00000000000..8e26eb29795 --- /dev/null +++ b/src/traces/indicator/attributes.js @@ -0,0 +1,398 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var extendFlat = require('../../lib/extend').extendFlat; +var extendDeep = require('../../lib/extend').extendDeep; +var overrideAll = require('../../plot_api/edit_types').overrideAll; +var fontAttrs = require('../../plots/font_attributes'); +var colorAttrs = require('../../components/color/attributes'); +var domainAttrs = require('../../plots/domain').attributes; +var axesAttrs = require('../../plots/cartesian/layout_attributes'); +var templatedArray = require('../../plot_api/plot_template').templatedArray; +var delta = require('../../constants/delta.js'); + +var textFontAttrs = fontAttrs({ + editType: 'plot', + colorEditType: 'plot' +}); + +var gaugeBarAttrs = { + color: { + valType: 'color', + editType: 'plot', + role: 'info', + description: [ + 'Sets the background color of the arc.' + ].join(' ') + }, + line: { + color: { + valType: 'color', + role: 'info', + dflt: colorAttrs.defaultLine, + editType: 'plot', + description: [ + 'Sets the color of the line enclosing each sector.' + ].join(' ') + }, + width: { + valType: 'number', + role: 'info', + min: 0, + dflt: 0, + editType: 'plot', + description: [ + 'Sets the width (in px) of the line enclosing each sector.' + ].join(' ') + }, + editType: 'calc' + }, + thickness: { + valType: 'number', + role: 'info', + min: 0, + max: 1, + dflt: 1, + editType: 'plot', + description: [ + 'Sets the thickness of the bar as a fraction of the total thickness of the gauge.' + ].join(' ') + }, + editType: 'calc' +}; + +var rangeAttr = { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', editType: 'plot'}, + {valType: 'number', editType: 'plot'} + ], + editType: 'plot', + description: [ + 'Sets the range of this axis.' + // TODO: add support for other axis type + // 'If the axis `type` is *log*, then you must take the log of your', + // 'desired range (e.g. to set the range from 1 to 100,', + // 'set the range from 0 to 2).', + // 'If the axis `type` is *date*, it should be date strings,', + // 'like date data, though Date objects and unix milliseconds', + // 'will be accepted and converted to strings.', + // 'If the axis `type` is *category*, it should be numbers,', + // 'using the scale where each category is assigned a serial', + // 'number from zero in the order it appears.' + ].join(' ') +}; + +var stepsAttrs = templatedArray('steps', extendDeep({}, gaugeBarAttrs, { + range: rangeAttr +})); + +module.exports = { + mode: { + valType: 'flaglist', + editType: 'calc', + role: 'info', + flags: ['number', 'delta', 'gauge'], + dflt: 'number', + description: [ + 'Determines how the value is displayed on the graph.', + '`number` displays the value numerically in text.', + '`delta` displays the difference to a reference value in text.', + 'Finally, `gauge` displays the value graphically on an axis.', + ].join(' ') + }, + value: { + valType: 'number', + editType: 'calc', + role: 'info', + anim: true, + description: [ + 'Sets the number to be displayed.' + ].join(' ') + }, + align: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + role: 'info', + editType: 'plot', + description: [ + 'Sets the horizontal alignment of the `text` within the box.', + 'Note that this attribute has no effect if an angular gauge is displayed:', + 'in this case, it is always centered' + ].join(' ') + }, + // position + domain: domainAttrs({name: 'indicator', trace: true, editType: 'calc'}), + + title: { + text: { + valType: 'string', + role: 'info', + editType: 'plot', + description: [ + 'Sets the title of this indicator.' + ].join(' ') + }, + align: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + role: 'info', + editType: 'plot', + description: [ + 'Sets the horizontal alignment of the title.', + 'It defaults to `center` except for bullet charts', + 'for which it defaults to right.' + ].join(' ') + }, + font: extendFlat({}, textFontAttrs, { + description: [ + 'Set the font used to display the title' + ].join(' ') + }), + editType: 'plot' + }, + number: { + valueformat: { + valType: 'string', + dflt: '.3s', + role: 'info', + editType: 'plot', + description: [ + 'Sets the value formatting rule using d3 formatting mini-language', + 'which is similar to those of Python. See', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format' + ].join(' ') + }, + font: extendFlat({}, textFontAttrs, { + description: [ + 'Set the font used to display main number' + ].join(' ') + }), + prefix: { + valType: 'string', + dflt: '', + role: 'info', + editType: 'plot', + description: [ + 'Sets a prefix appearing before the number.' + ].join(' ') + }, + suffix: { + valType: 'string', + dflt: '', + role: 'info', + editType: 'plot', + description: [ + 'Sets a suffix appearing next to the number.' + ].join(' ') + }, + editType: 'plot' + }, + delta: { + reference: { + valType: 'number', + role: 'info', + editType: 'calc', + description: [ + 'Sets the reference value to compute the delta.', + 'By default, it is set to the current value.' + ].join(' ') + }, + position: { + valType: 'enumerated', + values: ['top', 'bottom', 'left', 'right'], + role: 'info', + dflt: 'bottom', + editType: 'plot', + description: [ + 'Sets the position of delta with respect to the number.' + ].join(' ') + }, + relative: { + valType: 'boolean', + editType: 'plot', + role: 'info', + dflt: false, + description: [ + 'Show relative change' + ].join(' ') + }, + valueformat: { + valType: 'string', + role: 'info', + editType: 'plot', + description: [ + 'Sets the value formatting rule using d3 formatting mini-language', + 'which is similar to those of Python. See', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format' + ].join(' ') + }, + increasing: { + symbol: { + valType: 'string', + role: 'info', + dflt: delta.INCREASING.SYMBOL, + editType: 'plot', + description: [ + 'Sets the symbol to display for increasing value' + ].join(' ') + }, + color: { + valType: 'color', + role: 'info', + dflt: delta.INCREASING.COLOR, + editType: 'plot', + description: [ + 'Sets the color for increasing value.' + ].join(' ') + }, + // TODO: add attribute to show sign + editType: 'plot' + }, + decreasing: { + symbol: { + valType: 'string', + role: 'info', + dflt: delta.DECREASING.SYMBOL, + editType: 'plot', + description: [ + 'Sets the symbol to display for increasing value' + ].join(' ') + }, + color: { + valType: 'color', + role: 'info', + dflt: delta.DECREASING.COLOR, + editType: 'plot', + description: [ + 'Sets the color for increasing value.' + ].join(' ') + }, + // TODO: add attribute to hide sign + editType: 'plot' + }, + font: extendFlat({}, textFontAttrs, { + description: [ + 'Set the font used to display the delta' + ].join(' ') + }), + editType: 'calc' + }, + gauge: { + shape: { + valType: 'enumerated', + editType: 'plot', + role: 'info', + dflt: 'angular', + values: ['angular', 'bullet'], + description: [ + 'Set the shape of the gauge' + ].join(' ') + }, + bar: extendDeep({}, gaugeBarAttrs, { + color: {dflt: 'green'}, + description: [ + 'Set the appearance of the gauge\'s value' + ].join(' ') + }), + // Background of the gauge + bgcolor: { + valType: 'color', + role: 'info', + editType: 'plot', + description: 'Sets the gauge background color.' + }, + bordercolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'info', + editType: 'plot', + description: 'Sets the color of the border enclosing the gauge.' + }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'info', + editType: 'plot', + description: 'Sets the width (in px) of the border enclosing the gauge.' + }, + axis: overrideAll({ + range: rangeAttr, + visible: extendFlat({}, axesAttrs.visible, { + dflt: true + }), + // tick and title properties named and function exactly as in axes + tickmode: axesAttrs.tickmode, + nticks: axesAttrs.nticks, + tick0: axesAttrs.tick0, + dtick: axesAttrs.dtick, + tickvals: axesAttrs.tickvals, + ticktext: axesAttrs.ticktext, + ticks: extendFlat({}, axesAttrs.ticks, {dflt: 'outside'}), + ticklen: axesAttrs.ticklen, + tickwidth: axesAttrs.tickwidth, + tickcolor: axesAttrs.tickcolor, + showticklabels: axesAttrs.showticklabels, + tickfont: fontAttrs({ + description: 'Sets the color bar\'s tick label font' + }), + tickangle: axesAttrs.tickangle, + tickformat: axesAttrs.tickformat, + tickformatstops: axesAttrs.tickformatstops, + tickprefix: axesAttrs.tickprefix, + showtickprefix: axesAttrs.showtickprefix, + ticksuffix: axesAttrs.ticksuffix, + showticksuffix: axesAttrs.showticksuffix, + separatethousands: axesAttrs.separatethousands, + exponentformat: axesAttrs.exponentformat, + showexponent: axesAttrs.showexponent, + editType: 'plot' + }, 'plot'), + // Steps (or ranges) and thresholds + steps: stepsAttrs, + threshold: { + line: { + color: extendFlat({}, gaugeBarAttrs.line.color, { + description: [ + 'Sets the color of the threshold line.' + ].join(' ') + }), + width: extendFlat({}, gaugeBarAttrs.line.width, { + dflt: 1, + description: [ + 'Sets the width (in px) of the threshold line.' + ].join(' ') + }), + editType: 'plot' + }, + thickness: extendFlat({}, gaugeBarAttrs.thickness, { + dflt: 0.85, + description: [ + 'Sets the thickness of the threshold line as a fraction of the thickness of the gauge.' + ].join(' ') + }), + value: { + valType: 'number', + editType: 'calc', + dflt: false, + role: 'info', + description: [ + 'Sets a treshold value drawn as a line.' + ].join(' ') + }, + editType: 'plot' + }, + description: 'The gauge of the Indicator plot.', + editType: 'plot' + // TODO: in future version, add marker: (bar|needle) + } +}; diff --git a/src/traces/indicator/base_plot.js b/src/traces/indicator/base_plot.js new file mode 100644 index 00000000000..07a0e8b35de --- /dev/null +++ b/src/traces/indicator/base_plot.js @@ -0,0 +1,29 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Registry = require('../../registry'); +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; + +var name = exports.name = 'indicator'; + +exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { + var _module = Registry.getModule(name); + var cdmodule = getModuleCalcData(gd.calcdata, _module)[0]; + _module.plot(gd, cdmodule, transitionOpts, makeOnCompleteCallback); +}; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var had = (oldFullLayout._has && oldFullLayout._has(name)); + var has = (newFullLayout._has && newFullLayout._has(name)); + + if(had && !has) { + oldFullLayout._indicatorlayer.selectAll('g.trace').remove(); + } +}; diff --git a/src/traces/indicator/calc.js b/src/traces/indicator/calc.js new file mode 100644 index 00000000000..30e9a891a8c --- /dev/null +++ b/src/traces/indicator/calc.js @@ -0,0 +1,30 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +// var Lib = require('../../lib'); + +function calc(gd, trace) { + var cd = []; + + var lastReading = trace.value; + var secondLastReading = trace.delta ? trace.delta.reference : trace._lastValue || trace.value; + cd[0] = { + y: lastReading, + lastY: secondLastReading, + + delta: lastReading - secondLastReading, + relativeDelta: (lastReading - secondLastReading) / secondLastReading, + }; + return cd; +} + +module.exports = { + calc: calc +}; diff --git a/src/traces/indicator/constants.js b/src/traces/indicator/constants.js new file mode 100644 index 00000000000..b11d4d3e5bc --- /dev/null +++ b/src/traces/indicator/constants.js @@ -0,0 +1,19 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = { + // Defaults for delta + defaultNumberFontSize: 80, + bulletNumberDomainSize: 0.25, + bulletPadding: 0.025, + innerRadius: 0.75, + valueThickness: 0.5, // thickness of value bars relative to full thickness, + titlePadding: 5 +}; diff --git a/src/traces/indicator/defaults.js b/src/traces/indicator/defaults.js new file mode 100644 index 00000000000..4c13c5ad713 --- /dev/null +++ b/src/traces/indicator/defaults.js @@ -0,0 +1,160 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); +var attributes = require('./attributes'); +var handleDomainDefaults = require('../../plots/domain').defaults; +var Template = require('../../plot_api/plot_template'); +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); +var cn = require('./constants.js'); + +var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); +var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); +var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); + +function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleDomainDefaults(traceOut, layout, coerce); + + // Mode + coerce('mode'); + traceOut._hasNumber = traceOut.mode.indexOf('number') !== -1; + traceOut._hasDelta = traceOut.mode.indexOf('delta') !== -1; + traceOut._hasGauge = traceOut.mode.indexOf('gauge') !== -1; + + coerce('value'); + + // Number attributes + var auto = new Array(2); + var bignumberFontSize; + if(traceOut._hasNumber) { + coerce('number.valueformat'); + coerce('number.font.color', layout.font.color); + coerce('number.font.family', layout.font.family); + coerce('number.font.size'); + if(traceOut.number.font.size === undefined) { + traceOut.number.font.size = cn.defaultNumberFontSize; + auto[0] = true; + } + coerce('number.prefix'); + coerce('number.suffix'); + bignumberFontSize = traceOut.number.font.size; + } + + // delta attributes + var deltaFontSize; + if(traceOut._hasDelta) { + coerce('delta.font.color', layout.font.color); + coerce('delta.font.family', layout.font.family); + coerce('delta.font.size'); + if(traceOut.delta.font.size === undefined) { + traceOut.delta.font.size = (traceOut._hasNumber ? 0.5 : 1) * (bignumberFontSize || cn.defaultNumberFontSize); + auto[1] = true; + } + coerce('delta.reference', traceOut.value); + coerce('delta.relative'); + coerce('delta.valueformat', traceOut.delta.relative ? '2%' : '.3s'); + coerce('delta.increasing.symbol'); + coerce('delta.increasing.color'); + coerce('delta.decreasing.symbol'); + coerce('delta.decreasing.color'); + coerce('delta.position'); + deltaFontSize = traceOut.delta.font.size; + } + traceOut._scaleNumbers = (!traceOut._hasNumber || auto[0]) && (!traceOut._hasDelta || auto[1]) || false; + + // Title attributes + coerce('title.font.color', layout.font.color); + coerce('title.font.family', layout.font.family); + coerce('title.font.size', 0.25 * (bignumberFontSize || deltaFontSize || cn.defaultNumberFontSize)); + coerce('title.text'); + + // Gauge attributes + var gaugeIn, gaugeOut, axisIn, axisOut; + function coerceGauge(attr, dflt) { + return Lib.coerce(gaugeIn, gaugeOut, attributes.gauge, attr, dflt); + } + function coerceGaugeAxis(attr, dflt) { + return Lib.coerce(axisIn, axisOut, attributes.gauge.axis, attr, dflt); + } + if(traceOut._hasGauge) { + gaugeIn = traceIn.gauge; + if(!gaugeIn) gaugeIn = {}; + gaugeOut = Template.newContainer(traceOut, 'gauge'); + coerceGauge('shape'); + var isBullet = traceOut._isBullet = traceOut.gauge.shape === 'bullet'; + if(!isBullet) { + coerce('title.align', 'center'); + } + var isAngular = traceOut._isAngular = traceOut.gauge.shape === 'angular'; + if(!isAngular) { + coerce('align', 'center'); + } + + // gauge background + coerceGauge('bgcolor', layout.paper_bgcolor); + coerceGauge('borderwidth'); + coerceGauge('bordercolor'); + + // gauge bar indicator + coerceGauge('bar.color'); + coerceGauge('bar.line.color'); + coerceGauge('bar.line.width'); + var defaultBarThickness = cn.valueThickness * (traceOut.gauge.shape === 'bullet' ? 0.5 : 1); + coerceGauge('bar.thickness', defaultBarThickness); + + // Gauge steps + handleArrayContainerDefaults(gaugeIn, gaugeOut, { + name: 'steps', + handleItemDefaults: stepDefaults + }); + + // Gauge threshold + coerceGauge('threshold.value'); + coerceGauge('threshold.thickness'); + coerceGauge('threshold.line.width'); + coerceGauge('threshold.line.color'); + + // Gauge axis + axisIn = {}; + if(gaugeIn) axisIn = gaugeIn.axis || {}; + axisOut = Template.newContainer(gaugeOut, 'axis'); + coerceGaugeAxis('visible'); + coerceGaugeAxis('range', [0, 1.5 * traceOut.value]); + + var opts = {outerTicks: true}; + handleTickValueDefaults(axisIn, axisOut, coerceGaugeAxis, 'linear'); + handleTickLabelDefaults(axisIn, axisOut, coerceGaugeAxis, 'linear', opts); + handleTickMarkDefaults(axisIn, axisOut, coerceGaugeAxis, opts); + } else { + coerce('title.align', 'center'); + coerce('align', 'center'); + traceOut._isAngular = traceOut._isBullet = false; + } +} + +function stepDefaults(stepIn, stepOut) { + function coerce(attr, dflt) { + return Lib.coerce(stepIn, stepOut, attributes.gauge.steps, attr, dflt); + } + + coerce('color'); + coerce('line.color'); + coerce('line.width'); + coerce('range'); + coerce('thickness'); +} + +module.exports = { + supplyDefaults: supplyDefaults +}; diff --git a/src/traces/indicator/index.js b/src/traces/indicator/index.js new file mode 100644 index 00000000000..24e93101bf3 --- /dev/null +++ b/src/traces/indicator/index.js @@ -0,0 +1,30 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = { + moduleType: 'trace', + name: 'indicator', + basePlotModule: require('./base_plot'), + categories: ['svg', 'noOpacity', 'noHover'], + animatable: true, + + attributes: require('./attributes'), + supplyDefaults: require('./defaults').supplyDefaults, + + calc: require('./calc').calc, + + plot: require('./plot'), + + meta: { + description: [ + 'TODO: add description' + ].join(' ') + } +}; diff --git a/src/traces/indicator/plot.js b/src/traces/indicator/plot.js new file mode 100644 index 00000000000..fe8fb1438c9 --- /dev/null +++ b/src/traces/indicator/plot.js @@ -0,0 +1,775 @@ +/** +* Copyright 2012-2019, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); + +var Lib = require('../../lib'); +var rad2deg = Lib.rad2deg; +var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; +var Drawing = require('../../components/drawing'); +var cn = require('./constants'); +var svgTextUtils = require('../../lib/svg_text_utils'); + +var Axes = require('../../plots/cartesian/axes'); +var handleAxisDefaults = require('../../plots/cartesian/axis_defaults'); +var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults'); +var axisLayoutAttrs = require('../../plots/cartesian/layout_attributes'); + +var Color = require('../../components/color'); +var anchor = { + 'left': 'start', + 'center': 'middle', + 'right': 'end' +}; +var position = { + 'left': 0, + 'center': 0.5, + 'right': 1 +}; + +module.exports = function plot(gd, cdModule, transitionOpts, makeOnCompleteCallback) { + var fullLayout = gd._fullLayout; + var onComplete; + + // If transition config is provided, then it is only a partial replot and traces not + // updated are removed. + var hasTransition = transitionOpts && transitionOpts.duration > 0; + + if(hasTransition) { + if(makeOnCompleteCallback) { + // If it was passed a callback to register completion, make a callback. If + // this is created, then it must be executed on completion, otherwise the + // pos-transition redraw will not execute: + onComplete = makeOnCompleteCallback(); + } + } + + Lib.makeTraceGroups(fullLayout._indicatorlayer, cdModule, 'trace').each(function(cd) { + var cd0 = cd[0]; + var trace = cd0.trace; + var plotGroup = d3.select(this); + + // Elements in trace + var hasGauge = trace._hasGauge; + var isAngular = trace._isAngular; + var isBullet = trace._isBullet; + + // Domain size + var domain = trace.domain; + var size = { + w: fullLayout._size.w * (domain.x[1] - domain.x[0]), + h: fullLayout._size.h * (domain.y[1] - domain.y[0]), + l: fullLayout._size.l + fullLayout._size.w * domain.x[0], + r: fullLayout._size.r + fullLayout._size.w * (1 - domain.x[1]), + t: fullLayout._size.t + fullLayout._size.h * (1 - domain.y[1]), + b: fullLayout._size.b + fullLayout._size.h * (domain.y[0]) + }; + var centerX = size.l + size.w / 2; + var centerY = size.t + size.h / 2; + + + // Angular gauge size + var radius = Math.min(size.w / 2, size.h); // fill domain + var innerRadius = cn.innerRadius * radius; + + // Position numbers based on mode and set the scaling logic + var numbersX, numbersY, numbersScaler; + var numbersAlign = trace.align || 'center'; + + numbersY = centerY; + if(!hasGauge) { + numbersX = size.l + position[numbersAlign] * size.w; + numbersScaler = function(el) { + return fitTextInsideBox(el, size.w, size.h); + }; + } else { + if(isAngular) { + numbersX = centerX; + numbersY = centerY + radius / 2; + numbersScaler = function(el) { + return fitTextInsideCircle(el, 0.9 * innerRadius); + }; + } + if(isBullet) { + var padding = cn.bulletPadding; + var p = (1 - cn.bulletNumberDomainSize) + padding; + numbersX = size.l + (p + (1 - p) * position[numbersAlign]) * size.w; + numbersScaler = function(el) { + return fitTextInsideBox(el, (cn.bulletNumberDomainSize - padding) * size.w, size.h); + }; + } + } + + // Draw numbers + var numbersOpts = { + numbersX: numbersX, + numbersY: numbersY, + numbersScaler: numbersScaler, + hasTransition: hasTransition, + transitionOpts: transitionOpts, + onComplete: onComplete + }; + drawNumbers(gd, plotGroup, cd, numbersOpts); + + // Reexpress our gauge background attributes for drawing + var gaugeBg, gaugeOutline; + if(hasGauge) { + gaugeBg = { + range: trace.gauge.axis.range, + color: trace.gauge.bgcolor, + line: { + color: trace.gauge.bordercolor, + width: 0 + }, + thickness: 1 + }; + + gaugeOutline = { + range: trace.gauge.axis.range, + color: 'rgba(0, 0, 0, 0)', + line: { + color: trace.gauge.bordercolor, + width: trace.gauge.borderwidth + }, + thickness: 1 + }; + } + + // Prepare angular gauge layers + var angularGauge = plotGroup.selectAll('g.angular').data(isAngular ? cd : []); + angularGauge.exit().remove(); + var angularaxisLayer = plotGroup.selectAll('g.angularaxis').data(isAngular ? cd : []); + angularaxisLayer.exit().remove(); + + var gaugeOpts = { + size: size, + radius: radius, + innerRadius: innerRadius, + gaugeBg: gaugeBg, + gaugeOutline: gaugeOutline, + angularaxisLayer: angularaxisLayer, + angularGauge: angularGauge, + hasTransition: hasTransition, + transitionOpts: transitionOpts, + onComplete: onComplete + }; + if(isAngular) drawAngularGauge(gd, plotGroup, cd, gaugeOpts); + + // Prepare bullet layers + var bulletGauge = plotGroup.selectAll('g.bullet').data(isBullet ? cd : []); + bulletGauge.exit().remove(); + var bulletaxisLayer = plotGroup.selectAll('g.bulletaxis').data(isBullet ? cd : []); + bulletaxisLayer.exit().remove(); + + gaugeOpts = { + size: size, + gaugeBg: gaugeBg, + gaugeOutline: gaugeOutline, + bulletGauge: bulletGauge, + bulletaxisLayer: bulletaxisLayer, + hasTransition: hasTransition, + transitionOpts: transitionOpts, + onComplete: onComplete + }; + if(isBullet) drawBulletGauge(gd, plotGroup, cd, gaugeOpts); + + // title + var title = plotGroup.selectAll('text.title').data(cd); + title.exit().remove(); + title.enter().append('text').classed('title', true); + title + .attr('text-anchor', function() { + return isBullet ? anchor.right : anchor[trace.title.align]; + }) + .text(trace.title.text) + .call(Drawing.font, trace.title.font) + .call(svgTextUtils.convertToTspans, gd); + + // Position title + title.attr('transform', function() { + var titleX = size.l + size.w * position[trace.title.align]; + var titleY; + var titlePadding = cn.titlePadding; + var titlebBox = Drawing.bBox(title.node()); + if(hasGauge) { + if(isAngular) { + // position above axis ticks/labels + if(trace.gauge.axis.visible) { + var bBox = Drawing.bBox(angularaxisLayer.node()); + titleY = (bBox.top - titlePadding) - titlebBox.bottom; + } else { + titleY = size.t + size.h / 2 - radius / 2 - titlebBox.bottom - titlePadding; + } + } + if(isBullet) { + // position outside domain + titleY = numbersY - (titlebBox.top + titlebBox.bottom) / 2; + titleX = size.l - cn.bulletPadding * size.w; // Outside domain, on the left + } + } else { + // position above numbers + titleY = (trace._numbersTop - titlePadding) - titlebBox.bottom; + } + return strTranslate(titleX, titleY); + }); + }); +}; + +function drawBulletGauge(gd, plotGroup, cd, gaugeOpts) { + var trace = cd[0].trace; + + var bullet = gaugeOpts.bulletGauge; + var bulletaxis = gaugeOpts.bulletaxisLayer; + var gaugeBg = gaugeOpts.gaugeBg; + var gaugeOutline = gaugeOpts.gaugeOutline; + var size = gaugeOpts.size; + var domain = trace.domain; + + var hasTransition = gaugeOpts.hasTransition; + var transitionOpts = gaugeOpts.transitionOpts; + var onComplete = gaugeOpts.onComplete; + + // preparing axis + var ax, vals, transFn, tickSign, shift; + var opts = trace.gauge.axis; + + // Enter bullet, axis + bullet.enter().append('g').classed('bullet', true); + bullet.attr('transform', 'translate(' + size.l + ', ' + size.t + ')'); + + bulletaxis.enter().append('g') + .classed('bulletaxis', true) + .classed('crisp', true); + bulletaxis.selectAll('g.' + 'xbulletaxis' + 'tick,path,text').remove(); + + // Draw bullet + var bulletHeight = size.h; // use all vertical domain + var innerBulletHeight = trace.gauge.bar.thickness * bulletHeight; + var bulletLeft = domain.x[0]; + var bulletRight = domain.x[0] + (domain.x[1] - domain.x[0]) * ((trace._hasNumber || trace._hasDelta) ? (1 - cn.bulletNumberDomainSize) : 1); + + ax = mockAxis(gd, opts, trace.gauge.axis.range); + ax._id = 'xbulletaxis'; + ax.domain = [bulletLeft, bulletRight]; + ax.setScale(); + + vals = Axes.calcTicks(ax); + transFn = Axes.makeTransFn(ax); + tickSign = Axes.getTickSigns(ax)[2]; + + shift = size.t + size.h; + if(ax.visible) { + Axes.drawTicks(gd, ax, { + vals: ax.ticks === 'inside' ? Axes.clipEnds(ax, vals) : vals, + layer: bulletaxis, + path: Axes.makeTickPath(ax, shift, tickSign), + transFn: transFn + }); + + Axes.drawLabels(gd, ax, { + vals: vals, + layer: bulletaxis, + transFn: transFn, + labelFns: Axes.makeLabelFns(ax, shift) + }); + } + + function drawRect(s) { + s + .attr('width', function(d) { return Math.max(0, ax.c2p(d.range[1] - d.range[0]));}) + .attr('x', function(d) { return ax.c2p(d.range[0]);}) + .attr('y', function(d) { return 0.5 * (1 - d.thickness) * bulletHeight;}) + .attr('height', function(d) { return d.thickness * bulletHeight; }); + } + + // Draw bullet background, steps + var boxes = [gaugeBg].concat(trace.gauge.steps); + var bgBullet = bullet.selectAll('g.bg-bullet').data(boxes); + bgBullet.enter().append('g').classed('bg-bullet', true).append('rect'); + bgBullet.select('rect') + .call(drawRect) + .call(styleShape); + bgBullet.exit().remove(); + + // Draw value bar with transitions + var fgBullet = bullet.selectAll('g.value-bullet').data([trace.gauge.bar]); + fgBullet.enter().append('g').classed('value-bullet', true).append('rect'); + fgBullet.select('rect') + .attr('height', innerBulletHeight) + .attr('y', (bulletHeight - innerBulletHeight) / 2) + .call(styleShape); + if(hasTransition) { + fgBullet.select('rect') + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each('end', function() { onComplete && onComplete(); }) + .each('interrupt', function() { onComplete && onComplete(); }) + .attr('width', Math.max(0, ax.c2p(Math.min(trace.gauge.axis.range[1], cd[0].y)))); + } else { + fgBullet.select('rect') + .attr('width', Math.max(0, ax.c2p(Math.min(trace.gauge.axis.range[1], cd[0].y)))); + } + fgBullet.exit().remove(); + + var data = cd.filter(function() {return trace.gauge.threshold.value;}); + var threshold = bullet.selectAll('g.threshold-bullet').data(data); + threshold.enter().append('g').classed('threshold-bullet', true).append('line'); + threshold.select('line') + .attr('x1', ax.c2p(trace.gauge.threshold.value)) + .attr('x2', ax.c2p(trace.gauge.threshold.value)) + .attr('y1', (1 - trace.gauge.threshold.thickness) / 2 * bulletHeight) + .attr('y2', (1 - (1 - trace.gauge.threshold.thickness) / 2) * bulletHeight) + .call(Color.stroke, trace.gauge.threshold.line.color) + .style('stroke-width', trace.gauge.threshold.line.width); + threshold.exit().remove(); + + var bulletOutline = bullet.selectAll('g.gauge-outline').data([gaugeOutline]); + bulletOutline.enter().append('g').classed('gauge-outline', true).append('rect'); + bulletOutline.select('rect') + .call(drawRect) + .call(styleShape); + bulletOutline.exit().remove(); +} + +function drawAngularGauge(gd, plotGroup, cd, gaugeOpts) { + var trace = cd[0].trace; + + var size = gaugeOpts.size; + var radius = gaugeOpts.radius; + var innerRadius = gaugeOpts.innerRadius; + var gaugeBg = gaugeOpts.gaugeBg; + var gaugeOutline = gaugeOpts.gaugeOutline; + var gaugePosition = [size.l + size.w / 2, size.t + size.h / 2 + radius / 2]; + var angularGauge = gaugeOpts.angularGauge; + var angularaxisLayer = gaugeOpts.angularaxisLayer; + + var hasTransition = gaugeOpts.hasTransition; + var transitionOpts = gaugeOpts.transitionOpts; + var onComplete = gaugeOpts.onComplete; + + // circular gauge + var theta = Math.PI / 2; + function valueToAngle(v) { + var min = trace.gauge.axis.range[0]; + var max = trace.gauge.axis.range[1]; + var angle = (v - min) / (max - min) * Math.PI - theta; + if(angle < -theta) return -theta; + if(angle > theta) return theta; + return angle; + } + + function arcPathGenerator(size) { + return d3.svg.arc() + .innerRadius((innerRadius + radius) / 2 - size / 2 * (radius - innerRadius)) + .outerRadius((innerRadius + radius) / 2 + size / 2 * (radius - innerRadius)) + .startAngle(-theta); + } + + function drawArc(p) { + p + .attr('d', function(d) { + return arcPathGenerator(d.thickness) + .startAngle(valueToAngle(d.range[0])) + .endAngle(valueToAngle(d.range[1]))(); + }); + } + + // preparing axis + var ax, vals, transFn, tickSign; + var opts = trace.gauge.axis; + + // Enter gauge and axis + angularGauge.enter().append('g').classed('angular', true); + angularGauge.attr('transform', strTranslate(gaugePosition[0], gaugePosition[1])); + + angularaxisLayer.enter().append('g') + .classed('angularaxis', true) + .classed('crisp', true); + angularaxisLayer.selectAll('g.' + 'xangularaxis' + 'tick,path,text').remove(); + + ax = mockAxis(gd, opts); + ax.type = 'linear'; + ax.range = trace.gauge.axis.range; + ax._id = 'xangularaxis'; // or 'y', but I don't think this makes a difference here + ax.setScale(); + + // 't'ick to 'g'eometric radians is used all over the place here + var t2g = function(d) { + return (ax.range[0] - d.x) / (ax.range[1] - ax.range[0]) * Math.PI + Math.PI; + }; + + var labelFns = {}; + var out = Axes.makeLabelFns(ax, 0); + var labelStandoff = out.labelStandoff; + labelFns.xFn = function(d) { + var rad = t2g(d); + return Math.cos(rad) * labelStandoff; + }; + labelFns.yFn = function(d) { + var rad = t2g(d); + var ff = Math.sin(rad) > 0 ? 0.2 : 1; + return -Math.sin(rad) * (labelStandoff + d.fontSize * ff) + + Math.abs(Math.cos(rad)) * (d.fontSize * MID_SHIFT); + }; + labelFns.anchorFn = function(d) { + var rad = t2g(d); + var cos = Math.cos(rad); + return Math.abs(cos) < 0.1 ? + 'middle' : + (cos > 0 ? 'start' : 'end'); + }; + labelFns.heightFn = function(d, a, h) { + var rad = t2g(d); + return -0.5 * (1 + Math.sin(rad)) * h; + }; + var _transFn = function(rad) { + return strTranslate( + gaugePosition[0] + radius * Math.cos(rad), + gaugePosition[1] - radius * Math.sin(rad) + ); + }; + transFn = function(d) { + return _transFn(t2g(d)); + }; + var transFn2 = function(d) { + var rad = t2g(d); + return _transFn(rad) + 'rotate(' + -rad2deg(rad) + ')'; + }; + vals = Axes.calcTicks(ax); + tickSign = Axes.getTickSigns(ax)[2]; + if(ax.visible) { + tickSign = ax.ticks === 'inside' ? -1 : 1; + var pad = (ax.linewidth || 1) / 2; + Axes.drawTicks(gd, ax, { + vals: vals, + layer: angularaxisLayer, + path: 'M' + (tickSign * pad) + ',0h' + (tickSign * ax.ticklen), + transFn: transFn2 + }); + Axes.drawLabels(gd, ax, { + vals: vals, + layer: angularaxisLayer, + transFn: transFn, + labelFns: labelFns + }); + } + + // Draw background + steps + var arcs = [gaugeBg].concat(trace.gauge.steps); + var bgArc = angularGauge.selectAll('g.bg-arc').data(arcs); + bgArc.enter().append('g').classed('bg-arc', true).append('path'); + bgArc.select('path').call(drawArc).call(styleShape); + bgArc.exit().remove(); + + // Draw foreground with transition + var valueArcPathGenerator = arcPathGenerator(trace.gauge.bar.thickness); + var valueArc = angularGauge.selectAll('g.value-arc').data([trace.gauge.bar]); + valueArc.enter().append('g').classed('value-arc', true).append('path'); + var valueArcPath = valueArc.select('path'); + if(hasTransition) { + valueArcPath + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each('end', function() { trace._lastValue = cd[0].y; onComplete && onComplete(); }) + .each('interrupt', function() { onComplete && onComplete(); }) + .attrTween('d', arcTween(valueArcPathGenerator, valueToAngle(cd[0].lastY), valueToAngle(cd[0].y))); + } else { + valueArcPath + .attr('d', valueArcPathGenerator.endAngle(valueToAngle(cd[0].y))); + } + valueArcPath.call(styleShape); + valueArc.exit().remove(); + + // Draw threshold + arcs = []; + var v = trace.gauge.threshold.value; + if(v) { + arcs.push({ + range: [v, v], + color: trace.gauge.threshold.color, + line: { + color: trace.gauge.threshold.line.color, + width: trace.gauge.threshold.line.width + }, + thickness: trace.gauge.threshold.thickness + }); + } + var thresholdArc = angularGauge.selectAll('g.threshold-arc').data(arcs); + thresholdArc.enter().append('g').classed('threshold-arc', true).append('path'); + thresholdArc.select('path').call(drawArc).call(styleShape); + thresholdArc.exit().remove(); + + // Draw border last + var gaugeBorder = angularGauge.selectAll('g.gauge-outline').data([gaugeOutline]); + gaugeBorder.enter().append('g').classed('gauge-outline', true).append('path'); + gaugeBorder.select('path').call(drawArc).call(styleShape); + gaugeBorder.exit().remove(); +} + +function drawNumbers(gd, plotGroup, cd, opts) { + var trace = cd[0].trace; + var numbersX = opts.numbersX; + var numbersY = opts.numbersY; + var numbersAnchor = anchor[trace.align || 'center']; + + var hasTransition = opts.hasTransition; + var transitionOpts = opts.transitionOpts; + var onComplete = opts.onComplete; + + var bignumberFontSize, deltaFontSize; + if(trace._hasNumber) bignumberFontSize = trace.number.font.size; + if(trace._hasDelta) deltaFontSize = trace.delta.font.size; + + // Position delta relative to bignumber + var deltaDy = 0; + var deltaX = 0; + var bignumberY = 0; + + if(trace._hasDelta && trace._hasNumber) { + if(trace.delta.position === 'bottom') { + deltaDy = deltaFontSize * 1.5; + } + if(trace.delta.position === 'top') { + deltaDy = -bignumberFontSize + MID_SHIFT * deltaFontSize; + } + if(trace.delta.position === 'right') { + deltaX = undefined; + } + if(trace.delta.position === 'left') { + deltaX = undefined; + bignumberY = MID_SHIFT * bignumberFontSize / 2; + } + } + deltaDy -= MID_SHIFT * deltaFontSize; + + var numbers = Lib.ensureSingle(plotGroup, 'text', 'numbers'); + + var data = []; + if(trace._hasNumber) data.push('number'); + if(trace._hasDelta) { + data.push('delta'); + if(trace.delta.position === 'left') data.reverse(); + } + var sel = numbers.selectAll('tspan').data(data); + sel.enter().append('tspan'); + sel + .attr('text-anchor', function() {return numbersAnchor;}) + .attr('class', function(d) { return d;}) + .attr('dx', function(d, i) { + // Add padding to the second tspan when it's a one-liner + if(i === 1) { + var pos = trace.delta.position; + if(pos === 'left' || pos === 'right') return 10; + } + return null; + }); + sel.exit().remove(); + + function drawBignumber() { + // bignumber + var bignumberAx = mockAxis(gd, {tickformat: trace.number.valueformat}); + var fmt = function(v) { return Axes.tickText(bignumberAx, v).text;}; + var bignumberSuffix = trace.number.suffix; + var bignumberPrefix = trace.number.prefix; + + var number = numbers.select('tspan.number'); + number + .call(Drawing.font, trace.number.font) + .attr('x', null) + .attr('dy', bignumberY); + + if(hasTransition) { + number + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each('end', function() { trace._lastValue = cd[0].y; onComplete && onComplete(); }) + .each('interrupt', function() { onComplete && onComplete(); }) + .attrTween('text', function() { + var that = d3.select(this); + var interpolator = d3.interpolateNumber(cd[0].lastY, cd[0].y); + return function(t) { + that.text(bignumberPrefix + fmt(interpolator(t)) + bignumberSuffix); + }; + }); + } else { + number.text(bignumberPrefix + fmt(cd[0].y) + bignumberSuffix); + } + } + + function drawDelta() { + // delta + var deltaAx = mockAxis(gd, {tickformat: trace.delta.valueformat}); + var deltaFmt = function(v) { return Axes.tickText(deltaAx, v).text;}; + if(!trace._deltaLastValue) trace._deltaLastValue = 0; + var deltaValue = function(d) { + var value = trace.delta.relative ? d.relativeDelta : d.delta; + return value; + }; + var deltaFormatText = function(value) { + if(value === 0) return '-'; + return (value > 0 ? trace.delta.increasing.symbol : trace.delta.decreasing.symbol) + deltaFmt(value); + }; + var deltaFill = function(d) { + return d.delta >= 0 ? trace.delta.increasing.color : trace.delta.decreasing.color; + }; + var delta = numbers.select('tspan.delta'); + delta + .call(Drawing.font, trace.delta.font) + .call(Color.fill, deltaFill(cd[0])) + .attr('x', deltaX) + .attr('dy', deltaDy); + + if(hasTransition) { + delta + .transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each('end', function(d) { trace._deltaLastValue = deltaValue(d); onComplete && onComplete(); }) + .each('interrupt', function() { onComplete && onComplete(); }) + .attrTween('text', function() { + var that = d3.select(this); + var to = deltaValue(cd[0]); + var from = trace._deltaLastValue; + var interpolator = d3.interpolateNumber(from, to); + return function(t) { + that.text(deltaFormatText(interpolator(t))); + }; + }); + } else { + delta.text(function() { + return deltaFormatText(deltaValue(cd[0])); + }); + } + } + + if(trace._hasDelta) drawDelta(); + if(trace._hasNumber) drawBignumber(); + + // Resize numbers to fit within space and position + numbers.attr('transform', function() { + var m = opts.numbersScaler(numbers); + var key = m[2]; + if(!(trace._numbersScale && trace._numbersScale.key === key)) { + trace._numbersScale = {key: key, value: 1}; + } + var scaleRatio = trace._numbersScale.value = Math.min(trace._numbersScale.value, m[0]); + var numbersbBox = m[1]; + var translateY; + if(!trace._scaleNumbers) scaleRatio = 1; + if(trace._isAngular) { + // align vertically to bottom + translateY = numbersY - scaleRatio * numbersbBox.bottom; + } else { + // align vertically to center + translateY = numbersY - scaleRatio * (numbersbBox.top + numbersbBox.bottom) / 2; + } + + // Stash the top position of numbersbBox for title positioning + trace._numbersTop = scaleRatio * (numbersbBox.top) + translateY; + + return strTranslate(numbersX, translateY) + ' scale(' + scaleRatio + ')'; + }); +} + +// Apply fill, stroke, stroke-width to SVG shape +function styleShape(p) { + p + .each(function(d) { Color.stroke(d3.select(this), d.line.color);}) + .each(function(d) { Color.fill(d3.select(this), d.color);}) + .style('stroke-width', function(d) { return d.line.width;}); +} + +// Returns a tween for a transition’s "d" attribute, transitioning any selected +// arcs from their current angle to the specified new angle. +function arcTween(arc, endAngle, newAngle) { + return function() { + var interpolate = d3.interpolate(endAngle, newAngle); + return function(t) { + return arc.endAngle(interpolate(t))(); + }; + }; +} + +// mocks our axis +function mockAxis(gd, opts, zrange) { + var fullLayout = gd._fullLayout; + + var axisIn = { + visible: opts.visible, + type: 'linear', + ticks: 'outside', + range: zrange, + tickmode: opts.tickmode, + nticks: opts.nticks, + tick0: opts.tick0, + dtick: opts.dtick, + tickvals: opts.tickvals, + ticktext: opts.ticktext, + ticklen: opts.ticklen, + tickwidth: opts.tickwidth, + tickcolor: opts.tickcolor, + showticklabels: opts.showticklabels, + tickfont: opts.tickfont, + tickangle: opts.tickangle, + tickformat: opts.tickformat, + exponentformat: opts.exponentformat, + separatethousands: opts.separatethousands, + showexponent: opts.showexponent, + showtickprefix: opts.showtickprefix, + tickprefix: opts.tickprefix, + showticksuffix: opts.showticksuffix, + ticksuffix: opts.ticksuffix, + title: opts.title, + showline: true + }; + + var axisOut = { + type: 'linear', + _id: 'x' + opts._id + }; + + var axisOptions = { + letter: 'x', + font: fullLayout.font, + noHover: true, + noTickson: true + }; + + function coerce(attr, dflt) { + return Lib.coerce(axisIn, axisOut, axisLayoutAttrs, attr, dflt); + } + + handleAxisDefaults(axisIn, axisOut, coerce, axisOptions, fullLayout); + handleAxisPositionDefaults(axisIn, axisOut, coerce, axisOptions); + + return axisOut; +} + +function strTranslate(x, y) { + return 'translate(' + x + ',' + y + ')'; +} + +function fitTextInsideBox(el, width, height) { + // compute scaling ratio to have text fit within specified width and height + var textBB = Drawing.bBox(el.node()); + var ratio = Math.min(width / textBB.width, height / textBB.height); + return [ratio, textBB, width + 'x' + height]; +} + +function fitTextInsideCircle(el, radius) { + // compute scaling ratio to have text fit within specified radius + var textBB = Drawing.bBox(el.node()); + var elRadius = Math.sqrt((textBB.width / 2) * (textBB.width / 2) + textBB.height * textBB.height); + var ratio = radius / elRadius; + return [ratio, textBB, radius]; +} diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index c2051f24a2c..5a6167ca732 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -13,9 +13,10 @@ var extendFlat = require('../../lib').extendFlat; var scatterAttrs = require('../scatter/attributes'); var dash = require('../../components/drawing/attributes').dash; var fxAttrs = require('../../components/fx/attributes'); +var delta = require('../../constants/delta.js'); -var INCREASING_COLOR = '#3D9970'; -var DECREASING_COLOR = '#FF4136'; +var INCREASING_COLOR = delta.INCREASING.COLOR; +var DECREASING_COLOR = delta.DECREASING.COLOR; var lineAttrs = scatterAttrs.line; diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js index 8243cacc6a8..8c88b743f7e 100644 --- a/src/traces/ohlc/hover.js +++ b/src/traces/ohlc/hover.js @@ -13,10 +13,11 @@ var Lib = require('../../lib'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); var fillText = require('../../lib').fillText; +var delta = require('../../constants/delta.js'); var DIRSYMBOL = { - increasing: '▲', - decreasing: '▼' + increasing: delta.INCREASING.SYMBOL, + decreasing: delta.DECREASING.SYMBOL }; function hoverPoints(pointData, xval, yval, hovermode) { diff --git a/src/traces/waterfall/defaults.js b/src/traces/waterfall/defaults.js index b56e488a115..cbff5eb6427 100644 --- a/src/traces/waterfall/defaults.js +++ b/src/traces/waterfall/defaults.js @@ -15,9 +15,10 @@ var handleText = require('../bar/defaults').handleText; var handleXYDefaults = require('../scatter/xy_defaults'); var attributes = require('./attributes'); var Color = require('../../components/color'); +var delta = require('../../constants/delta.js'); -var INCREASING_COLOR = '#3D9970'; -var DECREASING_COLOR = '#FF4136'; +var INCREASING_COLOR = delta.INCREASING.COLOR; +var DECREASING_COLOR = delta.DECREASING.COLOR; var TOTALS_COLOR = '#4499FF'; function handleDirection(coerce, direction, defaultColor) { diff --git a/src/traces/waterfall/hover.js b/src/traces/waterfall/hover.js index 477c59b8bbb..ab82bab9215 100644 --- a/src/traces/waterfall/hover.js +++ b/src/traces/waterfall/hover.js @@ -11,10 +11,11 @@ var hoverLabelText = require('../../plots/cartesian/axes').hoverLabelText; var opacity = require('../../components/color').opacity; var hoverOnBars = require('../bar/hover').hoverOnBars; +var delta = require('../../constants/delta.js'); var DIRSYMBOL = { - increasing: '▲', - decreasing: '▼' + increasing: delta.INCREASING.SYMBOL, + decreasing: delta.DECREASING.SYMBOL }; module.exports = function hoverPoints(pointData, xval, yval, hovermode) { diff --git a/test/image/baselines/gl3d_indicator_scatter3d.png b/test/image/baselines/gl3d_indicator_scatter3d.png new file mode 100644 index 00000000000..9738f8f4d46 Binary files /dev/null and b/test/image/baselines/gl3d_indicator_scatter3d.png differ diff --git a/test/image/baselines/indicator_bignumber.png b/test/image/baselines/indicator_bignumber.png new file mode 100644 index 00000000000..5fa8ed5f271 Binary files /dev/null and b/test/image/baselines/indicator_bignumber.png differ diff --git a/test/image/baselines/indicator_bullet.png b/test/image/baselines/indicator_bullet.png new file mode 100644 index 00000000000..ad4847f8a2f Binary files /dev/null and b/test/image/baselines/indicator_bullet.png differ diff --git a/test/image/baselines/indicator_datacard.png b/test/image/baselines/indicator_datacard.png new file mode 100644 index 00000000000..27378fcf290 Binary files /dev/null and b/test/image/baselines/indicator_datacard.png differ diff --git a/test/image/baselines/indicator_datacard2.png b/test/image/baselines/indicator_datacard2.png new file mode 100644 index 00000000000..22648e8179b Binary files /dev/null and b/test/image/baselines/indicator_datacard2.png differ diff --git a/test/image/baselines/indicator_gauge.png b/test/image/baselines/indicator_gauge.png new file mode 100644 index 00000000000..6f588335d61 Binary files /dev/null and b/test/image/baselines/indicator_gauge.png differ diff --git a/test/image/baselines/indicator_grid_template.png b/test/image/baselines/indicator_grid_template.png new file mode 100644 index 00000000000..1ff32b2836b Binary files /dev/null and b/test/image/baselines/indicator_grid_template.png differ diff --git a/test/image/baselines/indicator_scatter.png b/test/image/baselines/indicator_scatter.png new file mode 100644 index 00000000000..29bb15d6292 Binary files /dev/null and b/test/image/baselines/indicator_scatter.png differ diff --git a/test/image/mocks/gl3d_indicator_scatter3d.json b/test/image/mocks/gl3d_indicator_scatter3d.json new file mode 100644 index 00000000000..af6a5791487 --- /dev/null +++ b/test/image/mocks/gl3d_indicator_scatter3d.json @@ -0,0 +1,52 @@ +{ + "data": [{ + "domain": { + "y": [0, 1], + "x": [0.25, 0.75] + }, + "title": {"text": "Sensor"}, + "type": "indicator", + "mode": "number+delta", + "delta": {"reference": 9}, + "ticker": { + "showticker": true, + "showpercentage": true + }, + "value": 10 + }, { + "type": "scatter3d", + "name": "Sensor", + "z":[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "x":[0, 1, 3, 5, 7, 9, 12, 15, 13, 15], + "y":[50, 32, 12, 78, 65, 54, 69, 23, 32, 100], + "marker": { + "opacity": 1, + "color": "rgba(125, 125, 125, 0.1)" + } + }], + "layout": { + "width": 400, + "height": 300, + "scene": { + "camera": { + "eye": {"x":-1.172852145187388,"y":1.8080244316159928,"z":0.20728121045818126} + } + }, + "font": { + "color": "white", + "family": "Dosis" + }, + "paper_bgcolor": "black", + "plot_bgcolor": "black", + "margin": { + "t": 25, + "r": 25, + "l": 25, + "b": 25 + }, + "autosize": true + }, + "config": { + "responsive": true + } +} diff --git a/test/image/mocks/indicator_bignumber.json b/test/image/mocks/indicator_bignumber.json new file mode 100644 index 00000000000..59876ba35cf --- /dev/null +++ b/test/image/mocks/indicator_bignumber.json @@ -0,0 +1,191 @@ +{ + "data": [{ + "domain": { + "x": [0, 0.33], + "y": [0, 0.33] + }, + "title": {"text": "Accounts"}, + "type": "indicator", + "mode": "number+delta", + "delta": { + "increasing": { + "color": "green", + "symbol": "+" + }, + "valueformat": ".1%", + "reference": 400, + "relative": true + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 450 + }, { + "domain": { + "x": [0, 0.33], + "y": [0.33, 0.66] + }, + "title": {"text": "Accounts"}, + "type": "indicator", + "mode": "delta", + "delta": { + "reference": 400, + "relative": true + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 350 + }, { + "domain": { + "x": [0, 0.33], + "y": [0.66, 1.0] + }, + "title": {"text": "Accounts"}, + "type": "indicator", + "mode": "number", + "delta": { + "reference": 400, + "relative": true + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 430 + }, { + "domain": { + "x": [0.33, 0.66], + "y": [0, 0.66] + }, + "title": {"text": "Accounts"}, + "type": "indicator", + "mode": "number+delta", + "delta": { + "reference": 400, + "relative": true, + "position": "top" + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 450 + }, { + "domain": { + "x": [0.33, 0.66], + "y": [0.66, 1.0] + }, + "title": {"text": "Accounts"}, + "type": "indicator", + "mode": "number+delta", + "delta": { + "reference": 400, + "relative": true + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 350 + }, { + "domain": { + "x": [0.66, 1], + "y": [0, 1] + }, + "title": {"text": "Accounts
Subtitle
Subsubtitle"}, + "type": "indicator", + "mode": "number+delta", + "delta": { + "reference": 400, + "relative": true + }, + "gauge": { + "bgcolor": "rgba(255, 255, 255, 0.25)" + }, + "value": 450 + } + ], + "layout": { + "width": 750, + "height": 500, + "paper_bgcolor": "black", + "font": { + "color": "white", + "family": "Dosis" + }, + "margin": { + "t": 25, + "r": 25, + "l": 25, + "b": 25 + }, + "shapes": [{ + "type": "rect", + "x0": 0, + "x1": 0.33, + "y0": 0.66, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0, + "x1": 0.33, + "y0": 0.33, + "y1": 0.66, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0, + "x1": 0.33, + "y0": 0, + "y1": 0.33, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.33, + "x1": 0.66, + "y0": 0.66, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.33, + "x1": 0.66, + "y0": 0, + "y1": 0.66, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.66, + "x1": 1.0, + "y0": 0, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }] + }, + "config": { + "responsive": true + } +} diff --git a/test/image/mocks/indicator_bullet.json b/test/image/mocks/indicator_bullet.json new file mode 100644 index 00000000000..db0145d2dfe --- /dev/null +++ b/test/image/mocks/indicator_bullet.json @@ -0,0 +1,168 @@ +{ + "data": [{ + "domain": { + "x": [0.25, 1], + "y": [0.0833, 0.25] + }, + "title": { + "text": "Revenue
U.S. $ (1,000s)", + "font": {"size": 14} + }, + "type": "indicator", + "mode": "number+gauge+delta", + "delta": {"reference": 200, "position": "right"}, + "gauge": { + "shape": "bullet", + "axis": { + "range": [null, 300] + }, + "threshold": { + "line": { + "color": "black", + "width": 2 + }, + "thickness": 0.75, + "value": 170 + }, + "bgcolor": "rgba(0, 0, 0, 0.1)", + "borderwidth": 2, + "bordercolor": "rgba(0, 0, 0, 0.1)", + "steps": [{ + "range": [0, 150], + "color": "rgba(0, 0, 0, 0.5)" + }, { + "range": [150, 250], + "color": "rgba(0, 0, 0, 0.25)" + }], + "bar": { + "color": "rgba(0, 0, 0, 1)" + } + }, + "value": 180 + }, { + "domain": { + "x": [0.25, 1], + "y": [0.4166, 0.5833] + }, + "title": {"text": "Profit", "font": {"size": 14}}, + "type": "indicator", + "mode": "number+gauge+delta", + "delta": {"reference": 200, "position": "bottom"}, + "target": 75, + "gauge": { + "shape": "bullet", + "axis": { + "range": [null, 100] + }, + "threshold": { + "line": { + "color": "black", + "width": 2 + }, + "thickness": 0.75, + "value": 50 + }, + "bgcolor": "rgba(0, 0, 0, 0.1)", + "borderwidth": 2, + "bordercolor": "rgba(0, 0, 0, 0.1)", + "steps": [{ + "range": [0, 25], + "color": "rgba(0, 0, 0, 0.5)" + }, { + "range": [25, 75], + "color": "rgba(0, 0, 0, 0.25)" + }], + "bar": { + "color": "rgba(0, 0, 0, 1)" + } + }, + "value": 35 + }, { + "domain": { + "x": [0.25, 1], + "y": [0.7499, 0.9166] + }, + "title": { + "text" :"Avg Order Size
U.S. $", + "font": {"size": 14} + }, + "type": "indicator", + "mode": "number+gauge+delta", + "delta": {"reference": 200, "position": "top"}, + "gauge": { + "shape": "bullet", + "axis": { + "range": [null, 300] + }, + "threshold": { + "line": { + "color": "black", + "width": 2 + }, + "thickness": 0.75, + "value": 210 + }, + "bgcolor": "rgba(0, 0, 0, 0.1)", + "borderwidth": 2, + "bordercolor": "rgba(0, 0, 0, 0.1)", + "steps": [{ + "range": [0, 150], + "color": "rgba(0, 0, 0, 0.5)" + }, { + "range": [150, 250], + "color": "rgba(0, 0, 0, 0.25)" + }], + "bar": { + "color": "rgba(0, 0, 0, 1)" + } + }, + "value": 220 + }], + "layout": { + "width": 600, + "height": 250, + "margin": { + "t": 10, + "r": 25, + "l": 25, + "b": 10 + }, + "shapes": [{ + "type": "rect", + "x0": 0.25, + "x1": 1, + "y0": 0.0833, + "y1": 0.25, + "line": { + "color": "rgba(0, 0, 0, 0.25)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.25, + "x1": 1, + "y0": 0.4166, + "y1": 0.5833, + "line": { + "color": "rgba(0, 0, 0, 0.25)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.25, + "x1": 1, + "y0": 0.7499, + "y1": 0.9166, + "line": { + "color": "rgba(0, 0, 0, 0.25)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }] + }, + "config": { + "responsive": true + } +} diff --git a/test/image/mocks/indicator_datacard.json b/test/image/mocks/indicator_datacard.json new file mode 100644 index 00000000000..f2a34579967 --- /dev/null +++ b/test/image/mocks/indicator_datacard.json @@ -0,0 +1,106 @@ +{ + "data": [{ + "domain": {"x": [0, 0.5], "y": [0, 0.5]}, + "type": "indicator", + "mode": "number+delta", + "number": { + "align": "middle", + "suffix": " km/h" + }, + "value": 43, + "delta": { + "position": "left", + "reference": 20 + } + }, { + "domain": {"x": [0, 0.5], "y": [0.5, 1.0]}, + "type": "indicator", + "mode": "number+delta", + "number": { + "align": "middle", + "suffix": " km/h" + }, + "value": 43, + "delta": { + "position": "bottom", + "reference": 20 + } + }, { + "domain": {"x": [0.5, 1.0], "y": [0, 0.5]}, + "type": "indicator", + "mode": "number+delta", + "number": { + "align": "middle", + "suffix": " km/h" + }, + "value": 43, + "delta": { + "position": "right", + "reference": 20 + } + }, { + "domain": {"x": [0.5, 1.0], "y": [0.5, 1.0]}, + "type": "indicator", + "mode": "number+delta", + "number": { + "align": "middle", + "suffix": " km/h" + }, + "value": 43, + "delta": { + "position": "top", + "reference": 20 + } + }], + "layout": { + "paper_bgcolor": "orange", + "width": 600, + "height": 200, + "margin": {"t": 0, "b": 0, "l": 0, "r": 0}, + "shapes": [{ + "type": "rect", + "x0": 0, + "x1": 0.5, + "y0": 0.5, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0, + "x1": 0.5, + "y0": 0, + "y1": 0.5, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.5, + "x1": 1.0, + "y0": 0.5, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.5, + "x1": 1.0, + "y0": 0, + "y1": 0.5, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }] + } +} diff --git a/test/image/mocks/indicator_datacard2.json b/test/image/mocks/indicator_datacard2.json new file mode 100644 index 00000000000..c9a53809e9c --- /dev/null +++ b/test/image/mocks/indicator_datacard2.json @@ -0,0 +1,111 @@ +{ + "data": [{ + "domain": {"x": [0, 0.5], "y": [0, 0.5]}, + "type": "indicator", + "mode": "number+delta", + "align": "left", + "number": { + "suffix": " km/h", + "font": {"size": 20} + }, + "value": 43, + "delta": { + "position": "left", + "reference": 20 + } + }, { + "domain": {"x": [0, 0.5], "y": [0.5, 1.0]}, + "type": "indicator", + "mode": "number+delta", + "align": "right", + "number": { + "prefix": "prefix: ", + "suffix": " km/h", + "font": {"size": 20} + }, + "value": 43, + "delta": { + "position": "bottom", + "reference": 20 + } + }, { + "domain": {"x": [0.5, 1.0], "y": [0, 0.5]}, + "type": "indicator", + "mode": "number+delta", + "align": "left", + "number": { + "suffix": " km/h", + "font": {"size": 20} + }, + "value": 43, + "delta": { + "position": "right", + "reference": 20 + } + }, { + "domain": {"x": [0.5, 1.0], "y": [0.5, 1.0]}, + "type": "indicator", + "mode": "number+delta", + "align": "right", + "number": { + "suffix": " km/h", + "font": {"size": 20} + }, + "value": 43, + "delta": { + "position": "top", + "reference": 20 + } + }], + "layout": { + "paper_bgcolor": "orange", + "width": 400, + "height": 100, + "margin": {"t": 0, "b": 0, "l": 0, "r": 0}, + "shapes": [{ + "type": "rect", + "x0": 0, + "x1": 0.5, + "y0": 0.5, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0, + "x1": 0.5, + "y0": 0, + "y1": 0.5, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.5, + "x1": 1.0, + "y0": 0.5, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.5, + "x1": 1.0, + "y0": 0, + "y1": 0.5, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }] + } +} diff --git a/test/image/mocks/indicator_gauge.json b/test/image/mocks/indicator_gauge.json new file mode 100644 index 00000000000..9993ad95a34 --- /dev/null +++ b/test/image/mocks/indicator_gauge.json @@ -0,0 +1,252 @@ +{ + "data": [{ + "domain": { + "x": [0, 0.4], + "y": [0.5, 1] + }, + "value": 450, + "title": {"text": "Speed"}, + "type": "indicator", + "mode": "gauge+number", + "delta": {"reference": 400}, + "gauge": { + "axis": { + "range": [null, 500], + "tickwidth": 1, + "tickcolor": "white" + }, + "bgcolor": "rgba(255, 255, 255, 0.25)", + "borderwidth": 2, + "bordercolor": "rgba(255, 255, 255, 0.5)", + "steps": [{ + "range": [0, 250], + "color": "rgba(255, 255, 0, 0.5)" + }, { + "range": [250, 400], + "color": "rgba(0, 0, 255, 0.75)" + }], + "threshold": { + "line": { + "color": "rgba(255, 0, 0, 1)", + "width": 4 + }, + "thickness": 0.75, + "value": 490 + } + } + }, { + "domain": { + "x": [0.474, 0.789], + "y": [0.5, 0.83333] + }, + "value": 450, + "title": {"text": "Speed
Subtitle"}, + "type": "indicator", + "mode": "gauge+delta", + "delta": {"reference": 400}, + "gauge": { + "axis": { + "range": [null, 500], + "tickwidth": 1, + "tickcolor": "white" + }, + "bgcolor": "rgba(255, 255, 255, 0.25)", + "borderwidth": 2, + "bordercolor": "rgba(255, 255, 255, 0.5)", + "steps": [{ + "range": [0, 250], + "color": "rgba(255, 255, 0, 0.5)" + }, { + "range": [250, 400], + "color": "rgba(0, 0, 255, 0.75)" + }], + "threshold": { + "line": { + "color": "rgba(255, 0, 0, 1)", + "width": 4 + }, + "thickness": 0.75, + "value": 490 + } + } + }, { + "domain": { + "x": [0, 0.789], + "y": [0, 0.4] + }, + "value": 240, + "title": {"text": "Speed"}, + "type": "indicator", + "mode": "gauge+number+delta", + "delta": {"reference": 400}, + "gauge": { + "axis": { + "range": [null, 500], + "tickwidth": 1, + "tickcolor": "white" + }, + "bgcolor": "rgba(255, 255, 255, 0.25)", + "borderwidth": 2, + "bordercolor": "rgba(255, 255, 255, 0.5)", + "steps": [{ + "range": [0, 250], + "color": "rgba(255, 255, 0, 0.5)" + }, { + "range": [250, 400], + "color": "rgba(0, 0, 255, 0.5)" + }], + "threshold": { + "line": { + "color": "rgba(255, 0, 0, 1)", + "width": 4 + }, + "thickness": 0.75, + "value": 490 + } + } + }, { + "domain": { + "x": [0.789, 1], + "y": [0.75, 1] + }, + "value": 450, + "title": {"text": "Speed"}, + "type": "indicator", + "mode": "gauge+number+delta", + "delta": {"reference": 400}, + "gauge": { + "axis": { + "range": [null, 500], + "tickwidth": 1, + "tickcolor": "white" + }, + "bgcolor": "rgba(255, 255, 255, 0.25)", + "borderwidth": 2, + "bordercolor": "rgba(255, 255, 255, 0.5)", + "steps": [{ + "range": [0, 250], + "color": "rgba(255, 255, 0, 0.5)" + }, { + "range": [250, 400], + "color": "rgba(0, 0, 255, 0.75)" + }], + "threshold": { + "line": { + "color": "rgba(255, 0, 0, 1)", + "width": 4 + }, + "thickness": 0.75, + "value": 490 + } + } + },{ + "domain": { + "x": [0.789, 1], + "y": [0, 0.75] + }, + "value": 450, + "title": {"text": "Speed"}, + "type": "indicator", + "mode": "gauge+number+delta", + "delta": {"reference": 400, "position": "top"}, + "gauge": { + "axis": { + "range": [null, 500], + "tickwidth": 1, + "tickcolor": "white" + }, + "bgcolor": "rgba(255, 255, 255, 0.25)", + "borderwidth": 2, + "bordercolor": "rgba(255, 255, 255, 0.5)", + "steps": [{ + "range": [0, 250], + "color": "rgba(255, 255, 0, 0.5)" + }, { + "range": [250, 400], + "color": "rgba(0, 0, 255, 0.75)" + }], + "threshold": { + "line": { + "color": "rgba(255, 0, 0, 1)", + "width": 4 + }, + "thickness": 0.75, + "value": 410 + } + } + }], + "layout": { + "width": 900, + "height": 600, + "paper_bgcolor": "black", + "margin": { + "t": 25, + "r": 25, + "l": 25, + "b": 25 + }, + "font": { + "color": "white", + "family": "Arial" + }, + "shapes": [{ + "type": "rect", + "x0": 0, + "x1": 0.4, + "y0": 0.5, + "y1": 1, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }, { + "type": "rect", + "x0": 0.474, + "x1": 0.789, + "y0": 0.5, + "y1": 0.8333, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + },{ + "type": "rect", + "x0": 0.789, + "x1": 1.0, + "y0": 0.75, + "y1": 1.0, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + },{ + "type": "rect", + "x0": 0.789, + "x1": 1.0, + "y0": 0, + "y1": 0.75, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + },{ + "type": "rect", + "x0": 0, + "x1": 0.789, + "y0": 0, + "y1": 0.4, + "line": { + "color": "rgba(255, 255, 255, 0.35)", + "width": 1 + }, + "fill": "rgba(0,0,0,0)" + }] + }, + "config": { + "responsive": true + } +} diff --git a/test/image/mocks/indicator_grid_template.json b/test/image/mocks/indicator_grid_template.json new file mode 100644 index 00000000000..884c0303afb --- /dev/null +++ b/test/image/mocks/indicator_grid_template.json @@ -0,0 +1,39 @@ +{ + "data": [{ + "type": "indicator", + "value": 120, + "delta": {"reference": 60}, + "gauge": {"axis": {"visible": false}}, + "domain": {"row": 0, "column": 0} + }, { + "type": "indicator", + "value": 120, + "gauge": {"shape": "bullet", "axis": {"visible": false}}, + "domain": {"x": [0.05, 0.5], "y": [0.15, 0.35]} + }, { + "type": "indicator", + "mode": "number+delta", + "value": 120, + "domain": {"row": 0, "column": 1} + }, { + "type": "indicator", + "mode": "delta", + "value": 40, + "domain": {"row": 1, "column": 1} + }], + "layout": { + "width": 700, + "height": 400, + "margin": {"t": 25, "b": 25, "l": 25, "r": 25}, + "grid": {"rows": 2, "columns": 2, "pattern": "independent"}, + "template": { + "data": { + "indicator": [{ + "title": {"text": "Title"}, + "mode": "number+delta+gauge", + "delta": {"reference": 60} + }] + } + } + } +} diff --git a/test/image/mocks/indicator_scatter.json b/test/image/mocks/indicator_scatter.json new file mode 100644 index 00000000000..21262992212 --- /dev/null +++ b/test/image/mocks/indicator_scatter.json @@ -0,0 +1,51 @@ +{ + "data": [{ + "domain": { + "y": [0, 1], + "x": [0.25, 0.75] + }, + "title": {"text": "Users online"}, + "type": "indicator", + "mode": "number+delta", + "delta": {"reference": 512, "valueformat": ".0f"}, + "ticker": { + "showticker": true + }, + "vmax": 500, + "value": 492 + }, { + "name": "Users online", + "y": [325, 324, 405, 400, 424, 404, 417, 432, 419, 394, 410, 426, 413, 419, 404, 408, 401, 377, 368, 361, 356, 359, 375, 397, 394, 418, 437, 450, 430, 442, 424, 443, 420, 418, 423, 423, 426, 440, 437, 436, 447, 460, 478, 472, 450, 456, 436, 418, 429, 412, 429, 442, 464, 447, 434, 457, 474, 480, 499, 497, 480, 502, 512, 492], + "marker": { + "color": "rgba(255, 255, 255, 0.5)" + } + }], + "layout": { + "width": 400, + "height": 300, + "xaxis": { + "autorange": false, + "range": [0, 62], + "visible": false + }, + "yaxis": { + "visible": false + }, + "font": { + "color": "white", + "family": "Dosis" + }, + "paper_bgcolor": "black", + "plot_bgcolor": "black", + "margin": { + "t": 25, + "r": 25, + "l": 25, + "b": 25 + }, + "autosize": true + }, + "config": { + "responsive": true + } +} diff --git a/test/jasmine/tests/indicator_test.js b/test/jasmine/tests/indicator_test.js new file mode 100644 index 00000000000..9b298ea32bf --- /dev/null +++ b/test/jasmine/tests/indicator_test.js @@ -0,0 +1,627 @@ +var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); +// var Lib = require('@src/lib'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var delay = require('../assets/delay'); +var failTest = require('../assets/fail_test'); +var supplyAllDefaults = require('../assets/supply_defaults'); +// var calc = require('@src/traces/indicator/calc').calc; +var customAssertions = require('../assets/custom_assertions.js'); +var indicatorAttrs = require('@src/traces/indicator/attributes.js'); +var cn = require('@src/traces/indicator/constants.js'); + +describe('Indicator defaults', function() { + function _supply(trace, layout) { + var gd = { + data: [trace], + layout: layout || {} + }; + + supplyAllDefaults(gd); + + return gd._fullData[0]; + } + + it('to number mode', function() { + var out = _supply({type: 'indicator', value: 1}); + expect(out.mode).toBe('number'); + }); + + indicatorAttrs.mode.flags.forEach(function(mode) { + it('should not coerce container ' + mode + ' if not used', function() { + var allModes = indicatorAttrs.mode.flags.slice(); + allModes.splice(allModes.indexOf(mode), 1); + var out = _supply({type: 'indicator', mode: allModes.join('+'), value: 1}); + expect(out[mode]).toBe(undefined); + }); + }); + + it('defaults to formatting numbers using SI prefix', function() { + var out = _supply({type: 'indicator', mode: 'number+delta', value: 1}); + expect(out.number.valueformat).toBe('.3s'); + expect(out.delta.valueformat).toBe('.3s'); + }); + + it('defaults to displaying relative changes in percentage', function() { + var out = _supply({type: 'indicator', mode: 'delta', delta: {relative: true}, value: 1}); + expect(out.delta.valueformat).toBe('2%'); + }); + + it('defaults delta.reference to current value', function() { + var out = _supply({type: 'indicator', mode: 'delta', value: 1}); + expect(out.delta.reference).toBe(1); + }); + + it('defaults gauge.axis.range[0] to 0', function() { + var out = _supply({type: 'indicator', mode: 'gauge', value: 1, gauge: {axis: {range: [null, 500]}}}); + expect(out.gauge.axis.range[0]).toBe(0); + }); + + it('defaults gauge.axis.range[1] to 1.5 * value', function() { + var out = _supply({type: 'indicator', mode: 'gauge', value: 100, gauge: {axis: {range: [50, null]}}}); + expect(out.gauge.axis.range[0]).toBe(50); + expect(out.gauge.axis.range[1]).toBe(150); + }); + + // text alignment + ['number'].forEach(function(mode) { + it('aligns to center', function() { + var out = _supply({ + type: 'indicator', + mode: mode, + value: 1, + gauge: {shape: 'angular'} + }); + expect(out.align).toBe('center'); + expect(out.title.align).toBe('center'); + }); + }); + + it('should NOT set number alignment when angular', function() { + var out = _supply({type: 'indicator', mode: 'number+gauge', gauge: {shape: 'angular'}, value: 1}); + expect(out.align).toBe(undefined); + expect(out.title.align).toBe('center'); + }); + + it('should NOT set title alignment when bullet', function() { + var out = _supply({type: 'indicator', mode: 'number+gauge', gauge: {shape: 'bullet'}, value: 1}); + expect(out.align).toBe('center'); + expect(out.title.align).toBe(undefined); + }); + + // font-size + it('number font size to a large value', function() { + var out = _supply({type: 'indicator', value: 1}); + expect(out.number.font.size).toBe(80); + }); + + it('delta font size to a fraction of number if present', function() { + var out = _supply({type: 'indicator', mode: 'delta+number', value: 1, number: {font: {size: 50}}}); + expect(out.number.font.size).toBe(50); + expect(out.delta.font.size).toBe(25); + }); + + it('delta font size to default number font size if no number', function() { + var out = _supply({type: 'indicator', mode: 'delta', value: 1}); + expect(out.delta.font.size).toBe(80); + }); + + it('title font size to a fraction of number font size', function() { + var out = _supply({type: 'indicator', value: 1, number: {font: {size: 50}}}); + expect(out.number.font.size).toBe(50); + expect(out.title.font.size).toBe(12.5); + }); + + it('title font size to a fraction of delta number font size', function() { + var out = _supply({type: 'indicator', mode: 'delta', value: 1, delta: {font: {size: 50}}}); + expect(out.title.font.size).toBe(12.5); + }); + + it('title font size to a fraction of default number font size if no numbers', function() { + var out = _supply({type: 'indicator', value: 1}); + expect(out.title.font.size).toBe(20); + }); + + it('will not scale numbers if either number.font.size or delta.font.size is set', function() { + var out = _supply({type: 'indicator', mode: 'number+delta', value: 1, number: {font: {size: 20}}}); + expect(out._scaleNumbers).toBe(false); + + out = _supply({type: 'indicator', mode: 'number+delta', value: 1, delta: {font: {size: 20}}}); + expect(out._scaleNumbers).toBe(false); + }); +}); + +describe('Indicator plot', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + describe('numbers', function() { + function checkNumbersScale(value, msg) { + var numbers = d3.selectAll('text.numbers'); + expect(numbers.length).toBe(1); + + var transform = numbers.attr('transform'); + expect(transform.match('scale')).toBeTruthy('cannot find scale attribute on text.numbers[0]'); + var scale = transform.match(/.*scale\((.*)\)/)[1]; + + expect(scale).toBeCloseTo(value, 1, msg); + } + + it('scale down to fit figure size', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 500, + number: {valueformat: '0.f'} + }], {width: 400, height: 400}) + .then(function() { + checkNumbersScale(1, 'initialy at normal scale'); + return Plotly.relayout(gd, {width: 200, height: 200}); + }) + .then(function() { + checkNumbersScale(0.2, 'should scale down'); + return Plotly.relayout(gd, {width: 400, height: 400}); + }) + .then(function() { + checkNumbersScale(1, 'should scale up'); + }) + .catch(failTest) + .then(done); + }); + + it('scale down but never back up if domain size is constant', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 1, + number: {valueformat: '0.f'} + }], {width: 400, height: 400}) + .then(function() { + checkNumbersScale(1, 'initialy at normal scale'); + return Plotly.restyle(gd, 'value', [1E6]); + }) + .then(function() { + checkNumbersScale(0.8, 'should scale down'); + return Plotly.restyle(gd, 'value', [1]); + }) + .then(function() { + checkNumbersScale(0.8, 'should not scale up'); + }) + .catch(failTest) + .then(done); + }); + + ['number', 'delta'].forEach(function(numberType) { + it('if ' + numberType + ' font-size is specified, never scale', function(done) { + var figure = { + type: 'indicator', + mode: 'number+delta', + value: 1, + number: {valueformat: '0.f'} + }; + figure[numberType] = {font: {size: 100}}; + Plotly.newPlot(gd, [figure], {width: 400, height: 400}) + .then(function() { + checkNumbersScale(1, 'initialy at normal scale'); + return Plotly.restyle(gd, 'value', [1E6]); + }) + .then(function() { + checkNumbersScale(1, 'should not rescale'); + return Plotly.restyle(gd, 'value', [1]); + }) + .then(function() { + checkNumbersScale(1, 'should not rescale'); + }) + .catch(failTest) + .then(done); + }); + }); + + it('always positions tspans in the right order', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 10 + }]) + .then(function() { + customAssertions.assertMultiNodeOrder(['tspan.number']); + return Plotly.restyle(gd, 'mode', 'delta'); + }) + .then(function() { + customAssertions.assertMultiNodeOrder(['tspan.delta']); + return Plotly.restyle(gd, 'mode', 'number+delta'); + }) + .then(function() { + customAssertions.assertMultiNodeOrder(['tspan.number', 'tspan.delta']); + return Plotly.restyle(gd, 'delta.position', 'left'); + }) + .then(function() { + customAssertions.assertMultiNodeOrder(['tspan.delta', 'tspan.number']); + return Plotly.restyle(gd, 'mode', 'gauge'); + }) + .then(function() { + customAssertions.assertMultiNodeOrder([]); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('number', function() { + function assertContent(txt) { + var sel = d3.selectAll('tspan.number'); + expect(sel.length).toBe(1); + expect(sel.text()).toBe(txt); + } + it('formats value via `valueformat`', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + mode: 'number', + value: 220, + }]) + .then(function() { + assertContent('220'); + return Plotly.restyle(gd, 'number.valueformat', '0.3f'); + }) + .then(function() { + assertContent('220.000'); + return Plotly.restyle(gd, 'number.valueformat', '$'); + }) + .then(function() { + assertContent('$220'); + }) + .catch(failTest) + .then(done); + }); + + it('supports suffix', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + mode: 'number', + number: {suffix: ' potatoes'}, + value: 220, + }]) + .then(function() { + assertContent('220 potatoes'); + }) + .catch(failTest) + .then(done); + }); + + it('supports prefix', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + mode: 'number', + number: {prefix: 'Speed: '}, + value: 220, + }]) + .then(function() { + assertContent('Speed: 220'); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('delta', function() { + function assertContent(txt) { + var sel = d3.selectAll('tspan.delta'); + expect(sel.length).toBe(1); + expect(sel.text()).toBe(txt); + } + it('can display relative changes', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + mode: 'number+delta', + value: 220, + delta: {reference: 200} + }], {width: 400, height: 400}) + .then(function() { + assertContent(gd._fullData[0].delta.increasing.symbol + '20.0'); + return Plotly.restyle(gd, 'delta.relative', true); + }) + .then(function() { + assertContent(gd._fullData[0].delta.increasing.symbol + '10%'); + return Plotly.restyle(gd, 'delta.valueformat', '.3f'); + }) + .then(function() { + assertContent(gd._fullData[0].delta.increasing.symbol + '0.100'); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('angular gauge', function() { + it('properly order elements', function(done) { + Plotly.newPlot(gd, {data: [{ + type: 'indicator', + mode: 'gauge', + gauge: { + shape: 'angular', + steps: [{ + range: [0, 250], + }], + threshold: { + value: 410 + } + } + }]}) + .then(function() { + customAssertions.assertMultiNodeOrder(['g.bg-arc', 'g.value-arc', 'g.threshold-arc', 'g.gauge-outline']); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('bullet gauge', function() { + it('properly order elements', function(done) { + Plotly.newPlot(gd, {data: [{ + type: 'indicator', + mode: 'gauge', + gauge: { + shape: 'bullet', + steps: [{ + range: [0, 250], + }], + threshold: { + value: 410 + } + } + }]}) + .then(function() { + customAssertions.assertMultiNodeOrder(['g.bg-bullet', 'g.value-bullet', 'g.threshold-bullet', 'g.gauge-outline']); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('title', function() { + beforeEach(function() { + // hide the div + gd.style.display = 'none'; + gd.style.top = 100; + gd.style.left = 100; + }); + + it('positions it above the numbers', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 1, + title: {text: 'Value'}, + mode: 'number' + }]) + .then(function() { + gd.style.display = 'block'; + + var t = d3.selectAll('text.title').node(); + var titleBBox = t.getBoundingClientRect(); + + var numbers = d3.selectAll('text.numbers').node(); + var numbersBBox = numbers.getBoundingClientRect(); + + expect(titleBBox.bottom).toBeCloseTo(numbersBBox.top - cn.titlePadding, 0); + }) + .catch(failTest) + .then(done); + }); + + it('position it above angular axes', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 1, + title: {text: 'Value'}, + mode: 'gauge', + gauge: {shape: 'angular'} + }]) + .then(function() { + gd.style.display = 'block'; + var t = d3.selectAll('text.title').node(); + var titleBBox = t.getBoundingClientRect(); + + var ax = d3.selectAll('g.angularaxis').node(); + var axBBox = ax.getBoundingClientRect(); + expect(titleBBox.bottom).toBeCloseTo(axBBox.top - cn.titlePadding, 0); + }) + .catch(failTest) + .then(done); + }); + + it('position it left of bullet', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 1, + title: {text: 'Value'}, + mode: 'gauge', + gauge: {shape: 'bullet'} + }]) + .then(function() { + gd.style.display = 'block'; + var t = d3.selectAll('text.title').node(); + var titleBBox = t.getBoundingClientRect(); + + var ax = d3.selectAll('g.bulletaxis').node(); + var axBBox = ax.getBoundingClientRect(); + expect(titleBBox.right < axBBox.left).toBe(true); + }) + .catch(failTest) + .then(done); + }); + }); + + it('restyle between modes', function(done) { + function assertElementCnt(sel, cnt) { + var el = d3.selectAll(sel); + expect(el.size()).toBe(cnt, 'selection "' + sel + '" does not have size ' + cnt); + } + function assertGauge(shape, cnt) { + assertElementCnt(shape, cnt); + assertElementCnt(shape + 'axis', cnt); + } + function assert(flags) { + // flags is an array denoting whether the figure [hasNumber, hasDelta, hasAngular, hasBullet] + var selector = ['tspan.number', 'tspan.delta', 'g.angular', 'g.bullet']; + [0, 1].forEach(function(i) { assertElementCnt(selector[i], flags[i]);}); + [2, 3].forEach(function(i) { assertGauge(selector[i], flags[i]);}); + + var order = selector.filter(function(sel, i) { return flags[i] !== 0;}); + customAssertions.assertMultiNodeOrder(order); + } + + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 100, + mode: 'number+delta+gauge' + }]) + .then(function() { + assert([1, 1, 1, 0]); + return Plotly.restyle(gd, 'mode', 'number+delta'); + }) + .then(function() { + assert([1, 1, 0, 0]); + return Plotly.restyle(gd, 'mode', 'number'); + }) + .then(function() { + assert([1, 0, 0, 0]); + return Plotly.restyle(gd, 'mode', 'delta'); + }) + .then(function() { + assert([0, 1, 0, 0]); + return Plotly.restyle(gd, 'mode', 'gauge'); + }) + .then(function() { + assert([0, 0, 1, 0]); + return Plotly.restyle(gd, 'gauge.shape', 'bullet'); + }) + .then(function() { + assert([0, 0, 0, 1]); + return Plotly.restyle(gd, 'mode', 'number+delta+gauge'); + }) + .then(function() { + assert([1, 1, 0, 1]); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('Indicator animations', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + [['number', ''], ['delta', ''], ['number+delta', ''], + ['gauge', 'angular'], ['gauge', 'bullet']].forEach(function(comb) { + it('should transition via `Plotly.react` in mode ' + comb[0] + ', ' + comb[1], function(done) { + var mock = {data: [{ + type: 'indicator', + mode: comb[0], + gauge: {shape: comb[1]}, + value: 100 + }], layout: {}}; + mock.layout.transition = {duration: 200}; + + spyOn(Plots, 'transitionFromReact').and.callThrough(); + + Plotly.plot(gd, mock) + .then(function() { + gd.data[0].value = '400'; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(delay(300)) + .then(function() { + expect(Plots.transitionFromReact).toHaveBeenCalledTimes(1); + }) + .catch(failTest) + .then(done); + }); + }); +}); + +describe('Indicator attributes', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + it('are inherited from template', function(done) { + Plotly.newPlot(gd, [{ + type: 'indicator', + value: 5, + mode: 'number+delta+gauge' + }], {template: { + data: { + indicator: [{ + delta: { + valueformat: '0.9f', + reference: -100, + increasing: { + symbol: 'a', + color: 'blue' + }, + font: { + family: 'ArialDelta', + size: 20 + } + }, + number: { + valueformat: '0.8f', + suffix: 'km/h', + font: { + family: 'ArialNumber', + color: 'blue' + } + }, + gauge: { + axis: { + range: [0, 500], + tickcolor: 'white', + tickangle: 20, + tickwidth: 1 + }, + steps: [{ + range: [0, 250], + color: 'rgba(255, 255, 0, 0.5)' + }, { + range: [250, 400], + color: 'rgba(0, 0, 255, 0.75)' + }] + } + }] + } + }}) + .then(function() { + // Check number + expect(gd._fullData[0].number.valueformat).toEqual('0.8f'); + expect(gd._fullData[0].number.suffix).toEqual('km/h'); + expect(gd._fullData[0].number.font.color).toEqual('blue'); + expect(gd._fullData[0].number.font.family).toEqual('ArialNumber'); + + // Check delta + expect(gd._fullData[0].delta.valueformat).toEqual('0.9f'); + expect(gd._fullData[0].delta.reference).toEqual(-100); + expect(gd._fullData[0].delta.increasing.symbol).toEqual('a'); + expect(gd._fullData[0].delta.font.family).toEqual('ArialDelta'); + expect(gd._fullData[0].delta.font.size).toEqual(20); + + // Check gauge axis + expect(gd._fullData[0].gauge.axis.range).toEqual([0, 500], 'wrong gauge.axis.range'); + expect(gd._fullData[0].gauge.axis.tickangle).toEqual(20, 'wrong gauge.axis.tickangle'); + expect(gd._fullData[0].gauge.axis.tickcolor).toBe('white', 'wrong gauge.axis.tickcolor'); + + // TODO: check this works once handleArrayContainerDefaults supports template + // expect(gd._fullData[0].gauge.steps[0].range).toEqual([0, 250], 'wrong gauge.steps[0].range'); + // expect(gd._fullData[0].gauge.steps[0].color).toEqual('rgba(255, 255, 0, 0.5)'); + }) + .catch(failTest) + .then(done); + }); +}); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 60d08a664fb..30dca4169f5 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -348,6 +348,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{type: 'scatter'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -411,6 +412,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; + gd._fullData = [{type: 'scatter'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -428,6 +430,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'gl3d' }]; + gd._fullData = [{type: 'scatter3d'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -445,6 +448,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'geo' }]; + gd._fullData = [{type: 'scattergeo'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -485,6 +489,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'mapbox' }]; + gd._fullData = [{type: 'scattermapbox'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -526,6 +531,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'gl2d' }]; gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{type: 'scattergl'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -541,6 +547,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'pie' }]; + gd._fullData = [{type: 'pie'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -558,6 +565,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'gl3d' }]; + gd._fullData = [{type: 'scatter'}, {type: 'scatter3d'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -576,6 +584,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'geo' }]; gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{type: 'scatter'}, {type: 'scattergeo'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -641,6 +650,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'geo' }, { name: 'gl3d' }]; + gd._fullData = [{type: 'scattergeo'}, {type: 'scatter3d'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -657,6 +667,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'ternary' }]; + gd._fullData = [{type: 'scatterternary'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -695,6 +706,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'ternary' }, { name: 'cartesian' }]; + gd._fullData = [{type: 'scatterternary'}, {type: 'scatter'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -712,6 +724,37 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'ternary' }, { name: 'gl3d' }]; + gd._fullData = [{ type: 'scatterternary' }, { type: 'scatter3d' }]; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar without hover button when all traces are noHover', function() { + var buttons = getButtons([ + ['toImage'] + ]); + + var gd = getMockGraphInfo(); + gd._fullData = [{ type: 'indicator' }]; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar with hover button even in the presence of one noHover trace', function() { + var buttons = getButtons([ + ['toImage'], + ['hoverClosestPie'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._basePlotModules = [{ name: 'pie' }]; + gd._fullData = [{ type: 'indicator' }, {type: 'pie'}]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -764,6 +807,7 @@ describe('ModeBar', function() { it('displays/hides cloud link according to showSendToCloud config arg', function() { var gd = getMockGraphInfo(); gd._fullLayout._basePlotModules = [{ name: 'pie' }]; + gd._fullData = [{type: 'pie'}]; manageModeBar(gd); checkButtons(gd._fullLayout._modeBar, getButtons([ ['toImage'], @@ -793,6 +837,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{type: 'scatter'}]; return gd; } @@ -803,6 +848,7 @@ describe('ModeBar', function() { expect(countButtons(gd._fullLayout._modeBar)).toEqual(11); gd._fullLayout._basePlotModules = [{ name: 'gl3d' }]; + gd._fullData = [{type: 'scatter3d'}]; manageModeBar(gd); expect(countButtons(gd._fullLayout._modeBar)).toEqual(9); diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js index 62743768fca..6c4a4dc3fb0 100644 --- a/test/jasmine/tests/snapshot_test.js +++ b/test/jasmine/tests/snapshot_test.js @@ -255,6 +255,35 @@ describe('Plotly.Snapshot', function() { expect((actual || '').substr(0, 6)).toBe('url(\"#', msg); } + it('- tspans', function(done) { + var fontFamily = '"Times New Roman"'; + Plotly.newPlot(gd, [{ + type: 'indicator', + mode: 'delta+number', + value: 10 + }], {font: {family: fontFamily}}) + .then(function() { + d3.selectAll('tspan').each(function() { + expect(this.style.fontFamily).toEqual(fontFamily); + }); + + return Plotly.Snapshot.toSVG(gd); + }) + .then(function(svg) { + var svgDOM = parser.parseFromString(svg, 'image/svg+xml'); + var i; + + var tspanElements = svgDOM.getElementsByTagName('tspan'); + expect(tspanElements.length).toEqual(2); + + for(i = 0; i < tspanElements.length; i++) { + expect(tspanElements[i].style.fontFamily).toEqual(fontFamily); + } + }) + .catch(failTest) + .then(done); + }); + it('- marker-gradient case', function(done) { Plotly.plot(gd, [{ y: [1, 2, 1],