diff --git a/draftlogs/7673_add.md b/draftlogs/7673_add.md new file mode 100644 index 00000000000..04d803b98c6 --- /dev/null +++ b/draftlogs/7673_add.md @@ -0,0 +1 @@ + - Add support for dashed marker lines in scatter plots [[#7673](https://github.com/plotly/plotly.js/pull/7673)], with thanks to @chrimaho for the contribution! diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 38e8686d102..861df3131a5 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -965,6 +965,8 @@ drawing.singlePointStyle = function (d, sel, trace, fns, gd, pt) { } } + const lineDash = d.mld || (markerLine || {}).dash; + if (lineDash) drawing.dashLine(sel, lineDash, lineWidth); if (d.om) { // open markers can't have zero linewidth, default to 1px, // and use fill color as stroke color diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 849271d3962..c62970cdc0d 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -221,6 +221,7 @@ module.exports = function style(s, gd, legend) { dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); dEdit.mlc = boundVal('marker.line.color', pickFirst); dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5], CST_MARKER_LINE_WIDTH); + dEdit.mld = boundVal('marker.line.dash', pickFirst); tEdit.marker = { sizeref: 1, sizemin: 1, diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index efa5332498c..07eac057140 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -38,6 +38,7 @@ module.exports = function arraysToCalcdata(cd, trace) { if(marker.line) { Lib.mergeArray(markerLine.color, cd, 'mlc'); Lib.mergeArrayCastPositive(markerLine.width, cd, 'mlw'); + Lib.mergeArray(markerLine.dash, cd, 'mld'); } var markerGradient = marker.gradient; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 18231a889b7..255496750a0 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -555,6 +555,9 @@ module.exports = { anim: true, description: 'Sets the width (in px) of the lines bounding the marker points.' }, + dash: extendFlat({}, dash, { + arrayOk: true + }), editType: 'calc' }, colorScaleAttrs('marker.line', { anim: true }) diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index c4358730534..b7edca3e6dd 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -12,70 +12,65 @@ var subTypes = require('./subtypes'); * gradient: caller supports gradients * noSelect: caller does not support selected/unselected attribute containers */ -module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) { +module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts = {}) { var isBubble = subTypes.isBubble(traceIn); var lineColor = (traceIn.line || {}).color; var defaultMLC; - opts = opts || {}; - // marker.color inherit from line.color (even if line.color is an array) - if(lineColor) defaultColor = lineColor; + if (lineColor) defaultColor = lineColor; coerce('marker.symbol'); coerce('marker.opacity', isBubble ? 0.7 : 1); coerce('marker.size'); - if(!opts.noAngle) { + if (!opts.noAngle) { coerce('marker.angle'); - if(!opts.noAngleRef) { - coerce('marker.angleref'); - } - - if(!opts.noStandOff) { - coerce('marker.standoff'); - } + if (!opts.noAngleRef) coerce('marker.angleref'); + if (!opts.noStandOff) coerce('marker.standoff'); } coerce('marker.color', defaultColor); - if(hasColorscale(traceIn, 'marker')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'}); + if (hasColorscale(traceIn, 'marker')) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { prefix: 'marker.', cLetter: 'c' }); } - if(!opts.noSelect) { + if (!opts.noSelect) { coerce('selected.marker.color'); coerce('unselected.marker.color'); coerce('selected.marker.size'); coerce('unselected.marker.size'); } - if(!opts.noLine) { + if (!opts.noLine) { // if there's a line with a different color than the marker, use // that line color as the default marker line color // (except when it's an array) // mostly this is for transparent markers to behave nicely - if(lineColor && !Array.isArray(lineColor) && (traceOut.marker.color !== lineColor)) { + if (lineColor && !Array.isArray(lineColor) && traceOut.marker.color !== lineColor) { defaultMLC = lineColor; - } else if(isBubble) defaultMLC = Color.background; - else defaultMLC = Color.defaultLine; + } else if (isBubble) { + defaultMLC = Color.background; + } else { + defaultMLC = Color.defaultLine; + } coerce('marker.line.color', defaultMLC); - if(hasColorscale(traceIn, 'marker.line')) { - colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'}); + if (hasColorscale(traceIn, 'marker.line')) { + colorscaleDefaults(traceIn, traceOut, layout, coerce, { prefix: 'marker.line.', cLetter: 'c' }); } coerce('marker.line.width', isBubble ? 1 : 0); + if (!opts.noLineDash) coerce('marker.line.dash'); } - if(isBubble) { + if (isBubble) { coerce('marker.sizeref'); coerce('marker.sizemin'); coerce('marker.sizemode'); } - if(opts.gradient) { + if (opts.gradient) { var gradientType = coerce('marker.gradient.type'); - if(gradientType !== 'none') { - coerce('marker.gradient.color'); - } + if (gradientType !== 'none') coerce('marker.gradient.color'); } }; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index e99182fef69..eb366090612 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -32,7 +32,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode'); if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noSelect: true, noAngle: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngle: true, + noLineDash: true, + noSelect: true + }); } if (subTypes.hasLines(traceOut)) { diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 68caaade24a..ded94305d8a 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -97,6 +97,7 @@ module.exports = { line: extendFlat( { width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash, editType: 'calc' }, colorScaleAttrs('marker.line') diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 0f8cede9834..f2fb2639cf8 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -139,7 +139,8 @@ module.exports = overrideAll( colorbar: scatterMarkerAttrs.colorbar, line: extendFlat( { - width: scatterMarkerLineAttrs.width + width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash }, colorAttributes('marker.line') ), diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index efaecbb5e63..47f1dbd7e10 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -41,7 +41,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode', defaultMode); if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); coerce('marker.line.width', isOpen || isBubble ? 1 : 0); } diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js index 7f095c4ea71..c540adc2acd 100644 --- a/src/traces/scatterpolargl/defaults.js +++ b/src/traces/scatterpolargl/defaults.js @@ -33,7 +33,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if (subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); } if (subTypes.hasLines(traceOut)) { diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 5954d376f6f..c51c1abdfbb 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -126,6 +126,7 @@ module.exports = { line: extendFlat( { width: scatterMarkerLineAttrs.width, + dash: scatterMarkerLineAttrs.dash, editType: 'calc' }, colorScaleAttrs('marker.line') diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index d3c4154b9ba..a7ee35aedbe 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -37,7 +37,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('xhoverformat'); coerce('yhoverformat'); - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { noAngleRef: true, noStandOff: true }); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, { + noAngleRef: true, + noLineDash: true, + noStandOff: true + }); var isOpen = isOpenSymbol(traceOut.marker.symbol); var isBubble = subTypes.isBubble(traceOut); diff --git a/test/image/baselines/text_on_shapes_basic.png b/test/image/baselines/text_on_shapes_basic.png index ec1e63dd288..519474824b7 100644 Binary files a/test/image/baselines/text_on_shapes_basic.png and b/test/image/baselines/text_on_shapes_basic.png differ diff --git a/test/image/baselines/zz-scatter_marker_line_dash.png b/test/image/baselines/zz-scatter_marker_line_dash.png new file mode 100644 index 00000000000..e00765926e1 Binary files /dev/null and b/test/image/baselines/zz-scatter_marker_line_dash.png differ diff --git a/test/image/mocks/zz-scatter_marker_line_dash.json b/test/image/mocks/zz-scatter_marker_line_dash.json new file mode 100644 index 00000000000..44ffeef3f01 --- /dev/null +++ b/test/image/mocks/zz-scatter_marker_line_dash.json @@ -0,0 +1,82 @@ +{ + "data": [ + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [1, 1, 1, 1, 1, 1], + "marker": { + "size": 30, + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "black", + "width": 3, + "dash": ["solid", "dot", "dash", "longdash", "dashdot", "longdashdot"] + } + }, + "name": "Array of dashes" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [2, 2, 2, 2, 2, 2], + "marker": { + "size": 30, + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "red", + "width": 4, + "dash": "dash" + } + }, + "name": "Single dash" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [3, 3, 3, 3, 3, 3], + "marker": { + "size": 30, + "symbol": "square", + "color": "rgba(255,0,0,0.2)", + "line": { + "color": "blue", + "width": 2, + "dash": "dot" + } + }, + "name": "Dot with squares" + }, + { + "type": "scatter", + "mode": "markers", + "x": [1, 2, 3, 4, 5, 6], + "y": [4, 4, 4, 4, 4, 4], + "marker": { + "size": 30, + "symbol": "circle-open", + "color": "green", + "line": { + "width": 2, + "dash": "dash" + } + }, + "name": "Open markers with dash" + } + ], + "layout": { + "title": { + "text": "Scatter Marker Line Dash Support" + }, + "xaxis": { + "range": [0, 7] + }, + "yaxis": { + "range": [0, 5] + }, + "width": 600, + "height": 400 + } +} diff --git a/test/jasmine/tests/scatter_marker_line_dash_test.js b/test/jasmine/tests/scatter_marker_line_dash_test.js new file mode 100644 index 00000000000..7e5e2976f58 --- /dev/null +++ b/test/jasmine/tests/scatter_marker_line_dash_test.js @@ -0,0 +1,135 @@ +var Plotly = require('../../../lib/index'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('Test scatter marker line dash:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should support marker line dash', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + size: 20, + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + markers.forEach(function(node) { + // In plotly.js, dash is applied via stroke-dasharray + expect(node.style.strokeDasharray).not.toBe(''); + }); + }) + .then(done, done.fail); + }); + + it('should support array marker line dash', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + size: 20, + line: { + color: 'red', + width: 2, + dash: ['solid', 'dot', 'dash'] + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + // 'solid' should have no dasharray or 'none' (represented as empty string in node.style.strokeDasharray) + // 'dot' and 'dash' should have numerical dasharrays + expect(markers[0].style.strokeDasharray).toBe(''); + expect(markers[1].style.strokeDasharray).not.toBe(''); + expect(markers[2].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + + it('should show marker line dash in the legend', function(done) { + Plotly.newPlot( + gd, + [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }], + { showlegend: true } + ) + .then(function () { + var legendPoints = gd.querySelectorAll('.legendpoints path.scatterpts'); + expect(legendPoints.length).toBe(1); + expect(legendPoints[0].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + + it('should update marker line dash via restyle', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + line: { + color: 'red', + width: 2, + dash: 'solid' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers[0].style.strokeDasharray).toBe(''); + + return Plotly.restyle(gd, {'marker.line.dash': 'dot'}); + }).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers[0].style.strokeDasharray).not.toBe(''); + }) + .then(done, done.fail); + }); + it('should support marker line dash on open markers', function(done) { + Plotly.newPlot(gd, [{ + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + symbol: 'circle-open', + line: { + color: 'red', + width: 2, + dash: 'dash' + } + } + }]).then(function() { + var markers = gd.querySelectorAll('.point'); + expect(markers.length).toBe(3); + + markers.forEach(function(node) { + expect(node.style.strokeDasharray).not.toBe(''); + }); + }) + .then(done, done.fail); + });}); diff --git a/test/plot-schema.json b/test/plot-schema.json index 211da680a56..04e324e6977 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -60613,6 +60613,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -65817,6 +65837,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -68123,6 +68163,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "calc", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -75980,6 +76040,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -80482,6 +80562,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.", @@ -82800,6 +82900,26 @@ "editType": "none", "valType": "string" }, + "dash": { + "arrayOk": true, + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "style", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "dashsrc": { + "description": "Sets the source reference on Chart Studio Cloud for `dash`.", + "editType": "none", + "valType": "string" + }, "editType": "calc", "reversescale": { "description": "Reverses the color mapping if true. Has an effect only if in `marker.line.color` is set to a numerical array. If true, `marker.line.cmin` will correspond to the last color in the array and `marker.line.cmax` will correspond to the first color.",