diff --git a/src/components/errorbars/index.js b/src/components/errorbars/index.js index 4f68e91d37b..4986e60c6b1 100644 --- a/src/components/errorbars/index.js +++ b/src/components/errorbars/index.js @@ -9,14 +9,6 @@ 'use strict'; -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'); @@ -49,123 +41,9 @@ errorBars.calcFromTrace = function(trace, layout) { return calcdataMock; }; -// the main drawing function for errorbars -errorBars.plot = function(gd, plotinfo, cd) { - // ___ <-- "errorhats" - // | - // | <-- "errorbars" - // | - // ___ <-- "errorshoes" - - var xa = plotinfo.x(), - ya = plotinfo.y(); - - // first remove all existing errorbars - // TODO: use enter/exit instead - plotinfo.plot.select('.errorlayer').selectAll('g.errorbars').remove(); - var coords; - - // draw the errorbars - plotinfo.plot.select('.errorlayer').selectAll('g.errorbars') - .data(cd) - .enter().append('g') - .attr('class','errorbars') - .each(function(d) { - var trace = d[0].trace, - xObj = trace.error_x, - yObj = trace.error_y, - sparse = subTypes.hasMarkers(trace) && - trace.marker.maxdisplayed>0; - - if(!yObj.visible && !xObj.visible) return; - - d3.select(this).selectAll('g') - .data(Lib.identity) - .enter().append('g') - .each(function(d) { - coords = errorcoords(d, xa, ya); - var eb = d3.select(this), - path; - if(sparse && !d.vis) return; - - if(yObj.visible && isNumeric(coords.x) && - isNumeric(coords.yh) && - isNumeric(coords.ys)) { - var yw = yObj.width; - path = 'M'+(coords.x-yw)+','+coords.yh+'h'+(2*yw) + // hat - 'm-'+yw+',0V'+coords.ys; // bar - if(!coords.noYS) path += 'm-'+yw+',0h'+(2*yw); // shoe - - eb.append('path') - .classed('yerror', true) - .attr('d', path); - } - if(xObj.visible && isNumeric(coords.y) && - isNumeric(coords.xh) && - isNumeric(coords.xs)) { - var xw = (xObj.copy_ystyle ? yObj : xObj).width; - path = 'M'+coords.xh+','+(coords.y-xw)+'v'+(2*xw) + // hat - 'm0,-'+xw+'H'+coords.xs; // bar - if(!coords.noXS) path += 'm0,-'+xw+'v'+(2*xw); // shoe - - eb.append('path') - .classed('xerror', true) - .attr('d', path); - } - }); - }); -}; - -errorBars.style = function(gd) { - d3.select(gd).selectAll('g.errorbars').each(function(d) { - var eb = d3.select(this), - trace = d[0].trace, - yObj = trace.error_y||{}, - xObj = trace.error_x||{}; - - eb.selectAll('g path.yerror') - .style('stroke-width', yObj.thickness+'px') - .call(Color.stroke, yObj.color); - - if(xObj.copy_ystyle) xObj = yObj; - - eb.selectAll('g path.xerror') - .style('stroke-width', xObj.thickness+'px') - .call(Color.stroke, xObj.color); - }); -}; - -function errorcoords(d, xa, ya) { - // compute the coordinates of the error-bar objects - var out = { - x: xa.c2p(d.x), - y: ya.c2p(d.y) - }; - - // calculate the error bar size and hat and shoe locations - if(d.yh!==undefined) { - out.yh = ya.c2p(d.yh); - out.ys = ya.c2p(d.ys); - - // if the shoes go off-scale (ie log scale, error bars past zero) - // clip the bar and hide the shoes - if(!isNumeric(out.ys)) { - out.noYS = true; - out.ys = ya.c2p(d.ys, true); - } - } - if(d.xh!==undefined) { - out.xh = xa.c2p(d.xh); - out.xs = xa.c2p(d.xs); - - if(!isNumeric(out.xs)) { - out.noXS = true; - out.xs = xa.c2p(d.xs, true); - } - } +errorBars.plot = require('./plot'); - return out; -} +errorBars.style = require('./style'); errorBars.hoverInfo = function(calcPoint, trace, hoverPoint) { if(trace.error_y.visible) { diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js new file mode 100644 index 00000000000..0df1b4060b8 --- /dev/null +++ b/src/components/errorbars/plot.js @@ -0,0 +1,115 @@ +/** +* 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 isNumeric = require('fast-isnumeric'); + +var Lib = require('../../lib'); +var subTypes = require('../../traces/scatter/subtypes'); + + +module.exports = function plot(traces, plotinfo) { + var xa = plotinfo.x(), + ya = plotinfo.y(); + + traces.each(function(d) { + var trace = d[0].trace, + xObj = trace.error_x, + yObj = trace.error_y; + + var sparse = ( + subTypes.hasMarkers(trace) && + trace.marker.maxdisplayed > 0 + ); + + if(!yObj.visible && !xObj.visible) return; + + var errorbars = d3.select(this).selectAll('g.errorbar') + .data(Lib.identity); + + errorbars.enter().append('g') + .classed('errorbar', true); + + errorbars.each(function(d) { + var errorbar = d3.select(this); + var coords = errorCoords(d, xa, ya); + + if(sparse && !d.vis) return; + + var path; + + if(yObj.visible && isNumeric(coords.x) && + isNumeric(coords.yh) && + isNumeric(coords.ys)) { + var yw = yObj.width; + + path = 'M' + (coords.x - yw) + ',' + + coords.yh + 'h' + (2 * yw) + // hat + 'm-' + yw + ',0V' + coords.ys; // bar + + if(!coords.noYS) path += 'm-' + yw +',0h' + (2 * yw); // shoe + + errorbar.append('path') + .classed('yerror', true) + .attr('d', path); + } + + if(xObj.visible && isNumeric(coords.y) && + isNumeric(coords.xh) && + isNumeric(coords.xs)) { + var xw = (xObj.copy_ystyle ? yObj : xObj).width; + + path = 'M' + coords.xh + ',' + + (coords.y - xw) + 'v' + (2 * xw) + // hat + 'm0,-' + xw + 'H' + coords.xs; // bar + + if(!coords.noXS) path += 'm0,-' + xw + 'v' + (2 * xw); // shoe + + errorbar.append('path') + .classed('xerror', true) + .attr('d', path); + } + }); + }); +}; + +// compute the coordinates of the error-bar objects +function errorCoords(d, xa, ya) { + var out = { + x: xa.c2p(d.x), + y: ya.c2p(d.y) + }; + + // calculate the error bar size and hat and shoe locations + if(d.yh !== undefined) { + out.yh = ya.c2p(d.yh); + out.ys = ya.c2p(d.ys); + + // if the shoes go off-scale (ie log scale, error bars past zero) + // clip the bar and hide the shoes + if(!isNumeric(out.ys)) { + out.noYS = true; + out.ys = ya.c2p(d.ys, true); + } + } + + if(d.xh !== undefined) { + out.xh = xa.c2p(d.xh); + out.xs = xa.c2p(d.xs); + + if(!isNumeric(out.xs)) { + out.noXS = true; + out.xs = xa.c2p(d.xs, true); + } + } + + return out; +} diff --git a/src/components/errorbars/style.js b/src/components/errorbars/style.js new file mode 100644 index 00000000000..6b87f1dd127 --- /dev/null +++ b/src/components/errorbars/style.js @@ -0,0 +1,35 @@ +/** +* 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 Color = require('../color'); + + +module.exports = function style(traces) { + traces.each(function(d) { + var trace = d[0].trace, + yObj = trace.error_y || {}, + xObj = trace.error_x || {}; + + var s = d3.select(this); + + s.selectAll('path.yerror') + .style('stroke-width', yObj.thickness + 'px') + .call(Color.stroke, yObj.color); + + if(xObj.copy_ystyle) xObj = yObj; + + s.selectAll('path.xerror') + .style('stroke-width', xObj.thickness + 'px') + .call(Color.stroke, xObj.color); + }); +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index eeea7c0c1f5..026356a1f04 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2672,7 +2672,6 @@ function makeCartesianPlotFramwork(gd, subplots) { svg.append('g').classed('imagelayer', true); svg.append('g').classed('maplayer', true); svg.append('g').classed('barlayer', true); - svg.append('g').classed('errorlayer', true); svg.append('g').classed('boxlayer', true); svg.append('g').classed('scatterlayer', true); } diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index ff15bfc4217..a43a40b906b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -11,8 +11,6 @@ var Lib = require('../../lib'); var Plots = require('../plots'); -var ErrorBars = require('../../components/errorbars'); - exports.name = 'cartesian'; @@ -71,8 +69,7 @@ exports.plot = function(gd) { for(var i = 0; i < subplots.length; i++) { var subplot = subplots[i], subplotInfo = fullLayout._plots[subplot], - cdSubplot = getCdSubplot(calcdata, subplot), - cdError = []; + cdSubplot = getCdSubplot(calcdata, subplot); // remove old traces, then redraw everything // TODO: use enter/exit appropriately in the plot functions @@ -91,17 +88,9 @@ exports.plot = function(gd) { // plot all traces of this type on this subplot at once var cdModule = getCdModule(cdSubplot, _module); _module.plot(gd, subplotInfo, cdModule); - Lib.markTime('done ' + (cdModule[0] && cdModule[0][0].trace.type)); - // collect the traces that may have error bars - if(cdModule[0] && cdModule[0][0].trace && Plots.traceIs(cdModule[0][0].trace, 'errorBarsOK')) { - cdError = cdError.concat(cdModule); - } + Lib.markTime('done ' + (cdModule[0] && cdModule[0][0].trace.type)); } - - // finally do all error bars at once - ErrorBars.plot(gd, subplotInfo, cdError); - Lib.markTime('done ErrorBars'); } // now draw stuff not on subplots (ie, only pies at the moment) diff --git a/src/plots/plots.js b/src/plots/plots.js index 990b715edc6..1cd9555a01f 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -805,10 +805,11 @@ plots.purge = function(gd) { }; plots.style = function(gd) { - var modulesWithErrorBars = gd._modules.concat(Plotly.ErrorBars); + var _modules = gd._modules; + + for(var i = 0; i < _modules.length; i++) { + var _module = _modules[i]; - for(var i = 0; i < modulesWithErrorBars.length; i++) { - var _module = modulesWithErrorBars[i]; if(_module.style) _module.style(gd); } }; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 316e813965b..9a6e161791a 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -14,6 +14,7 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var Color = require('../../components/color'); +var ErrorBars = require('../../components/errorbars'); var arraysToCalcdata = require('./arrays_to_calcdata'); @@ -102,4 +103,8 @@ module.exports = function plot(gd, plotinfo, cdbar) { 'M'+x0+','+y0+'V'+y1+'H'+x1+'V'+y0+'Z'); }); }); + + // error bars are on the top + bartraces.call(ErrorBars.plot, plotinfo); + }; diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js index 7b6ebd43f50..c53f68f5b8d 100644 --- a/src/traces/bar/style.js +++ b/src/traces/bar/style.js @@ -13,6 +13,7 @@ var d3 = require('d3'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); +var ErrorBars = require('../../components/errorbars'); module.exports = function style(gd) { @@ -71,4 +72,6 @@ module.exports = function style(gd) { // d3.select(this).selectAll('text') // .call(Plotly.Drawing.textPointStyle,d.t||d[0].t); }); + + s.call(ErrorBars.style); }; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 8be8f3274be..20f59d43516 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -13,6 +13,7 @@ var d3 = require('d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); +var ErrorBars = require('../../components/errorbars'); var subTypes = require('./subtypes'); var arraysToCalcdata = require('./arrays_to_calcdata'); @@ -30,10 +31,14 @@ module.exports = function plot(gd, plotinfo, cdscatter) { var scattertraces = plotinfo.plot.select('.scatterlayer') .selectAll('g.trace.scatter') .data(cdscatter); + scattertraces.enter().append('g') .attr('class', 'trace scatter') .style('stroke-miterlimit', 2); + // error bars are at the bottom + scattertraces.call(ErrorBars.plot, plotinfo); + // BUILD LINES AND FILLS var prevpath = '', tozero, tonext, nexttonext; diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index a6d434ff2af..ace5d004aa0 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -12,6 +12,7 @@ var d3 = require('d3'); var Drawing = require('../../components/drawing'); +var ErrorBars = require('../../components/errorbars'); module.exports = function style(gd) { @@ -34,4 +35,6 @@ module.exports = function style(gd) { s.selectAll('g.trace path.js-fill') .call(Drawing.fillGroupStyle); + + s.call(ErrorBars.style); }; diff --git a/test/image/baselines/error_bar_layers.png b/test/image/baselines/error_bar_layers.png new file mode 100644 index 00000000000..d1b65edd0c4 Binary files /dev/null and b/test/image/baselines/error_bar_layers.png differ diff --git a/test/image/baselines/error_bar_style.png b/test/image/baselines/error_bar_style.png index 28f5d931c2f..b08842c0d5c 100644 Binary files a/test/image/baselines/error_bar_style.png and b/test/image/baselines/error_bar_style.png differ diff --git a/test/image/mocks/error_bar_layers.json b/test/image/mocks/error_bar_layers.json new file mode 100644 index 00000000000..99a686f0d21 --- /dev/null +++ b/test/image/mocks/error_bar_layers.json @@ -0,0 +1,68 @@ +{ + "data": [ + { + "x": [ + 1, + 1, + 2, + 2, + 1 + ], + "y": [ + 1, + 2, + 2, + 1, + 1 + ], + "mode": "lines", + "fill": "tozerox", + "fillcolor": "black", + "uid": "116a4f" + }, + { + "x": [ + 1.5, + 2.5 + ], + "y": [ + 1.5, + 2.5 + ], + "error_y": { + "array": [ + 1, + 2 + ], + "color": "green", + "thickness": 3 + }, + "marker": { + "size": 20, + "color": "red" + }, + "uid": "0f7846" + } + ], + "layout": { + "xaxis": { + "type": "linear", + "range": [ + 1, + 2.608493696084937 + ], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [ + 0.2777777777777778, + 4.722222222222222 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +}