diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 31f8661a882..d9b30a2ea97 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -123,9 +123,12 @@ module.exports = function style(s, gd) { dEdit.mlc = boundVal('marker.line.color', pickFirst); dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]); tEdit.marker = { + sizemode: 'diameter', sizeref: 1, sizemin: 1, - sizemode: 'diameter' + sizemax: null, + sizedatamin: null, + sizedatamax: null }; } diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index e132100b5af..cab9ba70820 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -288,6 +288,28 @@ module.exports = { '*0* corresponds to no limit.' ].join(' ') }, + + sizemode: { + valType: 'enumerated', + values: ['diameter', 'area', 'log-diameter', 'log-area'], + dflt: 'diameter', + role: 'info', + editType: 'calc', + description: [ + 'Has an effect only if `marker.size` is set to a numerical array.', + 'Sets the rule for which the data in `size` is converted', + 'to pixels.', + 'When set to *diameter*, `marker.size` data is proportional to', + 'marker points\' diameter.', + 'When set to *area*, `marker.size` data is proportional to', + 'marker points\' area.', + 'When set to *log-diameter*, the base10 log of the `marker.size` data', + 'is proportional to marker points\' diameter .', + 'When set to *log-area*, the base10 log of the `marker.size` data', + 'is proportional to marker points\' area.', + 'In each case the proportionality constant is set using `sizeref`.' + ].join(' ') + }, sizeref: { valType: 'number', dflt: 1, @@ -295,8 +317,8 @@ module.exports = { editType: 'calc', description: [ 'Has an effect only if `marker.size` is set to a numerical array.', - 'Sets the scale factor used to determine the rendered size of', - 'marker points. Use with `sizemin` and `sizemode`.' + 'Sets the proportionality constant in the `marker.size` data', + 'to marker point size conversion (More info under `sizemode`).' ].join(' ') }, sizemin: { @@ -310,16 +332,37 @@ module.exports = { 'Sets the minimum size (in px) of the rendered marker points.' ].join(' ') }, - sizemode: { - valType: 'enumerated', - values: ['diameter', 'area'], - dflt: 'diameter', - role: 'info', + sizemax: { + valType: 'number', + min: 0, + role: 'style', editType: 'calc', description: [ 'Has an effect only if `marker.size` is set to a numerical array.', - 'Sets the rule for which the data in `size` is converted', - 'to pixels.' + 'Sets the maximum size (in px) of the rendered marker points.' + ].join(' ') + }, + sizedatamin: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + editType: 'calc', + description: [ + 'Has an effect only if `marker.size` is set to a numerical array.', + 'Sets the minimum `marker.size` value to be scaled.', + 'Values smaller than `sizedatamin` are displayed with `sizemin`.' + ].join(' ') + }, + sizedatamax: { + valType: 'number', + min: 0, + role: 'style', + editType: 'calc', + description: [ + 'Has an effect only if `marker.size` is set to a numerical array.', + 'Sets the maximum `marker.size` value to be scaled.', + 'Values greater than `sizedatamin` are displayed with `sizemax`.' ].join(' ') }, diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 49f3105f9e9..dac1a8cfc3c 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -11,6 +11,7 @@ var isNumeric = require('fast-isnumeric'); +var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var BADNUM = require('../../constants/numerical').BADNUM; @@ -18,6 +19,7 @@ var subTypes = require('./subtypes'); var calcColorscale = require('./colorscale_calc'); var arraysToCalcdata = require('./arrays_to_calcdata'); var calcSelection = require('./calc_selection'); +var makeBubbleSizeFn = require('./make_bubble_size_func'); function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'); @@ -101,21 +103,22 @@ function calcMarkerSize(trace, serieslen) { // Treat size like x or y arrays --- Run d2c // this needs to go before ppad computation var marker = trace.marker; - var sizeref = 1.6 * (trace.marker.sizeref || 1); - var markerTrans; - - if(trace.marker.sizemode === 'area') { - markerTrans = function(v) { - return Math.max(Math.sqrt((v || 0) / sizeref), 3); - }; - } else { - markerTrans = function(v) { - return Math.max((v || 0) / sizeref, 3); - }; - } + + // use a modified version of the bubble size function + // with smaller sizeref (resulting in a pad factor) + // and making non-numeric and size-0 points count as pad=3. + var sizeFn = makeBubbleSizeFn({ + marker: Lib.extendFlat({}, marker, { + sizemode: marker.sizemode || 'diameter', + sizeref: 0.8 * (marker.sizeref || 1), + }) + }); + var markerTrans = function(v) { + return Math.max(sizeFn(v), 3); + }; if(Array.isArray(marker.size)) { - // I tried auto-type but category and dates dont make much sense. + // no need to auto-type as category and dates do not make much sense. var ax = {type: 'linear'}; Axes.setConvert(ax); @@ -123,9 +126,9 @@ function calcMarkerSize(trace, serieslen) { if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); return s.map(markerTrans); - } else { - return markerTrans(marker.size); } + + return markerTrans(marker.size); } module.exports = { diff --git a/src/traces/scatter/make_bubble_size_func.js b/src/traces/scatter/make_bubble_size_func.js index 4426dc92c7d..4c98b248dd8 100644 --- a/src/traces/scatter/make_bubble_size_func.js +++ b/src/traces/scatter/make_bubble_size_func.js @@ -6,35 +6,49 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var isNumeric = require('fast-isnumeric'); +function toLog(v) { + return Math.log(v) / Math.LN10; +} -// used in the drawing step for 'scatter' and 'scattegeo' and -// in the convert step for 'scatter3d' module.exports = function makeBubbleSizeFn(trace) { - var marker = trace.marker, - sizeRef = marker.sizeref || 1, - sizeMin = marker.sizemin || 0; - - // for bubble charts, allow scaling the provided value linearly - // and by area or diameter. - // Note this only applies to the array-value sizes - - var baseFn = (marker.sizemode === 'area') ? - function(v) { return Math.sqrt(v / sizeRef); } : - function(v) { return v / sizeRef; }; - - // TODO add support for position/negative bubbles? - // TODO add 'sizeoffset' attribute? + var marker = trace.marker; + var sizeRef = marker.sizeref || 1; + var sizeMin = marker.sizemin || 0; + var sizeMax = marker.sizemax || Infinity; + var sizeDataMin = marker.sizedatamin || 0; + var sizeDataMax = marker.sizedatamax || Infinity; + var baseFn; + + switch(marker.sizemode) { + case 'area': + baseFn = function(v) { return Math.sqrt(v / sizeRef); }; + break; + case 'log-diameter': + baseFn = function(v) { return toLog(v) / sizeRef; }; + break; + case 'log-area': + baseFn = function(v) { return Math.sqrt(toLog(v) / sizeRef); }; + break; + default: + baseFn = function(v) { return v / sizeRef; }; + break; + } + + // non-numeric and negative data values AND sizes correspond to size=0 points, + // clamp positive data values outside data range to px extrema return function(v) { - var baseSize = baseFn(v / 2); - - // don't show non-numeric and negative sizes - return (isNumeric(baseSize) && (baseSize > 0)) ? - Math.max(baseSize, sizeMin) : + if(isNumeric(v) && v > 0) { + if(v <= sizeDataMin) return sizeMin; + if(v >= sizeDataMax) return sizeMax; + } + + var s = baseFn(v / 2); + return isNumeric(s) && s > 0 ? + Math.min(Math.max(s, sizeMin), sizeMax) : 0; }; }; diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index e0b9cf4fd68..66dd6dd5c84 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -67,9 +67,12 @@ module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout } if(isBubble) { + coerce('marker.sizemode'); coerce('marker.sizeref'); coerce('marker.sizemin'); - coerce('marker.sizemode'); + coerce('marker.sizemax'); + coerce('marker.sizedatamin'); + coerce('marker.sizedatamax'); } if(opts.gradient) { diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js index 229f51e1c5f..468674a98bd 100644 --- a/src/traces/scatter3d/attributes.js +++ b/src/traces/scatter3d/attributes.js @@ -139,9 +139,12 @@ var attrs = module.exports = overrideAll({ description: 'Sets the marker symbol type.' }, size: extendFlat({}, scatterMarkerAttrs.size, {dflt: 8}), + sizemode: scatterMarkerAttrs.sizemode, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, + sizemax: scatterMarkerAttrs.sizemax, + sizedatamin: scatterMarkerAttrs.sizedatamin, + sizedatamax: scatterMarkerAttrs.sizedatamax, opacity: extendFlat({}, scatterMarkerAttrs.opacity, { arrayOk: false, description: [ diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 19ee866b509..4ff5ca2a588 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -92,9 +92,12 @@ module.exports = { opacity: scatterMarkerAttrs.opacity, maxdisplayed: scatterMarkerAttrs.maxdisplayed, size: scatterMarkerAttrs.size, + sizemode: scatterMarkerAttrs.sizemode, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, + sizemax: scatterMarkerAttrs.sizemax, + sizedatamin: scatterMarkerAttrs.sizedatamin, + sizedatamax: scatterMarkerAttrs.sizedatamax, line: extendFlat({ width: scatterMarkerLineAttrs.width, editType: 'calc' diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 0cedfe2aed9..0192cd78d4b 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -89,9 +89,12 @@ module.exports = overrideAll({ symbol: scatterMarkerAttrs.symbol, opacity: scatterMarkerAttrs.opacity, size: scatterMarkerAttrs.size, + sizemode: scatterMarkerAttrs.sizemode, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, + sizemax: scatterMarkerAttrs.sizemax, + sizedatamin: scatterMarkerAttrs.sizedatamin, + sizedatamax: scatterMarkerAttrs.sizedatamax, showscale: scatterMarkerAttrs.showscale, colorbar: scatterMarkerAttrs.colorbar, line: extendFlat({ diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 0c69b843c0a..faa73fcf640 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -59,9 +59,12 @@ var attrs = module.exports = overrideAll({ marker: extendFlat({}, colorAttributes('marker'), { symbol: scatterMarkerAttrs.symbol, size: scatterMarkerAttrs.size, + sizemode: scatterMarkerAttrs.sizemode, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, + sizemax: scatterMarkerAttrs.sizemax, + sizedatamin: scatterMarkerAttrs.sizedatamin, + sizedatamax: scatterMarkerAttrs.sizedatamax, opacity: scatterMarkerAttrs.opacity, showscale: scatterMarkerAttrs.showscale, colorbar: scatterMarkerAttrs.colorbar, diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 1c9005ec2ac..915b2e1acd3 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -85,9 +85,12 @@ module.exports = overrideAll({ }, opacity: markerAttrs.opacity, size: markerAttrs.size, + sizemode: markerAttrs.sizemode, sizeref: markerAttrs.sizeref, sizemin: markerAttrs.sizemin, - sizemode: markerAttrs.sizemode, + sizemax: markerAttrs.sizemax, + sizedatamin: markerAttrs.sizedatamin, + sizedatamax: markerAttrs.sizedatamax, color: markerAttrs.color, colorscale: markerAttrs.colorscale, cauto: markerAttrs.cauto, diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index d4b6ed4107c..ac98686c34b 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -121,9 +121,12 @@ module.exports = { opacity: scatterMarkerAttrs.opacity, maxdisplayed: scatterMarkerAttrs.maxdisplayed, size: scatterMarkerAttrs.size, + sizemode: scatterMarkerAttrs.sizemode, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, - sizemode: scatterMarkerAttrs.sizemode, + sizemax: scatterMarkerAttrs.sizemax, + sizedatamin: scatterMarkerAttrs.sizedatamin, + sizedatamax: scatterMarkerAttrs.sizedatamax, line: extendFlat({ width: scatterMarkerLineAttrs.width, editType: 'calc' diff --git a/test/image/baselines/bubble_scale.png b/test/image/baselines/bubble_scale.png new file mode 100644 index 00000000000..ef557ed183f Binary files /dev/null and b/test/image/baselines/bubble_scale.png differ diff --git a/test/image/mocks/bubble_scale.json b/test/image/mocks/bubble_scale.json new file mode 100644 index 00000000000..af4d9a3784d --- /dev/null +++ b/test/image/mocks/bubble_scale.json @@ -0,0 +1,48 @@ +{ + "data": [ + { + "type": "scatter", + "mode": "markers", + "name": "log-diameter", + "x": [0, 9, 18, 27, 36, 45, 54, 63, 72, 81], + "y": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + "marker": { + "size": [10, 100, 1000, 20, 400, 5000, 50, 300, 7000], + "sizemode": "log-diameter", + "sizeref": 0.1 + } + }, + { + "type": "scatter", + "mode": "markers", + "name": "log-area", + "x": [0, 9, 18, 27, 36, 45, 54, 63, 72, 81], + "y": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4], + "marker": { + "size": [10, 100, 1000, 20, 400, 5000, 50, 300, 7000], + "sizemode": "log-area", + "sizeref": 0.01 + } + }, + { + "type": "scatter", + "mode": "markers", + "name": "data min-max", + "x": [0, 9, 18, 27, 36, 45, 54, 63, 72, 81], + "y": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "marker": { + "size": [10, 100, 1000, 20, 400, 5000, 50, 300, 7000], + "sizedatamin": 20, + "sizedatamax": 100, + "sizemin": 50, + "sizemax": 60 + } + } + ], + "layout": { + "hovermode": "closest", + "showlegend": true, + "width": 800, + "height": 500 + } +}