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],