From d9fab152e777933c2efb3ffd9250778c57c4110a Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Thu, 9 Mar 2017 20:45:55 -0800 Subject: [PATCH 01/26] Cartesian dropline support Draw droplines to the x and y axes on hover if the option is enabled and we're not on 'compare' hovermode. --- src/components/dragelement/unhover.js | 1 + src/plot_api/plot_config.js | 3 + src/plots/cartesian/graph_interact.js | 87 ++++++++++++++++++++------- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js index 4caf14ce456..204863de1e2 100644 --- a/src/components/dragelement/unhover.js +++ b/src/components/dragelement/unhover.js @@ -41,6 +41,7 @@ unhover.raw = function unhoverRaw(gd, evt) { } fullLayout._hoverlayer.selectAll('g').remove(); + fullLayout._hoverlayer.selectAll('line').remove(); gd._hoverdata = undefined; if(evt.target && oldhoverdata) { diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 71871dad6be..940f8757984 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -48,6 +48,9 @@ module.exports = { // new users see some hints about interactivity showTips: true, + // display droplines on cartesian graphs + showDroplines: false, + // enable axis pan/zoom drag handles showAxisDragHandles: true, diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 85f0130a5e8..76cac0f4234 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -559,30 +559,8 @@ function hover(gd, evt, subplot) { // nothing left: remove all labels and quit if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); - // if there's more than one horz bar trace, - // rotate the labels so they don't overlap - var rotateLabels = hovermode === 'y' && searchData.length > 1; - hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); - var bgColor = Color.combine( - fullLayout.plot_bgcolor || Color.background, - fullLayout.paper_bgcolor - ); - - var labelOpts = { - hovermode: hovermode, - rotateLabels: rotateLabels, - bgColor: bgColor, - container: fullLayout._hoverlayer, - outerContainer: fullLayout._paperdiv - }; - var hoverLabels = createHoverText(hoverData, labelOpts); - - hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); - - alignHoverText(hoverLabels, rotateLabels); - // lastly, emit custom hover/unhover events var oldhoverdata = gd._hoverdata, newhoverdata = []; @@ -614,6 +592,38 @@ function hover(gd, evt, subplot) { gd._hoverdata = newhoverdata; + if(gd._context.showDroplines && hoverChanged(gd, evt, oldhoverdata)) { + var droplineOpts = { + hovermode: hovermode, + container: fullLayout._hoverlayer, + outerContainer: fullLayout._paperdiv + }; + createDroplines(hoverData, droplineOpts); + } + + // if there's more than one horz bar trace, + // rotate the labels so they don't overlap + var rotateLabels = hovermode === 'y' && searchData.length > 1; + + var bgColor = Color.combine( + fullLayout.plot_bgcolor || Color.background, + fullLayout.paper_bgcolor + ); + + var labelOpts = { + hovermode: hovermode, + rotateLabels: rotateLabels, + bgColor: bgColor, + container: fullLayout._hoverlayer, + outerContainer: fullLayout._paperdiv + }; + + var hoverLabels = createHoverText(hoverData, labelOpts); + + hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); + + alignHoverText(hoverLabels, rotateLabels); + // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true // we should improve the "fx" API so other plots can use it without these hack. if(evt.target && evt.target.tagName) { @@ -818,8 +828,41 @@ fx.loneUnhover = function(containerOrSelection) { d3.select(containerOrSelection); selection.selectAll('g.hovertext').remove(); + selection.selectAll('line.dropline').remove(); }; +function createDroplines(hoverData, opts) { + var hovermode = opts.hovermode, + container = opts.container; + + if(hovermode !== 'closest') return; + var c0 = hoverData[0]; + var x = (c0.x0 + c0.x1) / 2; + var y = (c0.y0 + c0.y1) / 2; + var xOffset = c0.xa._offset; + var yOffset = c0.ya._offset; + container.selectAll('line.dropline').remove(); + container.append('line') + .attr('x1', xOffset + (c0.ya.side === 'right' ? c0.xa._length : 0)) + .attr('x2', xOffset + x) + .attr('y1', yOffset + y) + .attr('y2', yOffset + y) + .attr('stroke-width', 3) + .attr('stroke', c0.color) + .attr('stroke-dasharray', '5,5') + .attr('class', 'dropline'); + + container.append('line') + .attr('x1', xOffset + x) + .attr('x2', xOffset + x) + .attr('y1', yOffset + y) + .attr('y2', yOffset + c0.ya._length) + .attr('stroke-width', 3) + .attr('stroke', c0.color) + .attr('stroke-dasharray', '5,5') + .attr('class', 'dropline'); +} + function createHoverText(hoverData, opts) { var hovermode = opts.hovermode, rotateLabels = opts.rotateLabels, From 5d39b53367b13c74396c4f71cc39185a1c0b30aa Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 10 Mar 2017 18:12:00 -0800 Subject: [PATCH 02/26] Removed chart config for showDroplines Added axis layout option for showspikes Added background behind droplines Added axis indicator marker, including on free anchored axes Always draw dropline to chart limit, including for shared axes - not drawing all the way to free anchored axes --- src/components/dragelement/unhover.js | 1 + src/plot_api/plot_config.js | 3 - src/plots/cartesian/axis_defaults.js | 2 + src/plots/cartesian/graph_interact.js | 118 +++++++++++++++++------ src/plots/cartesian/layout_attributes.js | 9 ++ 5 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js index 204863de1e2..58475678209 100644 --- a/src/components/dragelement/unhover.js +++ b/src/components/dragelement/unhover.js @@ -42,6 +42,7 @@ unhover.raw = function unhoverRaw(gd, evt) { fullLayout._hoverlayer.selectAll('g').remove(); fullLayout._hoverlayer.selectAll('line').remove(); + fullLayout._hoverlayer.selectAll('circle').remove(); gd._hoverdata = undefined; if(evt.target && oldhoverdata) { diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 940f8757984..71871dad6be 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -48,9 +48,6 @@ module.exports = { // new users see some hints about interactivity showTips: true, - // display droplines on cartesian graphs - showDroplines: false, - // enable axis pan/zoom drag handles showAxisDragHandles: true, diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 1ed0a669392..31fbcf49713 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -99,6 +99,8 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); + coerce('showspikes'); + handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); handleTickMarkDefaults(containerIn, containerOut, coerce, options); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 76cac0f4234..a761b7dee8f 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -592,7 +592,7 @@ function hover(gd, evt, subplot) { gd._hoverdata = newhoverdata; - if(gd._context.showDroplines && hoverChanged(gd, evt, oldhoverdata)) { + if(hoverChanged(gd, evt, oldhoverdata)) { var droplineOpts = { hovermode: hovermode, container: fullLayout._hoverlayer, @@ -829,38 +829,102 @@ fx.loneUnhover = function(containerOrSelection) { selection.selectAll('g.hovertext').remove(); selection.selectAll('line.dropline').remove(); + selection.selectAll('circle.dropline').remove(); }; function createDroplines(hoverData, opts) { var hovermode = opts.hovermode, - container = opts.container; - + container = opts.container, + outerContainer = opts.outerContainer; if(hovermode !== 'closest') return; - var c0 = hoverData[0]; - var x = (c0.x0 + c0.x1) / 2; - var y = (c0.y0 + c0.y1) / 2; - var xOffset = c0.xa._offset; - var yOffset = c0.ya._offset; + var c0 = hoverData[0], + x = (c0.x0 + c0.x1) / 2, + y = (c0.y0 + c0.y1) / 2, + xOffset = c0.xa._offset, + yOffset = c0.ya._offset, + xPoint = xOffset + x, + yPoint = yOffset + y, + xSide = c0.xa.side, + ySide = c0.ya.side, + xLength = c0.xa._length, + yLength = c0.ya._length, + xEdge = c0.ya._boundingBox.left + (ySide === 'left' ? c0.ya._boundingBox.width : 0), + yEdge = c0.xa._boundingBox.top + (xSide === 'top' ? c0.xa._boundingBox.height : 0), + outerBBox = outerContainer.node().getBoundingClientRect(), + xFreeBase = xOffset + (ySide === 'right' ? xLength : 0), + yFreeBase = yOffset + (xSide === 'top' ? yLength : 0), + xAnchoredBase = xEdge - outerBBox.left, + yAnchoredBase = yEdge - outerBBox.top, + xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, + yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, + color = c0.color; + + // Remove old dropline items container.selectAll('line.dropline').remove(); - container.append('line') - .attr('x1', xOffset + (c0.ya.side === 'right' ? c0.xa._length : 0)) - .attr('x2', xOffset + x) - .attr('y1', yOffset + y) - .attr('y2', yOffset + y) - .attr('stroke-width', 3) - .attr('stroke', c0.color) - .attr('stroke-dasharray', '5,5') - .attr('class', 'dropline'); - - container.append('line') - .attr('x1', xOffset + x) - .attr('x2', xOffset + x) - .attr('y1', yOffset + y) - .attr('y2', yOffset + c0.ya._length) - .attr('stroke-width', 3) - .attr('stroke', c0.color) - .attr('stroke-dasharray', '5,5') - .attr('class', 'dropline'); + container.selectAll('circle.dropline').remove(); + + + if(c0.ya.showspikes) { + // Background horizontal Line (to y-axis) + container.append('line') + .attr('x1', xBase) + .attr('x2', xPoint) + .attr('y1', yPoint) + .attr('y2', yPoint) + .attr('stroke-width', 5) + .attr('stroke', '#fff') + .attr('class', 'dropline'); + + // Foreground horizontal line (to y-axis) + container.append('line') + .attr('x1', xBase) + .attr('x2', xPoint) + .attr('y1', yPoint) + .attr('y2', yPoint) + .attr('stroke-width', 3) + .attr('stroke', color) + .attr('stroke-dasharray', '5,5') + .attr('class', 'dropline'); + + // Y axis marker + container.append('circle') + .attr('cx', xBase) + .attr('cy', yPoint) + .attr('r', 3) + .attr('fill', color) + .attr('class', 'dropline'); + } + + if(c0.xa.showspikes) { + // Background vertical line (to x-axis) + container.append('line') + .attr('x1', xPoint) + .attr('x2', xPoint) + .attr('y1', yPoint) + .attr('y2', yBase) + .attr('stroke-width', 5) + .attr('stroke', '#fff') + .attr('class', 'dropline'); + + // Foreground vertical line (to x-axis) + container.append('line') + .attr('x1', xPoint) + .attr('x2', xPoint) + .attr('y1', yPoint) + .attr('y2', yBase) + .attr('stroke-width', 3) + .attr('stroke', color) + .attr('stroke-dasharray', '5,5') + .attr('class', 'dropline'); + + // X axis marker + container.append('circle') + .attr('cx', xPoint) + .attr('cy', yBase) + .attr('r', 3) + .attr('fill', color) + .attr('class', 'dropline'); + } } function createHoverText(hoverData, opts) { diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b917c4cf8e1..b77d3428fb5 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -244,6 +244,15 @@ module.exports = { role: 'style', description: 'Determines whether or not the tick labels are drawn.' }, + showspikes: { + valType: 'boolean', + dflt: false, + role: 'style', + description: [ + 'Determines whether or not spikes (aka droplines) are drawn for this axis.', + 'Note: This only takes affect when hovermode = closest' + ].join(' ') + }, tickfont: extendFlat({}, fontAttrs, { description: 'Sets the tick font.' }), From be4753358a18330c95fb8d1e2d90c48943b27ef5 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Thu, 6 Apr 2017 21:24:40 -0400 Subject: [PATCH 03/26] Add spikecolor, spikethickness, spikedash and spikemode Move 'dash' function to lib for re-use Use dynamic contrast color based on same logic as hovertext contrast text --- src/components/drawing/index.js | 15 +-- src/lib/index.js | 28 +++++ src/plots/cartesian/axis_defaults.js | 4 + src/plots/cartesian/graph_interact.js | 128 +++++++++++++---------- src/plots/cartesian/layout_attributes.js | 46 +++++++- 5 files changed, 154 insertions(+), 67 deletions(-) diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index fa995b0641e..8420a07f8d2 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -113,19 +113,8 @@ drawing.lineGroupStyle = function(s, lw, lc, ld) { drawing.dashLine = function(s, dash, lineWidth) { lineWidth = +lineWidth || 0; - var dlw = Math.max(lineWidth, 3); - - if(dash === 'solid') dash = ''; - else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; - else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; - else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; - else if(dash === 'dashdot') { - dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; - } - else if(dash === 'longdashdot') { - dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; - } - // otherwise user wrote the dasharray themselves - leave it be + + dash = Lib.dash(dash, lineWidth); s.style({ 'stroke-dasharray': dash, diff --git a/src/lib/index.js b/src/lib/index.js index e1b6475a29b..360a923ee23 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -636,3 +636,31 @@ lib.numSeparate = function(value, separators, separatethousands) { return x1 + x2; }; + +/** + * Accepts a named dash style or stroke-dasharray string + * and returns a dasharray representing the named type. + * + * @param {string} dash named dash format, or dasharray + * @param {number} lineWidth width of the line, used to scale the dashes + * + * @returns {string} SVG stroke-dasharray formatted string + */ +lib.dash = function(dash, lineWidth) { + lineWidth = +lineWidth || 1; + var dlw = Math.max(lineWidth, 3); + + if(dash === 'solid') dash = ''; + else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; + else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; + else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; + else if(dash === 'dashdot') { + dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; + } + else if(dash === 'longdashdot') { + dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; + } + // otherwise user wrote the dasharray themselves - leave it be + + return dash; +}; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 31fbcf49713..050a073ba0d 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -100,6 +100,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, containerOut.cleanRange(); coerce('showspikes'); + coerce('spikecolor'); + coerce('spikethickness'); + coerce('spikedash'); + coerce('spikemode'); handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index a761b7dee8f..35a2b312ce3 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -857,7 +857,22 @@ function createDroplines(hoverData, opts) { yAnchoredBase = yEdge - outerBBox.top, xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, - color = c0.color; + xColor = c0.xa.spikecolor ? c0.xa.spikecolor : c0.color, + yColor = c0.ya.spikecolor ? c0.ya.spikecolor : c0.color, + xContrastColor = tinycolor(xColor).getBrightness() > 128 ? + '#000' : Color.background, + yContrastColor = tinycolor(yColor).getBrightness() > 128 ? + '#000' : Color.background, + xThickness = c0.xa.spikethickness, + yThickness = c0.ya.spikethickness, + xDash = Lib.dash(c0.xa.spikedash, xThickness), + yDash = Lib.dash(c0.xa.spikedash, yThickness), + xMarker = c0.xa.spikemode.indexOf('marker') !== -1, + yMarker = c0.ya.spikemode.indexOf('marker') !== -1, + xSpikeLine = c0.xa.spikemode.indexOf('toaxis') !== -1 || c0.xa.spikemode.indexOf('across') !== -1, + ySpikeLine = c0.ya.spikemode.indexOf('toaxis') !== -1 || c0.ya.spikemode.indexOf('across') !== -1, + xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? xBase + xLength : xPoint, + yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? yBase - yLength : yPoint; // Remove old dropline items container.selectAll('line.dropline').remove(); @@ -865,65 +880,72 @@ function createDroplines(hoverData, opts) { if(c0.ya.showspikes) { - // Background horizontal Line (to y-axis) - container.append('line') - .attr('x1', xBase) - .attr('x2', xPoint) - .attr('y1', yPoint) - .attr('y2', yPoint) - .attr('stroke-width', 5) - .attr('stroke', '#fff') - .attr('class', 'dropline'); - - // Foreground horizontal line (to y-axis) - container.append('line') - .attr('x1', xBase) - .attr('x2', xPoint) - .attr('y1', yPoint) - .attr('y2', yPoint) - .attr('stroke-width', 3) - .attr('stroke', color) - .attr('stroke-dasharray', '5,5') - .attr('class', 'dropline'); - + if(ySpikeLine) { + // Background horizontal Line (to y-axis) + container.append('line') + .attr('x1', xBase) + .attr('x2', xEndSpike) + .attr('y1', yPoint) + .attr('y2', yPoint) + .attr('stroke-width', yThickness + 2) + .attr('stroke', yContrastColor) + .attr('class', 'dropline'); + + // Foreground horizontal line (to y-axis) + container.append('line') + .attr('x1', xBase) + .attr('x2', xEndSpike) + .attr('y1', yPoint) + .attr('y2', yPoint) + .attr('stroke-width', yThickness) + .attr('stroke', yColor) + .attr('stroke-dasharray', yDash) + .attr('class', 'dropline'); + } // Y axis marker - container.append('circle') - .attr('cx', xBase) - .attr('cy', yPoint) - .attr('r', 3) - .attr('fill', color) - .attr('class', 'dropline'); + if(yMarker) { + container.append('circle') + .attr('cx', xAnchoredBase) + .attr('cy', yPoint) + .attr('r', yThickness) + .attr('fill', yColor) + .attr('class', 'dropline'); + } } if(c0.xa.showspikes) { + if(xSpikeLine) { // Background vertical line (to x-axis) - container.append('line') - .attr('x1', xPoint) - .attr('x2', xPoint) - .attr('y1', yPoint) - .attr('y2', yBase) - .attr('stroke-width', 5) - .attr('stroke', '#fff') - .attr('class', 'dropline'); - - // Foreground vertical line (to x-axis) - container.append('line') - .attr('x1', xPoint) - .attr('x2', xPoint) - .attr('y1', yPoint) - .attr('y2', yBase) - .attr('stroke-width', 3) - .attr('stroke', color) - .attr('stroke-dasharray', '5,5') - .attr('class', 'dropline'); + container.append('line') + .attr('x1', xPoint) + .attr('x2', xPoint) + .attr('y1', yEndSpike) + .attr('y2', yBase) + .attr('stroke-width', xThickness + 2) + .attr('stroke', xContrastColor) + .attr('class', 'dropline'); + + // Foreground vertical line (to x-axis) + container.append('line') + .attr('x1', xPoint) + .attr('x2', xPoint) + .attr('y1', yEndSpike) + .attr('y2', yBase) + .attr('stroke-width', xThickness) + .attr('stroke', xColor) + .attr('stroke-dasharray', xDash) + .attr('class', 'dropline'); + } // X axis marker - container.append('circle') - .attr('cx', xPoint) - .attr('cy', yBase) - .attr('r', 3) - .attr('fill', color) - .attr('class', 'dropline'); + if(xMarker) { + container.append('circle') + .attr('cx', xPoint) + .attr('cy', yAnchoredBase) + .attr('r', xThickness) + .attr('fill', xColor) + .attr('class', 'dropline'); + } } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b77d3428fb5..b49ede520fa 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -246,13 +246,57 @@ module.exports = { }, showspikes: { valType: 'boolean', - dflt: false, + dflt: true, role: 'style', description: [ 'Determines whether or not spikes (aka droplines) are drawn for this axis.', 'Note: This only takes affect when hovermode = closest' ].join(' ') }, + spikecolor: { + valType: 'color', + dflt: null, + role: 'style', + description: 'Sets the spike color. If undefined, will use the series color' + }, + spikethickness: { + valType: 'number', + dflt: 3, + role: 'style', + description: 'Sets the width (in px) of the zero line.' + }, + spikedash: { + valType: 'string', + // string type usually doesn't take values... this one should really be + // a special type or at least a special coercion function, from the GUI + // you only get these values but elsewhere the user can supply a list of + // dash lengths in px, and it will be honored + values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], + dflt: 'dash', + role: 'style', + description: [ + 'Sets the style of the lines. Set to a dash string type', + 'or a dash length in px.' + ].join(' ') + }, + spikemode: { + valType: 'flaglist', + flags: ['toaxis', 'across', 'marker'], + extras: ['none'], + role: 'style', + dflt: 'toaxis', + description: [ + 'Determines the drawing mode for the spike line', + 'If *toaxis*, the line is drawn from the data point to the axis the ', + 'series is plotted on.', + + 'If *across*, the line is drawn across the entire plot area, and', + 'supercedes *toaxis*.', + + 'If *marker*, then a marker dot is drawn on the axis the series is', + 'plotted on' + ].join(' ') + }, tickfont: extendFlat({}, fontAttrs, { description: 'Sets the tick font.' }), From 17d815c71e57c19157b9730faa38e7665e0edf61 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Thu, 6 Apr 2017 23:50:52 -0400 Subject: [PATCH 04/26] Add cartesian spikeline modebar support --- src/components/modebar/buttons.js | 49 +++++++++++++++++++++++- src/components/modebar/manage.js | 2 +- src/plot_api/plot_api.js | 9 +++-- src/plots/cartesian/axes.js | 29 ++++++++++++++ src/plots/cartesian/axis_defaults.js | 2 +- src/plots/cartesian/graph_interact.js | 4 +- src/plots/cartesian/layout_attributes.js | 4 +- 7 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index aae0503e368..c618dfe88c1 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -183,7 +183,8 @@ function handleCartesian(gd, ev) { var mag = (val === 'in') ? 0.5 : 2, r0 = (1 + mag) / 2, r1 = (1 - mag) / 2, - axList = Axes.list(gd, null, true); + axList = Axes.list(gd, null, true), + allEnabled = 'on'; var ax, axName; @@ -202,6 +203,12 @@ function handleCartesian(gd, ev) { aobj[axName + '.range[0]'] = rangeInitial[0]; aobj[axName + '.range[1]'] = rangeInitial[1]; } + if(ax._showSpikeInitial !== undefined) { + aobj[axName + '.showspike'] = ax._showSpikeInitial; + if(allEnabled === 'on' && !ax._showSpikeInitial) { + allEnabled = 'off'; + } + } } else { var rangeNow = [ @@ -219,12 +226,17 @@ function handleCartesian(gd, ev) { } } } + fullLayout._cartesianSpikesEnabled = allEnabled; } else { // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y' if(astr === 'hovermode' && (val === 'x' || val === 'y')) { val = fullLayout._isHoriz ? 'y' : 'x'; button.setAttribute('data-val', val); + if(val !== 'closest') { + fullLayout._cartesianSpikesEnabled = 'off'; + aobj = setSpikelineVisibility(gd); + } } aobj[astr] = val; @@ -518,3 +530,38 @@ modeBarButtons.resetViews = { // geo subplots. } }; + +modeBarButtons.toggleSpikelines = { + name: 'toggleSpikelines', + title: 'Toggle Spike Lines', + icon: Icons.home, + attr: '_cartesianSpikesEnabled', + val: 'on', + click: function(gd) { + var fullLayout = gd._fullLayout; + + fullLayout._cartesianSpikesEnabled = fullLayout.hovermode === 'closest' ? + (fullLayout._cartesianSpikesEnabled === 'on' ? 'off' : 'on') : 'on'; + + var aobj = setSpikelineVisibility(gd); + + aobj.hovermode = 'closest'; + Plotly.relayout(gd, aobj); + } +}; + +function setSpikelineVisibility(gd) { + var fullLayout = gd._fullLayout, + axList = Axes.list(gd, null, true), + ax, + axName, + aobj = {}; + + for(var i = 0; i < axList.length; i++) { + ax = axList[i]; + axName = ax._name; + aobj[axName + '.showspike'] = fullLayout._cartesianSpikesEnabled === 'on' ? true : false; + } + + return aobj; +} diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 57e5e2d17b3..f3732850f8c 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -138,7 +138,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { addGroup(['hoverClosestGl2d']); } else if(hasCartesian) { - addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); + addGroup(['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']); } else if(hasPie) { addGroup(['hoverClosestPie']); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 53fe0fe56cf..c9a11a7eaad 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -151,9 +151,12 @@ Plotly.plot = function(gd, data, layout, config) { makePlotFramework(gd); } - // save initial axis range once per graph - if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd); - + if(graphWasEmpty) { + // save initial axis range once per graph + Plotly.Axes.saveRangeInitial(gd); + // save initial show spikes once per graph + Plotly.Axes.saveShowSpikeInitial(gd); + } // prepare the data and find the autorange diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index e77bc52831e..d53b16063ad 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -361,6 +361,35 @@ axes.saveRangeInitial = function(gd, overwrite) { return hasOneAxisChanged; }; +// save a copy of the initial spike visibility +axes.saveShowSpikeInitial = function(gd, overwrite) { + var axList = axes.list(gd, '', true), + hasOneAxisChanged = false, + allEnabled = 'on'; + + for(var i = 0; i < axList.length; i++) { + var ax = axList[i]; + + var isNew = (ax._showSpikeInitial === undefined); + var hasChanged = ( + isNew || !( + ax.showspike === ax._showspike + ) + ); + + if((isNew) || (overwrite && hasChanged)) { + ax._showSpikeInitial = ax.showspike; + hasOneAxisChanged = true; + } + + if(allEnabled === 'on' && !ax.showspike) { + allEnabled = 'off'; + } + } + gd._fullLayout._cartesianSpikesEnabled = allEnabled; + return hasOneAxisChanged; +}; + // axes.expand: if autoranging, include new data in the outer limits // for this axis // data is an array of numbers (ie already run through ax.d2c) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 050a073ba0d..524964c97b8 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -99,7 +99,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce('showspikes'); + coerce('showspike'); coerce('spikecolor'); coerce('spikethickness'); coerce('spikedash'); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 35a2b312ce3..da0076a82b4 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -879,7 +879,7 @@ function createDroplines(hoverData, opts) { container.selectAll('circle.dropline').remove(); - if(c0.ya.showspikes) { + if(c0.ya.showspike) { if(ySpikeLine) { // Background horizontal Line (to y-axis) container.append('line') @@ -913,7 +913,7 @@ function createDroplines(hoverData, opts) { } } - if(c0.xa.showspikes) { + if(c0.xa.showspike) { if(xSpikeLine) { // Background vertical line (to x-axis) container.append('line') diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b49ede520fa..550d8bfdcd7 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -244,9 +244,9 @@ module.exports = { role: 'style', description: 'Determines whether or not the tick labels are drawn.' }, - showspikes: { + showspike: { valType: 'boolean', - dflt: true, + dflt: false, role: 'style', description: [ 'Determines whether or not spikes (aka droplines) are drawn for this axis.', From 762b54ab3a30189c14d4833dc4eebd11552bb158 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 7 Apr 2017 11:38:34 -0400 Subject: [PATCH 05/26] Move dashStyle to Drawing Make single attr calls Add crisp style Fix edge case when leaving and returning to closest hovermode --- src/components/drawing/index.js | 21 +++++- src/components/modebar/buttons.js | 23 ++++-- src/lib/index.js | 28 ------- src/plots/cartesian/graph_interact.js | 102 +++++++++++++++----------- 4 files changed, 96 insertions(+), 78 deletions(-) diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 8420a07f8d2..67245728b47 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -114,7 +114,7 @@ drawing.lineGroupStyle = function(s, lw, lc, ld) { drawing.dashLine = function(s, dash, lineWidth) { lineWidth = +lineWidth || 0; - dash = Lib.dash(dash, lineWidth); + dash = drawing.dashStyle(dash, lineWidth); s.style({ 'stroke-dasharray': dash, @@ -122,6 +122,25 @@ drawing.dashLine = function(s, dash, lineWidth) { }); }; +drawing.dashStyle = function(dash, lineWidth) { + lineWidth = +lineWidth || 1; + var dlw = Math.max(lineWidth, 3); + + if(dash === 'solid') dash = ''; + else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; + else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; + else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; + else if(dash === 'dashdot') { + dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; + } + else if(dash === 'longdashdot') { + dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; + } + // otherwise user wrote the dasharray themselves - leave it be + + return dash; +}; + drawing.fillGroupStyle = function(s) { s.style('stroke-width', 0) .each(function(d) { diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index c618dfe88c1..8c3a1028795 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -177,18 +177,20 @@ function handleCartesian(gd, ev) { astr = button.getAttribute('data-attr'), val = button.getAttribute('data-val') || true, fullLayout = gd._fullLayout, - aobj = {}; + aobj = {}, + axList = Axes.list(gd, null, true), + ax, + allEnabled = 'on', + i; if(astr === 'zoom') { var mag = (val === 'in') ? 0.5 : 2, r0 = (1 + mag) / 2, - r1 = (1 - mag) / 2, - axList = Axes.list(gd, null, true), - allEnabled = 'on'; + r1 = (1 - mag) / 2; - var ax, axName; + var axName; - for(var i = 0; i < axList.length; i++) { + for(i = 0; i < axList.length; i++) { ax = axList[i]; if(!ax.fixedrange) { @@ -235,8 +237,15 @@ function handleCartesian(gd, ev) { button.setAttribute('data-val', val); if(val !== 'closest') { fullLayout._cartesianSpikesEnabled = 'off'; - aobj = setSpikelineVisibility(gd); } + } else if(astr === 'hovermode' && val === 'closest') { + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + if(allEnabled === 'on' && !ax.showspike) { + allEnabled = 'off'; + } + } + fullLayout._cartesianSpikesEnabled = allEnabled; } aobj[astr] = val; diff --git a/src/lib/index.js b/src/lib/index.js index 360a923ee23..e1b6475a29b 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -636,31 +636,3 @@ lib.numSeparate = function(value, separators, separatethousands) { return x1 + x2; }; - -/** - * Accepts a named dash style or stroke-dasharray string - * and returns a dasharray representing the named type. - * - * @param {string} dash named dash format, or dasharray - * @param {number} lineWidth width of the line, used to scale the dashes - * - * @returns {string} SVG stroke-dasharray formatted string - */ -lib.dash = function(dash, lineWidth) { - lineWidth = +lineWidth || 1; - var dlw = Math.max(lineWidth, 3); - - if(dash === 'solid') dash = ''; - else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; - else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; - else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; - else if(dash === 'dashdot') { - dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; - } - else if(dash === 'longdashdot') { - dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; - } - // otherwise user wrote the dasharray themselves - leave it be - - return dash; -}; diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 5893dfa7b98..6127edc7473 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -868,8 +868,8 @@ function createDroplines(hoverData, opts) { '#000' : Color.background, xThickness = c0.xa.spikethickness, yThickness = c0.ya.spikethickness, - xDash = Lib.dash(c0.xa.spikedash, xThickness), - yDash = Lib.dash(c0.xa.spikedash, yThickness), + xDash = Drawing.dashStyle(c0.xa.spikedash, xThickness), + yDash = Drawing.dashStyle(c0.xa.spikedash, yThickness), xMarker = c0.xa.spikemode.indexOf('marker') !== -1, yMarker = c0.ya.spikemode.indexOf('marker') !== -1, xSpikeLine = c0.xa.spikemode.indexOf('toaxis') !== -1 || c0.xa.spikemode.indexOf('across') !== -1, @@ -886,33 +886,42 @@ function createDroplines(hoverData, opts) { if(ySpikeLine) { // Background horizontal Line (to y-axis) container.append('line') - .attr('x1', xBase) - .attr('x2', xEndSpike) - .attr('y1', yPoint) - .attr('y2', yPoint) - .attr('stroke-width', yThickness + 2) - .attr('stroke', yContrastColor) - .attr('class', 'dropline'); + .attr({ + 'x1': xBase, + 'x2': xEndSpike, + 'y1': yPoint, + 'y2': yPoint, + 'stroke-width': yThickness + 2, + 'stroke': yContrastColor + }) + .classed('dropline', true) + .classed('crisp', true); // Foreground horizontal line (to y-axis) container.append('line') - .attr('x1', xBase) - .attr('x2', xEndSpike) - .attr('y1', yPoint) - .attr('y2', yPoint) - .attr('stroke-width', yThickness) - .attr('stroke', yColor) - .attr('stroke-dasharray', yDash) - .attr('class', 'dropline'); + .attr({ + 'x1': xBase, + 'x2': xEndSpike, + 'y1': yPoint, + 'y2': yPoint, + 'stroke-width': yThickness, + 'stroke': yColor, + 'stroke-dasharray': yDash + }) + .classed('dropline', true) + .classed('crisp', true); } // Y axis marker if(yMarker) { container.append('circle') - .attr('cx', xAnchoredBase) - .attr('cy', yPoint) - .attr('r', yThickness) - .attr('fill', yColor) - .attr('class', 'dropline'); + .attr({ + 'cx': xAnchoredBase, + 'cy': yPoint, + 'r': yThickness, + 'fill': yColor + }) + .classed('dropline', true) + .classed('crisp', true); } } @@ -920,34 +929,43 @@ function createDroplines(hoverData, opts) { if(xSpikeLine) { // Background vertical line (to x-axis) container.append('line') - .attr('x1', xPoint) - .attr('x2', xPoint) - .attr('y1', yEndSpike) - .attr('y2', yBase) - .attr('stroke-width', xThickness + 2) - .attr('stroke', xContrastColor) - .attr('class', 'dropline'); + .attr({ + 'x1': xPoint, + 'x2': xPoint, + 'y1': yEndSpike, + 'y2': yBase, + 'stroke-width': xThickness + 2, + 'stroke': xContrastColor + }) + .classed('dropline', true) + .classed('crisp', true); // Foreground vertical line (to x-axis) container.append('line') - .attr('x1', xPoint) - .attr('x2', xPoint) - .attr('y1', yEndSpike) - .attr('y2', yBase) - .attr('stroke-width', xThickness) - .attr('stroke', xColor) - .attr('stroke-dasharray', xDash) - .attr('class', 'dropline'); + .attr({ + 'x1': xPoint, + 'x2': xPoint, + 'y1': yEndSpike, + 'y2': yBase, + 'stroke-width': xThickness, + 'stroke': xColor, + 'stroke-dasharray': xDash + }) + .classed('dropline', true) + .classed('crisp', true); } // X axis marker if(xMarker) { container.append('circle') - .attr('cx', xPoint) - .attr('cy', yAnchoredBase) - .attr('r', xThickness) - .attr('fill', xColor) - .attr('class', 'dropline'); + .attr({ + 'cx': xPoint, + 'cy': yAnchoredBase, + 'r': xThickness, + 'fill': xColor + }) + .classed('dropline', true) + .classed('crisp', true); } } } From d680bd4953eb2730c123e59bac6cccf4e90b36c2 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 7 Apr 2017 12:21:32 -0400 Subject: [PATCH 06/26] Name back to showspikes Keep spike background color to the chart background, unless the series color is too close, then use contrast color --- src/components/modebar/buttons.js | 6 ++--- src/plots/cartesian/axes.js | 6 ++--- src/plots/cartesian/axis_defaults.js | 2 +- src/plots/cartesian/graph_interact.js | 31 +++++++++++++----------- src/plots/cartesian/layout_attributes.js | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 8c3a1028795..bb6e14ac4aa 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -206,7 +206,7 @@ function handleCartesian(gd, ev) { aobj[axName + '.range[1]'] = rangeInitial[1]; } if(ax._showSpikeInitial !== undefined) { - aobj[axName + '.showspike'] = ax._showSpikeInitial; + aobj[axName + '.showspikes'] = ax._showSpikeInitial; if(allEnabled === 'on' && !ax._showSpikeInitial) { allEnabled = 'off'; } @@ -241,7 +241,7 @@ function handleCartesian(gd, ev) { } else if(astr === 'hovermode' && val === 'closest') { for(i = 0; i < axList.length; i++) { ax = axList[i]; - if(allEnabled === 'on' && !ax.showspike) { + if(allEnabled === 'on' && !ax.showspikes) { allEnabled = 'off'; } } @@ -569,7 +569,7 @@ function setSpikelineVisibility(gd) { for(var i = 0; i < axList.length; i++) { ax = axList[i]; axName = ax._name; - aobj[axName + '.showspike'] = fullLayout._cartesianSpikesEnabled === 'on' ? true : false; + aobj[axName + '.showspikes'] = fullLayout._cartesianSpikesEnabled === 'on' ? true : false; } return aobj; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index d53b16063ad..820b269251e 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -373,16 +373,16 @@ axes.saveShowSpikeInitial = function(gd, overwrite) { var isNew = (ax._showSpikeInitial === undefined); var hasChanged = ( isNew || !( - ax.showspike === ax._showspike + ax.showspikes === ax._showspikes ) ); if((isNew) || (overwrite && hasChanged)) { - ax._showSpikeInitial = ax.showspike; + ax._showSpikeInitial = ax.showspikes; hasOneAxisChanged = true; } - if(allEnabled === 'on' && !ax.showspike) { + if(allEnabled === 'on' && !ax.showspikes) { allEnabled = 'off'; } } diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 0928cbba64e..8e474035476 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -75,7 +75,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce('showspike'); + coerce('showspikes'); coerce('spikecolor'); coerce('spikethickness'); coerce('spikedash'); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 6127edc7473..172836cb45d 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -860,12 +860,15 @@ function createDroplines(hoverData, opts) { yAnchoredBase = yEdge - outerBBox.top, xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, - xColor = c0.xa.spikecolor ? c0.xa.spikecolor : c0.color, - yColor = c0.ya.spikecolor ? c0.ya.spikecolor : c0.color, - xContrastColor = tinycolor(xColor).getBrightness() > 128 ? - '#000' : Color.background, - yContrastColor = tinycolor(yColor).getBrightness() > 128 ? - '#000' : Color.background, + contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor), + xColor = c0.xa.spikecolor ? c0.xa.spikecolor : ( + tinycolor.readability(c0.color,contrastColor) < 1.5 ? ( + tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) + : c0.color), + yColor = c0.ya.spikecolor ? c0.ya.spikecolor : ( + tinycolor.readability(c0.color,contrastColor) < 1.5 ? ( + tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) + : c0.color), xThickness = c0.xa.spikethickness, yThickness = c0.ya.spikethickness, xDash = Drawing.dashStyle(c0.xa.spikedash, xThickness), @@ -882,7 +885,7 @@ function createDroplines(hoverData, opts) { container.selectAll('circle.dropline').remove(); - if(c0.ya.showspike) { + if(c0.ya.showspikes) { if(ySpikeLine) { // Background horizontal Line (to y-axis) container.append('line') @@ -892,7 +895,7 @@ function createDroplines(hoverData, opts) { 'y1': yPoint, 'y2': yPoint, 'stroke-width': yThickness + 2, - 'stroke': yContrastColor + 'stroke': contrastColor }) .classed('dropline', true) .classed('crisp', true); @@ -925,17 +928,17 @@ function createDroplines(hoverData, opts) { } } - if(c0.xa.showspike) { + if(c0.xa.showspikes) { if(xSpikeLine) { // Background vertical line (to x-axis) container.append('line') .attr({ 'x1': xPoint, 'x2': xPoint, - 'y1': yEndSpike, - 'y2': yBase, + 'y1': yBase, + 'y2': yEndSpike, 'stroke-width': xThickness + 2, - 'stroke': xContrastColor + 'stroke': contrastColor }) .classed('dropline', true) .classed('crisp', true); @@ -945,8 +948,8 @@ function createDroplines(hoverData, opts) { .attr({ 'x1': xPoint, 'x2': xPoint, - 'y1': yEndSpike, - 'y2': yBase, + 'y1': yBase, + 'y2': yEndSpike, 'stroke-width': xThickness, 'stroke': xColor, 'stroke-dasharray': xDash diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 4652f174fb2..e1688ff0dcd 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -279,7 +279,7 @@ module.exports = { role: 'style', description: 'Determines whether or not the tick labels are drawn.' }, - showspike: { + showspikes: { valType: 'boolean', dflt: false, role: 'style', From 2433d4cb4da0e66524d186abf92a3a43761b3573 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 7 Apr 2017 15:25:57 -0400 Subject: [PATCH 07/26] Lint, test and implementation fixes --- src/plots/cartesian/axis_defaults.js | 11 ++++++----- src/plots/cartesian/graph_interact.js | 17 +++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 8e474035476..8e86e044193 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -75,11 +75,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce('showspikes'); - coerce('spikecolor'); - coerce('spikethickness'); - coerce('spikedash'); - coerce('spikemode'); + if(coerce2('showspikes')) { + coerce2('spikecolor'); + coerce2('spikethickness'); + coerce2('spikedash'); + coerce2('spikemode'); + } handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 172836cb45d..56be767307d 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -591,9 +591,10 @@ function hover(gd, evt, subplot) { gd._hoverdata = newhoverdata; - if(hoverChanged(gd, evt, oldhoverdata)) { + if(hoverChanged(gd, evt, oldhoverdata) && gd._hasCartesian) { var droplineOpts = { hovermode: hovermode, + fullLayout: fullLayout, container: fullLayout._hoverlayer, outerContainer: fullLayout._paperdiv }; @@ -860,15 +861,15 @@ function createDroplines(hoverData, opts) { yAnchoredBase = yEdge - outerBBox.top, xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, - contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor), + contrastColor = Color.combine(opts.fullLayout.plot_bgcolor, opts.fullLayout.paper_bgcolor), xColor = c0.xa.spikecolor ? c0.xa.spikecolor : ( - tinycolor.readability(c0.color,contrastColor) < 1.5 ? ( - tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) - : c0.color), + tinycolor.readability(c0.color, contrastColor) < 1.5 ? ( + tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) : + c0.color), yColor = c0.ya.spikecolor ? c0.ya.spikecolor : ( - tinycolor.readability(c0.color,contrastColor) < 1.5 ? ( - tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) - : c0.color), + tinycolor.readability(c0.color, contrastColor) < 1.5 ? ( + tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) : + c0.color), xThickness = c0.xa.spikethickness, yThickness = c0.ya.spikethickness, xDash = Drawing.dashStyle(c0.xa.spikedash, xThickness), From 75da2fe2b808541b7dd0d006da4407b9162fa01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 7 Apr 2017 17:08:27 -0400 Subject: [PATCH 08/26] add spikeline icon --- build/ploticon.js | 6 ++++++ src/components/modebar/buttons.js | 2 +- src/components/modebar/modebar.js | 10 +++++++--- src/fonts/ploticon/ploticon.svg | 3 ++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/build/ploticon.js b/build/ploticon.js index 84b86fb0aa6..4dd7accc816 100644 --- a/build/ploticon.js +++ b/build/ploticon.js @@ -114,5 +114,11 @@ module.exports = { 'path': 'm0 850l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-285l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z', 'ascent': 850, 'descent': -150 + }, + 'spikeline': { + 'width': 1000, + 'path': 'M512 409c0-57-46-104-103-104-57 0-104 47-104 104 0 57 47 103 104 103 57 0 103-46 103-103z m-327-39l92 0 0 92-92 0z m-185 0l92 0 0 92-92 0z m370-186l92 0 0 93-92 0z m0-184l92 0 0 92-92 0z', + 'ascent': 850, + 'descent': -150 } }; diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index bb6e14ac4aa..776a75df610 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -543,7 +543,7 @@ modeBarButtons.resetViews = { modeBarButtons.toggleSpikelines = { name: 'toggleSpikelines', title: 'Toggle Spike Lines', - icon: Icons.home, + icon: Icons.spikeline, attr: '_cartesianSpikesEnabled', val: 'on', click: function(gd) { diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js index 054a8a91928..5e87b2413a8 100644 --- a/src/components/modebar/modebar.js +++ b/src/components/modebar/modebar.js @@ -149,7 +149,7 @@ proto.createButton = function(config) { button.setAttribute('data-toggle', config.toggle || false); if(config.toggle) d3.select(button).classed('active', true); - button.appendChild(this.createIcon(config.icon || Icons.question)); + button.appendChild(this.createIcon(config.icon || Icons.question, config.name)); button.setAttribute('data-gravity', config.gravity || 'n'); return button; @@ -162,7 +162,7 @@ proto.createButton = function(config) { * @Param {string} thisIcon.path * @Return {HTMLelement} */ -proto.createIcon = function(thisIcon) { +proto.createIcon = function(thisIcon, name) { var iconHeight = thisIcon.ascent - thisIcon.descent, svgNS = 'http://www.w3.org/2000/svg', icon = document.createElementNS(svgNS, 'svg'), @@ -172,8 +172,12 @@ proto.createIcon = function(thisIcon) { icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em'); icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); + var transform = name === 'toggleSpikelines' ? + 'matrix(1.5 0 0 -1.5 0 ' + thisIcon.ascent + ')' : + 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'; + path.setAttribute('d', thisIcon.path); - path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'); + path.setAttribute('transform', transform); icon.appendChild(path); return icon; diff --git a/src/fonts/ploticon/ploticon.svg b/src/fonts/ploticon/ploticon.svg index a72ac5b551d..5007169983b 100644 --- a/src/fonts/ploticon/ploticon.svg +++ b/src/fonts/ploticon/ploticon.svg @@ -25,6 +25,7 @@ + - \ No newline at end of file + From 795348894e4ccc55bcd8cbc541fe47b631ccf176 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Fri, 7 Apr 2017 17:29:05 -0400 Subject: [PATCH 09/26] Fix cartesian check and fine-tune marker placement --- src/plots/cartesian/graph_interact.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 56be767307d..e0cedfe657b 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -591,7 +591,7 @@ function hover(gd, evt, subplot) { gd._hoverdata = newhoverdata; - if(hoverChanged(gd, evt, oldhoverdata) && gd._hasCartesian) { + if(hoverChanged(gd, evt, oldhoverdata) && fullLayout._hasCartesian) { var droplineOpts = { hovermode: hovermode, fullLayout: fullLayout, @@ -919,7 +919,7 @@ function createDroplines(hoverData, opts) { if(yMarker) { container.append('circle') .attr({ - 'cx': xAnchoredBase, + 'cx': xAnchoredBase + yThickness, 'cy': yPoint, 'r': yThickness, 'fill': yColor @@ -964,7 +964,7 @@ function createDroplines(hoverData, opts) { container.append('circle') .attr({ 'cx': xPoint, - 'cy': yAnchoredBase, + 'cy': yAnchoredBase - xThickness, 'r': xThickness, 'fill': xColor }) From b9f6ab1536d24470dc325e4b9544238ff27fa3d9 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Mon, 10 Apr 2017 12:57:16 -0400 Subject: [PATCH 10/26] Refactor dropline to spikeline Fix modebar tests, and add some new ones Add spikeline tests Final adjustment to marker placement to account for y-axis on right side --- src/plots/cartesian/axis_defaults.js | 11 ++-- src/plots/cartesian/graph_interact.js | 37 ++++++------- test/jasmine/tests/hover_spikeline_test.js | 43 +++++++++++++++ test/jasmine/tests/modebar_test.js | 64 +++++++++++++++++----- 4 files changed, 116 insertions(+), 39 deletions(-) create mode 100644 test/jasmine/tests/hover_spikeline_test.js diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 8e86e044193..187af3c2d19 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -75,12 +75,11 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - if(coerce2('showspikes')) { - coerce2('spikecolor'); - coerce2('spikethickness'); - coerce2('spikedash'); - coerce2('spikemode'); - } + coerce2('showspikes'); + coerce2('spikecolor'); + coerce2('spikethickness'); + coerce2('spikedash'); + coerce2('spikemode'); handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index e0cedfe657b..2b700581e68 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -592,13 +592,13 @@ function hover(gd, evt, subplot) { gd._hoverdata = newhoverdata; if(hoverChanged(gd, evt, oldhoverdata) && fullLayout._hasCartesian) { - var droplineOpts = { + var spikelineOpts = { hovermode: hovermode, fullLayout: fullLayout, container: fullLayout._hoverlayer, outerContainer: fullLayout._paperdiv }; - createDroplines(hoverData, droplineOpts); + createSpikelines(hoverData, spikelineOpts); } // if there's more than one horz bar trace, @@ -631,7 +631,8 @@ function hover(gd, evt, subplot) { overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : ''); } - if(!hoverChanged(gd, evt, oldhoverdata)) return; + // don't emit events if called manually + if(evt.target && !hoverChanged(gd, evt, oldhoverdata)) return; if(oldhoverdata) { gd.emit('plotly_unhover', { @@ -832,11 +833,11 @@ fx.loneUnhover = function(containerOrSelection) { d3.select(containerOrSelection); selection.selectAll('g.hovertext').remove(); - selection.selectAll('line.dropline').remove(); - selection.selectAll('circle.dropline').remove(); + selection.selectAll('line.spikeline').remove(); + selection.selectAll('circle.spikeline').remove(); }; -function createDroplines(hoverData, opts) { +function createSpikelines(hoverData, opts) { var hovermode = opts.hovermode, container = opts.container, outerContainer = opts.outerContainer; @@ -881,9 +882,9 @@ function createDroplines(hoverData, opts) { xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? xBase + xLength : xPoint, yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? yBase - yLength : yPoint; - // Remove old dropline items - container.selectAll('line.dropline').remove(); - container.selectAll('circle.dropline').remove(); + // Remove old spikeline items + container.selectAll('line.spikeline').remove(); + container.selectAll('circle.spikeline').remove(); if(c0.ya.showspikes) { @@ -898,7 +899,7 @@ function createDroplines(hoverData, opts) { 'stroke-width': yThickness + 2, 'stroke': contrastColor }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); // Foreground horizontal line (to y-axis) @@ -912,19 +913,19 @@ function createDroplines(hoverData, opts) { 'stroke': yColor, 'stroke-dasharray': yDash }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); } // Y axis marker if(yMarker) { container.append('circle') .attr({ - 'cx': xAnchoredBase + yThickness, + 'cx': xAnchoredBase + (ySide !== 'right' ? yThickness : -yThickness), 'cy': yPoint, 'r': yThickness, 'fill': yColor }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); } } @@ -941,7 +942,7 @@ function createDroplines(hoverData, opts) { 'stroke-width': xThickness + 2, 'stroke': contrastColor }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); // Foreground vertical line (to x-axis) @@ -955,7 +956,7 @@ function createDroplines(hoverData, opts) { 'stroke': xColor, 'stroke-dasharray': xDash }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); } @@ -968,7 +969,7 @@ function createDroplines(hoverData, opts) { 'r': xThickness, 'fill': xColor }) - .classed('dropline', true) + .classed('spikeline', true) .classed('crisp', true); } } @@ -1484,9 +1485,7 @@ function alignHoverText(hoverLabels, rotateLabels) { } function hoverChanged(gd, evt, oldhoverdata) { - // don't emit any events if nothing changed or - // if fx.hover was called manually - if(!evt.target) return false; + // don't emit any events if nothing changed if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true; for(var i = oldhoverdata.length - 1; i >= 0; i--) { diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js new file mode 100644 index 00000000000..f8651bbc7ed --- /dev/null +++ b/test/jasmine/tests/hover_spikeline_test.js @@ -0,0 +1,43 @@ +var d3 = require('d3'); + +var Plotly = require('@lib/index'); +var Fx = require('@src/plots/cartesian/graph_interact'); +var Lib = require('@src/lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('spikeline', function() { + 'use strict'; + + var mock = require('@mocks/19.json'); + + afterEach(destroyGraphDiv); + + describe('hover', function() { + var mockCopy = Lib.extendDeep({}, mock); + + mockCopy.layout.xaxis.showspikes = true; + mockCopy.layout.xaxis.spikemode = 'toaxis'; + mockCopy.layout.yaxis.showspikes = true; + mockCopy.layout.yaxis.spikemode = 'toaxis+marker'; + mockCopy.layout.xaxis2.showspikes = true; + mockCopy.layout.xaxis2.spikemode = 'toaxis'; + mockCopy.layout.hovermode = 'closest'; + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done); + }); + + it('draws lines and markers on enabled axes', function() { + Fx.hover('graph', {xval: 2, yval: 3}, 'xy'); + expect(d3.selectAll('line.spikeline').size()).toEqual(4); + expect(d3.selectAll('circle.spikeline').size()).toEqual(1); + }); + + it('doesn\'t draw lines and markers on disabled axes', function() { + Fx.hover('graph', {xval: 30, yval: 40}, 'x2y2'); + expect(d3.selectAll('line.spikeline').size()).toEqual(2); + expect(d3.selectAll('circle.spikeline').size()).toEqual(0); + }); + }); +}); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index b3be9a51b43..5133e5fc975 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -185,7 +185,7 @@ describe('ModeBar', function() { ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(); @@ -203,7 +203,7 @@ describe('ModeBar', function() { ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(); @@ -225,7 +225,7 @@ describe('ModeBar', function() { it('creates mode bar (cartesian fixed-axes version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(); @@ -412,7 +412,7 @@ describe('ModeBar', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d'], - ['hoverClosestCartesian', 'hoverCompareCartesian'] + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(); @@ -544,7 +544,7 @@ describe('ModeBar', function() { var modeBar = gd._fullLayout._modeBar; expect(countGroups(modeBar)).toEqual(6); - expect(countButtons(modeBar)).toEqual(10); + expect(countButtons(modeBar)).toEqual(11); }); it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove (2)', function() { @@ -564,7 +564,7 @@ describe('ModeBar', function() { var modeBar = gd._fullLayout._modeBar; expect(countGroups(modeBar)).toEqual(7); - expect(countButtons(modeBar)).toEqual(12); + expect(countButtons(modeBar)).toEqual(13); }); it('sets up buttons with fully custom modeBarButtons', function() { @@ -612,7 +612,7 @@ describe('ModeBar', function() { }); describe('modebar on clicks', function() { - var gd, modeBar; + var gd, modeBar, buttonClosest, buttonCompare, buttonToggle, hovermodeButtons; beforeAll(function() { jasmine.addMatchers(customMatchers); @@ -685,6 +685,10 @@ describe('ModeBar', function() { gd = createGraphDiv(); Plotly.plot(gd, mockData, mockLayout).then(function() { modeBar = gd._fullLayout._modeBar; + buttonToggle = selectButton(modeBar, 'toggleSpikelines'); + buttonCompare = selectButton(modeBar, 'hoverCompareCartesian'); + buttonClosest = selectButton(modeBar, 'hoverClosestCartesian'); + hovermodeButtons = [buttonCompare, buttonClosest]; done(); }); }); @@ -758,21 +762,53 @@ describe('ModeBar', function() { }); describe('buttons hoverCompareCartesian and hoverClosestCartesian ', function() { - it('should update layout hovermode', function() { - var buttonCompare = selectButton(modeBar, 'hoverCompareCartesian'), - buttonClosest = selectButton(modeBar, 'hoverClosestCartesian'), - buttons = [buttonCompare, buttonClosest]; + it('should update layout hovermode', function() { expect(gd._fullLayout.hovermode).toBe('x'); - assertActive(buttons, buttonCompare); + assertActive(hovermodeButtons, buttonCompare); buttonClosest.click(); expect(gd._fullLayout.hovermode).toBe('closest'); - assertActive(buttons, buttonClosest); + assertActive(hovermodeButtons, buttonClosest); buttonCompare.click(); expect(gd._fullLayout.hovermode).toBe('x'); - assertActive(buttons, buttonCompare); + assertActive(hovermodeButtons, buttonCompare); + }); + }); + + describe('button toggleSpikelines', function() { + it('should update layout hovermode', function() { + expect(gd._fullLayout.hovermode).toBe('x'); + assertActive(hovermodeButtons, buttonCompare); + + buttonToggle.click(); + expect(gd._fullLayout.hovermode).toBe('closest'); + assertActive(hovermodeButtons, buttonClosest); + }); + it('should makes spikelines visible', function() { + buttonToggle.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on'); + + buttonToggle.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off'); + }); + it('should become disabled when hovermode is switched off closest', function() { + buttonToggle.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on'); + + buttonCompare.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off'); + }); + it('should be re-enabled when hovermode is set to closest if it was previously on', function() { + buttonToggle.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on'); + + buttonCompare.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off'); + + buttonClosest.click(); + expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on'); }); }); }); From 34a1562a896b4f6608a850c246d2c91c2a7315c8 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Mon, 10 Apr 2017 14:43:14 -0400 Subject: [PATCH 11/26] Switch from getBoundingClientRect to offsetLeft and offsetTop. --- src/plots/cartesian/graph_interact.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 2b700581e68..7d6bf031f61 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -855,11 +855,10 @@ function createSpikelines(hoverData, opts) { yLength = c0.ya._length, xEdge = c0.ya._boundingBox.left + (ySide === 'left' ? c0.ya._boundingBox.width : 0), yEdge = c0.xa._boundingBox.top + (xSide === 'top' ? c0.xa._boundingBox.height : 0), - outerBBox = outerContainer.node().getBoundingClientRect(), xFreeBase = xOffset + (ySide === 'right' ? xLength : 0), yFreeBase = yOffset + (xSide === 'top' ? yLength : 0), - xAnchoredBase = xEdge - outerBBox.left, - yAnchoredBase = yEdge - outerBBox.top, + xAnchoredBase = xEdge - outerContainer.node().offsetLeft, + yAnchoredBase = yEdge - outerContainer.node().offsetTop, xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, contrastColor = Color.combine(opts.fullLayout.plot_bgcolor, opts.fullLayout.paper_bgcolor), From 8c7ac76f2f100998b3f5d479224c9c0f180efd85 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Mon, 10 Apr 2017 16:57:26 -0400 Subject: [PATCH 12/26] Move spike setup from axis to layout. Fix 'across' for right-side axes. --- src/plots/cartesian/axis_defaults.js | 6 ------ src/plots/cartesian/graph_interact.js | 4 +++- src/plots/cartesian/layout_defaults.js | 6 ++++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 187af3c2d19..c0f0ea8e35a 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -75,12 +75,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce2('showspikes'); - coerce2('spikecolor'); - coerce2('spikethickness'); - coerce2('spikedash'); - coerce2('spikemode'); - handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); handleTickMarkDefaults(containerIn, containerOut, coerce, options); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 7d6bf031f61..52a9862dcef 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -878,7 +878,9 @@ function createSpikelines(hoverData, opts) { yMarker = c0.ya.spikemode.indexOf('marker') !== -1, xSpikeLine = c0.xa.spikemode.indexOf('toaxis') !== -1 || c0.xa.spikemode.indexOf('across') !== -1, ySpikeLine = c0.ya.spikemode.indexOf('toaxis') !== -1 || c0.ya.spikemode.indexOf('across') !== -1, - xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? xBase + xLength : xPoint, + xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? + (ySide === 'left' ? xBase + xLength : xBase - xLength) : + xPoint, yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? yBase - yLength : yPoint; // Remove old spikeline items diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 468e685234b..228bac73753 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -173,6 +173,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); + coerce('showspikes'); + coerce('spikecolor'); + coerce('spikethickness'); + coerce('spikedash'); + coerce('spikemode'); + var positioningOptions = { letter: axLetter, counterAxes: counterAxes[axLetter], From 6e243de55da834244ed26f48607da235b9a97234 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Tue, 11 Apr 2017 11:32:50 -0400 Subject: [PATCH 13/26] Fix marker positioning and across rendering for top-side x-axes. --- src/plots/cartesian/graph_interact.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 52a9862dcef..dcdb2f402d1 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -881,13 +881,14 @@ function createSpikelines(hoverData, opts) { xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? (ySide === 'left' ? xBase + xLength : xBase - xLength) : xPoint, - yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? yBase - yLength : yPoint; + yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? + (xSide === 'bottom' ? yBase - yLength : yBase + yLength) : + yPoint; // Remove old spikeline items container.selectAll('line.spikeline').remove(); container.selectAll('circle.spikeline').remove(); - if(c0.ya.showspikes) { if(ySpikeLine) { // Background horizontal Line (to y-axis) @@ -966,7 +967,7 @@ function createSpikelines(hoverData, opts) { container.append('circle') .attr({ 'cx': xPoint, - 'cy': yAnchoredBase - xThickness, + 'cy': yAnchoredBase - (xSide !== 'top' ? xThickness : -xThickness), 'r': xThickness, 'fill': xColor }) From 08371c26d296125996f66bf200ffff7ec616ff45 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Apr 2017 22:44:50 -0400 Subject: [PATCH 14/26] short-circuit redraw for spike attribute relayouts --- src/plot_api/plot_api.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 43f701a8e42..e712a42047c 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2124,14 +2124,16 @@ function _relayout(gd, aobj) { flags.doticks = flags.dolayoutstyle = true; } /* - * hovermode and dragmode don't need any redrawing, since they just - * affect reaction to user input, everything else, assume full replot. + * hovermode, dragmode, and spikes don't need any redrawing, since they just + * affect reaction to user input. Everything else, assume full replot. * height, width, autosize get dealt with below. Except for the case of * of subplots - scenes - which require scene.updateFx to be called. */ - else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true; - else if(['hovermode', 'dragmode', 'height', - 'width', 'autosize'].indexOf(ai) === -1) { + else if(['hovermode', 'dragmode'].indexOf(ai) !== -1 || + ai.indexOf('spike') !== -1) { + flags.domodebar = true; + } + else if(['height', 'width', 'autosize'].indexOf(ai) === -1) { flags.doplot = true; } From 69dc78147de4a3255225a1878e653800c6092107 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Apr 2017 22:49:45 -0400 Subject: [PATCH 15/26] fix manual-hover logic for event emitting --- src/plots/cartesian/graph_interact.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 72739128634..9d6b5e37b94 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -629,7 +629,7 @@ function hover(gd, evt, subplot) { } // don't emit events if called manually - if(evt.target && !hoverChanged(gd, evt, oldhoverdata)) return; + if(!evt.target || !hoverChanged(gd, evt, oldhoverdata)) return; if(oldhoverdata) { gd.emit('plotly_unhover', { From 37b77fe69e52442fc553de7367d1d3073e1bdc32 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Apr 2017 22:53:24 -0400 Subject: [PATCH 16/26] don't make spike markers crisp --- src/plots/cartesian/graph_interact.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 9d6b5e37b94..488d6a4078c 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -943,8 +943,7 @@ function createSpikelines(hoverData, opts) { 'r': yThickness, 'fill': yColor }) - .classed('spikeline', true) - .classed('crisp', true); + .classed('spikeline', true); } } @@ -987,8 +986,7 @@ function createSpikelines(hoverData, opts) { 'r': xThickness, 'fill': xColor }) - .classed('spikeline', true) - .classed('crisp', true); + .classed('spikeline', true); } } } From 710d2d64b8d43a92e952926bc6b0b4e5d2d5a462 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Apr 2017 22:55:26 -0400 Subject: [PATCH 17/26] alter logic for where spikes start and end - toaxis always goes from the point to the axis, even for free axes - across spans all counteraxes with subplots for this axis, and extends to the free axis if it exists - fixed a bug with positioning after scrolling gd within a container - also short-circuited calculations we don't need --- src/plots/cartesian/axes.js | 82 ++++++++++++++---- src/plots/cartesian/graph_interact.js | 117 ++++++++++++-------------- 2 files changed, 121 insertions(+), 78 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 820b269251e..8d811c1288b 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1632,7 +1632,7 @@ axes.doTicks = function(gd, axid, skipTitle) { // set scaling to pixels ax.setScale(); - var axletter = axid.charAt(0), + var axLetter = axid.charAt(0), counterLetter = axes.counterLetter(axid), vals = axes.calcTicks(ax), datafn = function(d) { return [d.text, d.x, ax.mirror].join('_'); }, @@ -1646,7 +1646,7 @@ axes.doTicks = function(gd, axid, skipTitle) { gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1), zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth), tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1), - sides, transfn, tickpathfn, + sides, transfn, tickpathfn, subplots, i; if(ax._counterangle && ax.ticks === 'outside') { @@ -1656,7 +1656,7 @@ axes.doTicks = function(gd, axid, skipTitle) { } // positioning arguments for x vs y axes - if(axletter === 'x') { + if(axLetter === 'x') { sides = ['bottom', 'top']; transfn = function(d) { return 'translate(' + ax.l2p(d.x) + ',0)'; @@ -1669,7 +1669,7 @@ axes.doTicks = function(gd, axid, skipTitle) { else return 'M0,' + shift + 'v' + len; }; } - else if(axletter === 'y') { + else if(axLetter === 'y') { sides = ['left', 'right']; transfn = function(d) { return 'translate(0,' + ax.l2p(d.x) + ')'; @@ -1690,7 +1690,7 @@ axes.doTicks = function(gd, axid, skipTitle) { // which direction do the side[0], side[1], and free ticks go? // then we flip if outside XOR y axis ticksign = [-1, 1, axside === sides[1] ? 1 : -1]; - if((ax.ticks !== 'inside') === (axletter === 'x')) { + if((ax.ticks !== 'inside') === (axLetter === 'x')) { ticksign = ticksign.map(function(v) { return -v; }); } @@ -1724,12 +1724,12 @@ axes.doTicks = function(gd, axid, skipTitle) { var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn); if(!ax.showticklabels || !isNumeric(position)) { tickLabels.remove(); - drawAxTitle(axid); + drawAxTitle(); return; } var labelx, labely, labelanchor, labelpos0, flipit; - if(axletter === 'x') { + if(axLetter === 'x') { flipit = (axside === 'bottom') ? 1 : -1; labelx = function(d) { return d.dx + labelShift * flipit; }; labelpos0 = position + (labelStandoff + pad) * flipit; @@ -1845,7 +1845,7 @@ axes.doTicks = function(gd, axid, skipTitle) { // check for auto-angling if x labels overlap // don't auto-angle at all for log axes with // base and digit format - if(axletter === 'x' && !isNumeric(ax.tickangle) && + if(axLetter === 'x' && !isNumeric(ax.tickangle) && (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) { var lbbArray = []; tickLabels.each(function(d) { @@ -1890,12 +1890,59 @@ axes.doTicks = function(gd, axid, skipTitle) { // (so it can move out of the way if needed) // TODO: separate out scoot so we don't need to do // a full redraw of the title (mostly relevant for MathJax) - drawAxTitle(axid); + drawAxTitle(); return axid + ' done'; } function calcBoundingBox() { - ax._boundingBox = container.node().getBoundingClientRect(); + var bBox = container.node().getBoundingClientRect(); + var gdBB = gd.getBoundingClientRect(); + + /* + * the way we're going to use this, the positioning that matters + * is relative to the origin of gd. This is important particularly + * if gd is scrollable, and may have been scrolled between the time + * we calculate this and the time we use it + */ + var bbFinal = ax._boundingBox = { + width: bBox.width, + height: bBox.height, + left: bBox.left - gdBB.left, + right: bBox.right - gdBB.left, + top: bBox.top - gdBB.top, + bottom: bBox.bottom - gdBB.top + }; + + /* + * for spikelines: what's the full domain of positions in the + * opposite direction that are associated with this axis? + * This means any axes that we make a subplot with, plus the + * position of the axis itself if it's free. + */ + if(subplots) { + var fullRange = ax._counterSpan = [Infinity, -Infinity]; + + for(i = 0; i < subplots.length; i++) { + var subplot = fullLayout._plots[subplots[i]]; + var counterAxis = subplot[(axLetter === 'x') ? 'yaxis' : 'xaxis']; + + extendRange(fullRange, [ + counterAxis._offset, + counterAxis._offset + counterAxis._length + ]); + } + + if(ax.anchor === 'free') { + extendRange(fullRange, (axLetter === 'x') ? + [ax._boundingBox.bottom, ax._boundingBox.top] : + [ax._boundingBox.right, ax._boundingBox.left]); + } + } + + function extendRange(range, newRange) { + range[0] = Math.min(range[0], newRange[0]); + range[1] = Math.max(range[1], newRange[1]); + } } var done = Lib.syncOrAsync([ @@ -1907,7 +1954,7 @@ axes.doTicks = function(gd, axid, skipTitle) { return done; } - function drawAxTitle(axid) { + function drawAxTitle() { if(skipTitle) return; // now this only applies to regular cartesian axes; colorbars and @@ -1978,16 +2025,16 @@ axes.doTicks = function(gd, axid, skipTitle) { function traceHasBarsOrFill(trace, subplot) { if(trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) return false; - if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axletter]) return true; - return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter; + if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axLetter]) return true; + return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter; } function drawGrid(plotinfo, counteraxis, subplot) { var gridcontainer = plotinfo.gridlayer, zlcontainer = plotinfo.zerolinelayer, - gridvals = plotinfo['hidegrid' + axletter] ? [] : valsClipped, + gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped, gridpath = ax._gridpath || - 'M0,0' + ((axletter === 'x') ? 'v' : 'h') + counteraxis._length, + 'M0,0' + ((axLetter === 'x') ? 'v' : 'h') + counteraxis._length, grid = gridcontainer.selectAll('path.' + gcls) .data((ax.showgrid === false) ? [] : gridvals, datafn); grid.enter().append('path').classed(gcls, 1) @@ -2041,12 +2088,13 @@ axes.doTicks = function(gd, axid, skipTitle) { return drawLabels(ax._axislayer, ax._pos); } else { - var alldone = axes.getSubplots(gd, ax).map(function(subplot) { + subplots = axes.getSubplots(gd, ax); + var alldone = subplots.map(function(subplot) { var plotinfo = fullLayout._plots[subplot]; if(!fullLayout._has('cartesian')) return; - var container = plotinfo[axletter + 'axislayer'], + var container = plotinfo[axLetter + 'axislayer'], // [bottom or left, top or right, free, main] linepositions = ax._linepositions[subplot] || [], diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 488d6a4078c..9265753e9d5 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -11,6 +11,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); var Lib = require('../../lib'); var Events = require('../../lib/events'); @@ -849,64 +850,45 @@ fx.loneUnhover = function(containerOrSelection) { d3.select(containerOrSelection); selection.selectAll('g.hovertext').remove(); - selection.selectAll('line.spikeline').remove(); - selection.selectAll('circle.spikeline').remove(); + selection.selectAll('.spikeline').remove(); }; function createSpikelines(hoverData, opts) { - var hovermode = opts.hovermode, - container = opts.container, - outerContainer = opts.outerContainer; - if(hovermode !== 'closest') return; - var c0 = hoverData[0], - x = (c0.x0 + c0.x1) / 2, - y = (c0.y0 + c0.y1) / 2, - xOffset = c0.xa._offset, - yOffset = c0.ya._offset, - xPoint = xOffset + x, - yPoint = yOffset + y, - xSide = c0.xa.side, - ySide = c0.ya.side, - xLength = c0.xa._length, - yLength = c0.ya._length, - xEdge = c0.ya._boundingBox.left + (ySide === 'left' ? c0.ya._boundingBox.width : 0), - yEdge = c0.xa._boundingBox.top + (xSide === 'top' ? c0.xa._boundingBox.height : 0), - xFreeBase = xOffset + (ySide === 'right' ? xLength : 0), - yFreeBase = yOffset + (xSide === 'top' ? yLength : 0), - xAnchoredBase = xEdge - outerContainer.node().offsetLeft, - yAnchoredBase = yEdge - outerContainer.node().offsetTop, - xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase, - yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase, - contrastColor = Color.combine(opts.fullLayout.plot_bgcolor, opts.fullLayout.paper_bgcolor), - xColor = c0.xa.spikecolor ? c0.xa.spikecolor : ( - tinycolor.readability(c0.color, contrastColor) < 1.5 ? ( - tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) : - c0.color), - yColor = c0.ya.spikecolor ? c0.ya.spikecolor : ( - tinycolor.readability(c0.color, contrastColor) < 1.5 ? ( - tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) : - c0.color), - xThickness = c0.xa.spikethickness, - yThickness = c0.ya.spikethickness, - xDash = Drawing.dashStyle(c0.xa.spikedash, xThickness), - yDash = Drawing.dashStyle(c0.xa.spikedash, yThickness), - xMarker = c0.xa.spikemode.indexOf('marker') !== -1, - yMarker = c0.ya.spikemode.indexOf('marker') !== -1, - xSpikeLine = c0.xa.spikemode.indexOf('toaxis') !== -1 || c0.xa.spikemode.indexOf('across') !== -1, - ySpikeLine = c0.ya.spikemode.indexOf('toaxis') !== -1 || c0.ya.spikemode.indexOf('across') !== -1, - xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ? - (ySide === 'left' ? xBase + xLength : xBase - xLength) : - xPoint, - yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ? - (xSide === 'bottom' ? yBase - yLength : yBase + yLength) : - yPoint; + var hovermode = opts.hovermode; + var container = opts.container; + var c0 = hoverData[0]; + var xa = c0.xa; + var ya = c0.ya; + var showX = xa.showspikes; + var showY = ya.showspikes; // Remove old spikeline items - container.selectAll('line.spikeline').remove(); - container.selectAll('circle.spikeline').remove(); + container.selectAll('.spikeline').remove(); + + if(hovermode !== 'closest' || !(showX || showY)) return; + + var fullLayout = opts.fullLayout; + var xPoint = xa._offset + (c0.x0 + c0.x1) / 2; + var yPoint = ya._offset + (c0.y0 + c0.y1) / 2; + var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor); + var dfltDashColor = tinycolor.readability(c0.color, contrastColor) < 1.5 ? + Color.contrast(contrastColor) : c0.color; + + if(showY) { + var yMode = ya.spikemode; + var yThickness = ya.spikethickness; + var yColor = ya.spikecolor || dfltDashColor; + var yBB = ya._boundingBox; + var xEdge = ((yBB.left + yBB.right) / 2) < xPoint ? yBB.right : yBB.left; + + if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) { + var xBase = xEdge; + var xEndSpike = xPoint; + if(yMode.indexOf('across') !== -1) { + xBase = ya._counterSpan[0]; + xEndSpike = ya._counterSpan[1]; + } - if(c0.ya.showspikes) { - if(ySpikeLine) { // Background horizontal Line (to y-axis) container.append('line') .attr({ @@ -929,16 +911,16 @@ function createSpikelines(hoverData, opts) { 'y2': yPoint, 'stroke-width': yThickness, 'stroke': yColor, - 'stroke-dasharray': yDash + 'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness) }) .classed('spikeline', true) .classed('crisp', true); } // Y axis marker - if(yMarker) { + if(yMode.indexOf('marker') !== -1) { container.append('circle') .attr({ - 'cx': xAnchoredBase + (ySide !== 'right' ? yThickness : -yThickness), + 'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness), 'cy': yPoint, 'r': yThickness, 'fill': yColor @@ -947,9 +929,22 @@ function createSpikelines(hoverData, opts) { } } - if(c0.xa.showspikes) { - if(xSpikeLine) { - // Background vertical line (to x-axis) + if(showX) { + var xMode = xa.spikemode; + var xThickness = xa.spikethickness; + var xColor = xa.spikecolor || dfltDashColor; + var xBB = xa._boundingBox; + var yEdge = ((xBB.top + xBB.bottom) / 2) < yPoint ? xBB.bottom : xBB.top; + + if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) { + var yBase = yEdge; + var yEndSpike = yPoint; + if(xMode.indexOf('across') !== -1) { + yBase = xa._counterSpan[0]; + yEndSpike = xa._counterSpan[1]; + } + + // Background vertical line (to x-axis) container.append('line') .attr({ 'x1': xPoint, @@ -971,18 +966,18 @@ function createSpikelines(hoverData, opts) { 'y2': yEndSpike, 'stroke-width': xThickness, 'stroke': xColor, - 'stroke-dasharray': xDash + 'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness) }) .classed('spikeline', true) .classed('crisp', true); } // X axis marker - if(xMarker) { + if(xMode.indexOf('marker') !== -1) { container.append('circle') .attr({ 'cx': xPoint, - 'cy': yAnchoredBase - (xSide !== 'top' ? xThickness : -xThickness), + 'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness), 'r': xThickness, 'fill': xColor }) From e509fa7b21ed587a6b61fa1e02760c53e78e08fa Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Apr 2017 23:11:41 -0400 Subject: [PATCH 18/26] lint --- src/plots/cartesian/axes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 8d811c1288b..7903c792782 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1904,7 +1904,7 @@ axes.doTicks = function(gd, axid, skipTitle) { * if gd is scrollable, and may have been scrolled between the time * we calculate this and the time we use it */ - var bbFinal = ax._boundingBox = { + ax._boundingBox = { width: bBox.width, height: bBox.height, left: bBox.left - gdBB.left, From 75121d28343ed505091471b634532f07deb28694 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 15:53:43 -0400 Subject: [PATCH 19/26] :hocho: spikemode: 'none' --- src/plots/cartesian/layout_attributes.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index e1688ff0dcd..ccbbd8597a5 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -317,7 +317,6 @@ module.exports = { spikemode: { valType: 'flaglist', flags: ['toaxis', 'across', 'marker'], - extras: ['none'], role: 'style', dflt: 'toaxis', description: [ From bd1156704c022f7ba3ea425e5cda82df4c0b06e5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 16:49:29 -0400 Subject: [PATCH 20/26] make drawing/attributes.dash - and clean up some dashes that don't fit the pattern --- src/components/drawing/attributes.js | 26 ++++++++++++++++++++++++ src/components/shapes/attributes.js | 7 +++---- src/plots/cartesian/layout_attributes.js | 16 ++------------- src/traces/contour/attributes.js | 3 ++- src/traces/ohlc/attributes.js | 11 +++++----- src/traces/scatter/attributes.js | 16 ++------------- src/traces/scatter3d/attributes.js | 9 +++++++- src/traces/scattergeo/attributes.js | 3 ++- src/traces/scattermapbox/attributes.js | 4 ++-- src/traces/scatterternary/attributes.js | 3 ++- 10 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 src/components/drawing/attributes.js diff --git a/src/components/drawing/attributes.js b/src/components/drawing/attributes.js new file mode 100644 index 00000000000..0ea5dbe3620 --- /dev/null +++ b/src/components/drawing/attributes.js @@ -0,0 +1,26 @@ +/** +* Copyright 2012-2017, 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'; + +exports.dash = { + valType: 'string', + // string type usually doesn't take values... this one should really be + // a special type or at least a special coercion function, from the GUI + // you only get these values but elsewhere the user can supply a list of + // dash lengths in px, and it will be honored + values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], + dflt: 'solid', + role: 'style', + 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*).' + ].join(' ') +}; diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index a8974f2825e..83407b34067 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -9,11 +9,10 @@ 'use strict'; var annAttrs = require('../annotations/attributes'); -var scatterAttrs = require('../../traces/scatter/attributes'); +var scatterLineAttrs = require('../../traces/scatter/attributes').line; +var dash = require('../drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; -var scatterLineAttrs = scatterAttrs.line; - module.exports = { _isLinkedToArray: 'shape', @@ -151,7 +150,7 @@ module.exports = { line: { color: scatterLineAttrs.color, width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, + dash: dash, role: 'info' }, fillcolor: { diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index ccbbd8597a5..f85f3bdf551 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -10,6 +10,7 @@ var fontAttrs = require('../font_attributes'); var colorAttrs = require('../../components/color/attributes'); +var dash = require('../../components/drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; var constants = require('./constants'); @@ -300,20 +301,7 @@ module.exports = { role: 'style', description: 'Sets the width (in px) of the zero line.' }, - spikedash: { - valType: 'string', - // string type usually doesn't take values... this one should really be - // a special type or at least a special coercion function, from the GUI - // you only get these values but elsewhere the user can supply a list of - // dash lengths in px, and it will be honored - values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], - dflt: 'dash', - role: 'style', - description: [ - 'Sets the style of the lines. Set to a dash string type', - 'or a dash length in px.' - ].join(' ') - }, + spikedash: extendFlat({}, dash, {dflt: 'dash'}), spikemode: { valType: 'flaglist', flags: ['toaxis', 'across', 'marker'], diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js index ee547aa5dbe..069a965af9c 100644 --- a/src/traces/contour/attributes.js +++ b/src/traces/contour/attributes.js @@ -12,6 +12,7 @@ var heatmapAttrs = require('../heatmap/attributes'); var scatterAttrs = require('../scatter/attributes'); var colorscaleAttrs = require('../../components/colorscale/attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); +var dash = require('../../components/drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; var scatterLineAttrs = scatterAttrs.line; @@ -118,7 +119,7 @@ module.exports = extendFlat({}, { ].join(' ') }), width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, + dash: dash, smoothing: extendFlat({}, scatterLineAttrs.smoothing, { description: [ 'Sets the amount of smoothing for the contour lines,', diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index 02d50c76486..938a1d1c81a 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -11,6 +11,7 @@ var Lib = require('../../lib'); var scatterAttrs = require('../scatter/attributes'); +var dash = require('../../components/drawing/attributes').dash; var INCREASING_COLOR = '#3D9970'; var DECREASING_COLOR = '#FF4136'; @@ -38,9 +39,9 @@ var directionAttrs = { }, line: { - color: Lib.extendFlat({}, lineAttrs.color), - width: Lib.extendFlat({}, lineAttrs.width), - dash: Lib.extendFlat({}, lineAttrs.dash), + color: lineAttrs.color, + width: lineAttrs.width, + dash: dash, } }; @@ -87,9 +88,9 @@ module.exports = { '`decreasing.line.width`.' ].join(' ') }), - dash: Lib.extendFlat({}, lineAttrs.dash, { + dash: Lib.extendFlat({}, dash, { description: [ - lineAttrs.dash, + dash.description, 'Note that this style setting can also be set per', 'direction via `increasing.line.dash` and', '`decreasing.line.dash`.' diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 38b3015a4bb..d1bc72ba14c 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -11,6 +11,7 @@ var colorAttributes = require('../../components/colorscale/color_attributes'); var errorBarAttrs = require('../../components/errorbars/attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); +var dash = require('../../components/drawing/attributes').dash; var Drawing = require('../../components/drawing'); var constants = require('./constants'); @@ -163,20 +164,7 @@ module.exports = { '*0* corresponds to no smoothing (equivalent to a *linear* shape).' ].join(' ') }, - dash: { - valType: 'string', - // string type usually doesn't take values... this one should really be - // a special type or at least a special coercion function, from the GUI - // you only get these values but elsewhere the user can supply a list of - // dash lengths in px, and it will be honored - values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], - dflt: 'solid', - role: 'style', - description: [ - 'Sets the style of the lines. Set to a dash string type', - 'or a dash length in px.' - ].join(' ') - }, + dash: dash, simplify: { valType: 'boolean', dflt: true, diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js index ac0b957be63..7a74fd5b256 100644 --- a/src/traces/scatter3d/attributes.js +++ b/src/traces/scatter3d/attributes.js @@ -11,6 +11,7 @@ var scatterAttrs = require('../scatter/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); var errorBarAttrs = require('../../components/errorbars/attributes'); +var DASHES = require('../../constants/gl3d_dashes'); var MARKER_SYMBOLS = require('../../constants/gl_markers'); var extendFlat = require('../../lib/extend').extendFlat; @@ -114,7 +115,13 @@ module.exports = { connectgaps: scatterAttrs.connectgaps, line: extendFlat({}, { width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, + dash: { + valType: 'enumerated', + values: Object.keys(DASHES), + dflt: 'solid', + role: 'style', + description: 'Sets the dash style of the lines.' + }, showscale: { valType: 'boolean', role: 'info', diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 1b5ddeffc19..a341110e24f 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -11,6 +11,7 @@ var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); +var dash = require('../../components/drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; @@ -79,7 +80,7 @@ module.exports = { line: { color: scatterLineAttrs.color, width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash + dash: dash }, connectgaps: scatterAttrs.connectgaps, diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 1518093db19..c061f1141aa 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -61,10 +61,10 @@ module.exports = { line: { color: lineAttrs.color, - width: lineAttrs.width, + width: lineAttrs.width // TODO - dash: lineAttrs.dash + // dash: dash }, connectgaps: scatterAttrs.connectgaps, diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index b0c0d4a2acf..4e8c36c48a8 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -12,6 +12,7 @@ var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); +var dash = require('../../components/drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; @@ -88,7 +89,7 @@ module.exports = { line: { color: scatterLineAttrs.color, width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, + dash: dash, shape: extendFlat({}, scatterLineAttrs.shape, {values: ['linear', 'spline']}), smoothing: scatterLineAttrs.smoothing From 3a9aa58c051afc64730c2e215370427305ccf120 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 18:10:12 -0400 Subject: [PATCH 21/26] short-circuit spike properties when spikes are off --- src/plots/cartesian/layout_defaults.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 228bac73753..871b9c6520c 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -173,11 +173,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); - coerce('showspikes'); - coerce('spikecolor'); - coerce('spikethickness'); - coerce('spikedash'); - coerce('spikemode'); + var showSpikes = coerce('showspikes'); + if(showSpikes) { + coerce('spikecolor'); + coerce('spikethickness'); + coerce('spikedash'); + coerce('spikemode'); + } var positioningOptions = { letter: axLetter, From 052417bb27cdcaffe449b47bac8f9b7d9a81161f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 18:10:34 -0400 Subject: [PATCH 22/26] test hover event response to manual and "real" events --- test/jasmine/tests/hover_label_test.js | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b8830036e8e..15bdc0f4a70 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -9,6 +9,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); var click = require('../assets/click'); +var delay = require('../assets/delay'); var doubleClick = require('../assets/double_click'); var fail = require('../assets/fail_test'); @@ -532,6 +533,44 @@ describe('hover info', function() { expect(hovers.size()).toEqual(0); }); }); + + describe('hover events', function() { + var data = [{x: [1, 2, 3], y: [1, 3, 2], type: 'bar'}]; + var layout = {width: 600, height: 400}; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, data, layout).then(done); + }); + + fit('should emit events only if the event looks user-driven', function(done) { + var hoverHandler = jasmine.createSpy(); + gd.on('plotly_hover', hoverHandler); + + var gdBB = gd.getBoundingClientRect(); + var event = {clientX: gdBB.left + 300, clientY: gdBB.top + 200}; + + Promise.resolve().then(function() { + Fx.hover(gd, event, 'xy'); + }) + .then(delay(constants.HOVERMINTIME * 1.1)) + .then(function() { + Fx.unhover(gd); + }) + .then(function() { + expect(hoverHandler).not.toHaveBeenCalled(); + var dragger = gd.querySelector('.nsewdrag'); + + Fx.hover(gd, Lib.extendFlat({target: dragger}, event), 'xy'); + }) + .then(function() { + expect(hoverHandler).toHaveBeenCalledTimes(1); + }) + .catch(fail) + .then(done); + }); + }); }); describe('hover info on stacked subplots', function() { From a3a0a4fd0471fd5c06191e401e31fcc4439e6c98 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 18:23:21 -0400 Subject: [PATCH 23/26] :hocho: fit --- test/jasmine/tests/hover_label_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 15bdc0f4a70..eeb0a665743 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -544,7 +544,7 @@ describe('hover info', function() { Plotly.plot(gd, data, layout).then(done); }); - fit('should emit events only if the event looks user-driven', function(done) { + it('should emit events only if the event looks user-driven', function(done) { var hoverHandler = jasmine.createSpy(); gd.on('plotly_hover', hoverHandler); From f7b467e1230d6c7f038794d2fc91538effa55672 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 20:45:42 -0400 Subject: [PATCH 24/26] fix scattermapbox defaults for omitted options I had to make a mock marker.line object in _fullData to keep legends happy, but at least it doesn't respond to values in data. --- src/traces/scatter/line_defaults.js | 4 ++-- src/traces/scatter/marker_defaults.js | 34 ++++++++++++++------------- src/traces/scattermapbox/defaults.js | 18 +++++++------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/traces/scatter/line_defaults.js b/src/traces/scatter/line_defaults.js index f0fc1660492..176cbaccc2a 100644 --- a/src/traces/scatter/line_defaults.js +++ b/src/traces/scatter/line_defaults.js @@ -13,7 +13,7 @@ var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleDefaults = require('../../components/colorscale/defaults'); -module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { +module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) { var markerColor = (traceIn.marker || {}).color; coerce('line.color', defaultColor); @@ -27,5 +27,5 @@ module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, } coerce('line.width'); - coerce('line.dash'); + if(!(opts || {}).noDash) coerce('line.dash'); }; diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index 3211d62426e..65560aecb75 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -16,7 +16,7 @@ var colorscaleDefaults = require('../../components/colorscale/defaults'); var subTypes = require('./subtypes'); -module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { +module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) { var isBubble = subTypes.isBubble(traceIn), lineColor = (traceIn.line || {}).color, defaultMLC; @@ -33,22 +33,24 @@ module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'}); } - // 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)) { - defaultMLC = lineColor; + 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)) { + defaultMLC = lineColor; + } + 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'}); + } + + coerce('marker.line.width', isBubble ? 1 : 0); } - 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'}); - } - - coerce('marker.line.width', isBubble ? 1 : 0); if(isBubble) { coerce('marker.sizeref'); diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index ad884eb7f32..c1bbffbfafd 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -26,14 +26,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - function coerceMarker(attr, dflt) { - var attrs = (attr.indexOf('.line') === -1) ? attributes : scatterAttrs; + // function coerceMarker(attr, dflt) { + // var attrs = (attr.indexOf('.line') === -1) ? attributes : scatterAttrs; - // use 'scatter' attributes for 'marker.line.' attr, - // so that we can reuse the scatter marker defaults + // // use 'scatter' attributes for 'marker.line.' attr, + // // so that we can reuse the scatter marker defaults - return Lib.coerce(traceIn, traceOut, attrs, attr, dflt); - } + // return Lib.coerce(traceIn, traceOut, attrs, attr, dflt); + // } var len = handleLonLatDefaults(traceIn, traceOut, coerce); if(!len) { @@ -46,16 +46,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode'); if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noDash: true}); coerce('connectgaps'); } if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerceMarker); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noLine: true}); // array marker.size and marker.color are only supported with circles var marker = traceOut.marker; + // we need mock marker.line object to make legends happy + marker.line = {width: 0}; if(marker.symbol !== 'circle') { if(Array.isArray(marker.size)) marker.size = marker.size[0]; From 7aa43593bc0383f366ca2bee66b4353b52fce3c9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 21:02:53 -0400 Subject: [PATCH 25/26] lint --- src/traces/scattermapbox/defaults.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index c1bbffbfafd..596167f6611 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -18,7 +18,6 @@ var handleTextDefaults = require('../scatter/text_defaults'); var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); var attributes = require('./attributes'); -var scatterAttrs = require('../scatter/attributes'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { From 5925049e6c6c4d49bb3b681354dee3f0ea589aac Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Apr 2017 21:03:57 -0400 Subject: [PATCH 26/26] :hocho: :hocho: --- src/traces/scattermapbox/defaults.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index 596167f6611..f7a51c65e95 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -25,15 +25,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - // function coerceMarker(attr, dflt) { - // var attrs = (attr.indexOf('.line') === -1) ? attributes : scatterAttrs; - - // // use 'scatter' attributes for 'marker.line.' attr, - // // so that we can reuse the scatter marker defaults - - // return Lib.coerce(traceIn, traceOut, attrs, attr, dflt); - // } - var len = handleLonLatDefaults(traceIn, traceOut, coerce); if(!len) { traceOut.visible = false;