diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index aa13fa03144..6f2a025ae73 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -13,6 +13,9 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var subTypes = require('../../traces/scatter/subtypes'); +var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); + var drawing = module.exports = {}; // ----------------------------------------------------- @@ -175,14 +178,14 @@ drawing.pointStyle = function(s, trace) { // only scatter & box plots get marker path and opacity // bars, histograms don't if(Plotly.Plots.traceIs(trace, 'symbols')) { - var sizeFn = Plotly.Scatter.getBubbleSizeFn(trace); + var sizeFn = makeBubbleSizeFn(trace); s.attr('d', function(d) { var r; // handle multi-trace graph edit case if(d.ms==='various' || marker.size==='various') r = 3; - else r = Plotly.Scatter.isBubble(trace) ? + else r = subTypes.isBubble(trace) ? sizeFn(d.ms) : (marker.size || 6) / 2; // store the calculated size so hover can use it diff --git a/src/components/errorbars/index.js b/src/components/errorbars/index.js index 4d1566701a1..2a4c9101c54 100644 --- a/src/components/errorbars/index.js +++ b/src/components/errorbars/index.js @@ -9,10 +9,14 @@ 'use strict'; -var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var Lib = require('../../lib'); +var Color = require('../color'); +var subTypes = require('../../traces/scatter/subtypes'); + + var errorBars = module.exports = {}; errorBars.attributes = require('./attributes'); @@ -70,13 +74,13 @@ errorBars.plot = function(gd, plotinfo, cd) { var trace = d[0].trace, xObj = trace.error_x, yObj = trace.error_y, - sparse = Plotly.Scatter.hasMarkers(trace) && + sparse = subTypes.hasMarkers(trace) && trace.marker.maxdisplayed>0; if(!yObj.visible && !xObj.visible) return; d3.select(this).selectAll('g') - .data(Plotly.Lib.identity) + .data(Lib.identity) .enter().append('g') .each(function(d){ coords = errorcoords(d, xa, ya); @@ -121,13 +125,13 @@ errorBars.style = function(gd){ eb.selectAll('g path.yerror') .style('stroke-width', yObj.thickness+'px') - .call(Plotly.Color.stroke, yObj.color); + .call(Color.stroke, yObj.color); if(xObj.copy_ystyle) xObj = yObj; eb.selectAll('g path.xerror') .style('stroke-width', xObj.thickness+'px') - .call(Plotly.Color.stroke, xObj.color); + .call(Color.stroke, xObj.color); }); }; diff --git a/src/components/legend/index.js b/src/components/legend/index.js index 2fb6e7c4b03..464774b1d79 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -12,6 +12,7 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); +var subTypes = require('../../traces/scatter/subtypes'); var styleOne = require('../../traces/pie/style_one'); var legend = module.exports = {}; @@ -80,7 +81,7 @@ legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { legend.lines = function(d){ var trace = d[0].trace, showFill = trace.visible && trace.fill && trace.fill!=='none', - showLine = Plotly.Scatter.hasLines(trace); + showLine = subTypes.hasLines(trace); var fill = d3.select(this).select('.legendfill').selectAll('path') .data(showFill ? [d] : []); @@ -100,9 +101,9 @@ legend.lines = function(d){ legend.points = function(d){ var d0 = d[0], trace = d0.trace, - showMarkers = Plotly.Scatter.hasMarkers(trace), - showText = Plotly.Scatter.hasText(trace), - showLines = Plotly.Scatter.hasLines(trace); + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace), + showLines = subTypes.hasLines(trace); var dMod, tMod; diff --git a/src/plotly.js b/src/plotly.js index b7834839b4a..cc0f90b584e 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -84,11 +84,8 @@ exports.register = function register(_modules) { } }; - -exports.register(require('./traces/scatter')); - // Scatter is the only trace included by default -exports.Scatter = Plots.getModule('scatter'); +exports.register(require('./traces/scatter')); // plot api require('./plot_api/plot_api'); diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js new file mode 100644 index 00000000000..f2ddc40bff5 --- /dev/null +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -0,0 +1,36 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Lib = require('../../lib'); + + +// arrayOk attributes, merge them into calcdata array +module.exports = function arraysToCalcdata(cd) { + var trace = cd[0].trace, + marker = trace.marker; + + Lib.mergeArray(trace.text, cd, 'tx'); + Lib.mergeArray(trace.textposition, cd, 'tp'); + if(trace.textfont) { + Lib.mergeArray(trace.textfont.size, cd, 'ts'); + Lib.mergeArray(trace.textfont.color, cd, 'tc'); + Lib.mergeArray(trace.textfont.family, cd, 'tf'); + } + + if(marker && marker.line) { + var markerLine = marker.line; + Lib.mergeArray(marker.opacity, cd, 'mo'); + Lib.mergeArray(marker.symbol, cd, 'mx'); + Lib.mergeArray(marker.color, cd, 'mc'); + Lib.mergeArray(markerLine.color, cd, 'mlc'); + Lib.mergeArray(markerLine.width, cd, 'mlw'); + } +}; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 75f8c8d7c95..0860f89c4c4 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -10,7 +10,8 @@ var Drawing = require('../../components/drawing'); -var PTS_LINESONLY = 20; // TODO put in constants/ +var constants = require('./constants'); + module.exports = { x: { @@ -76,7 +77,7 @@ module.exports = { }, mode: { valType: 'flaglist', - flags: ['lines','markers','text'], + flags: ['lines', 'markers', 'text'], extras: ['none'], role: 'info', description: [ @@ -84,7 +85,7 @@ module.exports = { 'If the provided `mode` includes *text* then the `text` elements', 'appear at the coordinates. Otherwise, the `text` elements', 'appear on hover.', - 'If there are less than ' + PTS_LINESONLY + ' points,', + 'If there are less than ' + constants.PTS_LINESONLY + ' points,', 'then the default is *lines+markers*. Otherwise, *lines*.' ].join(' ') }, diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js new file mode 100644 index 00000000000..27a8e158561 --- /dev/null +++ b/src/traces/scatter/calc.js @@ -0,0 +1,132 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +var Axes = require('../../plots/cartesian/axes'); +var Lib = require('../../lib'); + +var subTypes = require('./subtypes'); +var calcMarkerColorscale = require('./marker_colorscale_calc'); + + +module.exports = function calc(gd, trace) { + var xa = Axes.getFromId(gd, trace.xaxis || 'x'), + ya = Axes.getFromId(gd, trace.yaxis || 'y'); + Lib.markTime('in Scatter.calc'); + + var x = xa.makeCalcdata(trace, 'x'); + Lib.markTime('finished convert x'); + + var y = ya.makeCalcdata(trace, 'y'); + Lib.markTime('finished convert y'); + + var serieslen = Math.min(x.length, y.length), + marker, + s, + i; + + // cancel minimum tick spacings (only applies to bars and boxes) + xa._minDtick = 0; + ya._minDtick = 0; + + if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); + if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); + + // check whether bounds should be tight, padded, extended to zero... + // most cases both should be padded on both ends, so start with that. + var xOptions = {padded: true}, + yOptions = {padded: true}; + + if(subTypes.hasMarkers(trace)) { + + // Treat size like x or y arrays --- Run d2c + // this needs to go before ppad computation + marker = trace.marker; + s = marker.size; + + if (Array.isArray(s)) { + // I tried auto-type but category and dates dont make much sense. + var ax = {type: 'linear'}; + Axes.setConvert(ax); + s = ax.makeCalcdata(trace.marker, 'size'); + if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); + } + + var sizeref = 1.6 * (trace.marker.sizeref || 1), + markerTrans; + if(trace.marker.sizemode === 'area') { + markerTrans = function(v) { + return Math.max(Math.sqrt((v || 0) / sizeref), 3); + }; + } + else { + markerTrans = function(v) { + return Math.max((v || 0) / sizeref, 3); + }; + } + xOptions.ppad = yOptions.ppad = Array.isArray(s) ? + s.map(markerTrans) : markerTrans(s); + } + + calcMarkerColorscale(trace); + + // TODO: text size + + // include zero (tight) and extremes (padded) if fill to zero + // (unless the shape is closed, then it's just filling the shape regardless) + if(((trace.fill === 'tozerox') || + ((trace.fill === 'tonextx') && gd.firstscatter)) && + ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) { + xOptions.tozero = true; + } + + // if no error bars, markers or text, or fill to y=0 remove x padding + else if(!trace.error_y.visible && ( + ['tonexty', 'tozeroy'].indexOf(trace.fill) !== -1 || + (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)) + )) { + xOptions.padded = false; + xOptions.ppad = 0; + } + + // now check for y - rather different logic, though still mostly padded both ends + // include zero (tight) and extremes (padded) if fill to zero + // (unless the shape is closed, then it's just filling the shape regardless) + if(((trace.fill === 'tozeroy') || ((trace.fill === 'tonexty') && gd.firstscatter)) && + ((x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]))) { + yOptions.tozero = true; + } + + // tight y: any x fill + else if(['tonextx', 'tozerox'].indexOf(trace.fill) !== -1) { + yOptions.padded = false; + } + + Lib.markTime('ready for Axes.expand'); + Axes.expand(xa, x, xOptions); + Lib.markTime('done expand x'); + Axes.expand(ya, y, yOptions); + Lib.markTime('done expand y'); + + // create the "calculated data" to plot + var cd = new Array(serieslen); + for(i = 0; i < serieslen; i++) { + cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ? + {x: x[i], y: y[i]} : {x: false, y: false}; + } + + // this has migrated up from arraysToCalcdata as we have a reference to 's' here + if (typeof s !== undefined) Lib.mergeArray(s, cd, 'ms'); + + gd.firstscatter = false; + return cd; +}; diff --git a/src/traces/scatter/clean_data.js b/src/traces/scatter/clean_data.js new file mode 100644 index 00000000000..60ec2481e12 --- /dev/null +++ b/src/traces/scatter/clean_data.js @@ -0,0 +1,39 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + + +module.exports = function cleanData(fullData) { + var i, + tracei, + filli, + j, + tracej; + + // remove opacity for any trace that has a fill or is filled to + for(i = 0; i < fullData.length; i++) { + tracei = fullData[i]; + filli = tracei.fill; + if((filli === 'none') || (tracei.type !== 'scatter')) continue; + tracei.opacity = undefined; + + if(filli === 'tonexty' || filli === 'tonextx') { + for(j = i - 1; j >= 0; j--) { + tracej = fullData[j]; + if((tracej.type === 'scatter') && + (tracej.xaxis === tracei.xaxis) && + (tracej.yaxis === tracei.yaxis)) { + tracej.opacity = undefined; + break; + } + } + } + } +}; diff --git a/src/traces/scatter/colorbar.js b/src/traces/scatter/colorbar.js index ff2bcdba0f8..03237f1f17d 100644 --- a/src/traces/scatter/colorbar.js +++ b/src/traces/scatter/colorbar.js @@ -28,7 +28,7 @@ module.exports = function colorbar(gd, cd) { // TODO unify Scatter.colorbar and Heatmap.colorbar // TODO make Plotly[module].colorbar support multiple colorbar per trace - if(marker===undefined || !marker.showscale){ + if((marker === undefined) || !marker.showscale){ Plotly.Plots.autoMargin(gd, cbId); return; } diff --git a/src/traces/scatter/constants.js b/src/traces/scatter/constants.js new file mode 100644 index 00000000000..8336b9d7162 --- /dev/null +++ b/src/traces/scatter/constants.js @@ -0,0 +1,14 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +module.exports = { + PTS_LINESONLY: 20 +}; diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js new file mode 100644 index 00000000000..c18250b8208 --- /dev/null +++ b/src/traces/scatter/defaults.js @@ -0,0 +1,72 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Lib = require('../../lib'); + +var attributes = require('./attributes'); +var constants = require('./constants'); +var subTypes = require('./subtypes'); +var handleXYDefaults = require('./xy_defaults'); +var handleMarkerDefaults = require('./marker_defaults'); +var handleLineDefaults = require('./line_defaults'); +var handleTextDefaults = require('./text_defaults'); +var handleFillColorDefaults = require('./fillcolor_defaults'); +var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleXYDefaults(traceIn, traceOut, coerce), + // TODO: default mode by orphan points... + defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; + if(!len) { + traceOut.visible = false; + return; + } + + coerce('text'); + coerce('mode', defaultMode); + + if(subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, coerce); + lineShapeDefaults(traceIn, traceOut, coerce); + coerce('connectgaps'); + } + + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if(subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { + coerce('marker.maxdisplayed'); + } + + coerce('fill'); + if(traceOut.fill !== 'none') { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + if(!subTypes.hasLines(traceOut)) lineShapeDefaults(traceIn, traceOut, coerce); + } + + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); +}; + +function lineShapeDefaults(traceIn, traceOut, coerce) { + var shape = coerce('line.shape'); + if(shape === 'spline') coerce('line.smoothing'); +} diff --git a/src/traces/scatter/fillcolor_defaults.js b/src/traces/scatter/fillcolor_defaults.js new file mode 100644 index 00000000000..9871c09cd3b --- /dev/null +++ b/src/traces/scatter/fillcolor_defaults.js @@ -0,0 +1,37 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Color = require('../../components/color'); + + +// common to 'scatter' and 'scattergl' +module.exports = function fillColorDefaults(traceIn, traceOut, defaultColor, coerce) { + var inheritColorFromMarker = false; + + if(traceOut.marker) { + // don't try to inherit a color array + var markerColor = traceOut.marker.color, + markerLineColor = (traceOut.marker.line || {}).color; + + if(markerColor && !Array.isArray(markerColor)) { + inheritColorFromMarker = markerColor; + } + else if(markerLineColor && !Array.isArray(markerLineColor)) { + inheritColorFromMarker = markerLineColor; + } + } + + coerce('fillcolor', Color.addOpacity( + (traceOut.line || {}).color || + inheritColorFromMarker || + defaultColor, 0.5 + )); +}; diff --git a/src/traces/scatter/get_trace_color.js b/src/traces/scatter/get_trace_color.js new file mode 100644 index 00000000000..1eeec66afce --- /dev/null +++ b/src/traces/scatter/get_trace_color.js @@ -0,0 +1,51 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Color = require('../../components/color'); +var subtypes = require('./subtypes'); + + +module.exports = function getTraceColor(trace, di) { + var lc, tc; + + // TODO: text modes + + if(trace.mode === 'lines') { + lc = trace.line.color; + return (lc && Color.opacity(lc)) ? + lc : trace.fillcolor; + } + else if(trace.mode === 'none') { + return trace.fill ? trace.fillcolor : ''; + } + else { + var mc = di.mcc || (trace.marker || {}).color, + mlc = di.mlcc || ((trace.marker || {}).line || {}).color; + + tc = (mc && Color.opacity(mc)) ? mc : + (mlc && Color.opacity(mlc) && + (di.mlw || ((trace.marker || {}).line || {}).width)) ? mlc : ''; + + if(tc) { + // make sure the points aren't TOO transparent + if(Color.opacity(tc) < 0.3) { + return Color.addOpacity(tc, 0.3); + } + else return tc; + } + else { + lc = (trace.line || {}).color; + return (lc && Color.opacity(lc) && + subtypes.hasLines(trace) && trace.line.width) ? + lc : trace.fillcolor; + } + } +}; diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js new file mode 100644 index 00000000000..8ade45b51b1 --- /dev/null +++ b/src/traces/scatter/hover.js @@ -0,0 +1,69 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Fx = require('../../plots/cartesian/graph_interact'); +var ErrorBars = require('../../components/errorbars'); +var getTraceColor = require('./get_trace_color'); + + +module.exports = function hoverPoints(pointData, xval, yval, hovermode) { + var cd = pointData.cd, + trace = cd[0].trace, + xa = pointData.xa, + ya = pointData.ya, + dx = function(di) { + // scatter points: d.mrc is the calculated marker radius + // adjust the distance so if you're inside the marker it + // always will show up regardless of point size, but + // prioritize smaller points + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(xa.c2p(di.x)-xa.c2p(xval))-rad, 1-3/rad); + }, + dy = function(di) { + var rad = Math.max(3, di.mrc || 0); + return Math.max(Math.abs(ya.c2p(di.y)-ya.c2p(yval))-rad, 1-3/rad); + }, + dxy = function(di) { + var rad = Math.max(3, di.mrc || 0), + dx = Math.abs(xa.c2p(di.x)-xa.c2p(xval)), + dy = Math.abs(ya.c2p(di.y)-ya.c2p(yval)); + return Math.max(Math.sqrt(dx*dx + dy*dy)-rad, 1-3/rad); + }, + distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); + + Fx.getClosest(cd, distfn, pointData); + + // skip the rest (for this trace) if we didn't find a close point + if(pointData.index === false) return; + + // the closest data point + var di = cd[pointData.index], + xc = xa.c2p(di.x, true), + yc = ya.c2p(di.y, true), + rad = di.mrc || 1; + + pointData.color = getTraceColor(trace, di); + + pointData.x0 = xc - rad; + pointData.x1 = xc + rad; + pointData.xLabelVal = di.x; + + pointData.y0 = yc - rad; + pointData.y1 = yc + rad; + pointData.yLabelVal = di.y; + + if(di.tx) pointData.text = di.tx; + else if(trace.text) pointData.text = trace.text; + + ErrorBars.hoverInfo(di, trace, pointData); + + return [pointData]; +}; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 1a9291617f9..6c3b8629c3d 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -9,34 +9,31 @@ 'use strict'; -var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); - -var Fx = require('../../plots/cartesian/graph_interact'); -var Axes = require('../../plots/cartesian/axes'); - -var Color = require('../../components/color'); -var Colorscale = require('../../components/colorscale'); -var Drawing = require('../../components/drawing'); -var ErrorBars = require('../../components/errorbars'); +var Scatter = {}; var subtypes = require('./subtypes'); +Scatter.hasLines = subtypes.hasLines; +Scatter.hasMarkers = subtypes.hasMarkers; +Scatter.hasText = subtypes.hasText; +Scatter.isBubble = subtypes.isBubble; -var scatter = module.exports = {}; - -scatter.hasLines = subtypes.hasLines; -scatter.hasMarkers = subtypes.hasMarkers; -scatter.hasText = subtypes.hasText; -scatter.isBubble = subtypes.isBubble; - -scatter.selectPoints = require('./select'); - -scatter.moduleType = 'trace'; -scatter.name = 'scatter'; -scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend']; -scatter.meta = { +// traces with < this many points are by default shown +// with points and lines, > just get lines +Scatter.attributes = require('./attributes'); +Scatter.supplyDefaults = require('./defaults'); +Scatter.cleanData = require('./clean_data'); +Scatter.calc = require('./calc'); +Scatter.arraysToCalcdata = require('./arrays_to_calcdata'); +Scatter.plot = require('./plot'); +Scatter.colorbar = require('./colorbar'); +Scatter.style = require('./style'); +Scatter.hoverPoints = require('./hover'); +Scatter.selectPoints = require('./select'); + +Scatter.moduleType = 'trace'; +Scatter.name = 'scatter'; +Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend']; +Scatter.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', 'The data visualized as scatter point or lines is set in `x` and `y`.', @@ -46,815 +43,4 @@ scatter.meta = { ].join(' ') }; -// traces with < this many points are by default shown -// with points and lines, > just get lines -scatter.PTS_LINESONLY = 20; - -scatter.attributes = require('./attributes'); - -var handleXYDefaults = require('./xy_defaults'); - -scatter.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, scatter.attributes, attr, dflt); - } - - var len = handleXYDefaults(traceIn, traceOut, coerce), - // TODO: default mode by orphan points... - defaultMode = len < scatter.PTS_LINESONLY ? 'lines+markers' : 'lines'; - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - coerce('mode', defaultMode); - - if(scatter.hasLines(traceOut)) { - scatter.lineDefaults(traceIn, traceOut, defaultColor, coerce); - lineShapeDefaults(traceIn, traceOut, coerce); - coerce('connectgaps'); - } - - if(scatter.hasMarkers(traceOut)) { - scatter.markerDefaults(traceIn, traceOut, defaultColor, layout, coerce); - } - - if(scatter.hasText(traceOut)) { - scatter.textDefaults(traceIn, traceOut, layout, coerce); - } - - if(scatter.hasMarkers(traceOut) || scatter.hasText(traceOut)) { - coerce('marker.maxdisplayed'); - } - - coerce('fill'); - if(traceOut.fill !== 'none') { - scatter.fillColorDefaults(traceIn, traceOut, defaultColor, coerce); - if(!scatter.hasLines(traceOut)) lineShapeDefaults(traceIn, traceOut, coerce); - } - - ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); -}; - -// common to 'scatter', 'scatter3d', 'scattergeo' and 'scattergl' -scatter.lineDefaults = function(traceIn, traceOut, defaultColor, coerce) { - var markerColor = (traceIn.marker || {}).color; - - // don't try to inherit a color array - coerce('line.color', (Array.isArray(markerColor) ? false : markerColor) || - defaultColor); - coerce('line.width'); - coerce('line.dash'); -}; - -function lineShapeDefaults(traceIn, traceOut, coerce) { - var shape = coerce('line.shape'); - if(shape==='spline') coerce('line.smoothing'); -} - -// common to 'scatter', 'scatter3d', 'scattergeo' and 'scattergl' -scatter.markerDefaults = function(traceIn, traceOut, defaultColor, layout, coerce) { - var isBubble = scatter.isBubble(traceIn), - lineColor = (traceIn.line || {}).color, - defaultMLC; - - if(lineColor) defaultColor = lineColor; - - coerce('marker.symbol'); - coerce('marker.opacity', isBubble ? 0.7 : 1); - coerce('marker.size'); - - coerce('marker.color', defaultColor); - if(Colorscale.hasColorscale(traceIn, 'marker')) { - Colorscale.handleDefaults( - 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 - // mostly this is for transparent markers to behave nicely - if(lineColor && traceOut.marker.color!==lineColor) { - defaultMLC = lineColor; - } - else if(isBubble) defaultMLC = Color.background; - else defaultMLC = Color.defaultLine; - - coerce('marker.line.color', defaultMLC); - if(Colorscale.hasColorscale(traceIn, 'marker.line')) { - Colorscale.handleDefaults( - traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'} - ); - } - - coerce('marker.line.width', isBubble ? 1 : 0); - - if(isBubble) { - coerce('marker.sizeref'); - coerce('marker.sizemin'); - coerce('marker.sizemode'); - } -}; - -// common to 'scatter', 'scatter3d' and 'scattergeo' -scatter.textDefaults = function(traceIn, traceOut, layout, coerce) { - coerce('textposition'); - Lib.coerceFont(coerce, 'textfont', layout.font); -}; - -// common to 'scatter' and 'scattergl' -scatter.fillColorDefaults = function(traceIn, traceOut, defaultColor, coerce) { - var inheritColorFromMarker = false; - - if(traceOut.marker) { - // don't try to inherit a color array - var markerColor = traceOut.marker.color, - markerLineColor = (traceOut.marker.line || {}).color; - - if(markerColor && !Array.isArray(markerColor)) { - inheritColorFromMarker = markerColor; - } - else if(markerLineColor && !Array.isArray(markerLineColor)) { - inheritColorFromMarker = markerLineColor; - } - } - - coerce('fillcolor', Color.addOpacity( - (traceOut.line || {}).color || - inheritColorFromMarker || - defaultColor, 0.5 - )); -}; - -scatter.cleanData = function(fullData) { - var i, - tracei, - filli, - j, - tracej; - - // remove opacity for any trace that has a fill or is filled to - for(i = 0; i < fullData.length; i++) { - tracei = fullData[i]; - filli = tracei.fill; - if(filli==='none' || (tracei.type !== 'scatter')) continue; - tracei.opacity = undefined; - - if(filli === 'tonexty' || filli === 'tonextx') { - for(j = i - 1; j >= 0; j--) { - tracej = fullData[j]; - if((tracej.type === 'scatter') && - (tracej.xaxis === tracei.xaxis) && - (tracej.yaxis === tracei.yaxis)) { - tracej.opacity = undefined; - break; - } - } - } - } -}; - -scatter.colorbar = require('./colorbar'); - -// used in the drawing step for 'scatter' and 'scattegeo' and -// in the convert step for 'scatter3d' -scatter.getBubbleSizeFn = function(trace) { - var marker = trace.marker, - sizeRef = marker.sizeref || 1, - sizeMin = marker.sizemin || 0; - - // for bubble charts, allow scaling the provided value linearly - // and by area or diameter. - // Note this only applies to the array-value sizes - - var baseFn = marker.sizemode==='area' ? - function(v) { return Math.sqrt(v / sizeRef); } : - function(v) { return v / sizeRef; }; - - // TODO add support for position/negative bubbles? - // TODO add 'sizeoffset' attribute? - return function(v) { - var baseSize = baseFn(v / 2); - - // don't show non-numeric and negative sizes - return (isNumeric(baseSize) && baseSize>0) ? - Math.max(baseSize, sizeMin) : 0; - }; -}; - -scatter.calc = function(gd, trace) { - var xa = Axes.getFromId(gd,trace.xaxis||'x'), - ya = Axes.getFromId(gd,trace.yaxis||'y'); - Lib.markTime('in Scatter.calc'); - var x = xa.makeCalcdata(trace,'x'); - Lib.markTime('finished convert x'); - var y = ya.makeCalcdata(trace,'y'); - Lib.markTime('finished convert y'); - var serieslen = Math.min(x.length,y.length), - marker, - s, - i; - - // cancel minimum tick spacings (only applies to bars and boxes) - xa._minDtick = 0; - ya._minDtick = 0; - - if(x.length>serieslen) x.splice(serieslen, x.length-serieslen); - if(y.length>serieslen) y.splice(serieslen, y.length-serieslen); - - // check whether bounds should be tight, padded, extended to zero... - // most cases both should be padded on both ends, so start with that. - var xOptions = {padded:true}, - yOptions = {padded:true}; - - if(scatter.hasMarkers(trace)) { - - // Treat size like x or y arrays --- Run d2c - // this needs to go before ppad computation - marker = trace.marker; - s = marker.size; - - if (Array.isArray(s)) { - // I tried auto-type but category and dates dont make much sense. - var ax = {type: 'linear'}; - Axes.setConvert(ax); - s = ax.makeCalcdata(trace.marker, 'size'); - if(s.length>serieslen) s.splice(serieslen, s.length-serieslen); - } - - var sizeref = 1.6*(trace.marker.sizeref||1), - markerTrans; - if(trace.marker.sizemode==='area') { - markerTrans = function(v) { - return Math.max(Math.sqrt((v||0)/sizeref),3); - }; - } - else { - markerTrans = function(v) { - return Math.max((v||0)/sizeref,3); - }; - } - xOptions.ppad = yOptions.ppad = Array.isArray(s) ? - s.map(markerTrans) : markerTrans(s); - } - - scatter.calcMarkerColorscales(trace); - - // TODO: text size - - // include zero (tight) and extremes (padded) if fill to zero - // (unless the shape is closed, then it's just filling the shape regardless) - if((trace.fill==='tozerox' || (trace.fill==='tonextx' && gd.firstscatter)) && - (x[0]!==x[serieslen-1] || y[0]!==y[serieslen-1])) { - xOptions.tozero = true; - } - - // if no error bars, markers or text, or fill to y=0 remove x padding - else if(!trace.error_y.visible && ( - ['tonexty', 'tozeroy'].indexOf(trace.fill)!==-1 || - (!scatter.hasMarkers(trace) && !scatter.hasText(trace)) - )) { - xOptions.padded = false; - xOptions.ppad = 0; - } - - // now check for y - rather different logic, though still mostly padded both ends - // include zero (tight) and extremes (padded) if fill to zero - // (unless the shape is closed, then it's just filling the shape regardless) - if((trace.fill==='tozeroy' || (trace.fill==='tonexty' && gd.firstscatter)) && - (x[0]!==x[serieslen-1] || y[0]!==y[serieslen-1])) { - yOptions.tozero = true; - } - - // tight y: any x fill - else if(['tonextx', 'tozerox'].indexOf(trace.fill)!==-1) { - yOptions.padded = false; - } - - Lib.markTime('ready for Axes.expand'); - Axes.expand(xa, x, xOptions); - Lib.markTime('done expand x'); - Axes.expand(ya, y, yOptions); - Lib.markTime('done expand y'); - - // create the "calculated data" to plot - var cd = new Array(serieslen); - for(i = 0; i < serieslen; i++) { - cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ? - {x: x[i], y: y[i]} : {x: false, y: false}; - } - - // this has migrated up from arraysToCalcdata as we have a reference to 's' here - if (typeof s !== undefined) Lib.mergeArray(s, cd, 'ms'); - - gd.firstscatter = false; - return cd; -}; - -// common to 'scatter', 'scatter3d' and 'scattergeo' -scatter.calcMarkerColorscales = function(trace) { - if(!scatter.hasMarkers(trace)) return; - - var marker = trace.marker; - - // auto-z and autocolorscale if applicable - if(Colorscale.hasColorscale(trace, 'marker')) { - Colorscale.calc(trace, marker.color, 'marker', 'c'); - } - if(Colorscale.hasColorscale(trace, 'marker.line')) { - Colorscale.calc(trace, marker.line.color, 'marker.line', 'c'); - } -}; - -scatter.selectMarkers = function(gd, plotinfo, cdscatter) { - var xa = plotinfo.x(), - ya = plotinfo.y(), - xr = d3.extent(xa.range.map(xa.l2c)), - yr = d3.extent(ya.range.map(ya.l2c)); - - cdscatter.forEach(function(d,i) { - var trace = d[0].trace; - if(!scatter.hasMarkers(trace)) return; - // if marker.maxdisplayed is used, select a maximum of - // mnum markers to show, from the set that are in the viewport - var mnum = trace.marker.maxdisplayed; - - // TODO: remove some as we get away from the viewport? - if(mnum===0) return; - - var cd = d.filter(function(v) { - return v.x>=xr[0] && v.x<=xr[1] && v.y>=yr[0] && v.y<=yr[1]; - }), - inc = Math.ceil(cd.length/mnum), - tnum = 0; - cdscatter.forEach(function(cdj, j) { - var tracei = cdj[0].trace; - if(scatter.hasMarkers(tracei) && - tracei.marker.maxdisplayed>0 && j 1) { - tr.append('path').classed('js-line',true).attr('d', thispath); - } - } - if(tozero) { - if(pt0 && pt1) { - if(trace.fill.charAt(trace.fill.length-1)==='y') { - pt0[1]=pt1[1]=ya.c2p(0,true); - } - else pt0[0]=pt1[0]=xa.c2p(0,true); - - // fill to zero: full trace path, plus extension of - // the endpoints to the appropriate axis - tozero.attr('d',fullpath+'L'+pt1+'L'+pt0+'Z'); - } - } - else if(trace.fill.substr(0,6)==='tonext' && fullpath && prevpath) { - // fill to next: full trace path, plus the previous path reversed - tonext.attr('d',fullpath+prevpath+'Z'); - } - prevpath = revpath; - } - }); - - // remove paths that didn't get used - scattertraces.selectAll('path:not([d])').remove(); - - function visFilter(d){ - return d.filter(function(v){ return v.vis; }); - } - - scattertraces.append('g') - .attr('class','points') - .each(function(d){ - var trace = d[0].trace, - s = d3.select(this), - showMarkers = scatter.hasMarkers(trace), - showText = scatter.hasText(trace); - - if((!showMarkers && !showText) || trace.visible !== true) s.remove(); - else { - if(showMarkers) { - s.selectAll('path.point') - .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) - .enter().append('path') - .classed('point', true) - .call(Drawing.translatePoints, xa, ya); - } - if(showText) { - s.selectAll('g') - .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) - // each text needs to go in its own 'g' in case - // it gets converted to mathjax - .enter().append('g') - .append('text') - .call(Drawing.translatePoints, xa, ya); - } - } - }); -}; - -scatter.linePoints = function(d, opts) { - var xa = opts.xaxis, - ya = opts.yaxis, - connectGaps = opts.connectGaps, - baseTolerance = opts.baseTolerance, - linear = opts.linear, - segments = [], - badnum = Axes.BADNUM, - minTolerance = 0.2, // fraction of tolerance "so close we don't even consider it a new point" - pts = new Array(d.length), - pti = 0, - i, - - // pt variables are pixel coordinates [x,y] of one point - clusterStartPt, // these four are the outputs of clustering on a line - clusterEndPt, - clusterHighPt, - clusterLowPt, - thisPt, // "this" is the next point we're considering adding to the cluster - - clusterRefDist, - clusterHighFirst, // did we encounter the high point first, then a low point, or vice versa? - clusterUnitVector, // the first two points in the cluster determine its unit vector - // so the second is always in the "High" direction - thisVector, // the pixel delta from clusterStartPt - - // val variables are (signed) pixel distances along the cluster vector - clusterHighVal, - clusterLowVal, - thisVal, - - // deviation variables are (signed) pixel distances normal to the cluster vector - clusterMinDeviation, - clusterMaxDeviation, - thisDeviation; - - // turn one calcdata point into pixel coordinates - function getPt(index) { - var x = xa.c2p(d[index].x), - y = ya.c2p(d[index].y); - if(x === badnum || y === badnum) return false; - return [x, y]; - } - - // if we're off-screen, increase tolerance over baseTolerance - function getTolerance(pt) { - var xFrac = pt[0] / xa._length, - yFrac = pt[1] / ya._length; - return (1 + 10 * Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1)) * baseTolerance; - } - - function ptDist(pt1, pt2) { - var dx = pt1[0] - pt2[0], - dy = pt1[1] - pt2[1]; - return Math.sqrt(dx * dx + dy * dy); - } - - // loop over ALL points in this trace - for(i = 0; i < d.length; i++) { - clusterStartPt = getPt(i); - if(!clusterStartPt) continue; - - pti = 0; - pts[pti++] = clusterStartPt; - - // loop over one segment of the trace - for(i++; i < d.length; i++) { - clusterHighPt = getPt(i); - if(!clusterHighPt) { - if(connectGaps) continue; - else break; - } - - // can't decimate if nonlinear line shape - // TODO: we *could* decimate [hv]{2,3} shapes if we restricted clusters to horz or vert again - // but spline would be verrry awkward to decimate - if(!linear) { - pts[pti++] = clusterHighPt; - continue; - } - - clusterRefDist = ptDist(clusterHighPt, clusterStartPt); - - if(clusterRefDist < getTolerance(clusterHighPt) * minTolerance) continue; - - clusterUnitVector = [ - (clusterHighPt[0] - clusterStartPt[0]) / clusterRefDist, - (clusterHighPt[1] - clusterStartPt[1]) / clusterRefDist - ]; - - clusterLowPt = clusterStartPt; - clusterHighVal = clusterRefDist; - clusterLowVal = clusterMinDeviation = clusterMaxDeviation = 0; - clusterHighFirst = false; - clusterEndPt = clusterHighPt; - - // loop over one cluster of points that collapse onto one line - for(i++; i < d.length; i++) { - thisPt = getPt(i); - if(!thisPt) { - if(connectGaps) continue; - else break; - } - thisVector = [ - thisPt[0] - clusterStartPt[0], - thisPt[1] - clusterStartPt[1] - ]; - // cross product (or dot with normal to the cluster vector) - thisDeviation = thisVector[0] * clusterUnitVector[1] - thisVector[1] * clusterUnitVector[0]; - clusterMinDeviation = Math.min(clusterMinDeviation, thisDeviation); - clusterMaxDeviation = Math.max(clusterMaxDeviation, thisDeviation); - - if(clusterMaxDeviation - clusterMinDeviation > getTolerance(thisPt)) break; - - clusterEndPt = thisPt; - thisVal = thisVector[0] * clusterUnitVector[0] + thisVector[1] * clusterUnitVector[1]; - - if(thisVal > clusterHighVal) { - clusterHighVal = thisVal; - clusterHighPt = thisPt; - clusterHighFirst = false; - } else if(thisVal < clusterLowVal) { - clusterLowVal = thisVal; - clusterLowPt = thisPt; - clusterHighFirst = true; - } - } - - // insert this cluster into pts - // we've already inserted the start pt, now check if we have high and low pts - if(clusterHighFirst) { - pts[pti++] = clusterHighPt; - if(clusterEndPt !== clusterLowPt) pts[pti++] = clusterLowPt; - } else { - if(clusterLowPt !== clusterStartPt) pts[pti++] = clusterLowPt; - if(clusterEndPt !== clusterHighPt) pts[pti++] = clusterHighPt; - } - // and finally insert the end pt - pts[pti++] = clusterEndPt; - - // have we reached the end of this segment? - if(i >= d.length || !thisPt) break; - - // otherwise we have an out-of-cluster point to insert as next clusterStartPt - pts[pti++] = thisPt; - clusterStartPt = thisPt; - } - - segments.push(pts.slice(0, pti)); - } - - return segments; -}; - -scatter.style = function(gd) { - var s = d3.select(gd).selectAll('g.trace.scatter'); - - s.style('opacity',function(d){ return d[0].trace.opacity; }); - - s.selectAll('g.points') - .each(function(d){ - d3.select(this).selectAll('path.point') - .call(Drawing.pointStyle,d.trace||d[0].trace); - d3.select(this).selectAll('text') - .call(Drawing.textPointStyle,d.trace||d[0].trace); - }); - - s.selectAll('g.trace path.js-line') - .call(Drawing.lineGroupStyle); - - s.selectAll('g.trace path.js-fill') - .call(Drawing.fillGroupStyle); -}; - -scatter.getTraceColor = function(trace, di) { - var lc, tc; - - // TODO: text modes - - if(trace.mode === 'lines') { - lc = trace.line.color; - return (lc && Color.opacity(lc)) ? - lc : trace.fillcolor; - } - else if(trace.mode === 'none') { - return trace.fill ? trace.fillcolor : ''; - } - else { - var mc = di.mcc || (trace.marker || {}).color, - mlc = di.mlcc || ((trace.marker || {}).line || {}).color; - - tc = (mc && Color.opacity(mc)) ? mc : - (mlc && Color.opacity(mlc) && - (di.mlw || ((trace.marker || {}).line || {}).width)) ? mlc : ''; - - if(tc) { - // make sure the points aren't TOO transparent - if(Color.opacity(tc) < 0.3) { - return Color.addOpacity(tc, 0.3); - } - else return tc; - } - else { - lc = (trace.line || {}).color; - return (lc && Color.opacity(lc) && - scatter.hasLines(trace) && trace.line.width) ? - lc : trace.fillcolor; - } - } -}; - -scatter.hoverPoints = function(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - trace = cd[0].trace, - xa = pointData.xa, - ya = pointData.ya, - dx = function(di){ - // scatter points: d.mrc is the calculated marker radius - // adjust the distance so if you're inside the marker it - // always will show up regardless of point size, but - // prioritize smaller points - var rad = Math.max(3, di.mrc||0); - return Math.max(Math.abs(xa.c2p(di.x)-xa.c2p(xval))-rad, 1-3/rad); - }, - dy = function(di){ - var rad = Math.max(3, di.mrc||0); - return Math.max(Math.abs(ya.c2p(di.y)-ya.c2p(yval))-rad, 1-3/rad); - }, - dxy = function(di) { - var rad = Math.max(3, di.mrc||0), - dx = Math.abs(xa.c2p(di.x)-xa.c2p(xval)), - dy = Math.abs(ya.c2p(di.y)-ya.c2p(yval)); - return Math.max(Math.sqrt(dx*dx + dy*dy)-rad, 1-3/rad); - }, - distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); - - Fx.getClosest(cd, distfn, pointData); - - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index===false) return; - - // the closest data point - var di = cd[pointData.index], - xc = xa.c2p(di.x, true), - yc = ya.c2p(di.y, true), - rad = di.mrc||1; - - pointData.color = scatter.getTraceColor(trace, di); - - pointData.x0 = xc - rad; - pointData.x1 = xc + rad; - pointData.xLabelVal = di.x; - - pointData.y0 = yc - rad; - pointData.y1 = yc + rad; - pointData.yLabelVal = di.y; - - if(di.tx) pointData.text = di.tx; - else if(trace.text) pointData.text = trace.text; - - ErrorBars.hoverInfo(di, trace, pointData); - - return [pointData]; -}; +module.exports = Scatter; diff --git a/src/traces/scatter/line_defaults.js b/src/traces/scatter/line_defaults.js new file mode 100644 index 00000000000..64f4104bd31 --- /dev/null +++ b/src/traces/scatter/line_defaults.js @@ -0,0 +1,22 @@ +/** +* Copyright 2012-2016, 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'; + + +// common to 'scatter', 'scatter3d', 'scattergeo' and 'scattergl' +module.exports = function lineDefaults(traceIn, traceOut, defaultColor, coerce) { + var markerColor = (traceIn.marker || {}).color; + + // don't try to inherit a color array + coerce('line.color', (Array.isArray(markerColor) ? false : markerColor) || + defaultColor); + coerce('line.width'); + coerce('line.dash'); +}; diff --git a/src/traces/scatter/line_points.js b/src/traces/scatter/line_points.js new file mode 100644 index 00000000000..60d7e3c77ea --- /dev/null +++ b/src/traces/scatter/line_points.js @@ -0,0 +1,167 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Axes = require('../../plots/cartesian/axes'); + + +module.exports = function linePoints(d, opts) { + var xa = opts.xaxis, + ya = opts.yaxis, + connectGaps = opts.connectGaps, + baseTolerance = opts.baseTolerance, + linear = opts.linear, + segments = [], + badnum = Axes.BADNUM, + minTolerance = 0.2, // fraction of tolerance "so close we don't even consider it a new point" + pts = new Array(d.length), + pti = 0, + i, + + // pt variables are pixel coordinates [x,y] of one point + clusterStartPt, // these four are the outputs of clustering on a line + clusterEndPt, + clusterHighPt, + clusterLowPt, + thisPt, // "this" is the next point we're considering adding to the cluster + + clusterRefDist, + clusterHighFirst, // did we encounter the high point first, then a low point, or vice versa? + clusterUnitVector, // the first two points in the cluster determine its unit vector + // so the second is always in the "High" direction + thisVector, // the pixel delta from clusterStartPt + + // val variables are (signed) pixel distances along the cluster vector + clusterHighVal, + clusterLowVal, + thisVal, + + // deviation variables are (signed) pixel distances normal to the cluster vector + clusterMinDeviation, + clusterMaxDeviation, + thisDeviation; + + // turn one calcdata point into pixel coordinates + function getPt(index) { + var x = xa.c2p(d[index].x), + y = ya.c2p(d[index].y); + if(x === badnum || y === badnum) return false; + return [x, y]; + } + + // if we're off-screen, increase tolerance over baseTolerance + function getTolerance(pt) { + var xFrac = pt[0] / xa._length, + yFrac = pt[1] / ya._length; + return (1 + 10 * Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1)) * baseTolerance; + } + + function ptDist(pt1, pt2) { + var dx = pt1[0] - pt2[0], + dy = pt1[1] - pt2[1]; + return Math.sqrt(dx * dx + dy * dy); + } + + // loop over ALL points in this trace + for(i = 0; i < d.length; i++) { + clusterStartPt = getPt(i); + if(!clusterStartPt) continue; + + pti = 0; + pts[pti++] = clusterStartPt; + + // loop over one segment of the trace + for(i++; i < d.length; i++) { + clusterHighPt = getPt(i); + if(!clusterHighPt) { + if(connectGaps) continue; + else break; + } + + // can't decimate if nonlinear line shape + // TODO: we *could* decimate [hv]{2,3} shapes if we restricted clusters to horz or vert again + // but spline would be verrry awkward to decimate + if(!linear) { + pts[pti++] = clusterHighPt; + continue; + } + + clusterRefDist = ptDist(clusterHighPt, clusterStartPt); + + if(clusterRefDist < getTolerance(clusterHighPt) * minTolerance) continue; + + clusterUnitVector = [ + (clusterHighPt[0] - clusterStartPt[0]) / clusterRefDist, + (clusterHighPt[1] - clusterStartPt[1]) / clusterRefDist + ]; + + clusterLowPt = clusterStartPt; + clusterHighVal = clusterRefDist; + clusterLowVal = clusterMinDeviation = clusterMaxDeviation = 0; + clusterHighFirst = false; + clusterEndPt = clusterHighPt; + + // loop over one cluster of points that collapse onto one line + for(i++; i < d.length; i++) { + thisPt = getPt(i); + if(!thisPt) { + if(connectGaps) continue; + else break; + } + thisVector = [ + thisPt[0] - clusterStartPt[0], + thisPt[1] - clusterStartPt[1] + ]; + // cross product (or dot with normal to the cluster vector) + thisDeviation = thisVector[0] * clusterUnitVector[1] - thisVector[1] * clusterUnitVector[0]; + clusterMinDeviation = Math.min(clusterMinDeviation, thisDeviation); + clusterMaxDeviation = Math.max(clusterMaxDeviation, thisDeviation); + + if(clusterMaxDeviation - clusterMinDeviation > getTolerance(thisPt)) break; + + clusterEndPt = thisPt; + thisVal = thisVector[0] * clusterUnitVector[0] + thisVector[1] * clusterUnitVector[1]; + + if(thisVal > clusterHighVal) { + clusterHighVal = thisVal; + clusterHighPt = thisPt; + clusterHighFirst = false; + } else if(thisVal < clusterLowVal) { + clusterLowVal = thisVal; + clusterLowPt = thisPt; + clusterHighFirst = true; + } + } + + // insert this cluster into pts + // we've already inserted the start pt, now check if we have high and low pts + if(clusterHighFirst) { + pts[pti++] = clusterHighPt; + if(clusterEndPt !== clusterLowPt) pts[pti++] = clusterLowPt; + } else { + if(clusterLowPt !== clusterStartPt) pts[pti++] = clusterLowPt; + if(clusterEndPt !== clusterHighPt) pts[pti++] = clusterHighPt; + } + // and finally insert the end pt + pts[pti++] = clusterEndPt; + + // have we reached the end of this segment? + if(i >= d.length || !thisPt) break; + + // otherwise we have an out-of-cluster point to insert as next clusterStartPt + pts[pti++] = thisPt; + clusterStartPt = thisPt; + } + + segments.push(pts.slice(0, pti)); + } + + return segments; +}; diff --git a/src/traces/scatter/make_bubble_size_func.js b/src/traces/scatter/make_bubble_size_func.js new file mode 100644 index 00000000000..160c03ec667 --- /dev/null +++ b/src/traces/scatter/make_bubble_size_func.js @@ -0,0 +1,40 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + + +// used in the drawing step for 'scatter' and 'scattegeo' and +// in the convert step for 'scatter3d' +module.exports = function makeBubbleSizeFn(trace) { + var marker = trace.marker, + sizeRef = marker.sizeref || 1, + sizeMin = marker.sizemin || 0; + + // for bubble charts, allow scaling the provided value linearly + // and by area or diameter. + // Note this only applies to the array-value sizes + + var baseFn = (marker.sizemode === 'area') ? + function(v) { return Math.sqrt(v / sizeRef); } : + function(v) { return v / sizeRef; }; + + // TODO add support for position/negative bubbles? + // TODO add 'sizeoffset' attribute? + return function(v) { + var baseSize = baseFn(v / 2); + + // don't show non-numeric and negative sizes + return (isNumeric(baseSize) && (baseSize > 0)) ? + Math.max(baseSize, sizeMin) : + 0; + }; +}; diff --git a/src/traces/scatter/marker_colorscale_calc.js b/src/traces/scatter/marker_colorscale_calc.js new file mode 100644 index 00000000000..d8db642d9fa --- /dev/null +++ b/src/traces/scatter/marker_colorscale_calc.js @@ -0,0 +1,32 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var hasColorscale = require('../../components/colorscale/has_colorscale'); +var calcColorscale = require('../../components/colorscale/calc'); + +var subTypes = require('./subtypes'); + + +// common to 'scatter', 'scatter3d' and 'scattergeo' +module.exports = function calcMarkerColorscale(trace) { + if(!subTypes.hasMarkers(trace)) return; + + var marker = trace.marker; + + // auto-z and autocolorscale if applicable + if(hasColorscale(trace, 'marker')) { + calcColorscale(trace, marker.color, 'marker', 'c'); + } + + if(hasColorscale(trace, 'marker.line')) { + calcColorscale(trace, marker.line.color, 'marker.line', 'c'); + } +}; diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js new file mode 100644 index 00000000000..57b85dbeb1f --- /dev/null +++ b/src/traces/scatter/marker_defaults.js @@ -0,0 +1,61 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Color = require('../../components/color'); +var hasColorscale = require('../../components/colorscale/has_colorscale'); +var colorscaleDefaults = require('../../components/colorscale/defaults'); + +var subTypes = require('./subtypes'); + + +// common to 'scatter', 'scatter3d', 'scattergeo' and 'scattergl' +module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { + var isBubble = subTypes.isBubble(traceIn), + lineColor = (traceIn.line || {}).color, + defaultMLC; + + if(lineColor) defaultColor = lineColor; + + coerce('marker.symbol'); + coerce('marker.opacity', isBubble ? 0.7 : 1); + coerce('marker.size'); + + coerce('marker.color', defaultColor); + if(hasColorscale(traceIn, 'marker')) { + 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 + // mostly this is for transparent markers to behave nicely + if(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); + + if(isBubble) { + coerce('marker.sizeref'); + coerce('marker.sizemin'); + coerce('marker.sizemode'); + } +}; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js new file mode 100644 index 00000000000..09345bcea7d --- /dev/null +++ b/src/traces/scatter/plot.js @@ -0,0 +1,224 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var d3 = require('d3'); + +var Lib = require('../../lib'); +var Drawing = require('../../components/drawing'); + +var subTypes = require('./subtypes'); +var arraysToCalcdata = require('./arrays_to_calcdata'); +var linePoints = require('./line_points'); + + +module.exports = function plot(gd, plotinfo, cdscatter) { + selectMarkers(gd, plotinfo, cdscatter); + + var xa = plotinfo.x(), + ya = plotinfo.y(); + + // make the container for scatter plots + // (so error bars can find them along with bars) + var scattertraces = plotinfo.plot.select('.scatterlayer') + .selectAll('g.trace.scatter') + .data(cdscatter); + scattertraces.enter().append('g') + .attr('class', 'trace scatter') + .style('stroke-miterlimit', 2); + + // BUILD LINES AND FILLS + var prevpath = '', + tozero, tonext, nexttonext; + + scattertraces.each(function(d){ + var trace = d[0].trace, + line = trace.line, + tr = d3.select(this); + if(trace.visible !== true) return; + + d[0].node3 = tr; // store node for tweaking by selectPoints + + arraysToCalcdata(d); + + if(!subTypes.hasLines(trace) && trace.fill === 'none') return; + + var thispath, + // fullpath is all paths for this curve, joined together straight + // across gaps, for filling + fullpath = '', + // revpath is fullpath reversed, for fill-to-next + revpath = '', + // functions for converting a point array to a path + pathfn, revpathbase, revpathfn; + + // make the fill-to-zero path now, so it shows behind the line + // fill to next puts the fill associated with one trace + // grouped with the previous + if(trace.fill.substr(0, 6) === 'tozero' || + (trace.fill.substr(0, 2) === 'to' && !prevpath)) { + tozero = tr.append('path') + .classed('js-fill', true); + } + else tozero = null; + + // make the fill-to-next path now for the NEXT trace, so it shows + // behind both lines. + // nexttonext was created last time, but give it + // this curve's data for fill color + if(nexttonext) tonext = nexttonext.datum(d); + + // now make a new nexttonext for next time + nexttonext = tr.append('path').classed('js-fill', true); + + if(['hv', 'vh', 'hvh', 'vhv'].indexOf(line.shape) !== -1) { + pathfn = Drawing.steps(line.shape); + revpathbase = Drawing.steps( + line.shape.split('').reverse().join('') + ); + } + else if(line.shape === 'spline') { + pathfn = revpathbase = function(pts) { + return Drawing.smoothopen(pts, line.smoothing); + }; + } + else { + pathfn = revpathbase = function(pts) { + return 'M' + pts.join('L'); + }; + } + + revpathfn = function(pts) { + // note: this is destructive (reverses pts in place) so can't use pts after this + return 'L' + revpathbase(pts.reverse()).substr(1); + }; + + var segments = linePoints(d, { + xaxis: xa, + yaxis: ya, + connectGaps: trace.connectgaps, + baseTolerance: Math.max(line.width || 1, 3) / 4, + linear: line.shape === 'linear' + }); + + if(segments.length) { + var pt0 = segments[0][0], + lastSegment = segments[segments.length - 1], + pt1 = lastSegment[lastSegment.length - 1]; + + for(var i = 0; i < segments.length; i++) { + var pts = segments[i]; + thispath = pathfn(pts); + fullpath += fullpath ? ('L' + thispath.substr(1)) : thispath; + revpath = revpathfn(pts) + revpath; + if(subTypes.hasLines(trace) && pts.length > 1) { + tr.append('path').classed('js-line', true).attr('d', thispath); + } + } + if(tozero) { + if(pt0 && pt1) { + if(trace.fill.charAt(trace.fill.length - 1) === 'y') { + pt0[1] = pt1[1] = ya.c2p(0, true); + } + else pt0[0] = pt1[0] = xa.c2p(0, true); + + // fill to zero: full trace path, plus extension of + // the endpoints to the appropriate axis + tozero.attr('d', fullpath + 'L' + pt1 + 'L' + pt0 + 'Z'); + } + } + else if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevpath) { + // fill to next: full trace path, plus the previous path reversed + tonext.attr('d', fullpath + prevpath + 'Z'); + } + prevpath = revpath; + } + }); + + // remove paths that didn't get used + scattertraces.selectAll('path:not([d])').remove(); + + function visFilter(d){ + return d.filter(function(v) { return v.vis; }); + } + + scattertraces.append('g') + .attr('class', 'points') + .each(function(d) { + var trace = d[0].trace, + s = d3.select(this), + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace); + + if((!showMarkers && !showText) || trace.visible !== true) s.remove(); + else { + if(showMarkers) { + s.selectAll('path.point') + .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) + .enter().append('path') + .classed('point', true) + .call(Drawing.translatePoints, xa, ya); + } + if(showText) { + s.selectAll('g') + .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) + // each text needs to go in its own 'g' in case + // it gets converted to mathjax + .enter().append('g') + .append('text') + .call(Drawing.translatePoints, xa, ya); + } + } + }); +}; + +function selectMarkers(gd, plotinfo, cdscatter) { + var xa = plotinfo.x(), + ya = plotinfo.y(), + xr = d3.extent(xa.range.map(xa.l2c)), + yr = d3.extent(ya.range.map(ya.l2c)); + + cdscatter.forEach(function(d, i) { + var trace = d[0].trace; + if(!subTypes.hasMarkers(trace)) return; + // if marker.maxdisplayed is used, select a maximum of + // mnum markers to show, from the set that are in the viewport + var mnum = trace.marker.maxdisplayed; + + // TODO: remove some as we get away from the viewport? + if(mnum === 0) return; + + var cd = d.filter(function(v) { + return v.x>=xr[0] && v.x<=xr[1] && v.y>=yr[0] && v.y<=yr[1]; + }), + inc = Math.ceil(cd.length / mnum), + tnum = 0; + cdscatter.forEach(function(cdj, j) { + var tracei = cdj[0].trace; + if(subTypes.hasMarkers(tracei) && + tracei.marker.maxdisplayed>0 && j