From 5ad7bb951ed6ca786efe0abe9d75c0365efe7fbd Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 1 Dec 2016 17:00:00 -0500 Subject: [PATCH 001/132] Carpet scaffolding --- lib/carpet.js | 11 ++++ lib/index.js | 2 + src/lib/carpet.js | 11 ++++ src/traces/carpet/ab_defaults.js | 35 ++++++++++++ src/traces/carpet/attributes.js | 91 +++++++++++++++++++++++++++++++ src/traces/carpet/calc.js | 23 ++++++++ src/traces/carpet/constants.js | 13 +++++ src/traces/carpet/defaults.js | 36 ++++++++++++ src/traces/carpet/has_columns.js | 14 +++++ src/traces/carpet/index.js | 34 ++++++++++++ src/traces/carpet/plot.js | 18 ++++++ src/traces/carpet/xy_defaults.js | 71 ++++++++++++++++++++++++ test/jasmine/tests/carpet_test.js | 59 ++++++++++++++++++++ 13 files changed, 418 insertions(+) create mode 100644 lib/carpet.js create mode 100644 src/lib/carpet.js create mode 100644 src/traces/carpet/ab_defaults.js create mode 100644 src/traces/carpet/attributes.js create mode 100644 src/traces/carpet/calc.js create mode 100644 src/traces/carpet/constants.js create mode 100644 src/traces/carpet/defaults.js create mode 100644 src/traces/carpet/has_columns.js create mode 100644 src/traces/carpet/index.js create mode 100644 src/traces/carpet/plot.js create mode 100644 src/traces/carpet/xy_defaults.js create mode 100644 test/jasmine/tests/carpet_test.js diff --git a/lib/carpet.js b/lib/carpet.js new file mode 100644 index 00000000000..0d63cd2e79d --- /dev/null +++ b/lib/carpet.js @@ -0,0 +1,11 @@ +/** +* 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 = require('../src/traces/carpet'); diff --git a/lib/index.js b/lib/index.js index 2f4c821fb27..eafe257d193 100644 --- a/lib/index.js +++ b/lib/index.js @@ -35,6 +35,8 @@ Plotly.register([ require('./scattermapbox'), + require('./carpet'), + require('./ohlc'), require('./candlestick') ]); diff --git a/src/lib/carpet.js b/src/lib/carpet.js new file mode 100644 index 00000000000..0d63cd2e79d --- /dev/null +++ b/src/lib/carpet.js @@ -0,0 +1,11 @@ +/** +* 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 = require('../src/traces/carpet'); diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js new file mode 100644 index 00000000000..2d7c633b135 --- /dev/null +++ b/src/traces/carpet/ab_defaults.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 Lib = require('../../lib'); + +// var isNumeric = require('fast-isnumeric'); + +var hasColumns = require('./has_columns'); + + +module.exports = function handleABDefaults(traceIn, traceOut, coerce) { + var a = coerce('a'); + + if (!a) { + coerce('da'); + coerce('a0'); + } + + var b = coerce('b'); + + if (!b) { + coerce('db'); + coerce('b0'); + } + + return; +}; diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js new file mode 100644 index 00000000000..ffd0e9603d5 --- /dev/null +++ b/src/traces/carpet/attributes.js @@ -0,0 +1,91 @@ +/** +* 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 = { + carpetid: { + valType: 'string', + role: 'info', + description: [ + 'An identifier for this carpet, so that `scattercarpet` and', + '`scattercontour` traces can specify a carpet plot on which', + 'they lie' + ].join(' ') + }, + x: { + valType: 'data_array', + description: [ + 'A two dimensional array of x coordinates at each carpet point.', + 'If ommitted, the plot is a cheater plot and the xaxis is hidden', + 'by default.' + ].join(' ') + }, + y: { + valType: 'data_array', + descripotion: 'A two dimensional array of y coordinates at each carpet point.' + }, + a: { + valType: 'data_array', + description: [ + 'An array containing values of the first parameter value' + ].join(' ') + }, + a0: { + valType: 'number', + dflt: 0, + role: 'info', + description: [ + 'Alternate to `a`.', + 'Builds a linear space of a coordinates.', + 'Use with `da`', + 'where `a0` is the starting coordinate and `da` the step.' + ].join(' ') + }, + da: { + valType: 'number', + dflt: 1, + role: 'info', + description: [ + 'Sets the a coordinate step.', + 'See `a0` for more info.' + ].join(' ') + }, + b: { + valType: 'data_array', + descripotion: 'A two dimensional array of y coordinates at each carpet point.' + }, + b0: { + valType: 'number', + dflt: 0, + role: 'info', + description: [ + 'Alternate to `b`.', + 'Builds a linear space of a coordinates.', + 'Use with `db`', + 'where `b0` is the starting coordinate and `db` the step.' + ].join(' ') + }, + db: { + valType: 'number', + dflt: 1, + role: 'info', + description: [ + 'Sets the b coordinate step.', + 'See `b0` for more info.' + ].join(' ') + }, + cheaterslope: { + valType: 'number', + dflt: 1, + description: [ + 'The shift applied to each successive row of data in creating a cheater plot.', + 'Only used if `x` is been ommitted.' + ].join(' ') + }, +}; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js new file mode 100644 index 00000000000..9b264a6bcba --- /dev/null +++ b/src/traces/carpet/calc.js @@ -0,0 +1,23 @@ + +/** +* 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 Registry = require('../../registry'); +var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); + + +module.exports = function calc(gd, trace) { + // prepare the raw data + console.log('calc!:'); +}; diff --git a/src/traces/carpet/constants.js b/src/traces/carpet/constants.js new file mode 100644 index 00000000000..5c701c8f88c --- /dev/null +++ b/src/traces/carpet/constants.js @@ -0,0 +1,13 @@ +/** +* 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 = { +}; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js new file mode 100644 index 00000000000..f5a71ed5776 --- /dev/null +++ b/src/traces/carpet/defaults.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'); + +var attributes = require('./attributes'); +var constants = require('./constants'); +var handleXYDefaults = require('./xy_defaults'); +var handleABDefaults = require('./ab_defaults'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + coerce('carpetid'); + + var len = handleXYDefaults(traceIn, traceOut, coerce); + + if (!len) { + traceOut.visible = false; + return; + } + + handleABDefaults(traceIn, traceOut, coerce); + + coerce('cheaterslope'); +}; diff --git a/src/traces/carpet/has_columns.js b/src/traces/carpet/has_columns.js new file mode 100644 index 00000000000..a472f0f15f0 --- /dev/null +++ b/src/traces/carpet/has_columns.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 = function(data) { + return Array.isArray(data[0]); +}; diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js new file mode 100644 index 00000000000..3d6713e65c7 --- /dev/null +++ b/src/traces/carpet/index.js @@ -0,0 +1,34 @@ +/** +* 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 Carpet = {}; + +Carpet.attributes = require('./attributes'); +Carpet.supplyDefaults = require('./defaults'); +Carpet.plot = require('./plot'); +Carpet.calc = require('./calc'); +Carpet.animatable = false; + +Carpet.moduleType = 'trace'; +Carpet.name = 'carpet'; +Carpet.basePlotModule = require('../../plots/cartesian'); +Carpet.categories = ['cartesian', 'symbols', 'markerColorscale']; +Carpet.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`.', + 'Text (appearing either on the chart or on hover only) is via `text`.', + 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', + 'to numerical arrays.' + ].join(' ') +}; + +module.exports = Carpet; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js new file mode 100644 index 00000000000..28e702751e8 --- /dev/null +++ b/src/traces/carpet/plot.js @@ -0,0 +1,18 @@ +/** +* 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'); + +module.exports = function plot(gd, plotinfo, cdscatter) { +}; diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js new file mode 100644 index 00000000000..f428efc2ca9 --- /dev/null +++ b/src/traces/carpet/xy_defaults.js @@ -0,0 +1,71 @@ +/** +* 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 isNumeric = require('fast-isnumeric'); + +var hasColumns = require('./has_columns'); + + +module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { + var x = coerce('x'); + + if (x && !hasColumns(x)) { + // x absent is valid, but x present is only valid + // if x has columns + return 0; + } + + traceOut._cheater = !x; + + var y = coerce('y'); + + // y must be both present *and* must have columns + if (!y || !hasColumns(y)) { + return 0; + } + + return y.length; +}; + +/* +function isValidZ(z) { + var allRowsAreArrays = true, + oneRowIsFilled = false, + hasOneNumber = false, + zi; + + + // Without this step: + // + // hasOneNumber = false breaks contour but not heatmap + // allRowsAreArrays = false breaks contour but not heatmap + // oneRowIsFilled = false breaks both + + for(var i = 0; i < z.length; i++) { + zi = z[i]; + if(!Array.isArray(zi)) { + allRowsAreArrays = false; + break; + } + if(zi.length > 0) oneRowIsFilled = true; + for(var j = 0; j < zi.length; j++) { + if(isNumeric(zi[j])) { + hasOneNumber = true; + break; + } + } + } + + return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); +} +*/ diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js new file mode 100644 index 00000000000..56fce15a9ba --- /dev/null +++ b/test/jasmine/tests/carpet_test.js @@ -0,0 +1,59 @@ +var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); + +var Carpet = require('@src/traces/carpet'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var customMatchers = require('../assets/custom_matchers'); + + +describe('carpet supplyDefaults', function() { + 'use strict'; + + var traceIn, + traceOut; + + var supplyDefaults = Carpet.supplyDefaults; + + var defaultColor = '#444', + layout = { + font: Plots.layoutAttributes.font + }; + + beforeEach(function() { + traceOut = {}; + }); + + it('sets visible = false when x is not valid', function() { + traceIn = {y: [[1, 2], [3, 4]], x: [4]}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('sets visible = false when y is not valid', function() { + traceIn = {y: [1, 2]}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('sets _cheater = true when x is provided', function() { + traceIn = {y: [[1, 2], [3, 4]]}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut._cheater).toBe(true); + }); + + it('sets cheater = false when x is not valid', function() { + traceIn = {y: [[1, 2], [3, 4]], x: [[3, 4], [1, 2]]}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut._cheater).toBe(false); + }); + + it('defaults to cheaterslope = 1', function () { + traceIn = {y: [[1, 2], [3, 4]]}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.cheaterslope).toEqual(1); + }); +}); From 7e6858af3dce39d12b5f7e785597e267558cbaad Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 6 Dec 2016 10:12:53 -0500 Subject: [PATCH 002/132] Start building cheater supplydfeaults --- src/plots/plots.js | 1 + src/plots/subplot_defaults.js | 2 ++ src/traces/carpet/calc.js | 11 +++++++++-- src/traces/carpet/construct_cheater.js | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/traces/carpet/construct_cheater.js diff --git a/src/plots/plots.js b/src/plots/plots.js index 3f9e06fb1ae..3e54d61c1e3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -664,6 +664,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var trace = dataIn[i], fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); + console.log('fullLayout:', JSON.stringify(fullLayout)); fullTrace.index = i; fullTrace._input = trace; fullTrace._expandedIndex = cnt; diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index 15e42217b87..2b960b76bf3 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -49,6 +49,8 @@ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, o var ids = Plots.findSubplotIds(fullData, subplotType), idsLength = ids.length; + console.log('ids:', ids); + var subplotLayoutIn, subplotLayoutOut; function coerce(attr, dflt) { diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 9b264a6bcba..eac693a60e4 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -15,9 +15,16 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); +var maxRowLength = module.exports = function calc(gd, trace) { - // prepare the raw data - console.log('calc!:'); + var xa = Axes.getFromId(gd, trace.xaxis || 'x'), + ya = Axes.getFromId(gd, trace.yaxis || 'y'); + + + var cd0 = {}; + + + return [cd0]; }; diff --git a/src/traces/carpet/construct_cheater.js b/src/traces/carpet/construct_cheater.js new file mode 100644 index 00000000000..83dde089ac9 --- /dev/null +++ b/src/traces/carpet/construct_cheater.js @@ -0,0 +1,16 @@ +/** +* 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'; + +/* this function constructs an array of arrays with offset per row in the + * style of a cheater plot. + */ +module.exports = function constructCheater () { +}; From 752d2682aa600193fe86870ec3a6b0a6307addd8 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 7 Dec 2016 10:25:08 -0500 Subject: [PATCH 003/132] First signs of life with carpet --- src/plots/plots.js | 1 - src/plots/subplot_defaults.js | 2 - src/traces/carpet/array_minmax.js | 43 +++++++++++++++++ src/traces/carpet/calc.js | 17 +++++-- src/traces/carpet/cheater_basis.js | 23 ++++++++++ src/traces/carpet/plot.js | 74 +++++++++++++++++++++++++++++- 6 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/traces/carpet/array_minmax.js create mode 100644 src/traces/carpet/cheater_basis.js diff --git a/src/plots/plots.js b/src/plots/plots.js index 3e54d61c1e3..3f9e06fb1ae 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -664,7 +664,6 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var trace = dataIn[i], fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); - console.log('fullLayout:', JSON.stringify(fullLayout)); fullTrace.index = i; fullTrace._input = trace; fullTrace._expandedIndex = cnt; diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index 2b960b76bf3..15e42217b87 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -49,8 +49,6 @@ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, o var ids = Plots.findSubplotIds(fullData, subplotType), idsLength = ids.length; - console.log('ids:', ids); - var subplotLayoutIn, subplotLayoutOut; function coerce(attr, dflt) { diff --git a/src/traces/carpet/array_minmax.js b/src/traces/carpet/array_minmax.js new file mode 100644 index 00000000000..f894ab35e52 --- /dev/null +++ b/src/traces/carpet/array_minmax.js @@ -0,0 +1,43 @@ +/** +* 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 (a) { + return minMax(a, 0); +} + +function minMax (a, depth) { + // Limit to ten dimensional datasets. This seems *exceedingly* unlikely to + // ever cause problems or even be a concern. It's include strictly so that + // circular arrays could never cause this to loop. + if (!Array.isArray(a) || depth >= 10) { + return null; + } + + var min = Infinity; + var max = -Infinity; + var n = a.length; + for (var i = 0; i < n; i++) { + var datum = a[i]; + + if (Array.isArray(datum)) { + var result = minMax(datum, depth + 1); + + if (result) { + min = Math.min(result[0], min); + max = Math.max(result[1], max); + } + } else { + min = Math.min(datum, min); + max = Math.max(datum, max); + } + } + + return [min, max]; +}; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index eac693a60e4..4366d8a98a8 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -1,4 +1,3 @@ - /** * Copyright 2012-2016, Plotly, Inc. * All rights reserved. @@ -7,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var isNumeric = require('fast-isnumeric'); @@ -15,16 +13,27 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var maxRowLength = +var cheaterBasis = require('./cheater_basis'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), ya = Axes.getFromId(gd, trace.yaxis || 'y'); + var xdata; - var cd0 = {}; + if(trace._cheater) { + xdata = cheaterBasis(trace.a.length, trace.b.length, trace.cheaterslope); + } else { + xdata = trace.x; + } + var cd0 = { + x: xdata, + y: trace.y, + a: trace.a, + b: trace.b + }; return [cd0]; }; diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js new file mode 100644 index 00000000000..062cb8ad54e --- /dev/null +++ b/src/traces/carpet/cheater_basis.js @@ -0,0 +1,23 @@ +/** +* 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 (na, nb, cheaterslope) { + var i, j; + var data = []; + + for (i = 0; i < na; i++) { + data[i] = []; + for (j = 0; j < nb; j++) { + data[i][j] = i - j * cheaterslope; + } + } + + return data; +} diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 28e702751e8..78f2a5ba21f 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -14,5 +14,77 @@ var d3 = require('d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); -module.exports = function plot(gd, plotinfo, cdscatter) { +module.exports = function plot(gd, plotinfo, cdcarpet) { + for(var i = 0; i < cdcarpet.length; i++) { + plotOne(gd, plotinfo, cdcarpet[i]); + } }; + +function plotOne(gd, plotinfo, cd) { + var trace = cd[0].trace, + uid = trace.uid, + xa = plotinfo.xaxis, + ya = plotinfo.yaxis, + fullLayout = gd._fullLayout, + id = 'carpet' + uid; + + var x = cd[0].x; + var y = cd[0].y; + var a = cd[0].a; + var b = cd[0].b; + var xp = xa.c2p; + var yp = ya.c2p; + + window.x = x; + window.y = y; + window.xa = xa; + window.ya = ya; + + // XXX: Layer choice?? + var gridLayer = plotinfo.plot.selectAll('.maplayer'); + + var linesets = [{ + class: 'const-a-line', + data: b, + xc: function (i, j) { return xp(x[i][j]); }, + yc: function (i, j) { return yp(y[i][j]); }, + n: a.length + }, { + class: 'const-b-line', + data: a, + xc: function (i, j) { return xp(x[j][i]); }, + yc: function (i, j) { return yp(y[j][i]); }, + n: b.length + }]; + + for (var i = 0; i < linesets.length; i++) { + var lineset = linesets[i]; + drawGridLines(gridLayer, lineset); + } +} + +function drawAxisLabels () { +} + +function drawGridLines (layer, ls) { + var gridjoin = layer.selectAll('.' + ls.class).data(ls.data); + + gridjoin.enter().append('path') + .classed(ls.class, true) + .style('vector-effect', 'non-scaling-stroke'); + + gridjoin.each(function (d, i) { + var el = d3.select(this); + el.attr('d', function () { + var pts = []; + for(var k = 0; k < ls.n; k++) { + pts.push(ls.xc(i, k) + ',' + ls.yc(i, k)); + } + return 'M' + pts.join('L'); + }) + el.style('stroke-width', 1) + .style('stroke', 'gray') + .style('fill', 'none'); + }) + .exit().remove(); +} From 5c302aa4eb7d54f7f89e591f03281e506d33bf84 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 8 Dec 2016 08:15:08 -0500 Subject: [PATCH 004/132] First cut at base carpet axis trace type --- src/traces/carpet/ab_defaults.js | 7 +- src/traces/carpet/array_minmax.js | 16 +-- src/traces/carpet/attributes.js | 5 + src/traces/carpet/axis_attributes.js | 127 +++++++++++++++++ src/traces/carpet/calc.js | 24 +++- src/traces/carpet/cheater_basis.js | 8 +- src/traces/carpet/compute_bounds.js | 13 ++ src/traces/carpet/construct_cheater.js | 2 +- src/traces/carpet/defaults.js | 85 ++++++++++- src/traces/carpet/plot.js | 189 +++++++++++++++++++------ src/traces/carpet/xy_defaults.js | 4 +- test/jasmine/tests/carpet_test.js | 14 +- 12 files changed, 416 insertions(+), 78 deletions(-) create mode 100644 src/traces/carpet/axis_attributes.js create mode 100644 src/traces/carpet/compute_bounds.js diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index 2d7c633b135..8806402f5a2 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -13,20 +13,17 @@ // var isNumeric = require('fast-isnumeric'); -var hasColumns = require('./has_columns'); - - module.exports = function handleABDefaults(traceIn, traceOut, coerce) { var a = coerce('a'); - if (!a) { + if(!a) { coerce('da'); coerce('a0'); } var b = coerce('b'); - if (!b) { + if(!b) { coerce('db'); coerce('b0'); } diff --git a/src/traces/carpet/array_minmax.js b/src/traces/carpet/array_minmax.js index f894ab35e52..b3e00499075 100644 --- a/src/traces/carpet/array_minmax.js +++ b/src/traces/carpet/array_minmax.js @@ -8,28 +8,28 @@ 'use strict'; -module.exports = function (a) { +module.exports = function(a) { return minMax(a, 0); -} +}; -function minMax (a, depth) { +function minMax(a, depth) { // Limit to ten dimensional datasets. This seems *exceedingly* unlikely to // ever cause problems or even be a concern. It's include strictly so that // circular arrays could never cause this to loop. - if (!Array.isArray(a) || depth >= 10) { + if(!Array.isArray(a) || depth >= 10) { return null; } var min = Infinity; var max = -Infinity; var n = a.length; - for (var i = 0; i < n; i++) { + for(var i = 0; i < n; i++) { var datum = a[i]; - if (Array.isArray(datum)) { + if(Array.isArray(datum)) { var result = minMax(datum, depth + 1); - if (result) { + if(result) { min = Math.min(result[0], min); max = Math.max(result[1], max); } @@ -40,4 +40,4 @@ function minMax (a, depth) { } return [min, max]; -}; +} diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index ffd0e9603d5..ee955fcc0db 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -8,6 +8,9 @@ 'use strict'; +var extendFlat = require('../../lib/extend').extendFlat; +var axisAttrs = require('./axis_attributes'); + module.exports = { carpetid: { valType: 'string', @@ -88,4 +91,6 @@ module.exports = { 'Only used if `x` is been ommitted.' ].join(' ') }, + aaxis: extendFlat({}, axisAttrs), + baxis: extendFlat({}, axisAttrs), }; diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js new file mode 100644 index 00000000000..6dba8debabe --- /dev/null +++ b/src/traces/carpet/axis_attributes.js @@ -0,0 +1,127 @@ +/** +* 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 extendFlat = require('../../lib/extend').extendFlat; +var fontAttrs = require('../../plots/font_attributes'); +var colorAttrs = require('../../components/color/attributes'); + +module.exports = { + showlabels: { + valType: 'enumerated', + values: ['start', 'end', 'both', 'none'], + dflt: 'end', + role: 'style', + description: [ + 'Determines whether axis labels are drawn on the low side,', + 'the high side, both, or neither side of the axis.' + ] + }, + labelpadding: { + valType: 'integer', + role: 'style', + dflt: 10, + description: 'Extra padding between label and the axis' + }, + labelprefix: { + valType: 'string', + role: 'style', + description: 'Sets a axis label prefix.' + }, + labelsuffix: { + valType: 'string', + dflt: '', + role: 'style', + description: 'Sets a axis label suffix.' + }, + showstartlabel: { + valType: 'boolean', + dflt: true, + }, + showendlabel: { + valType: 'boolean', + dflt: true, + }, + showlabelprefix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: [ + 'If *all*, all tick labels are displayed with a prefix.', + 'If *first*, only the first tick is displayed with a prefix.', + 'If *last*, only the last tick is displayed with a suffix.', + 'If *none*, tick prefixes are hidden.' + ].join(' ') + }, + showlabelsuffix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: 'Same as `showtickprefix` but for tick suffixes.' + }, + labelfont: extendFlat({}, fontAttrs, { + description: 'Sets the label font.' + }), + gridoffset: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info', + description: 'The starting index of grid lines along the axis' + }, + gridstep: { + valType: 'integer', + min: 1, + dflt: 1, + role: 'info', + description: 'The stride between grid lines along the axis' + }, + gridwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + gridcolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, + minorgridoffset: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info', + description: 'The starting index of grid lines along the axis' + }, + minorgridstep: { + valType: 'integer', + min: 1, + dflt: 1, + role: 'info', + description: 'The stride between grid lines along the axis' + }, + minorgridwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + minorgridcolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, +}; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 4366d8a98a8..75b8c25e719 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -8,12 +8,9 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - -var Registry = require('../../registry'); -var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var cheaterBasis = require('./cheater_basis'); +var arrayMinmax = require('./array_minmax'); module.exports = function calc(gd, trace) { @@ -21,6 +18,7 @@ module.exports = function calc(gd, trace) { ya = Axes.getFromId(gd, trace.yaxis || 'y'); var xdata; + var ydata = trace.y; if(trace._cheater) { xdata = cheaterBasis(trace.a.length, trace.b.length, trace.cheaterslope); @@ -28,6 +26,24 @@ module.exports = function calc(gd, trace) { xdata = trace.x; } + // This is a rather expensive scan. Nothing guarantees monotonicity, + // so we need to scan through all data to get proper ranges: + var xrange = arrayMinmax(xdata); + var yrange = arrayMinmax(ydata); + + var dx = 0.5 * (xrange[1] - xrange[0]); + var xc = 0.5 * (xrange[1] + xrange[0]); + + var dy = 0.5 * (yrange[1] - yrange[0]); + var yc = 0.5 * (yrange[1] + yrange[0]); + + var grow = 1.3; + xrange = [xc - dx * grow, xc + dx * grow]; + yrange = [yc - dy * grow, yc + dy * grow]; + + Axes.expand(xa, xrange, {padded: true}); + Axes.expand(ya, yrange, {padded: true}); + var cd0 = { x: xdata, y: trace.y, diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 062cb8ad54e..17bcde7569d 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -8,16 +8,16 @@ 'use strict'; -module.exports = function (na, nb, cheaterslope) { +module.exports = function(na, nb, cheaterslope) { var i, j; var data = []; - for (i = 0; i < na; i++) { + for(i = 0; i < na; i++) { data[i] = []; - for (j = 0; j < nb; j++) { + for(j = 0; j < nb; j++) { data[i][j] = i - j * cheaterslope; } } return data; -} +}; diff --git a/src/traces/carpet/compute_bounds.js b/src/traces/carpet/compute_bounds.js new file mode 100644 index 00000000000..8491e24e01a --- /dev/null +++ b/src/traces/carpet/compute_bounds.js @@ -0,0 +1,13 @@ +/** +* 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 computeBounds() { + +}; diff --git a/src/traces/carpet/construct_cheater.js b/src/traces/carpet/construct_cheater.js index 83dde089ac9..f96629093c6 100644 --- a/src/traces/carpet/construct_cheater.js +++ b/src/traces/carpet/construct_cheater.js @@ -12,5 +12,5 @@ /* this function constructs an array of arrays with offset per row in the * style of a cheater plot. */ -module.exports = function constructCheater () { +module.exports = function constructCheater() { }; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index f5a71ed5776..47ae5037bb9 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -12,11 +12,10 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); -var constants = require('./constants'); var handleXYDefaults = require('./xy_defaults'); var handleABDefaults = require('./ab_defaults'); -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { +module.exports = function supplyDefaults(traceIn, traceOut) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } @@ -25,7 +24,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var len = handleXYDefaults(traceIn, traceOut, coerce); - if (!len) { + if(!len) { traceOut.visible = false; return; } @@ -33,4 +32,84 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleABDefaults(traceIn, traceOut, coerce); coerce('cheaterslope'); + + handleAxisDefaults(traceIn, traceOut, 'a'); + handleAxisDefaults(traceIn, traceOut, 'b'); }; + +function handleAxisDefaults(traceIn, traceOut, axis) { + var ax = traceOut[axis + 'axis'] = traceOut[axis + 'axis'] || {}; + var i; + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); + } + + coerce('showlabels'); + coerce('labelprefix', axis + ' = '); + coerce('labelsuffix'); + coerce('showlabelprefix'); + coerce('showlabelsuffix'); + coerce('gridwidth'); + coerce('gridcolor'); + coerce('gridoffset'); + coerce('gridstep'); + + coerce('minorgridwidth'); + coerce('minorgridcolor'); + coerce('minorgridoffset'); + coerce('minorgridstep'); + + coerce('showstartlabel'); + coerce('showendlabel'); + + coerce('labelpadding'); + + Lib.coerceFont(coerce, 'labelfont', {size: 12}); + + // Compute which labels to show. In a sense this is sort of a data computation + // that should go in calc.js, but it's so minimal for any conceivable case that + // I'll write it here for now: + + ax._gridIndices = []; + for(i = ax.gridoffset; i < traceIn[axis].length; i += ax.gridstep) { + ax._gridIndices.push(i); + } + + // Ensure the first grid line shows up: + if(ax._gridIndices[0] !== 0) { + ax._gridIndices.unshift(0); + } + + // Ensure the final grid line shows up: + if(ax._gridIndices[ax._gridIndices.length - 1] !== traceIn[axis].length - 1) { + ax._gridIndices.push(traceIn[axis].length - 1); + } + + // Labels don't require first and last, so just use user-provided offset + step: + ax._majorIndices = []; + for(i = ax.gridoffset; i < traceIn[axis].length; i += ax.gridstep) { + ax._majorIndices.push(i); + } + + if(ax.showstartlabel) { + // Ensure the first grid line shows up: + if(ax._majorIndices[0] !== 0) { + ax._majorIndices.unshift(0); + } + } + + if(ax.showendlabel) { + // Ensure the final grid line shows up: + if(ax._majorIndices[ax._majorIndices.length - 1] !== traceIn[axis].length - 1) { + ax._majorIndices.push(traceIn[axis].length - 1); + } + } + + ax._minorGridIndices = []; + for(i = ax.minorgridoffset; i < traceIn[axis].length; i += ax.minorgridstep) { + if(ax._gridIndices.indexOf(i) === -1) { + ax._minorGridIndices.push(i); + } + } +} diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 78f2a5ba21f..6efd9b52eea 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -11,7 +11,6 @@ var d3 = require('d3'); -var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); module.exports = function plot(gd, plotinfo, cdcarpet) { @@ -20,13 +19,18 @@ module.exports = function plot(gd, plotinfo, cdcarpet) { } }; +function makeg(el, type, klass) { + var join = el.selectAll(type + '.' + klass).data([0]); + join.enter().append(type).classed(klass, true); + return join; +} + function plotOne(gd, plotinfo, cd) { var trace = cd[0].trace, - uid = trace.uid, + // uid = trace.uid, xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - fullLayout = gd._fullLayout, - id = 'carpet' + uid; + ya = plotinfo.yaxis; + // id = 'carpet' + uid; var x = cd[0].x; var y = cd[0].y; @@ -35,56 +39,153 @@ function plotOne(gd, plotinfo, cd) { var xp = xa.c2p; var yp = ya.c2p; - window.x = x; - window.y = y; - window.xa = xa; - window.ya = ya; - // XXX: Layer choice?? var gridLayer = plotinfo.plot.selectAll('.maplayer'); - var linesets = [{ - class: 'const-a-line', - data: b, - xc: function (i, j) { return xp(x[i][j]); }, - yc: function (i, j) { return yp(y[i][j]); }, - n: a.length - }, { - class: 'const-b-line', - data: a, - xc: function (i, j) { return xp(x[j][i]); }, - yc: function (i, j) { return yp(y[j][i]); }, - n: b.length - }]; - - for (var i = 0; i < linesets.length; i++) { + var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); + var majorLayer = makeg(gridLayer, 'g', 'majorlayer'); + var labelLayer = makeg(gridLayer, 'g', 'labellayer'); + + var linesets = [ + { + baseClass: 'const-a-line', + data: a, + ax: trace.aaxis, + xc: function(i, j) { return xp(x[i][j]); }, + yc: function(i, j) { return yp(y[i][j]); }, + n: b.length + }, + { + baseClass: 'const-b-line', + data: b, + ax: trace.baxis, + xc: function(i, j) { return xp(x[j][i]); }, + yc: function(i, j) { return yp(y[j][i]); }, + n: a.length + } + ]; + + for(var i = 0; i < linesets.length; i++) { var lineset = linesets[i]; - drawGridLines(gridLayer, lineset); + drawGridLines(minorLayer, lineset, false); + drawGridLines(majorLayer, lineset, true); + + drawAxisLabels(labelLayer, lineset, 'lo'); + drawAxisLabels(labelLayer, lineset, 'hi'); } } -function drawAxisLabels () { +function drawAxisLabels(layer, ls, side) { + // We'll differentiate the screen-space coordinates to get the orientation of labels: + var ax = ls.ax; + + // Check whether labels are active for this set: + var active = [side === 'lo' ? 'start' : 'end', 'both'].indexOf(ax.showlabels) !== -1; + + // *Always* do the join. Otherwise labels can't disappear without a total replot + var joinData = active ? ax._majorIndices : []; + + var labelClass = ls.baseClass + '-axis-labels-' + side; + var labelJoin = layer.selectAll('text.' + labelClass).data(joinData); + + labelJoin.enter().append('text') + .classed(labelClass, true); + + var idx0 = side === 'lo' ? 0 : ls.n - 1; + var idx1 = side === 'lo' ? 1 : ls.n - 2; + + labelJoin.each(function(d, i) { + var lineIdx = ax._majorIndices[i]; + + var el = d3.select(this); + + var prefix; + switch(ax.showlabelprefix) { + case 'first': + prefix = i === 0 ? ax.labelprefix : ''; + break; + case 'all': + prefix = ax.labelprefix; + break; + case 'last': + prefix = i === ax._majorIndices.length - 1 ? ax.labelprefix : ''; + break; + } + + var suffix; + switch(ax.showlabelsuffix) { + case 'first': + suffix = i === 0 ? ax.labelsuffix : ''; + break; + case 'all': + suffix = ax.labelsuffix; + break; + case 'last': + suffix = i === ax._majorIndices.length - 1 ? ax.labelsuffix : ''; + break; + } + + var x0 = ls.xc(lineIdx, idx0); + var x1 = ls.xc(lineIdx, idx1); + var y0 = ls.yc(lineIdx, idx0); + var y1 = ls.yc(lineIdx, idx1); + + var dx = x1 - x0; + var dy = y1 - y0; + var l = Math.sqrt(dx * dx + dy * dy); + dx /= l; + dy /= l; + + var angle = Math.atan2(dy, dx) * 180 / Math.PI; + var endAnchor = true; + if(angle < -90) { + angle += 180; + endAnchor = !endAnchor; + } else if(angle > 90) { + angle -= 180; + endAnchor = !endAnchor; + } + + // XXX: Use existing text functions + el.attr('x', x0 + ax.labelpadding * (endAnchor ? -1 : 1)) // These are pre-transform offsets + .attr('y', y0 + 5) // Shift down to hackily vertically center + .attr('text-anchor', endAnchor ? 'end' : 'start') + .text(prefix + ls.data[lineIdx].toFixed(3) + suffix) + .attr('transform', 'rotate(' + angle + ' ' + x0 + ',' + y0 + ')') + .call(Drawing.font, ax.labelfont.family, ax.labelfont.size, ax.labelfont.color); + }); + + labelJoin.exit().remove(); } -function drawGridLines (layer, ls) { - var gridjoin = layer.selectAll('.' + ls.class).data(ls.data); +function drawGridLines(layer, ls, isMajor) { + var ax = ls.ax; + var data = isMajor ? ax._gridIndices : ax._minorGridIndices; + + var lineClass = 'const-' + ls.baseClass + '-lines' + (isMajor ? '' : '-minor'); + var gridjoin = layer.selectAll('.' + lineClass).data(data); gridjoin.enter().append('path') - .classed(ls.class, true) + .classed(lineClass, true) .style('vector-effect', 'non-scaling-stroke'); - gridjoin.each(function (d, i) { - var el = d3.select(this); - el.attr('d', function () { - var pts = []; - for(var k = 0; k < ls.n; k++) { - pts.push(ls.xc(i, k) + ',' + ls.yc(i, k)); - } - return 'M' + pts.join('L'); - }) - el.style('stroke-width', 1) - .style('stroke', 'gray') - .style('fill', 'none'); - }) - .exit().remove(); + var width = isMajor ? ax.gridwidth : ax.minorgridwidth; + var color = isMajor ? ax.gridcolor : ax.minorgridcolor; + + gridjoin.each(function(d, i) { + var gridIdx = data[i]; + + var el = d3.select(this); + el.attr('d', function() { + var pts = []; + for(var k = 0; k < ls.n; k++) { + pts.push(ls.xc(gridIdx, k) + ',' + ls.yc(gridIdx, k)); + } + return 'M' + pts.join('L'); + }); + el.style('stroke-width', width) + .style('stroke', color) + .style('fill', 'none'); + }) + .exit().remove(); } diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index f428efc2ca9..cfccf2ec1d5 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -19,7 +19,7 @@ var hasColumns = require('./has_columns'); module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var x = coerce('x'); - if (x && !hasColumns(x)) { + if(x && !hasColumns(x)) { // x absent is valid, but x present is only valid // if x has columns return 0; @@ -30,7 +30,7 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var y = coerce('y'); // y must be both present *and* must have columns - if (!y || !hasColumns(y)) { + if(!y || !hasColumns(y)) { return 0; } diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index 56fce15a9ba..5d918ada97d 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -1,13 +1,13 @@ -var Plotly = require('@lib/index'); +// var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); +// var Lib = require('@src/lib'); var Carpet = require('@src/traces/carpet'); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); +// var d3 = require('d3'); +// var createGraphDiv = require('../assets/create_graph_div'); +// var destroyGraphDiv = require('../assets/destroy_graph_div'); +// var customMatchers = require('../assets/custom_matchers'); describe('carpet supplyDefaults', function() { @@ -51,7 +51,7 @@ describe('carpet supplyDefaults', function() { expect(traceOut._cheater).toBe(false); }); - it('defaults to cheaterslope = 1', function () { + it('defaults to cheaterslope = 1', function() { traceIn = {y: [[1, 2], [3, 4]]}; supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.cheaterslope).toEqual(1); From 256ff18a8cc30de4eeb301d419bee7fd5e549bcb Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Sat, 10 Dec 2016 15:32:03 -0500 Subject: [PATCH 005/132] Work out a lot of details of spline evaluation --- lib/index.js | 1 + src/components/drawing/index.js | 2 + src/traces/carpet/axis_attributes.js | 75 +++- src/traces/carpet/calc.js | 185 +++++++++- src/traces/carpet/catmull_rom.js | 43 +++ src/traces/carpet/cheater_basis.js | 29 +- src/traces/carpet/compute_control_points.js | 352 +++++++++++++++++++ src/traces/carpet/create_spline_evaluator.js | 15 + src/traces/carpet/defaults.js | 32 +- src/traces/carpet/ensure_array.js | 29 ++ src/traces/carpet/evaluate_cubic.js | 11 + src/traces/carpet/index.js | 2 +- src/traces/carpet/map_2d_array.js | 43 +++ src/traces/carpet/plot.js | 71 +++- 14 files changed, 845 insertions(+), 45 deletions(-) create mode 100644 src/traces/carpet/catmull_rom.js create mode 100644 src/traces/carpet/compute_control_points.js create mode 100644 src/traces/carpet/create_spline_evaluator.js create mode 100644 src/traces/carpet/ensure_array.js create mode 100644 src/traces/carpet/evaluate_cubic.js create mode 100644 src/traces/carpet/map_2d_array.js diff --git a/lib/index.js b/lib/index.js index eafe257d193..586c60c1709 100644 --- a/lib/index.js +++ b/lib/index.js @@ -36,6 +36,7 @@ Plotly.register([ require('./scattermapbox'), require('./carpet'), + require('./scattercarpet'), require('./ohlc'), require('./candlestick') diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 9aa4af4bc36..c4b0e9dd9f1 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -24,6 +24,8 @@ var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); var drawing = module.exports = {}; +drawing.splineEvaluator = require('./spline_evaluator'); + // ----------------------------------------------------- // styling functions for plot elements // ----------------------------------------------------- diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 6dba8debabe..4e555cdd1a2 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -13,6 +13,23 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); module.exports = { + smoothing: { + valType: 'number', + dflt: 1.0, + role: 'info' + }, + cheatertype: { + valType: 'enumerated', + values: ['index', 'value'], + dflt: 'index', + role: 'info' + }, + tickmode: { + valType: 'enumerated', + values: ['linear', 'array'], + dflt: 'array', + role: 'info', + }, showlabels: { valType: 'enumerated', values: ['start', 'end', 'both', 'none'], @@ -70,14 +87,28 @@ module.exports = { labelfont: extendFlat({}, fontAttrs, { description: 'Sets the label font.' }), - gridoffset: { + tick0: { + valType: 'any', + min: 0, + dflt: 0, + role: 'info', + description: 'The starting index of grid lines along the axis' + }, + dtick: { + valType: 'any', + min: 1, + dflt: 1, + role: 'info', + description: 'The stride between grid lines along the axis' + }, + arraytick0: { valType: 'integer', min: 0, dflt: 0, role: 'info', description: 'The starting index of grid lines along the axis' }, - gridstep: { + arraydtick: { valType: 'integer', min: 1, dflt: 1, @@ -97,19 +128,41 @@ module.exports = { role: 'style', description: 'Sets the color of the grid lines.' }, - minorgridoffset: { - valType: 'integer', + startlinewidth: { + valType: 'number', min: 0, - dflt: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + startline: { + valType: 'boolean', role: 'info', - description: 'The starting index of grid lines along the axis' + dflt: true }, - minorgridstep: { - valType: 'integer', - min: 1, - dflt: 1, + endline: { + valType: 'boolean', role: 'info', - description: 'The stride between grid lines along the axis' + dflt: true + }, + startlinecolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, + endlinewidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + endlinecolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' }, minorgridwidth: { valType: 'number', diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 75b8c25e719..f92f5605e93 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -11,17 +11,26 @@ var Axes = require('../../plots/cartesian/axes'); var cheaterBasis = require('./cheater_basis'); var arrayMinmax = require('./array_minmax'); - +var search = require('../../lib/search').findBin; +var computeControlPoints = require('./compute_control_points'); +var map2dArray = require('./map_2d_array'); +var evaluateControlPoints = require('./evaluate_control_points'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'); + ya = Axes.getFromId(gd, trace.yaxis || 'y'), + aax = trace.aaxis, + bax = trace.baxis, + a = trace.a, + b = trace.b; var xdata; var ydata = trace.y; if(trace._cheater) { - xdata = cheaterBasis(trace.a.length, trace.b.length, trace.cheaterslope); + var avals = aax.cheatertype === 'index' ? a.length : a + var bvals = bax.cheatertype === 'index' ? b.length : b; + xdata = cheaterBasis(avals, bvals, trace.cheaterslope); } else { xdata = trace.x; } @@ -44,12 +53,174 @@ module.exports = function calc(gd, trace) { Axes.expand(xa, xrange, {padded: true}); Axes.expand(ya, yrange, {padded: true}); + // Precompute the cartesian-space coordinates of the grid lines: + var x = xdata; + var y = trace.y; + + // Convert cartesian-space x/y coordinates to screen space pixel coordinates: + trace.xp = map2dArray(trace.xp, x, xa.c2p); + trace.yp = map2dArray(trace.yp, y, ya.c2p); + + // Next compute the control points in whichever directions are necessary: + var result = computeControlPoints(trace.xctrl, trace.yctrl, x, y, aax.smoothing, bax.smoothing); + trace.xctrl = result[0]; + trace.yctrl = result[1]; + + + console.log('evaluateControlPoints(:', evaluateControlPoints([trace.xctrl, trace.yctrl], 0.5, 0.5)); + + + //trace.ab2c = getConvert(x, y, a, b, aax.smoothing, bax.smoothing); + + // This is just a transposed function that will be useful in making + // the computations a/b-agnostic: + //trace.ba2c = function (b, a) { return trace.ab2c(a, b); } + + //var agrid = makeGridLines(a, aax); + //var bgrid = makeGridLines(b, bax); + + //trace.aaxis._tickinfo = agrid; + //trace.baxis._tickinfo = bgrid; + var cd0 = { - x: xdata, - y: trace.y, - a: trace.a, - b: trace.b + x: x, + y: y, + a: a, + b: b }; return [cd0]; }; + + + +/* + * Interpolate in the b-direction along constant-a lines + * x, y: 2D data arrays to be interpolated + * aidx: index of the a gridline along which to evaluate + * bidx0: lower-index of the b cell in which to interpolate + * tb: an interpolant in [0, 1] + */ +/*function interpolateConstA (x, y, aidx, bidx0, tb, bsmoothing) { + if (bsmoothing) { + return [ + x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb, + y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb + ]; + } else { + return [ + x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb, + y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb + ]; + } +}*/ + +/* + * Interpolate in the a and b directions + * x, y: 2D data arrays to be interpolated + * aidx0: lower index of the a cell in which to interpolatel + * bidx0: lower index of the b cell in which to interpolate + * ta: an interpolant for the a direction in [0, 1] + * tb: an interpolant for the b direction in [0, 1] + */ +/*function interpolateAB (x, y, aidx0, ta, bidx0, tb, asmoothing, bsmoothing) { + if (asmoothing) { + var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing); + var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing); + + return [ + xy0[0] * (1 - ta) + xy1[0] * ta, + xy0[1] * (1 - ta) + xy1[1] * ta + ]; + } else { + var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing); + var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing); + + return [ + xy0[0] * (1 - ta) + xy1[0] * ta, + xy0[1] * (1 - ta) + xy1[1] * ta + ]; + } +}*/ + +/* + * Return a function that converts a/b coordinates to x/y values + */ +/*function getConvert (x, y, a, b, asmoothing, bsmoothing) { + return function (aval, bval) { + // Get the lower-indices of the box in which the point falls: + var aidx = Math.min(search(aval, a), a.length - 2); + var bidx = Math.min(search(bval, b), b.length - 2); + + var ta = (aval - a[aidx]) / (a[aidx + 1] - a[aidx]); + var tb = (bval - b[bidx]) / (b[bidx + 1] - b[bidx]); + + return interpolateAB(x, y, aidx, ta, bidx, tb, asmoothing, bsmoothing); + }; +}*/ + +/* + * Interpret the tick properties to return indices, interpolants, + * and values of grid lines + * + * Output is: + * ilower: lower index of the cell in which the gridline lies + * interpolant: fractional distance of this gridline to the next cell (in [0, 1]) + * values: value of the axis coordinate at each respective gridline + */ +/*function makeGridLines (data, axis) { + var gridlines = []; + var t = []; + var i0 = []; + var i1 = []; + var values = []; + switch(axis.tickmode) { + case 'array': + var start = axis.arraytick0 % axis.arraydtick; + var step = axis.arraydtick; + var end = data.length; + // This could be optimized, but we'll convert this to a/b space and then do + // the interpolation in + for (var i = start; i < end; i += step) { + // If it's the end point, we'll use the end of the previous range to + // avoid going out of bounds: + var endshift = i >= end - 1 ? 1 : 0; + + gridlines.push({ + param: i, + idx: i - endshift, + tInterp: endshift, + value: data[i] + }); + } + break; + case 'linear': + var v1 = getLinspaceStartingPoint(data[0], axis.tick0, axis.dtick); + var n = Math.ceil((data[data.length - 1] - v1) / axis.dtick); + + // This could be optimized, but we'll convert this to a/b space and then do + // the interpolation in + for (var i = 0; i < n; i++) { + var val = v1 + axis.dtick * i; + var idx = Math.min(search(val, data), data.length - 2); + gridlines.push({ + param: (val - data[0]) / (data[data.length - 1] - data[0]) * data.length, + idx: idx, + tInterp: (val - data[idx]) / (data[idx + 1] - data[idx]), + value: val + }); + } + } + + console.log('gridlines:', gridlines); + + return gridlines; +}*/ + +/* + * Given a data range from starting at x1, this function computes the first + * point distributed along x0 + n * dx that lies within the range. + */ +function getLinspaceStartingPoint (xlow, x0, dx) { + return x0 + dx * Math.ceil((xlow - x0) / dx); +} diff --git a/src/traces/carpet/catmull_rom.js b/src/traces/carpet/catmull_rom.js new file mode 100644 index 00000000000..b7561e216b8 --- /dev/null +++ b/src/traces/carpet/catmull_rom.js @@ -0,0 +1,43 @@ +/** +* 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'; + +/* + * Compute the tangent vector according to catmull-rom cubic splines (centripetal, + * I think). That differs from the control point in two ways: + * 1. It is a vector, not a position relative to the point + * 2. the vector is longer than the position relative to p1 by a factor of 3 + * + * Close to the boundaries, we'll use these as *quadratic control points, so that + * to make a nice grid, we'll need to divide the tangent by 2 instead of 3. (The + * math works out this way if you work through the bezier derivatives) + */ +var CatmullRomExp = 0.5; +module.exports = function makeControlPoints(p0, p1, p2, smoothness) { + var d1x = p0[0] - p1[0], + d1y = p0[1] - p1[1], + d2x = p2[0] - p1[0], + d2y = p2[1] - p1[1], + d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), + d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), + numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, + numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, + denom1 = d2a * (d1a + d2a) * 3, + denom2 = d1a * (d1a + d2a) * 3; + return [[ + p1[0] + (denom1 && numx / denom1), + p1[1] + (denom1 && numy / denom1) + ], [ + p1[0] - (denom2 && numx / denom2), + p1[1] - (denom2 && numy / denom2) + ]]; + + return [[dxL, dyL], [dxU, dyU]]; +} + diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 17bcde7569d..bb02ddb7b02 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -8,14 +8,39 @@ 'use strict'; -module.exports = function(na, nb, cheaterslope) { +var isArray = require('../../lib').isArray; + +/* + * Construct a 2D array of cheater values given a, b, and a slope. + * If + */ +module.exports = function(a, b, cheaterslope) { var i, j; var data = []; + var na = isArray(a) ? a.length : a; + var nb = isArray(b) ? b.length : b; + var adata = isArray(a) ? a : null; + var bdata = isArray(b) ? b : null; + + // If we're using data, scale it so that for data that's just barely + // not evenly spaced, the switch to value-based indexing is continuous. + // This means evenly spaced data should look the same whether value + // or index cheatertype. + if (adata) { + var ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]); + } + + if (bdata) { + var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]); + } + for(i = 0; i < na; i++) { data[i] = []; + var aval = adata ? (adata[i] - adata[0]) * ascal : i; for(j = 0; j < nb; j++) { - data[i][j] = i - j * cheaterslope; + var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j; + data[i][j] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/compute_control_points.js b/src/traces/carpet/compute_control_points.js new file mode 100644 index 00000000000..37ebfda6cc8 --- /dev/null +++ b/src/traces/carpet/compute_control_points.js @@ -0,0 +1,352 @@ +/** +* 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 makeControlPoints = require('./catmull_rom'); +var ensureArray = require('./ensure_array'); + +/* + * Turns a coarse grid into a fine grid with control points. + * + * Here's an ASCII representation: + * + * o ----- o ----- o ----- o + * | | | | + * | | | | + * | | | | + * o ----- o ----- o ----- o + * | | | | + * | | | | + * ^ | | | | + * | o ----- o ----- o ----- o + * b | | | | | + * | | | | | + * | | | | | + * o ----- o ----- o ----- o + * ------> + * a + * + * First of all, note that we want to do this in *cartesian* space. This means + * we might run into problems when there are extreme differences in x/y scaling, + * but the alternative is that the topology of the contours might actually be + * view-dependent, which seems worse. As a fallback, the only parameter that + * actually affects the result is the *aspect ratio*, so that we can at least + * improve the situation a bit without going all the way to screen coordinates. + * + * This function flattens the points + tangents into a slightly denser grid of + * *control points*. The resulting grid looks like this: + * + * 9 +--o-o--+ -o-o--+--o-o--+ + * 8 o o o o o o o o o o + * | | | | + * 7 o o o o o o o o o o + * 6 +--o-o--+ -o-o--+--o-o--+ + * 5 o o o o o o o o o o + * | | | | + * ^ 4 o o o o o o o o o o + * | 3 +--o-o--+ -o-o--+--o-o--+ + * b | 2 o o o o o o o o o o + * | | | | | + * | 1 o o o o o o o o o o + * 0 +--o-o--+ -o-o--+--o-o--+ + * 0 1 2 3 4 5 6 7 8 9 + * ------> + * a + * + * where `o`s represent newly-computed control points. the resulting dimension is + * + * (m - 1) * 3 + 1 + * = 3 * m - 2 + * + * We could simply store the tangents separately, but that's a nightmare to organize + * in two dimensions since we'll be slicing grid lines in both directions and since + * that basically requires very nearly just as much storage as just storing the dense + * grid. + * + * Wow! + */ + + + +/* + * Catmull-rom is biased at the boundaries toward the interior and we actually + * can't use catmull-rom to compute the control point closest to (but inside) + * the boundary. + * + * A note on plotly's spline interpolation. It uses the catmull rom control point + * closest to the boundary *as* a quadratic control point. This seems incorrect, + * so I've elected not to follow that. Given control points 0 and 1, regular plotly + * splines give *equivalent* cubic control points: + * + * Input: + * + * boundary + * | | + * p0 p2 p3 --> interior + * 0.0 0.667 1.0 + * | | + * + * Cubic-equivalent of what plotly splines draw:: + * + * boundary + * | | + * p0 p1 p2 p3 --> interior + * 0.0 0.4444 0.8888 1.0 + * | | + * + * What this function fills in: + * + * boundary + * | | + * p0 p1 p2 p3 --> interior + * 0.0 0.333 0.667 1.0 + * | | + * + * Parameters: + * p0: boundary point + * p2: catmull rom point based on computation at p3 + * p3: first grid point + * + * Of course it works whichever way it's oriented; you just need to interpret the + * input/output accordingly. + */ +function inferCubicControlPoint (p0, p2, p3) { + // Extend p1 away from p0 by 50%. This is the equivalent quadratic point that + // would give the same slope as catmull rom at p0. + var p2e0 = -0.5 * p3[0] + 1.5 * p2[0]; + var p2e1 = -0.5 * p3[1] + 1.5 * p2[1]; + + return [ + (2 * p2e0 + p0[0]) / 3, + (2 * p2e1 + p0[1]) / 3, + ]; +} + +module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoothing) { + var i, j, ie, je, xei, yei, xi, yi, qx, qy, cp, p1; + // At this point, we know these dimensions are correct and representative of + // the whole 2D arrays: + var na = x.length; + var nb = x[0].length; + + // (n)umber of (e)xpanded points: + var nea = asmoothing ? 3 * na - 2 : na; + var neb = bsmoothing ? 3 * nb - 2 : nb; + + xe = ensureArray(xe, nea); + ye = ensureArray(ye, nea); + + for (ie = 0; ie < nea; ie++) { + xe[ie] = ensureArray(xe[ie], neb); + ye[ie] = ensureArray(ye[ie], neb); + } + + // This loop fills in the X'd points: + // + // . . . . + // . . . . + // | | | | + // | | | | + // X ----- X ----- X ----- X + // | | | | + // | | | | + // | | | | + // X ----- X ----- X ----- X + // + // + // ie = (i) (e)xpanded: + for (i = 0, ie = 0; i < na; i++, ie += asmoothing ? 3 : 1) { + xei = xe[ie]; + yei = ye[ie]; + xi = x[i]; + yi = y[i]; + + // je = (j) (e)xpanded: + for (j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { + xei[je] = xi[j]; + yei[je] = yi[j]; + } + } + + if (asmoothing) { + // If there's a-smoothing, this loop fills in the X'd points with catmull-rom + // control points computed along the a-axis: + // . . . . + // . . . . + // | | | | + // | | | | + // o -Y-X- o -X-X- o -X-Y- o + // | | | | + // | | | | + // | | | | + // o -Y-X- o -X-X- o -X-Y- o + // + // i: 0 1 2 3 + // ie: 0 1 3 3 4 5 6 7 8 9 + // + // ------> + // a + // + for (j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { + // Fill in the points marked X for this a-row: + for (i = 1, ie = 3; i < na - 1; i++, ie += 3) { + cp = makeControlPoints( + [x[i - 1][j], y[i - 1][j]], + [x[i ][j], y[i ][j]], + [x[i + 1][j], y[i + 1][j]], + asmoothing + ); + + xe[ie - 1][je] = cp[0][0]; + ye[ie - 1][je] = cp[0][1]; + xe[ie + 1][je] = cp[1][0]; + ye[ie + 1][je] = cp[1][1]; + } + + // The very first cubic interpolation point (to the left for i = 1 above) is + // used as a *quadratic* interpolation point by the spline drawing function + // which isn't really correct. But for the sake of consistency, we'll use it + // as such. Since we're using cubic splines, that means we need to shorten the + // tangent by 1/3 and also construct a new cubic spline control point 1/3 from + // the original to the i = 0 point. + p1 = inferCubicControlPoint( + [xe[0][je], ye[0][je]], + [xe[2][je], ye[2][je]], + [xe[3][je], ye[3][je]] + ); + xe[1][je] = p1[0]; + ye[1][je] = p1[1]; + + // Ditto last points, sans explanation: + p1 = inferCubicControlPoint( + [xe[nea - 1][je], ye[nea - 1][je]], + [xe[nea - 3][je], ye[nea - 3][je]], + [xe[nea - 4][je], ye[nea - 4][je]] + ); + xe[nea - 2][je] = p1[0]; + ye[nea - 2][je] = p1[1]; + } + } + + if (bsmoothing) { + // If there's a-smoothing, this loop fills in the X'd points with catmull-rom + // control points computed along the b-axis: + // . . . . + // X X X X X X X X X X + // | | | | + // X X X X X X X X X X + // o -o-o- o -o-o- o -o-o- o + // X X X X X X X X X X + // | | | | + // Y Y Y Y Y Y Y Y Y Y + // o -o-o- o -o-o- o -o-o- o + // + // i: 0 1 2 3 + // ie: 0 1 3 3 4 5 6 7 8 9 + // + // ------> + // a + // + for (ie = 0; ie < nea; ie++) { + for (je = 3; je < neb - 3; je += 3) { + cp = makeControlPoints( + [xe[ie][je - 3], ye[ie][je - 3]], + [xe[ie][je ], ye[ie][je ]], + [xe[ie][je + 3], ye[ie][je + 3]], + bsmoothing + ); + + xe[ie][je - 1] = cp[0][0]; + ye[ie][je - 1] = cp[0][1]; + xe[ie][je + 1] = cp[1][0]; + ye[ie][je + 1] = cp[1][1]; + } + // Do the same boundary condition magic for these control points marked Y above: + p1 = inferCubicControlPoint( + [xe[ie][0], ye[ie][0]], + [xe[ie][2], ye[ie][2]], + [xe[ie][3], ye[ie][3]] + ); + xe[ie][1] = p1[0]; + ye[ie][1] = p1[1]; + + p1 = inferCubicControlPoint( + [xe[ie][neb - 1], ye[ie][neb - 1]], + [xe[ie][neb - 3], ye[ie][neb - 3]], + [xe[ie][neb - 4], ye[ie][neb - 4]] + ); + xe[ie][neb - 2] = p1[0]; + ye[ie][neb - 2] = p1[1]; + } + } + + if (asmoothing && bsmoothing) { + // Do one more pass, this time recomputing exactly what we just computed. + // It's overdetermined since we're peforming catmull-rom in two directions, + // so we'll just average the overdetermined. These points don't lie along the + // grid lines, so note that only grid lines will follow normal plotly spline + // interpolation. + // + // Unless of course there was no b smoothing. Then these intermediate points + // don't actually exist and this section is bypassed. + // . . . . + // o X X o X X o X X o + // | | | | + // o X X o X X o X X o + // o -o-o- o -o-o- o -o-o- o + // o X X o X X o X X o + // | | | | + // o Y Y o Y Y o Y Y o + // o -o-o- o -o-o- o -o-o- o + // + // i: 0 1 2 3 + // ie: 0 1 3 3 4 5 6 7 8 9 + // + // ------> + // a + // + for (je = 1; je < neb; je += (je + 1) % 3 === 0 ? 2 : 1) { + // Fill in the points marked X for this a-row: + for (ie = 3; ie < nea - 3; ie += 3) { + cp = makeControlPoints( + [xe[ie - 3][je], ye[ie - 3][je]], + [xe[ie ][je], ye[ie ][je]], + [xe[ie + 3][je], ye[ie + 3][je]], + asmoothing + ); + + xe[ie - 1][je] = 0.5 * (xe[ie - 1][je] + cp[0][0]); + ye[ie - 1][je] = 0.5 * (ye[ie - 1][je] + cp[0][1]); + xe[ie + 1][je] = 0.5 * (xe[ie + 1][je] + cp[1][0]); + ye[ie + 1][je] = 0.5 * (ye[ie + 1][je] + cp[1][1]); + } + + // This case is just slightly different. The computation is the same, + // but having computed this, we'll average with the existing result. + p1 = inferCubicControlPoint( + [xe[0][je], ye[0][je]], + [xe[2][je], ye[2][je]], + [xe[3][je], ye[3][je]] + ); + xe[1][je] = 0.5 * (xe[1][je] + p1[0]); + ye[1][je] = 0.5 * (ye[1][je] + p1[1]); + + p1 = inferCubicControlPoint( + [xe[nea - 1][je], ye[nea - 1][je]], + [xe[nea - 3][je], ye[nea - 3][je]], + [xe[nea - 4][je], ye[nea - 4][je]] + ); + xe[nea - 2][je] = 0.5 * (xe[nea - 2][je] + p1[0]); + ye[nea - 2][je] = 0.5 * (ye[nea - 2][je] + p1[1]); + } + } + + return [xe, ye]; +} + diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js new file mode 100644 index 00000000000..ed4b1754d0e --- /dev/null +++ b/src/traces/carpet/create_spline_evaluator.js @@ -0,0 +1,15 @@ +/** +* 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 (arrays, i, j, u, v, ) { + +} diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 47ae5037bb9..e333d1766dc 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -20,7 +20,12 @@ module.exports = function supplyDefaults(traceIn, traceOut) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + traceIn.cheatersope = parseFloat(traceIn.cheaterslope); + coerce('carpetid'); + coerce('cheaterslope'); + + traceOut.cheaterslope = parseFloat(traceIn.cheaterslope); var len = handleXYDefaults(traceIn, traceOut, coerce); @@ -31,8 +36,6 @@ module.exports = function supplyDefaults(traceIn, traceOut) { handleABDefaults(traceIn, traceOut, coerce); - coerce('cheaterslope'); - handleAxisDefaults(traceIn, traceOut, 'a'); handleAxisDefaults(traceIn, traceOut, 'b'); }; @@ -45,20 +48,35 @@ function handleAxisDefaults(traceIn, traceOut, axis) { return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); } + coerce('smoothing'); + coerce('cheatertype'); + coerce('showlabels'); coerce('labelprefix', axis + ' = '); coerce('labelsuffix'); coerce('showlabelprefix'); coerce('showlabelsuffix'); + + coerce('tickmode'); + coerce('tick0'); + coerce('dtick'); + coerce('arraytick0'); + coerce('arraydtick'); + //coerce('gridoffset'); + //coerce('gridstep'); + + coerce('startline'); + coerce('startlinewidth'); + coerce('startlinecolor'); + coerce('endline'); + coerce('endlinewidth'); + coerce('endlinecolor'); + coerce('gridwidth'); coerce('gridcolor'); - coerce('gridoffset'); - coerce('gridstep'); coerce('minorgridwidth'); coerce('minorgridcolor'); - coerce('minorgridoffset'); - coerce('minorgridstep'); coerce('showstartlabel'); coerce('showendlabel'); @@ -71,6 +89,7 @@ function handleAxisDefaults(traceIn, traceOut, axis) { // that should go in calc.js, but it's so minimal for any conceivable case that // I'll write it here for now: + /* ax._gridIndices = []; for(i = ax.gridoffset; i < traceIn[axis].length; i += ax.gridstep) { ax._gridIndices.push(i); @@ -112,4 +131,5 @@ function handleAxisDefaults(traceIn, traceOut, axis) { ax._minorGridIndices.push(i); } } + */ } diff --git a/src/traces/carpet/ensure_array.js b/src/traces/carpet/ensure_array.js new file mode 100644 index 00000000000..34e01fda2e1 --- /dev/null +++ b/src/traces/carpet/ensure_array.js @@ -0,0 +1,29 @@ +/** +* 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'; + +/* + * Ensures an array has the right amount of storage space. If it doesn't + * exist, it creates an array. If it does exist, it returns it if too + * short or truncates it in-place. + * + * The goal is to just reuse memory to avoid a bit of excessive garbage + * collection. + */ +module.exports = function ensureArray(out, n) { + if (!Array.isArray(out)) out = []; + + // If too long, truncate. (If too short, it will grow + // automatically so we don't care about that case) + out.length = n; + + return out; +} + + diff --git a/src/traces/carpet/evaluate_cubic.js b/src/traces/carpet/evaluate_cubic.js new file mode 100644 index 00000000000..b7406c6c0ea --- /dev/null +++ b/src/traces/carpet/evaluate_cubic.js @@ -0,0 +1,11 @@ +/** +* 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 (f1, f2, f3, f4, t) diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index 3d6713e65c7..e701bd88f48 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -15,7 +15,7 @@ Carpet.attributes = require('./attributes'); Carpet.supplyDefaults = require('./defaults'); Carpet.plot = require('./plot'); Carpet.calc = require('./calc'); -Carpet.animatable = false; +Carpet.animatable = true; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; diff --git a/src/traces/carpet/map_2d_array.js b/src/traces/carpet/map_2d_array.js new file mode 100644 index 00000000000..eb3e9cee65f --- /dev/null +++ b/src/traces/carpet/map_2d_array.js @@ -0,0 +1,43 @@ +/** +* 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'; + +/* + * Map an array of x or y coordinates (c) to screen-space pixel coordinates (p). + * The output array is optional, but if provided, it will be reused without + * reallocation to the extent possible. + */ +module.exports = function mapArray (out, data, func) { + var i, j; + + if (!Array.isArray(out)) { + // If not an array, make it an array: + out = []; + } else if (out.length > data.length) { + // If too long, truncate. (If too short, it will grow + // automatically so we don't care about that case) + out = out.slice(0, data.length) + } + + for (i = 0; i < data[0].length; i++) { + if (!Array.isArray(out[i])) { + // If not an array, make it an array: + out[i] = []; + } else if (out[i].length > data.length) { + // If too long, truncate. (If too short, it will grow + // automatically so we don't care about[i] that case) + out[i] = out[i].slice(0, data.length); + } + + for (j = 0; j < data[1].length; j++) { + out[i][j] = func(data[i][j]); + } + } + return out; +} diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 6efd9b52eea..997ece414f5 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -29,13 +29,28 @@ function plotOne(gd, plotinfo, cd) { var trace = cd[0].trace, // uid = trace.uid, xa = plotinfo.xaxis, - ya = plotinfo.yaxis; + ya = plotinfo.yaxis, + aax = trace.aaxis, + bax = trace.baxis; // id = 'carpet' + uid; var x = cd[0].x; var y = cd[0].y; var a = cd[0].a; var b = cd[0].b; + + trace.c2p = function (c) { + return [xa.c2p(c[0]), ya.c2p(c[1])]; + }; + + trace.ab2p = function (a, b) { + return trace.c2p(trace.ab2c(a, b)); + }; + + trace.ba2p = function (b, a) { + return trace.c2p(trace.ab2c(a, b)); + }; + var xp = xa.c2p; var yp = ya.c2p; @@ -44,34 +59,53 @@ function plotOne(gd, plotinfo, cd) { var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); var majorLayer = makeg(gridLayer, 'g', 'majorlayer'); + var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); var labelLayer = makeg(gridLayer, 'g', 'labellayer'); + return; + var linesets = [ { baseClass: 'const-a-line', - data: a, + data: aax._tickinfo.values, + crossdata: b, + smoothing: trace.baxis.smoothing, ax: trace.aaxis, - xc: function(i, j) { return xp(x[i][j]); }, - yc: function(i, j) { return yp(y[i][j]); }, - n: b.length + ab2p: trace.ab2p, }, { baseClass: 'const-b-line', - data: b, + data: bax._tickinfo.values, + crossdata: a, + smoothing: trace.aaxis.smoothing, + ax: trace.baxis, + ab2p: trace.ba2p, + }, + { + baseClass: 'const-a-boundary-line', + data: [a[0], a[a.length - 1]], + crossdata: b, + smoothing: trace.baxis.smoothing, + ax: trace.aaxis, + ab2p: trace.ab2p, + }, + { + baseClass: 'const-b-boundary-line', + data: [b[0], b[b.length - 1]], + crossdata: a, + smoothing: trace.aaxis.smoothing, ax: trace.baxis, - xc: function(i, j) { return xp(x[j][i]); }, - yc: function(i, j) { return yp(y[j][i]); }, - n: a.length + ab2p: trace.ba2p, } ]; for(var i = 0; i < linesets.length; i++) { var lineset = linesets[i]; - drawGridLines(minorLayer, lineset, false); - drawGridLines(majorLayer, lineset, true); + //drawGridLines(minorLayer, lineset, false); + //drawGridLines(majorLayer, lineset, true); - drawAxisLabels(labelLayer, lineset, 'lo'); - drawAxisLabels(labelLayer, lineset, 'hi'); + //drawAxisLabels(labelLayer, lineset, 'lo'); + //drawAxisLabels(labelLayer, lineset, 'hi'); } } @@ -160,7 +194,7 @@ function drawAxisLabels(layer, ls, side) { function drawGridLines(layer, ls, isMajor) { var ax = ls.ax; - var data = isMajor ? ax._gridIndices : ax._minorGridIndices; + var data = ls.data; var lineClass = 'const-' + ls.baseClass + '-lines' + (isMajor ? '' : '-minor'); var gridjoin = layer.selectAll('.' + lineClass).data(data); @@ -173,15 +207,16 @@ function drawGridLines(layer, ls, isMajor) { var color = isMajor ? ax.gridcolor : ax.minorgridcolor; gridjoin.each(function(d, i) { - var gridIdx = data[i]; + var value = data[i]; var el = d3.select(this); el.attr('d', function() { var pts = []; - for(var k = 0; k < ls.n; k++) { - pts.push(ls.xc(gridIdx, k) + ',' + ls.yc(gridIdx, k)); + for(var j = 0; j < ls.crossdata.length; j++) { + pts.push(ls.ab2p(value, ls.crossdata[j])); } - return 'M' + pts.join('L'); + return Drawing.smoothopen(pts, ls.smoothing); + //return 'M' + pts.join('L'); }); el.style('stroke-width', width) .style('stroke', color) From 1c064fcab858db0bd38ee2c571ac44f8c1137b76 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 13 Dec 2016 18:54:53 -0500 Subject: [PATCH 006/132] Work out carpet interpolation --- .../evaluate_cubic.js => lib/scattercarpet.js | 2 +- src/components/drawing/spline_evaluator.js | 97 +++++++++ src/traces/carpet/calc.js | 160 +-------------- src/traces/carpet/calc_gridlines.js | 190 ++++++++++++++++++ src/traces/carpet/cheater_basis.js | 8 +- .../carpet/create_i_derivative_evaluator.js | 136 +++++++++++++ .../carpet/create_j_derivative_evaluator.js | 107 ++++++++++ src/traces/carpet/create_spline_evaluator.js | 110 +++++++++- src/traces/carpet/interpolate.js | 29 +++ src/traces/carpet/makepath.js | 29 +++ src/traces/carpet/map_1d_array.js | 33 +++ src/traces/carpet/map_2d_array.js | 2 +- src/traces/carpet/plot.js | 153 ++++++-------- src/traces/carpet/set_convert.js | 166 +++++++++++++++ src/traces/scattercarpet/attributes.js | 123 ++++++++++++ src/traces/scattercarpet/calc.js | 110 ++++++++++ src/traces/scattercarpet/defaults.js | 103 ++++++++++ src/traces/scattercarpet/hover.js | 69 +++++++ src/traces/scattercarpet/index.js | 31 +++ src/traces/scattercarpet/plot.js | 44 ++++ src/traces/scattercarpet/select.js | 33 +++ src/traces/scattercarpet/style.js | 27 +++ 22 files changed, 1514 insertions(+), 248 deletions(-) rename src/traces/carpet/evaluate_cubic.js => lib/scattercarpet.js (78%) create mode 100644 src/components/drawing/spline_evaluator.js create mode 100644 src/traces/carpet/calc_gridlines.js create mode 100644 src/traces/carpet/create_i_derivative_evaluator.js create mode 100644 src/traces/carpet/create_j_derivative_evaluator.js create mode 100644 src/traces/carpet/interpolate.js create mode 100644 src/traces/carpet/makepath.js create mode 100644 src/traces/carpet/map_1d_array.js create mode 100644 src/traces/carpet/set_convert.js create mode 100644 src/traces/scattercarpet/attributes.js create mode 100644 src/traces/scattercarpet/calc.js create mode 100644 src/traces/scattercarpet/defaults.js create mode 100644 src/traces/scattercarpet/hover.js create mode 100644 src/traces/scattercarpet/index.js create mode 100644 src/traces/scattercarpet/plot.js create mode 100644 src/traces/scattercarpet/select.js create mode 100644 src/traces/scattercarpet/style.js diff --git a/src/traces/carpet/evaluate_cubic.js b/lib/scattercarpet.js similarity index 78% rename from src/traces/carpet/evaluate_cubic.js rename to lib/scattercarpet.js index b7406c6c0ea..238142c7b6f 100644 --- a/src/traces/carpet/evaluate_cubic.js +++ b/lib/scattercarpet.js @@ -8,4 +8,4 @@ 'use strict'; -module.exports = function (f1, f2, f3, f4, t) +module.exports = require('../src/traces/scattercarpet'); diff --git a/src/components/drawing/spline_evaluator.js b/src/components/drawing/spline_evaluator.js new file mode 100644 index 00000000000..050c5890430 --- /dev/null +++ b/src/components/drawing/spline_evaluator.js @@ -0,0 +1,97 @@ +/** +* 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'; + +// generalized Catmull-Rom splines, per +// http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf +var CatmullRomExp = 0.5; +function makeControlPoints(p0, p1, p2, smoothness) { + var d1x = p0[0] - p1[0], + d1y = p0[1] - p1[1], + d2x = p2[0] - p1[0], + d2y = p2[1] - p1[1], + d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), + d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), + numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, + numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, + denom1 = 3 * d2a * (d1a + d2a), + denom2 = 3 * d1a * (d1a + d2a); + var dxL = p1[0] + (denom1 && numx / denom1); + var dyL = p1[1] + (denom1 && numy / denom1); + var dxU = p1[0] - (denom2 && numx / denom2); + var dyU = p1[1] - (denom2 && numy / denom2); + + return [[dxL, dyL], [dxU, dyU]]; +} + +/* + * Computes centripetal catmull-rom control points. These shouldn't be confused + * with tangents, which are slightly different. Specifically, the length of the + * tangent vector is + * + * degree * (control point - reference point) + * + * In other words, it really doesn't make too much of a difference whether we + * work with tangents or control points. + */ +module.exports.computeControlPoints = function (pts, smoothness) { + var tangents = []; + for(i = 1; i < pts.length - 1; i++) { + tangents.push(makeControlPoints(pts[i - 1], pts[i], pts[i + 1], smoothness)); + } + return tangents; +} + +module.exports.evaluateSpline = function evaluateSpline (t, pts, controlpts, smoothness) { + var n = pts.length; + if (n < 3) { + var x = (1 - t) * pts[0][0] + t * pts[1][0]; + var y = (1 - t) * pts[0][1] + t * pts[1][1]; + return [x, y]; + } else { + // Compute the controlpts *once* at the beginning, and store them: + var a, b, c, d, p0, p1, p2, p3, i, t, ot; + param = Math.max(0, Math.min(n - 1, param)); + i = Math.min(Math.floor(param), n - 2); + t = param - i; + ot = 1 - t; + + p0 = pts[i]; + p3 = pts[i + 1]; + + if (i === 0 || i === n - 2) { + // Evaluate the quadratic first and last segments; + p1 = i === 0 ? controlpts[i][0] : controlpts[i - 1][1]; + a = ot * ot; + b = 2 * ot * t; + c = t * t; + return [ + a * p0[0] + b * p1[0] + c * p3[0], + a * p0[1] + b * p1[1] + c * p3[1] + ]; + } else { + // Evaluate internal cubic spline segments: + p1 = controlpts[i - 1][1]; + p2 = controlpts[i][0]; + p3 = pts[i + 1]; + + a = ot * ot * ot; + b = 3 * ot * ot * t; + c = 3 * ot * t * t; + d = t * t * t; + + return [ + a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], + a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1] + ]; + } + } +} + diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index f92f5605e93..d93497e49a8 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -14,7 +14,9 @@ var arrayMinmax = require('./array_minmax'); var search = require('../../lib/search').findBin; var computeControlPoints = require('./compute_control_points'); var map2dArray = require('./map_2d_array'); -var evaluateControlPoints = require('./evaluate_control_points'); +var createSplineEvaluator = require('./create_spline_evaluator'); +var setConvert = require('./set_convert'); +var calcGridlines = require('./calc_gridlines'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), @@ -30,7 +32,7 @@ module.exports = function calc(gd, trace) { if(trace._cheater) { var avals = aax.cheatertype === 'index' ? a.length : a var bvals = bax.cheatertype === 'index' ? b.length : b; - xdata = cheaterBasis(avals, bvals, trace.cheaterslope); + trace.x = xdata = cheaterBasis(avals, bvals, trace.cheaterslope); } else { xdata = trace.x; } @@ -61,162 +63,20 @@ module.exports = function calc(gd, trace) { trace.xp = map2dArray(trace.xp, x, xa.c2p); trace.yp = map2dArray(trace.yp, y, ya.c2p); - // Next compute the control points in whichever directions are necessary: - var result = computeControlPoints(trace.xctrl, trace.yctrl, x, y, aax.smoothing, bax.smoothing); - trace.xctrl = result[0]; - trace.yctrl = result[1]; + // Create conversions from one coordinate system to another: + setConvert(trace, xa, ya); + trace._agrid = calcGridlines(trace, 'a', 'b', xa, ya); + trace._bgrid = calcGridlines(trace, 'b', 'a', xa, ya); - console.log('evaluateControlPoints(:', evaluateControlPoints([trace.xctrl, trace.yctrl], 0.5, 0.5)); - - - //trace.ab2c = getConvert(x, y, a, b, aax.smoothing, bax.smoothing); - - // This is just a transposed function that will be useful in making - // the computations a/b-agnostic: - //trace.ba2c = function (b, a) { return trace.ab2c(a, b); } - - //var agrid = makeGridLines(a, aax); - //var bgrid = makeGridLines(b, bax); - - //trace.aaxis._tickinfo = agrid; - //trace.baxis._tickinfo = bgrid; - - var cd0 = { + return [{ x: x, y: y, a: a, b: b - }; - - return [cd0]; + }]; }; - - -/* - * Interpolate in the b-direction along constant-a lines - * x, y: 2D data arrays to be interpolated - * aidx: index of the a gridline along which to evaluate - * bidx0: lower-index of the b cell in which to interpolate - * tb: an interpolant in [0, 1] - */ -/*function interpolateConstA (x, y, aidx, bidx0, tb, bsmoothing) { - if (bsmoothing) { - return [ - x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb, - y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb - ]; - } else { - return [ - x[aidx][bidx0] * (1 - tb) + x[aidx][bidx0 + 1] * tb, - y[aidx][bidx0] * (1 - tb) + y[aidx][bidx0 + 1] * tb - ]; - } -}*/ - -/* - * Interpolate in the a and b directions - * x, y: 2D data arrays to be interpolated - * aidx0: lower index of the a cell in which to interpolatel - * bidx0: lower index of the b cell in which to interpolate - * ta: an interpolant for the a direction in [0, 1] - * tb: an interpolant for the b direction in [0, 1] - */ -/*function interpolateAB (x, y, aidx0, ta, bidx0, tb, asmoothing, bsmoothing) { - if (asmoothing) { - var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing); - var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing); - - return [ - xy0[0] * (1 - ta) + xy1[0] * ta, - xy0[1] * (1 - ta) + xy1[1] * ta - ]; - } else { - var xy0 = interpolateConstA(x, y, aidx0, bidx0, tb, bsmoothing); - var xy1 = interpolateConstA(x, y, aidx0 + 1, bidx0, tb, bsmoothing); - - return [ - xy0[0] * (1 - ta) + xy1[0] * ta, - xy0[1] * (1 - ta) + xy1[1] * ta - ]; - } -}*/ - -/* - * Return a function that converts a/b coordinates to x/y values - */ -/*function getConvert (x, y, a, b, asmoothing, bsmoothing) { - return function (aval, bval) { - // Get the lower-indices of the box in which the point falls: - var aidx = Math.min(search(aval, a), a.length - 2); - var bidx = Math.min(search(bval, b), b.length - 2); - - var ta = (aval - a[aidx]) / (a[aidx + 1] - a[aidx]); - var tb = (bval - b[bidx]) / (b[bidx + 1] - b[bidx]); - - return interpolateAB(x, y, aidx, ta, bidx, tb, asmoothing, bsmoothing); - }; -}*/ - -/* - * Interpret the tick properties to return indices, interpolants, - * and values of grid lines - * - * Output is: - * ilower: lower index of the cell in which the gridline lies - * interpolant: fractional distance of this gridline to the next cell (in [0, 1]) - * values: value of the axis coordinate at each respective gridline - */ -/*function makeGridLines (data, axis) { - var gridlines = []; - var t = []; - var i0 = []; - var i1 = []; - var values = []; - switch(axis.tickmode) { - case 'array': - var start = axis.arraytick0 % axis.arraydtick; - var step = axis.arraydtick; - var end = data.length; - // This could be optimized, but we'll convert this to a/b space and then do - // the interpolation in - for (var i = start; i < end; i += step) { - // If it's the end point, we'll use the end of the previous range to - // avoid going out of bounds: - var endshift = i >= end - 1 ? 1 : 0; - - gridlines.push({ - param: i, - idx: i - endshift, - tInterp: endshift, - value: data[i] - }); - } - break; - case 'linear': - var v1 = getLinspaceStartingPoint(data[0], axis.tick0, axis.dtick); - var n = Math.ceil((data[data.length - 1] - v1) / axis.dtick); - - // This could be optimized, but we'll convert this to a/b space and then do - // the interpolation in - for (var i = 0; i < n; i++) { - var val = v1 + axis.dtick * i; - var idx = Math.min(search(val, data), data.length - 2); - gridlines.push({ - param: (val - data[0]) / (data[data.length - 1] - data[0]) * data.length, - idx: idx, - tInterp: (val - data[idx]) / (data[idx + 1] - data[idx]), - value: val - }); - } - } - - console.log('gridlines:', gridlines); - - return gridlines; -}*/ - /* * Given a data range from starting at x1, this function computes the first * point distributed along x0 + n * dx that lies within the range. diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js new file mode 100644 index 00000000000..262f9e5210e --- /dev/null +++ b/src/traces/carpet/calc_gridlines.js @@ -0,0 +1,190 @@ +/** +* 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'); +var cheaterBasis = require('./cheater_basis'); +var arrayMinmax = require('./array_minmax'); +var search = require('../../lib/search').findBin; +var computeControlPoints = require('./compute_control_points'); +var map2dArray = require('./map_2d_array'); +var createSplineEvaluator = require('./create_spline_evaluator'); +var setConvert = require('./set_convert'); +var map2dArray = require('./map_2d_array'); +var map1dArray = require('./map_1d_array'); +var makepath = require('./makepath'); + +module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xaxis, yaxis) { + var i, j, gridline, j0, i0; + var gridlines = []; + + var data = trace[axisLetter]; + var axis = trace[axisLetter + 'axis']; + + var crossData = trace[crossAxisLetter]; + var crossAxis = trace[crossAxisLetter + 'axis']; + + var xcp = trace._xctrl; + var ycp = trace._yctrl; + var nea = xcp.length; + var neb = xcp[0].length; + var na = trace.a.length; + var nb = trace.b.length; + + if (axis.tickmode === 'array') { + // If the axes fall along array lines, then this is a much simpler process since + // we already have all the control points we need + for (j = 0; j < data.length; j++) { + var dxy_0, dxy_1; + var xpoints = []; + var ypoints = []; + + // If the cross axis uses bicubic interpolation, then the grid + // lines fall once every three expanded grid row/cols: + var stride = axis.smoothing ? 3 : 1; + + if (axisLetter === 'b') { + // In the tickmode: array case, this operation is a simple + // transfer of data: + for (i = 0; i < nea; i++) { + xpoints[i] = xcp[i][j * stride]; + ypoints[i] = ycp[i][j * stride]; + } + + j0 = Math.min(j, nb - 2); + dxy_0 = trace.dxydi(null, 0, j0, 0, j - j0); + dxy_1 = trace.dxydi(null, na - 2, j0, 1, j - j0); + } else { + // In the tickmode: array case, this operation is a simple + // transfer of data: + for (i = 0; i < neb; i++) { + xpoints[i] = xcp[j * stride][i]; + ypoints[i] = ycp[j * stride][i]; + } + + i0 = Math.min(i, na - 2); + dxy_0 = trace.dxydj(null, i0, 0, i - i0, 0); + dxy_1 = trace.dxydj(null, i0, nb - 2, i - i0, 1); + } + + gridlines.push({ + axisLetter: axisLetter, + axis: trace[axisLetter + 'axis'], + value: data[j], + constvar: crossAxisLetter, + index: j, + x: xpoints, + y: ypoints, + dxy_0: dxy_0, + dxy_1: dxy_1, + smoothing: crossAxis.smoothing, + }); + } + } else { + // If the lines do not fall along the axes, then we have to interpolate + // the contro points and so some math to figure out where the lines are + // in the first place. + + // Compute the integer boudns of tick0 + n * dtick that fall within the range + // (roughly speaking): + // Give this a nice generous epsilon. We use at as * (1 + eps) in order to make + // inequalities a little tolerant in a more or less correct manner: + var eps = 5e-15; + var bounds = [ + Math.floor((data[data.length - 1] - axis.tick0) / axis.dtick * (1 + eps)), + Math.ceil((data[0] - axis.tick0) / axis.dtick / (1 + eps)) + ].sort(function (a, b) {return a - b}); + + // Unpack sorted values so we can be sure to avoid infinite loops if something + // is backwards: + var n1 = bounds[0]; + var n2 = bounds[1]; + + for (var n = n1; n <= n2; n++) { + var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; + var xpoints = []; + var ypoints = []; + var value = axis.tick0 + axis.dtick * n; + + // Search for the fractional grid index giving this line: + if (axisLetter === 'b') { + j = trace.b2j(value); + j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); + tj = j - j0; + + for (var i = 0; i < na; i++) { + i0 = Math.min(na - 2, i); + ti = i - i0; + xy = trace._evalxy([], i0, j0, ti, tj); + + if (crossAxis.smoothing && i > 0) { + // First control point: + dxydi0 = trace.dxydi([], i - 1, j0, 0, tj); + xpoints.push(pxy[0] + dxydi0[0] / 3); + ypoints.push(pxy[1] + dxydi0[1] / 3); + + // Second control point: + dxydi1 = trace.dxydi([], i - 1, j0, 1, tj); + xpoints.push(xy[0] - dxydi1[0] / 3); + ypoints.push(xy[1] - dxydi1[1] / 3); + } + + xpoints.push(xy[0]) + ypoints.push(xy[1]) + + pxy = xy; + } + } else { + i = trace.a2i(value); + i0 = Math.floor(Math.max(0, Math.min(na - 2, i))); + ti = i - i0; + + for (var j = 0; j < nb; j++) { + j0 = Math.min(nb - 2, j); + tj = j - j0; + xy = trace._evalxy([], i0, j0, ti, tj); + + if (crossAxis.smoothing && j > 0) { + // First control point: + dxydj0 = trace.dxydj([], i0, j - 1, ti, 0); + xpoints.push(pxy[0] + dxydj0[0] / 3); + ypoints.push(pxy[1] + dxydj0[1] / 3); + + // Second control point: + dxydj1 = trace.dxydj([], i0, j - 1, ti, 1); + xpoints.push(xy[0] - dxydj1[0] / 3); + ypoints.push(xy[1] - dxydj1[1] / 3); + } + + xpoints.push(xy[0]) + ypoints.push(xy[1]) + + pxy = xy; + } + } + + + gridlines.push({ + axisLetter: axisLetter, + axis: trace[axisLetter + 'axis'], + value: value, + constvar: crossAxisLetter, + index: n, + x: xpoints, + y: ypoints, + //dxy_0: dxy_0, + //dxy_1: dxy_1, + smoothing: crossAxis.smoothing, + }); + } + } + + return gridlines; + +} diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index bb02ddb7b02..66cfd2fae6b 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -28,18 +28,18 @@ module.exports = function(a, b, cheaterslope) { // This means evenly spaced data should look the same whether value // or index cheatertype. if (adata) { - var ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]); + var ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]) / (na - 1); } if (bdata) { - var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]); + var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } for(i = 0; i < na; i++) { data[i] = []; - var aval = adata ? (adata[i] - adata[0]) * ascal : i; + var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); for(j = 0; j < nb; j++) { - var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j; + var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); data[i][j] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js new file mode 100644 index 00000000000..dec3e6e9ff9 --- /dev/null +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -0,0 +1,136 @@ +/** +* 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'; + +/* + * Evaluates the derivative of a list of control point arrays. That is, it expects an array or arrays + * that are expanded relative to the raw data to include the bicubic control points, if applicable. If + * only linear interpolation is desired, then the data points correspond 1-1 along that axis to the + * data itself. Since it's catmull-rom splines in either direction note in particular that the + * derivatives are discontinuous across cell boundaries. That's the reason you need both the *cell* + * and the *point within the cell*. + * + * Also note that the discontinuity of the derivative is in magnitude only. The direction *is* + * continuous across cell boundaries. + * + * For example, to compute the derivative of the xcoordinate halfway betwen the 7 and 8th i-gridpoints + * and the 10th and 11th j-gridpoints given bicubic smoothing in both dimensions, you'd write: + * + * var deriv = createIDerivativeEvaluator([x], 1, 1); + * + * var dxdi = deriv([], 7, 10, 0.5, 0.5); + * // => [0.12345] + * + * Since there'd be a bunch of duplicate computation to compute multiple derivatives, you can double + * this up by providing more arrays: + * + * var deriv = createIDerivativeEvaluator([x, y], 1, 1); + * + * var dxdi = deriv([], 7, 10, 0.5, 0.5); + * // => [0.12345, 0.78910] + * + * NB: It's presumed that at this point all data has been sanitized and is valid numerical data arrays + * of the correct dimension. + */ +module.exports = function (arrays, asmoothing, bsmoothing) { + if (asmoothing && bsmoothing) { + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, ak, k; + + // Since it's a grid of control points, the actual indices are * 3: + i0 *= 3; + j0 *= 3; + + // Precompute some numbers: + var u2 = u * u; + var ou = 1 - u; + var ou2 = ou * ou; + + var v2 = v * v; + var v3 = v2 * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ov3 = ov2 * ov; + + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + // Compute the derivatives in the u-direction: + f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); + f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); + f2 = 3 * ((u2 - 1) * ak[i0][j0 + 2] + ou2 * ak[i0 + 1][j0 + 2] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 2] + u2 * ak[i0 + 3][j0 + 2]); + f3 = 3 * ((u2 - 1) * ak[i0][j0 + 3] + ou2 * ak[i0 + 1][j0 + 3] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 3] + u2 * ak[i0 + 3][j0 + 3]); + + // Now just interpolate in the v-direction since it's all separable: + out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; + } + + return out; + }; + } else if (asmoothing) { + // Handle smooth in the a-direction but linear in the b-direction by performing four + // linear interpolations followed by one cubic interpolation of the result + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + i0 *= 3; + var u2 = u * u; + var u3 = u2 * u; + var ou = 1 - u; + var ou2 = ou * ou; + var ou3 = ou2 * ou; + var ov = 1 - v; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); + f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); + + out[k] = ov * f0 + v * f1; + } + return out; + }; + } else if (bsmoothing) { + // Same as the above case, except reversed: + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + j0 *= 3; + var v2 = v * v; + var v3 = v2 * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ov3 = ov2 * ov; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ak[i0 + 1][j0] - ak[i0][j0]; + f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; + f2 = ak[i0 + 1][j0 + 2] - ak[i0][j0 + 2]; + f3 = ak[i0 + 1][j0 + 3] - ak[i0][j0 + 3]; + + out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; + } + return out; + }; + } else { + // Finally, both directions are linear: + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, k, ak; + var ov = 1 - v; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ak[i0 + 1][j0] - ak[i0][j0]; + f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; + + out[k] = ov * f0 + v * f1; + } + return out; + }; + } +} diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js new file mode 100644 index 00000000000..055e1052b45 --- /dev/null +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -0,0 +1,107 @@ + +/** +* 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 (arrays, asmoothing, bsmoothing) { + if (asmoothing && bsmoothing) { + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, ak, k; + + // Since it's a grid of control points, the actual indices are * 3: + i0 *= 3; + j0 *= 3; + + // Precompute some numbers: + var u2 = u * u; + var u3 = u2 * u; + var ou = 1 - u; + var ou2 = ou * ou; + var ou3 = ou2 * ou; + + var v2 = v * v; + var v3 = v2 * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ov3 = ov2 * ov; + + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + // Compute the derivatives in the u-direction: + f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); + f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); + f2 = 3 * ((v2 - 1) * ak[i0 + 2][j0] + ov2 * ak[i0 + 2][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 2][j0 + 2] + v2 * ak[i0 + 2][j0 + 3]); + f3 = 3 * ((v2 - 1) * ak[i0 + 3][j0] + ov2 * ak[i0 + 3][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 3][j0 + 2] + v2 * ak[i0 + 3][j0 + 3]); + + // Now just interpolate in the v-direction since it's all separable: + out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; + } + + return out; + }; + } else if (asmoothing) { + // Handle smooth in the a-direction but linear in the b-direction by performing four + // linear interpolations followed by one cubic interpolation of the result + return function (out, i0, j0, v, u) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + i0 *= 3; + var u2 = u * u; + var u3 = u2 * u; + var ou = 1 - u; + var ou2 = ou * ou; + var ou3 = ou2 * ou; + var ov = 1 - v; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0][j0 ] + u * (2 - 3 * u) * ak[i0][j0 ] + u2 * ak[i0][j0 ]); + f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0][j0 + 1] + u * (2 - 3 * u) * ak[i0][j0 + 1] + u2 * ak[i0][j0 + 1]); + + out[k] = ov * f0 + v * f1; + } + return out; + }; + } else if (bsmoothing) { + // Same as the above case, except reversed: + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + j0 *= 3; + var v2 = v * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ou = 1 - u; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); + f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); + + out[k] = ou * f0 + u * f1; + } + return out; + }; + } else { + // Finally, both directions are linear: + return function (out, i0, j0, v, u) { + if (!out) out = []; + var f0, f1, k, ak; + var ov = 1 - v; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ak[i0 + 1][j0] - ak[i0][j0]; + f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; + + out[k] = ov * f0 + v * f1; + } + return out; + }; + } + +} diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js index ed4b1754d0e..e7f2c8ce730 100644 --- a/src/traces/carpet/create_spline_evaluator.js +++ b/src/traces/carpet/create_spline_evaluator.js @@ -9,7 +9,115 @@ 'use strict'; /* + * Return a function that evaluates a set of linear or bicubic control points. + * This will get evaluated a lot, so we'll at least do a bit of extra work to + * flatten some of the choices. In particular, we'll unroll the linear/bicubic + * combinations and we'll allow computing results in parallel to cut down + * on repeated arithmetic. + * + * Take note that we don't search for the correct range in this function. The + * reason is for consistency due to the corrresponding derivative function. In + * particular, the derivatives aren't continuous across cells, so it's important + * to be able control whether the derivative at a cell boundary is approached + * from one side or the other. */ -module.exports = function (arrays, i, j, u, v, ) { +module.exports = function (arrays, asmoothing, bsmoothing) { + if (asmoothing && bsmoothing) { + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, ak, k; + + // Since it's a grid of control points, the actual indices are * 3: + i0 *= 3; + j0 *= 3; + + // Precompute some numbers: + var u2 = u * u; + var u3 = u2 * u; + var ou = 1 - u; + var ou2 = ou * ou; + var ou3 = ou2 * ou; + + var v2 = v * v; + var v3 = v2 * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ov3 = ov2 * ov; + + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; + f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; + f2 = ou3 * ak[i0][j0 + 2] + 3 * (ou2 * u * ak[i0 + 1][j0 + 2] + ou * u2 * ak[i0 + 2][j0 + 2]) + u3 * ak[i0 + 3][j0 + 2]; + f3 = ou3 * ak[i0][j0 + 3] + 3 * (ou2 * u * ak[i0 + 1][j0 + 3] + ou * u2 * ak[i0 + 2][j0 + 3]) + u3 * ak[i0 + 3][j0 + 3]; + out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; + } + + return out; + }; + } else if (asmoothing) { + // Handle smooth in the a-direction but linear in the b-direction by performing four + // linear interpolations followed by one cubic interpolation of the result + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + i0 *= 3; + var u2 = u * u; + var u3 = u2 * u; + var ou = 1 - u; + var ou2 = ou * ou; + var ou3 = ou2 * ou; + var ov = 1 - v; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ov * ak[i0 ][j0] + v * ak[i0 ][j0 + 1]; + f1 = ov * ak[i0 + 1][j0] + v * ak[i0 + 1][j0 + 1]; + f2 = ov * ak[i0 + 2][j0] + v * ak[i0 + 2][j0 + 1]; + f3 = ov * ak[i0 + 3][j0] + v * ak[i0 + 3][j0 + 1]; + + out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; + } + return out; + }; + } else if (bsmoothing) { + // Same as the above case, except reversed: + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, f2, f3, k, ak; + j0 *= 3; + var v2 = v * v; + var v3 = v2 * v; + var ov = 1 - v; + var ov2 = ov * ov; + var ov3 = ov2 * ov; + var ou = 1 - u; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; + f2 = ou * ak[i0][j0 + 2] + u * ak[i0 + 1][j0 + 2]; + f3 = ou * ak[i0][j0 + 3] + u * ak[i0 + 1][j0 + 3]; + + out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; + } + return out; + }; + } else { + // Finally, both directions are linear: + return function (out, i0, j0, u, v) { + if (!out) out = []; + var f0, f1, k, ak; + var ov = 1 - v; + var ou = 1 - u; + for (k = 0; k < arrays.length; k++) { + ak = arrays[k]; + f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; + + out[k] = ov * f0 + v * f1; + } + return out; + }; + } } diff --git a/src/traces/carpet/interpolate.js b/src/traces/carpet/interpolate.js new file mode 100644 index 00000000000..7b9b0bc7d32 --- /dev/null +++ b/src/traces/carpet/interpolate.js @@ -0,0 +1,29 @@ +/** +* 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 (f, t) { + var ot2, ot3, t2, t3; + var ot = 1 - t; + + switch(f.length) { + case 4: + ot2 = ot * ot; + ot3 = ot2 * ot; + t2 = t * t; + t3 = t2 * t; + return f[0] * ot3 + 3 * (f[1] * ot2 * t + f[2] * ot * t2) + t3 * f[3]; + case 3: + ot2 = ot * ot; + t2 = t * t; + return f[0] * ot2 + 2 * f[1] * ot * t + t2 * f[2]; + case 2: + return ot * f[0] + t * f[1]; + } +} diff --git a/src/traces/carpet/makepath.js b/src/traces/carpet/makepath.js new file mode 100644 index 00000000000..67d22da2c48 --- /dev/null +++ b/src/traces/carpet/makepath.js @@ -0,0 +1,29 @@ +/** +* 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 makePath (xp, yp, isBicubic) { + // Prevent d3 errors that would result otherwise: + if (xp.length === 0) return ''; + + var i, path = []; + var stride = isBicubic ? 3 : 1; + for (i = 0; i < xp.length; i += stride) { + path.push(xp[i] + ',' + yp[i]); + + if (isBicubic && i < xp.length - stride) { + path.push('C'); + path.push([ + xp[i + 1] + ',' + yp[i + 1], + xp[i + 2] + ',' + yp[i + 2] + ' ', + ].join(' ')); + } + } + return 'M' + path.join(isBicubic ? '' : 'L'); +} diff --git a/src/traces/carpet/map_1d_array.js b/src/traces/carpet/map_1d_array.js new file mode 100644 index 00000000000..f6f5c144443 --- /dev/null +++ b/src/traces/carpet/map_1d_array.js @@ -0,0 +1,33 @@ +/** +* 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'; + +/* + * Map an array of x or y coordinates (c) to screen-space pixel coordinates (p). + * The output array is optional, but if provided, it will be reused without + * reallocation to the extent possible. + */ +module.exports = function mapArray (out, data, func) { + var i, j; + + if (!Array.isArray(out)) { + // If not an array, make it an array: + out = []; + } else if (out.length > data.length) { + // If too long, truncate. (If too short, it will grow + // automatically so we don't care about that case) + out = out.slice(0, data.length) + } + + for (i = 0; i < data.length; i++) { + out[i] = func(data[i]); + } + + return out; +} diff --git a/src/traces/carpet/map_2d_array.js b/src/traces/carpet/map_2d_array.js index eb3e9cee65f..13627597050 100644 --- a/src/traces/carpet/map_2d_array.js +++ b/src/traces/carpet/map_2d_array.js @@ -25,7 +25,7 @@ module.exports = function mapArray (out, data, func) { out = out.slice(0, data.length) } - for (i = 0; i < data[0].length; i++) { + for (i = 0; i < data.length; i++) { if (!Array.isArray(out[i])) { // If not an array, make it an array: out[i] = []; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 997ece414f5..946fd87bcf4 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -12,6 +12,8 @@ var d3 = require('d3'); var Drawing = require('../../components/drawing'); +var map1dArray = require('./map_1d_array'); +var makepath = require('./makepath'); module.exports = function plot(gd, plotinfo, cdcarpet) { for(var i = 0; i < cdcarpet.length; i++) { @@ -39,21 +41,6 @@ function plotOne(gd, plotinfo, cd) { var a = cd[0].a; var b = cd[0].b; - trace.c2p = function (c) { - return [xa.c2p(c[0]), ya.c2p(c[1])]; - }; - - trace.ab2p = function (a, b) { - return trace.c2p(trace.ab2c(a, b)); - }; - - trace.ba2p = function (b, a) { - return trace.c2p(trace.ab2c(a, b)); - }; - - var xp = xa.c2p; - var yp = ya.c2p; - // XXX: Layer choice?? var gridLayer = plotinfo.plot.selectAll('.maplayer'); @@ -62,54 +49,71 @@ function plotOne(gd, plotinfo, cd) { var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); var labelLayer = makeg(gridLayer, 'g', 'labellayer'); - return; + drawGridLines(xa, ya, majorLayer, aax, 'a', trace._agrid, true); + drawGridLines(xa, ya, majorLayer, bax, 'b', trace._bgrid, true); +} - var linesets = [ - { - baseClass: 'const-a-line', - data: aax._tickinfo.values, - crossdata: b, - smoothing: trace.baxis.smoothing, - ax: trace.aaxis, - ab2p: trace.ab2p, - }, - { - baseClass: 'const-b-line', - data: bax._tickinfo.values, - crossdata: a, - smoothing: trace.aaxis.smoothing, - ax: trace.baxis, - ab2p: trace.ba2p, - }, - { - baseClass: 'const-a-boundary-line', - data: [a[0], a[a.length - 1]], - crossdata: b, - smoothing: trace.baxis.smoothing, - ax: trace.aaxis, - ab2p: trace.ab2p, - }, - { - baseClass: 'const-b-boundary-line', - data: [b[0], b[b.length - 1]], - crossdata: a, - smoothing: trace.aaxis.smoothing, - ax: trace.baxis, - ab2p: trace.ba2p, - } - ]; +function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines, isMajor) { + var lineClass = 'const-' + axisLetter + '-lines' + (isMajor ? '' : '-minor'); + var gridjoin = layer.selectAll('.' + lineClass).data(gridlines); - for(var i = 0; i < linesets.length; i++) { - var lineset = linesets[i]; - //drawGridLines(minorLayer, lineset, false); - //drawGridLines(majorLayer, lineset, true); + gridjoin.enter().append('path') + .classed(lineClass, true) + .style('vector-effect', 'non-scaling-stroke'); - //drawAxisLabels(labelLayer, lineset, 'lo'); - //drawAxisLabels(labelLayer, lineset, 'hi'); - } + var width = isMajor ? axis.gridwidth : axis.minorgridwidth; + var color = isMajor ? axis.gridcolor : axis.minorgridcolor; + + gridjoin.each(function(d, i) { + var gridline = d; + var axis = gridline.axis; + var x = gridline.x; + var y = gridline.y; + + var xp = map1dArray([], x, xaxis.c2p); + var yp = map1dArray([], y, yaxis.c2p); + + var d = makepath(xp, yp, gridline.smoothing); + + var el = d3.select(this); + el.attr('d', d) + .style('stroke-width', width) + .style('stroke', color) + .style('fill', 'none'); + }) + .exit().remove(); } -function drawAxisLabels(layer, ls, side) { +function drawAxisLabels(xaxis, yaxis, layer, gridLines, side) { + gridjoin.enter().append('path') + .classed(lineClass, true) + .style('vector-effect', 'non-scaling-stroke'); + + var width = isMajor ? axis.gridwidth : axis.minorgridwidth; + var color = isMajor ? axis.gridcolor : axis.minorgridcolor; + + gridjoin.each(function(d, i) { + var gridline = d; + var axis = gridline.axis; + var x = gridline.x; + var y = gridline.y; + + var xp = map1dArray([], x, xaxis.c2p); + var yp = map1dArray([], y, yaxis.c2p); + + var d = makepath(xp, yp, gridline.smoothing); + + var el = d3.select(this); + el.attr('d', d) + .style('stroke-width', width) + .style('stroke', color) + .style('fill', 'none'); + }) + .exit().remove(); + + + + return; // We'll differentiate the screen-space coordinates to get the orientation of labels: var ax = ls.ax; @@ -191,36 +195,3 @@ function drawAxisLabels(layer, ls, side) { labelJoin.exit().remove(); } - -function drawGridLines(layer, ls, isMajor) { - var ax = ls.ax; - var data = ls.data; - - var lineClass = 'const-' + ls.baseClass + '-lines' + (isMajor ? '' : '-minor'); - var gridjoin = layer.selectAll('.' + lineClass).data(data); - - gridjoin.enter().append('path') - .classed(lineClass, true) - .style('vector-effect', 'non-scaling-stroke'); - - var width = isMajor ? ax.gridwidth : ax.minorgridwidth; - var color = isMajor ? ax.gridcolor : ax.minorgridcolor; - - gridjoin.each(function(d, i) { - var value = data[i]; - - var el = d3.select(this); - el.attr('d', function() { - var pts = []; - for(var j = 0; j < ls.crossdata.length; j++) { - pts.push(ls.ab2p(value, ls.crossdata[j])); - } - return Drawing.smoothopen(pts, ls.smoothing); - //return 'M' + pts.join('L'); - }); - el.style('stroke-width', width) - .style('stroke', color) - .style('fill', 'none'); - }) - .exit().remove(); -} diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js new file mode 100644 index 00000000000..5bab4796e1b --- /dev/null +++ b/src/traces/carpet/set_convert.js @@ -0,0 +1,166 @@ +/** +* 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 search = require('../../lib/search').findBin; +var computeControlPoints = require('./compute_control_points'); +var createSplineEvaluator = require('./create_spline_evaluator'); +var createIDerivativeEvaluator = require('./create_i_derivative_evaluator'); +var createJDerivativeEvaluator = require('./create_j_derivative_evaluator'); + +/* + * Create conversion functions to go from one basis to another. In particular the letter + * abbreviations are: + * + * i: i/j coordinates along the grid. Integer values correspond to data points + * a: real-valued coordinates along the a/b axes + * c: cartesian x-y coordinates + * p: screen-space pixel coordinates + */ +module.exports = function setConvert(trace, xa, ya) { + var a = trace.a; + var b = trace.b; + var na = trace.a.length; + var nb = trace.b.length; + var aax = trace.aaxis; + var bax = trace.baxis; + var x = trace.x; + var y = trace.y; + + // This is potentially a very expensive step! It does the bulk of the work of constructing + // an expanded basis of control points. Note in particular that it overwrites the existing + // basis without creating a new array since that would potentially thrash the garbage + // collector. + var result = computeControlPoints(trace._xctrl, trace._yctrl, x, y, aax.smoothing, bax.smoothing); + trace._xctrl = result[0]; + trace._yctrl = result[1]; + + // This step is the second step in the process, but it's somewhat simpler. It just unrolls + // some logic since it would be unnecessarily expensive to compute both interpolations + // nearly identically but separately and to include a bunch of linear vs. bicubic logic in + // every single call. + trace._evalxy = createSplineEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); + + trace.dxydi = createIDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); + trace.dxydj = createJDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); + + /* + * Convert from i/j data grid coordinates to a/b values. Note in particular that this + * is *linear* interpolation, even if the data is interpolated bicubically. + */ + trace.i2a = function (i) { + var i0 = Math.max(0, Math.floor(i[0]), na - 2); + var ti = i[0] - i0; + return (1 - ti) * a[i0] + ti * a[i0 + 1]; + }; + + trace.j2b = function (j) { + var j0 = Math.max(0, Math.floor(j[1]), na - 2); + var tj = j[1] - j0; + return (1 - tj) * b[j0] + tj * b[j0 + 1]; + }; + + trace.ij2ab = function (ij) { + return [trace.i2a(ij[0]), trace.j2b(ij[1])]; + }; + + /* + * Convert from a/b coordinates to i/j grid-numbered coordinates. This requires searching + * through the a/b data arrays and assumes they are monotonic, which is presumed to have + * been enforced already. + */ + trace.a2i = function (aval) { + var i0 = Math.max(0, Math.min(search(aval, a), na - 2)); + var a0 = a[i0]; + var a1 = a[i0 + 1]; + return Math.max(0, Math.min(na - 1, i0 + (aval - a0) / (a1 - a0))); + }; + + trace.b2j = function (bval) { + var j0 = Math.max(0, Math.min(search(bval, b), nb - 2)); + var b0 = b[j0]; + var b1 = b[j0 + 1]; + return Math.max(0, Math.min(nb - 1, j0 + (bval - b0) / (b1 - b0))); + }; + + trace.ab2ij = function (ab) { + return [trace.a2i(ab[0]), trace.b2j(ab[1])]; + } + + /* + * Convert from i/j coordinates to x/y caretesian coordinates. This means either bilinear + * or bicubic spline evaluation, but the hard part is already done at this point. + */ + trace.i2c = function (ij) { + var i0 = Math.max(0, Math.min(na - 2, Math.floor(ij[0]))); + var ti = ij[0] - i0; + var j0 = Math.max(0, Math.min(nb - 2, Math.floor(ij[1]))); + var tj = ij[1] - j0; + return trace._evalxy([], i0, j0, ti, tj); + }; + + trace.c2p = function (xy) { + return [xa.c2p(xy[0]), ya.c2p(xy[1])]; + }; + + trace.p2x = function (p) { + return [xa.p2c(p[0]), ya.p2c(p[1])]; + }; + + trace.dadi = function (i, u) { + // Right now only a piecewise linear a or b basis is permitted since smoother interpolation + // would cause monotonicity problems. As a retult, u is entirely disregarded in this + // computation, though we'll specify it as a parameter for the sake of completeness and + // future-proofing. It would be possible to use monotonic cubic interpolation, for example. + // + // See: https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + + // u = u || 0; + + var i0 = Math.max(0, Math.min(a.length - 2, i)); + + // The step (demoninator) is implicitly 1 since that's the grid spacing. + return a[i0 + 1] - a[i0]; + }; + + trace.dbdj = function (j, v) { + // See above caveats for dadi which also apply here + var j0 = Math.max(0, Math.min(b.length - 2, j)); + + // The step (demoninator) is implicitly 1 since that's the grid spacing. + return b[j0 + 1] - b[j0]; + }; + + // Takes: grid cell coordinate (i, j) and fractional grid cell coordinates (u, v) + // Returns: (dx/da, dy/db) + // + // NB: separate grid cell + fractional grid cell coordinate format is due to the discontinuous + // derivative, as described better in create_i_derivative_evaluator.js + trace.dxyda = function (i0, j0, u, v) { + var dxydi = trace.dxydi(null, i0, j0, u, v); + var dadi = trace.dadi(i0, u); + + return [dxydi[0] / dadi, dxydi[1] / dbdj]; + }; + + trace.dxydb = function (i0, j0, u, v) { + var dxydj = trace.dxydj(null, i0, j0, u, v); + var dbdj = trace.dbdj(j0, v); + + return [dxydj[0] / dbdj, dxydj[1] / dbdj]; + }; + + trace.dpdx = function () { + return xa._m; + }; + + trace.dpdy = function () { + return ya._m; + }; +} diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js new file mode 100644 index 00000000000..c973948f59e --- /dev/null +++ b/src/traces/scattercarpet/attributes.js @@ -0,0 +1,123 @@ +/** +* 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 scatterAttrs = require('../scatter/attributes'); +var plotAttrs = require('../../plots/attributes'); +var colorAttributes = require('../../components/colorscale/color_attributes'); +var colorbarAttrs = require('../../components/colorbar/attributes'); + +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterMarkerAttrs = scatterAttrs.marker, + scatterLineAttrs = scatterAttrs.line, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; + +module.exports = { + a: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + b: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + c: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + sum: { + valType: 'number', + role: 'info', + dflt: 0, + min: 0, + description: [ + 'The number each triplet should sum to,', + 'if only two of `a`, `b`, and `c` are provided.', + 'This overrides `ternary.sum` to normalize this specific', + 'trace, but does not affect the values displayed on the axes.', + '0 (or missing) means to use ternary.sum' + ].join(' ') + }, + mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}), + text: extendFlat({}, scatterAttrs.text, { + description: [ + 'Sets text elements associated with each (a,b,c) point.', + 'If a single string, the same string appears over', + 'all the data points.', + 'If an array of strings, the items are mapped in order to the', + 'the data points in (a,b,c).' + ].join(' ') + }), + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + shape: extendFlat({}, scatterLineAttrs.shape, + {values: ['linear', 'spline']}), + smoothing: scatterLineAttrs.smoothing + }, + connectgaps: scatterAttrs.connectgaps, + fill: extendFlat({}, scatterAttrs.fill, { + values: ['none', 'toself', 'tonext'], + description: [ + 'Sets the area to fill with a solid color.', + 'Use with `fillcolor` if not *none*.', + 'scatterternary has a subset of the options available to scatter.', + '*toself* connects the endpoints of the trace (or each segment', + 'of the trace if it has gaps) into a closed shape.', + '*tonext* fills the space between two traces if one completely', + 'encloses the other (eg consecutive contour lines), and behaves like', + '*toself* if there is no trace before it. *tonext* should not be', + 'used if one trace does not enclose the other.' + ].join(' ') + }), + fillcolor: scatterAttrs.fillcolor, + marker: extendFlat({}, { + symbol: scatterMarkerAttrs.symbol, + opacity: scatterMarkerAttrs.opacity, + maxdisplayed: scatterMarkerAttrs.maxdisplayed, + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + line: extendFlat({}, + {width: scatterMarkerLineAttrs.width}, + colorAttributes('marker'.line) + ) + }, colorAttributes('marker'), { + showscale: scatterMarkerAttrs.showscale, + colorbar: colorbarAttrs + }), + + textfont: scatterAttrs.textfont, + textposition: scatterAttrs.textposition, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ['a', 'b', 'c', 'text', 'name'] + }), + hoveron: scatterAttrs.hoveron, +}; diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js new file mode 100644 index 00000000000..1ef85810a3c --- /dev/null +++ b/src/traces/scattercarpet/calc.js @@ -0,0 +1,110 @@ +/** +* 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('../scatter/subtypes'); +var calcColorscale = require('../scatter/colorscale_calc'); + +var dataArrays = ['a', 'b', 'c']; +var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; + + +module.exports = function calc(gd, trace) { + var i, j, dataArray, newArray, fillArray1, fillArray2, trace; + + for (i = 0; i < gd.traces.length; i++) { + if (gd.traces[i].carpetid === trace.carpetid) { + trace = gd.traces[i]; + break; + } + } + + if (!trace) return; + + console.log('trace:', trace); + + return; + + var ternary = gd._fullLayout[trace.subplot], + displaySum = ternary.sum, + normSum = trace.sum || displaySum; + + // fill in one missing component + for(i = 0; i < dataArrays.length; i++) { + dataArray = dataArrays[i]; + if(trace[dataArray]) continue; + + fillArray1 = trace[arraysToFill[dataArray][0]]; + fillArray2 = trace[arraysToFill[dataArray][1]]; + newArray = new Array(fillArray1.length); + for(j = 0; j < fillArray1.length; j++) { + newArray[j] = normSum - fillArray1[j] - fillArray2[j]; + } + trace[dataArray] = newArray; + } + + // make the calcdata array + var serieslen = trace.a.length; + var cd = new Array(serieslen); + var a, b, c, norm, x, y; + for(i = 0; i < serieslen; i++) { + a = trace.a[i]; + b = trace.b[i]; + c = trace.c[i]; + if(isNumeric(a) && isNumeric(b) && isNumeric(c)) { + a = +a; + b = +b; + c = +c; + norm = displaySum / (a + b + c); + if(norm !== 1) { + a *= norm; + b *= norm; + c *= norm; + } + // map a, b, c onto x and y where the full scale of y + // is [0, sum], and x is [-sum, sum] + // TODO: this makes `a` always the top, `b` the bottom left, + // and `c` the bottom right. Do we want options to rearrange + // these? + y = a; + x = c - b; + cd[i] = {x: x, y: y, a: a, b: b, c: c}; + } + else cd[i] = {x: false, y: false}; + } + + // fill in some extras + var marker, s; + 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)) { + var ax = {type: 'linear'}; + Axes.setConvert(ax); + s = ax.makeCalcdata(trace.marker, 'size'); + if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); + } + } + + calcColorscale(trace); + + // this has migrated up from arraysToCalcdata as we have a reference to 's' here + if(typeof s !== 'undefined') Lib.mergeArray(s, cd, 'ms'); + + return cd; +}; diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js new file mode 100644 index 00000000000..34c5d345e49 --- /dev/null +++ b/src/traces/scattercarpet/defaults.js @@ -0,0 +1,103 @@ +/** +* 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 constants = require('../scatter/constants'); +var subTypes = require('../scatter/subtypes'); +var handleMarkerDefaults = require('../scatter/marker_defaults'); +var handleLineDefaults = require('../scatter/line_defaults'); +var handleLineShapeDefaults = require('../scatter/line_shape_defaults'); +var handleTextDefaults = require('../scatter/text_defaults'); +var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); + +var attributes = require('./attributes'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var a = coerce('a'), + b = coerce('b'), + c = coerce('c'), + len; + + // allow any one array to be missing, len is the minimum length of those + // present. Note that after coerce data_array's are either Arrays (which + // are truthy even if empty) or undefined. As in scatter, an empty array + // is different from undefined, because it can signify that this data is + // not known yet but expected in the future + if(a) { + len = a.length; + if(b) { + len = Math.min(len, b.length); + if(c) len = Math.min(len, c.length); + } + else if(c) len = Math.min(len, c.length); + else len = 0; + } + else if(b && c) { + len = Math.min(b.length, c.length); + } + + if(!len) { + traceOut.visible = false; + return; + } + + // cut all data arrays down to same length + if(a && len < a.length) traceOut.a = a.slice(0, len); + if(b && len < b.length) traceOut.b = b.slice(0, len); + if(c && len < c.length) traceOut.c = c.slice(0, len); + + coerce('sum'); + + coerce('text'); + + var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; + coerce('mode', defaultMode); + + if(subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleLineShapeDefaults(traceIn, traceOut, coerce); + coerce('connectgaps'); + } + + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if(subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + var dfltHoverOn = []; + + if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { + coerce('marker.maxdisplayed'); + dfltHoverOn.push('points'); + } + + coerce('fill'); + if(traceOut.fill !== 'none') { + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); + } + + coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined); + + if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') { + dfltHoverOn.push('fills'); + } + coerce('hoveron', dfltHoverOn.join('+') || 'points'); +}; diff --git a/src/traces/scattercarpet/hover.js b/src/traces/scattercarpet/hover.js new file mode 100644 index 00000000000..176786ed050 --- /dev/null +++ b/src/traces/scattercarpet/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 scatterHover = require('../scatter/hover'); +var Axes = require('../../plots/cartesian/axes'); + + +module.exports = function hoverPoints(pointData, xval, yval, hovermode) { + var scatterPointData = scatterHover(pointData, xval, yval, hovermode); + if(!scatterPointData || scatterPointData[0].index === false) return; + + var newPointData = scatterPointData[0]; + + // if hovering on a fill, we don't show any point data so the label is + // unchanged from what scatter gives us - except that it needs to + // be constrained to the trianglular plot area, not just the rectangular + // area defined by the synthetic x and y axes + // TODO: in some cases the vertical middle of the shape is not within + // the triangular viewport at all, so the label can become disconnected + // from the shape entirely. But calculating what portion of the shape + // is actually visible, as constrained by the diagonal axis lines, is not + // so easy and anyway we lost the information we would have needed to do + // this inside scatterHover. + if(newPointData.index === undefined) { + var yFracUp = 1 - (newPointData.y0 / pointData.ya._length), + xLen = pointData.xa._length, + xMin = xLen * yFracUp / 2, + xMax = xLen - xMin; + newPointData.x0 = Math.max(Math.min(newPointData.x0, xMax), xMin); + newPointData.x1 = Math.max(Math.min(newPointData.x1, xMax), xMin); + return scatterPointData; + } + + var cdi = newPointData.cd[newPointData.index]; + + newPointData.a = cdi.a; + newPointData.b = cdi.b; + newPointData.c = cdi.c; + + newPointData.xLabelVal = undefined; + newPointData.yLabelVal = undefined; + // TODO: nice formatting, and label by axis title, for a, b, and c? + + var trace = newPointData.trace, + ternary = trace._ternary, + hoverinfo = trace.hoverinfo.split('+'), + text = []; + + function textPart(ax, val) { + text.push(ax._hovertitle + ': ' + Axes.tickText(ax, val, 'hover').text); + } + + if(hoverinfo.indexOf('all') !== -1) hoverinfo = ['a', 'b', 'c']; + if(hoverinfo.indexOf('a') !== -1) textPart(ternary.aaxis, cdi.a); + if(hoverinfo.indexOf('b') !== -1) textPart(ternary.baxis, cdi.b); + if(hoverinfo.indexOf('c') !== -1) textPart(ternary.caxis, cdi.c); + + newPointData.extraText = text.join('
'); + + return scatterPointData; +}; diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js new file mode 100644 index 00000000000..67103f90f10 --- /dev/null +++ b/src/traces/scattercarpet/index.js @@ -0,0 +1,31 @@ +/** +* 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 ScatterCarpet = {}; + +ScatterCarpet.attributes = require('./attributes'); +ScatterCarpet.supplyDefaults = require('./defaults'); +//ScatterCarpet.colorbar = require('../scatter/colorbar'); +ScatterCarpet.calc = require('./calc'); +ScatterCarpet.plot = require('./plot'); +//ScatterCarpet.style = require('./style'); +//ScatterCarpet.hoverPoints = require('./hover'); +//ScatterCarpet.selectPoints = require('./select'); + +ScatterCarpet.moduleType = 'trace'; +ScatterCarpet.name = 'scattercarpet'; +ScatterCarpet.basePlotModule = require('../../plots/cartesian'); +ScatterCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend']; +ScatterCarpet.meta = { + hrName: 'scatter_carpet', + description: [].join(' ') +}; + +module.exports = ScatterCarpet; diff --git a/src/traces/scattercarpet/plot.js b/src/traces/scattercarpet/plot.js new file mode 100644 index 00000000000..742eced81e3 --- /dev/null +++ b/src/traces/scattercarpet/plot.js @@ -0,0 +1,44 @@ +/** +* 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 scatterPlot = require('../scatter/plot'); + + +module.exports = function plot(ternary, data) { + var plotContainer = ternary.plotContainer; + + // remove all nodes inside the scatter layer + plotContainer.select('.scatterlayer').selectAll('*').remove(); + + // mimic cartesian plotinfo + var plotinfo = { + xaxis: ternary.xaxis, + yaxis: ternary.yaxis, + plot: plotContainer + }; + + var calcdata = new Array(data.length), + fullCalcdata = ternary.graphDiv.calcdata; + + for(var i = 0; i < fullCalcdata.length; i++) { + var j = data.indexOf(fullCalcdata[i][0].trace); + + if(j === -1) continue; + + calcdata[j] = fullCalcdata[i]; + + // while we're here and have references to both the Ternary object + // and fullData, connect the two (for use by hover) + data[j]._ternary = ternary; + } + + scatterPlot(ternary.graphDiv, plotinfo, calcdata); +}; diff --git a/src/traces/scattercarpet/select.js b/src/traces/scattercarpet/select.js new file mode 100644 index 00000000000..bca550e040c --- /dev/null +++ b/src/traces/scattercarpet/select.js @@ -0,0 +1,33 @@ +/** +* 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 scatterSelect = require('../scatter/select'); + + +module.exports = function selectPoints(searchInfo, polygon) { + var selection = scatterSelect(searchInfo, polygon); + if(!selection) return; + + var cd = searchInfo.cd, + pt, cdi, i; + + for(i = 0; i < selection.length; i++) { + pt = selection[i]; + cdi = cd[pt.pointNumber]; + pt.a = cdi.a; + pt.b = cdi.b; + pt.c = cdi.c; + delete pt.x; + delete pt.y; + } + + return selection; +}; diff --git a/src/traces/scattercarpet/style.js b/src/traces/scattercarpet/style.js new file mode 100644 index 00000000000..25459e55e74 --- /dev/null +++ b/src/traces/scattercarpet/style.js @@ -0,0 +1,27 @@ +/** +* 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 scatterStyle = require('../scatter/style'); + + +module.exports = function style(gd) { + var modules = gd._fullLayout._modules; + + // we're just going to call scatter style... if we already + // called it, don't need to redo. + // Later though we may want differences, or we may make style + // more specific in its scope, then we can remove this. + for(var i = 0; i < modules.length; i++) { + if(modules[i].name === 'scatter') return; + } + + scatterStyle(gd); +}; From 3199d70615c3bae18fa24c546a23f2567932826d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 14 Dec 2016 09:34:19 -0500 Subject: [PATCH 007/132] Tweak grid defaults --- src/traces/carpet/defaults.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index e333d1766dc..4448e56ecad 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -38,6 +38,15 @@ module.exports = function supplyDefaults(traceIn, traceOut) { handleAxisDefaults(traceIn, traceOut, 'a'); handleAxisDefaults(traceIn, traceOut, 'b'); + + if (traceOut.a.length < 3) { + traceOut.aaxis.smoothing = 0; + } + + if (traceOut.b.length < 3) { + traceOut.baxis.smoothing = 0; + } + }; function handleAxisDefaults(traceIn, traceOut, axis) { @@ -65,16 +74,16 @@ function handleAxisDefaults(traceIn, traceOut, axis) { //coerce('gridoffset'); //coerce('gridstep'); - coerce('startline'); - coerce('startlinewidth'); - coerce('startlinecolor'); - coerce('endline'); - coerce('endlinewidth'); - coerce('endlinecolor'); - coerce('gridwidth'); coerce('gridcolor'); + coerce('startline'); + coerce('startlinewidth', traceOut.gridwidth); + coerce('startlinecolor', traceOut.gridcolor); + coerce('endline'); + coerce('endlinewidth', traceOut.gridwidth); + coerce('endlinecolor', traceOut.gridwidth); + coerce('minorgridwidth'); coerce('minorgridcolor'); From 2016ae25d4c94012938a6866bc632c14c4de6e7e Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 14 Dec 2016 15:17:01 -0500 Subject: [PATCH 008/132] Tweak gridline code --- src/traces/carpet/axis_attributes.js | 17 +- src/traces/carpet/calc.js | 4 +- src/traces/carpet/calc_gridlines.js | 335 ++++++++++++++++++--------- src/traces/carpet/defaults.js | 2 + src/traces/carpet/plot.js | 24 +- 5 files changed, 253 insertions(+), 129 deletions(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 4e555cdd1a2..f4fdecbbdeb 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -14,8 +14,8 @@ var colorAttrs = require('../../components/color/attributes'); module.exports = { smoothing: { - valType: 'number', - dflt: 1.0, + valType: 'boolean', + dflt: true, role: 'info' }, cheatertype: { @@ -124,7 +124,7 @@ module.exports = { }, gridcolor: { valType: 'color', - dflt: colorAttrs.defaultLine, + dflt: '#aaa', role: 'style', description: 'Sets the color of the grid lines.' }, @@ -147,7 +147,7 @@ module.exports = { }, startlinecolor: { valType: 'color', - dflt: colorAttrs.lightLine, + dflt: colorAttrs.defaultLine, role: 'style', description: 'Sets the color of the grid lines.' }, @@ -160,10 +160,17 @@ module.exports = { }, endlinecolor: { valType: 'color', - dflt: colorAttrs.lightLine, + dflt: colorAttrs.defaultLine, role: 'style', description: 'Sets the color of the grid lines.' }, + minorgridcount: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info', + description: 'Sets the number of minor grid ticks per major grid tick' + }, minorgridwidth: { valType: 'number', min: 0, diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index d93497e49a8..1b3239c27af 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -66,8 +66,8 @@ module.exports = function calc(gd, trace) { // Create conversions from one coordinate system to another: setConvert(trace, xa, ya); - trace._agrid = calcGridlines(trace, 'a', 'b', xa, ya); - trace._bgrid = calcGridlines(trace, 'b', 'a', xa, ya); + calcGridlines(trace, 'a', 'b', xa, ya); + calcGridlines(trace, 'b', 'a', xa, ya); return [{ x: x, diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 262f9e5210e..f7dc2b11d7b 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -19,14 +19,18 @@ var setConvert = require('./set_convert'); var map2dArray = require('./map_2d_array'); var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); +var extendFlat = require('../../lib/extend').extendFlat; module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xaxis, yaxis) { var i, j, gridline, j0, i0; - var gridlines = []; var data = trace[axisLetter]; var axis = trace[axisLetter + 'axis']; + var gridlines = axis._gridlines = []; + var minorgridlines = axis._minorgridlines = []; + var boundarylines = axis._boundarylines = []; + var crossData = trace[crossAxisLetter]; var crossAxis = trace[crossAxisLetter + 'axis']; @@ -37,54 +41,204 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xax var na = trace.a.length; var nb = trace.b.length; + // The default is an empty array that will cause the join to remove the gridline if + // it's just disappeared: + axis._startline = axis._endline = []; + + // If the cross axis uses bicubic interpolation, then the grid + // lines fall once every three expanded grid row/cols: + var stride = axis.smoothing ? 3 : 1; + + function constructValueGridline (value) { + var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; + var xpoints = []; + var ypoints = []; + // Search for the fractional grid index giving this line: + if (axisLetter === 'b') { + j = trace.b2j(value); + j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); + tj = j - j0; + + for (var i = 0; i < na; i++) { + i0 = Math.min(na - 2, i); + ti = i - i0; + xy = trace._evalxy([], i0, j0, ti, tj); + + if (crossAxis.smoothing && i > 0) { + // First control point: + dxydi0 = trace.dxydi([], i - 1, j0, 0, tj); + xpoints.push(pxy[0] + dxydi0[0] / 3); + ypoints.push(pxy[1] + dxydi0[1] / 3); + + // Second control point: + dxydi1 = trace.dxydi([], i - 1, j0, 1, tj); + xpoints.push(xy[0] - dxydi1[0] / 3); + ypoints.push(xy[1] - dxydi1[1] / 3); + } + + xpoints.push(xy[0]) + ypoints.push(xy[1]) + + pxy = xy; + } + } else { + i = trace.a2i(value); + i0 = Math.floor(Math.max(0, Math.min(na - 2, i))); + ti = i - i0; + + for (var j = 0; j < nb; j++) { + j0 = Math.min(nb - 2, j); + tj = j - j0; + xy = trace._evalxy([], i0, j0, ti, tj); + + if (crossAxis.smoothing && j > 0) { + // First control point: + dxydj0 = trace.dxydj([], i0, j - 1, ti, 0); + xpoints.push(pxy[0] + dxydj0[0] / 3); + ypoints.push(pxy[1] + dxydj0[1] / 3); + + // Second control point: + dxydj1 = trace.dxydj([], i0, j - 1, ti, 1); + xpoints.push(xy[0] - dxydj1[0] / 3); + ypoints.push(xy[1] - dxydj1[1] / 3); + } + + xpoints.push(xy[0]) + ypoints.push(xy[1]) + + pxy = xy; + } + } + + return { + axisLetter: axisLetter, + axis: trace[axisLetter + 'axis'], + value: value, + constvar: crossAxisLetter, + index: n, + x: xpoints, + y: ypoints, + //dxy_0: dxy_0, + //dxy_1: dxy_1, + smoothing: crossAxis.smoothing, + }; + } + + function constructArrayGridline (j) { + var dxy_0, dxy_1; + var xpoints = []; + var ypoints = []; + + if (axisLetter === 'b') { + // In the tickmode: array case, this operation is a simple + // transfer of data: + for (i = 0; i < nea; i++) { + xpoints[i] = xcp[i][j * stride]; + ypoints[i] = ycp[i][j * stride]; + } + + j0 = Math.min(j, nb - 2); + dxy_0 = trace.dxydi(null, 0, j0, 0, j - j0); + dxy_1 = trace.dxydi(null, na - 2, j0, 1, j - j0); + } else { + // In the tickmode: array case, this operation is a simple + // transfer of data: + for (i = 0; i < neb; i++) { + xpoints[i] = xcp[j * stride][i]; + ypoints[i] = ycp[j * stride][i]; + } + + i0 = Math.min(i, na - 2); + dxy_0 = trace.dxydj(null, i0, 0, i - i0, 0); + dxy_1 = trace.dxydj(null, i0, nb - 2, i - i0, 1); + } + + return { + axisLetter: axisLetter, + axis: trace[axisLetter + 'axis'], + value: data[j], + constvar: crossAxisLetter, + index: j, + x: xpoints, + y: ypoints, + dxy_0: dxy_0, + dxy_1: dxy_1, + smoothing: crossAxis.smoothing, + }; + }; + + if (axis.tickmode === 'array') { + var j0, j1; + //var j0 = axis.startline ? 1 : 0; + //var j1 = data.length - (axis.endline ? 1 : 0); + + var eps = 5e-15; + var bounds = [ + Math.floor(((data.length - 1) - axis.arraytick0) / axis.arraydtick * (1 + eps)), + Math.ceil((- axis.arraytick0) / axis.arraydtick / (1 + eps)) + ].sort(function (a, b) {return a - b}); + + // Unpack sorted values so we can be sure to avoid infinite loops if something + // is backwards: + var n1 = bounds[0] - 1; + var n2 = bounds[1] + 1; + // If the axes fall along array lines, then this is a much simpler process since // we already have all the control points we need - for (j = 0; j < data.length; j++) { - var dxy_0, dxy_1; - var xpoints = []; - var ypoints = []; - - // If the cross axis uses bicubic interpolation, then the grid - // lines fall once every three expanded grid row/cols: - var stride = axis.smoothing ? 3 : 1; - - if (axisLetter === 'b') { - // In the tickmode: array case, this operation is a simple - // transfer of data: - for (i = 0; i < nea; i++) { - xpoints[i] = xcp[i][j * stride]; - ypoints[i] = ycp[i][j * stride]; - } + for (n = n1; n < n2; n++) { + j = axis.arraytick0 + axis.arraydtick * n; + if (j < 0 || j > data.length - 1) continue; + gridlines.push(extendFlat(constructArrayGridline(j), { + color: axis.gridcolor, + width: axis.gridwidth + })); + } - j0 = Math.min(j, nb - 2); - dxy_0 = trace.dxydi(null, 0, j0, 0, j - j0); - dxy_1 = trace.dxydi(null, na - 2, j0, 1, j - j0); - } else { - // In the tickmode: array case, this operation is a simple - // transfer of data: - for (i = 0; i < neb; i++) { - xpoints[i] = xcp[j * stride][i]; - ypoints[i] = ycp[j * stride][i]; - } + for (n = n1; n < n2; n++) { + j0 = axis.arraytick0 + axis.arraydtick * n; + j1 = Math.min(j0 + axis.arraydtick, data.length - 1); - i0 = Math.min(i, na - 2); - dxy_0 = trace.dxydj(null, i0, 0, i - i0, 0); - dxy_1 = trace.dxydj(null, i0, nb - 2, i - i0, 1); + // TODO: fix the bounds computation so we don't have to do a large range and then throw + // out unneeded numbers + if (j0 < 0 || j0 > data.length - 1) continue; + if (j1 < 0 || j1 > data.length - 1) continue; + + var v0 = data[j0]; + var v1 = data[j1]; + + for (i = 0; i < axis.minorgridcount; i++) { + var d = j1 - j0; + + // TODO: fix the bounds computation so we don't have to do a large range and then throw + // out unneeded numbers + if (d <= 0) continue; + + // XXX: This calculation isn't quite right. Off by one somewhere? + var v = v0 + (v1 - v0) * (i + 1) / (axis.minorgridcount + 1) * (axis.arraydtick / d); + + // TODO: fix the bounds computation so we don't have to do a large range and then throw + // out unneeded numbers + if (v < data[0] || v > data[data.length - 1]) continue; + minorgridlines.push(extendFlat(constructValueGridline(v), { + color: axis.minorgridcolor, + width: axis.minorgridwidth + })); } + } + + if (axis.startline) { + boundarylines.push(extendFlat(constructArrayGridline(0), { + color: axis.startlinecolor, + width: axis.startlinewidth + })); + } - gridlines.push({ - axisLetter: axisLetter, - axis: trace[axisLetter + 'axis'], - value: data[j], - constvar: crossAxisLetter, - index: j, - x: xpoints, - y: ypoints, - dxy_0: dxy_0, - dxy_1: dxy_1, - smoothing: crossAxis.smoothing, - }); + if (axis.endline) { + boundarylines.push(extendFlat(constructArrayGridline(data.length - 1), { + color: axis.endlinecolor, + width: axis.endlinewidth + })); } } else { // If the lines do not fall along the axes, then we have to interpolate @@ -107,84 +261,39 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xax var n2 = bounds[1]; for (var n = n1; n <= n2; n++) { - var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; - var xpoints = []; - var ypoints = []; var value = axis.tick0 + axis.dtick * n; - // Search for the fractional grid index giving this line: - if (axisLetter === 'b') { - j = trace.b2j(value); - j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); - tj = j - j0; - - for (var i = 0; i < na; i++) { - i0 = Math.min(na - 2, i); - ti = i - i0; - xy = trace._evalxy([], i0, j0, ti, tj); - - if (crossAxis.smoothing && i > 0) { - // First control point: - dxydi0 = trace.dxydi([], i - 1, j0, 0, tj); - xpoints.push(pxy[0] + dxydi0[0] / 3); - ypoints.push(pxy[1] + dxydi0[1] / 3); - - // Second control point: - dxydi1 = trace.dxydi([], i - 1, j0, 1, tj); - xpoints.push(xy[0] - dxydi1[0] / 3); - ypoints.push(xy[1] - dxydi1[1] / 3); - } - - xpoints.push(xy[0]) - ypoints.push(xy[1]) - - pxy = xy; - } - } else { - i = trace.a2i(value); - i0 = Math.floor(Math.max(0, Math.min(na - 2, i))); - ti = i - i0; - - for (var j = 0; j < nb; j++) { - j0 = Math.min(nb - 2, j); - tj = j - j0; - xy = trace._evalxy([], i0, j0, ti, tj); - - if (crossAxis.smoothing && j > 0) { - // First control point: - dxydj0 = trace.dxydj([], i0, j - 1, ti, 0); - xpoints.push(pxy[0] + dxydj0[0] / 3); - ypoints.push(pxy[1] + dxydj0[1] / 3); - - // Second control point: - dxydj1 = trace.dxydj([], i0, j - 1, ti, 1); - xpoints.push(xy[0] - dxydj1[0] / 3); - ypoints.push(xy[1] - dxydj1[1] / 3); - } + gridlines.push(extendFlat(constructValueGridline(value), { + color: axis.gridcolor, + width: axis.gridwidth + })); + } - xpoints.push(xy[0]) - ypoints.push(xy[1]) + for (n = n1 - 1; n < n2 + 1; n++) { + value = axis.tick0 + axis.dtick * n; - pxy = xy; - } + for (i = 0; i < axis.minorgridcount; i++) { + var v = value + axis.dtick * (i + 1) / (axis.minorgridcount + 1) + if (v < data[0] || v > data[data.length - 1]) continue; + minorgridlines.push(extendFlat(constructValueGridline(v), { + color: axis.minorgridcolor, + width: axis.minorgridwidth + })); } + } + if (axis.startline) { + boundarylines.push(extendFlat(constructValueGridline(data[0]), { + color: axis.startlinecolor, + width: axis.startlinewidth + })); + } - gridlines.push({ - axisLetter: axisLetter, - axis: trace[axisLetter + 'axis'], - value: value, - constvar: crossAxisLetter, - index: n, - x: xpoints, - y: ypoints, - //dxy_0: dxy_0, - //dxy_1: dxy_1, - smoothing: crossAxis.smoothing, - }); + if (axis.endline) { + boundarylines.push(extendFlat(constructValueGridline(data[data.length - 1]), { + color: axis.endlinecolor, + width: axis.endlinewidth + })); } } - - return gridlines; - } diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 4448e56ecad..9cab51f51cd 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -58,6 +58,7 @@ function handleAxisDefaults(traceIn, traceOut, axis) { } coerce('smoothing'); + traceOut.smoothing = traceOut.smoothing ? 1 : 0; coerce('cheatertype'); coerce('showlabels'); @@ -84,6 +85,7 @@ function handleAxisDefaults(traceIn, traceOut, axis) { coerce('endlinewidth', traceOut.gridwidth); coerce('endlinecolor', traceOut.gridwidth); + coerce('minorgridcount'); coerce('minorgridwidth'); coerce('minorgridcolor'); diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 946fd87bcf4..522d6cfab79 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -47,23 +47,29 @@ function plotOne(gd, plotinfo, cd) { var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); var majorLayer = makeg(gridLayer, 'g', 'majorlayer'); var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); + var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); var labelLayer = makeg(gridLayer, 'g', 'labellayer'); - drawGridLines(xa, ya, majorLayer, aax, 'a', trace._agrid, true); - drawGridLines(xa, ya, majorLayer, bax, 'b', trace._bgrid, true); + drawGridLines(xa, ya, majorLayer, aax, 'a', aax._gridlines, true); + drawGridLines(xa, ya, majorLayer, bax, 'b', bax._gridlines, true); + + drawGridLines(xa, ya, minorLayer, aax, 'a', aax._minorgridlines, true); + drawGridLines(xa, ya, minorLayer, bax, 'b', bax._minorgridlines, true); + + // NB: These are not ommitted if the lines are not active. The joins must be executed + // in order for them to get cleaned up without a full redraw + drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); + drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); } -function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines, isMajor) { - var lineClass = 'const-' + axisLetter + '-lines' + (isMajor ? '' : '-minor'); +function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { + var lineClass = 'const-' + axisLetter + '-lines'; var gridjoin = layer.selectAll('.' + lineClass).data(gridlines); gridjoin.enter().append('path') .classed(lineClass, true) .style('vector-effect', 'non-scaling-stroke'); - var width = isMajor ? axis.gridwidth : axis.minorgridwidth; - var color = isMajor ? axis.gridcolor : axis.minorgridcolor; - gridjoin.each(function(d, i) { var gridline = d; var axis = gridline.axis; @@ -77,8 +83,8 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines, isMajor var el = d3.select(this); el.attr('d', d) - .style('stroke-width', width) - .style('stroke', color) + .style('stroke-width', gridline.width) + .style('stroke', gridline.color) .style('fill', 'none'); }) .exit().remove(); From ce290aed7e6387c688bdbab000a4c86f98ea0dd3 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 15 Dec 2016 09:30:05 -0500 Subject: [PATCH 009/132] Fix tons of interpolation bugs --- src/traces/carpet/calc.js | 8 +- src/traces/carpet/calc_gridlines.js | 126 ++++++++++++------ src/traces/carpet/calc_labels.js | 55 ++++++++ .../carpet/create_j_derivative_evaluator.js | 18 ++- src/traces/carpet/plot.js | 76 +++-------- src/traces/carpet/set_convert.js | 10 +- 6 files changed, 183 insertions(+), 110 deletions(-) create mode 100644 src/traces/carpet/calc_labels.js diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 1b3239c27af..c0033133665 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -17,6 +17,7 @@ var map2dArray = require('./map_2d_array'); var createSplineEvaluator = require('./create_spline_evaluator'); var setConvert = require('./set_convert'); var calcGridlines = require('./calc_gridlines'); +var calcLabels = require('./calc_labels'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), @@ -66,8 +67,11 @@ module.exports = function calc(gd, trace) { // Create conversions from one coordinate system to another: setConvert(trace, xa, ya); - calcGridlines(trace, 'a', 'b', xa, ya); - calcGridlines(trace, 'b', 'a', xa, ya); + calcGridlines(trace, 'a', 'b'); + calcGridlines(trace, 'b', 'a'); + + calcLabels(trace, aax); + calcLabels(trace, bax); return [{ x: x, diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index f7dc2b11d7b..8e75f882166 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -21,7 +21,7 @@ var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); var extendFlat = require('../../lib/extend').extendFlat; -module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xaxis, yaxis) { +module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { var i, j, gridline, j0, i0; var data = trace[axisLetter]; @@ -53,11 +53,24 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xax var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; var xpoints = []; var ypoints = []; + var ret = {}; // Search for the fractional grid index giving this line: if (axisLetter === 'b') { j = trace.b2j(value); j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); tj = j - j0; + ret.length = nb; + ret.crossLength = na; + + ret.xy = function (i) { + var i0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(i))); + var ti = i - i0; + return trace._evalxy([], i0, j0, ti, tj); + } + + ret.dxy = function (i0, ti) { + return trace.dxydi([], i0, j0, ti, tj); + }; for (var i = 0; i < na; i++) { i0 = Math.min(na - 2, i); @@ -85,6 +98,18 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xax i = trace.a2i(value); i0 = Math.floor(Math.max(0, Math.min(na - 2, i))); ti = i - i0; + ret.length = na; + ret.crossLength = nb; + + ret.xy = function (j) { + var j0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(j))); + var tj = j - j0; + return trace._evalxy([], i0, j0, ti, tj); + } + + ret.dxy = function (j0, tj) { + return trace.dxydj([], i0, j0, ti, tj); + }; for (var j = 0; j < nb; j++) { j0 = Math.min(nb - 2, j); @@ -110,61 +135,84 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter, xax } } - return { - axisLetter: axisLetter, - axis: trace[axisLetter + 'axis'], - value: value, - constvar: crossAxisLetter, - index: n, - x: xpoints, - y: ypoints, - //dxy_0: dxy_0, - //dxy_1: dxy_1, - smoothing: crossAxis.smoothing, - }; + ret.axisLetter = axisLetter; + ret.axis = axis; + ret.crossAxis = crossAxis; + ret.value = value; + ret.constvar = crossAxisLetter; + ret.index = n; + ret.x = xpoints; + ret.y = ypoints; + ret.smoothing = crossAxis.smoothing; + + return ret; } - function constructArrayGridline (j) { - var dxy_0, dxy_1; + function constructArrayGridline (idx) { var xpoints = []; var ypoints = []; + var ret = {}; + ret.length = data.length; + ret.crossLength = crossData.length; if (axisLetter === 'b') { + var j0 = Math.max(0, Math.min(nb - 2, idx)); + var tj = Math.min(1, Math.max(0, idx - j0)); + + ret.xy = function (i) { + var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); + var ti = Math.min(1, Math.max(0, i - i0)); + return trace._evalxy([], i0, j0, ti, tj); + }; + + ret.dxy = function (i) { + var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); + var ti = Math.min(1, Math.max(0, i - i0)); + return trace.dxydi([], i0, j0, ti, tj); + }; + // In the tickmode: array case, this operation is a simple // transfer of data: for (i = 0; i < nea; i++) { - xpoints[i] = xcp[i][j * stride]; - ypoints[i] = ycp[i][j * stride]; + xpoints[i] = xcp[i][idx * stride]; + ypoints[i] = ycp[i][idx * stride]; } - - j0 = Math.min(j, nb - 2); - dxy_0 = trace.dxydi(null, 0, j0, 0, j - j0); - dxy_1 = trace.dxydi(null, na - 2, j0, 1, j - j0); } else { + var i0 = Math.max(0, Math.min(na - 2, idx)); + var ti = Math.min(1, Math.max(0, idx - i0)); + + ret.xy = function (j) { + var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); + var tj = Math.min(1, Math.max(0, j - j0)); + return trace._evalxy([], i0, j0, ti, tj); + } + + + ret.dxy = function (j) { + var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); + var tj = Math.min(1, Math.max(0, j - j0)); + return trace.dxydj([], i0, j0, ti, tj); + }; + // In the tickmode: array case, this operation is a simple // transfer of data: for (i = 0; i < neb; i++) { - xpoints[i] = xcp[j * stride][i]; - ypoints[i] = ycp[j * stride][i]; + xpoints[i] = xcp[idx * stride][i]; + ypoints[i] = ycp[idx * stride][i]; } - - i0 = Math.min(i, na - 2); - dxy_0 = trace.dxydj(null, i0, 0, i - i0, 0); - dxy_1 = trace.dxydj(null, i0, nb - 2, i - i0, 1); } - return { - axisLetter: axisLetter, - axis: trace[axisLetter + 'axis'], - value: data[j], - constvar: crossAxisLetter, - index: j, - x: xpoints, - y: ypoints, - dxy_0: dxy_0, - dxy_1: dxy_1, - smoothing: crossAxis.smoothing, - }; + ret.axisLetter = axisLetter; + ret.axis = axis; + ret.crossAxis = crossAxis; + ret.value = data[idx]; + ret.constvar = crossAxisLetter; + ret.index = idx; + ret.x = xpoints; + ret.y = ypoints; + ret.smoothing = crossAxis.smoothing; + + return ret; }; diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js new file mode 100644 index 00000000000..aabf7b2d8a6 --- /dev/null +++ b/src/traces/carpet/calc_labels.js @@ -0,0 +1,55 @@ +/** +* 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'); +var cheaterBasis = require('./cheater_basis'); +var arrayMinmax = require('./array_minmax'); +var search = require('../../lib/search').findBin; +var computeControlPoints = require('./compute_control_points'); +var map2dArray = require('./map_2d_array'); +var createSplineEvaluator = require('./create_spline_evaluator'); +var setConvert = require('./set_convert'); +var map2dArray = require('./map_2d_array'); +var map1dArray = require('./map_1d_array'); +var makepath = require('./makepath'); +var extendFlat = require('../../lib/extend').extendFlat; + +function normalize (x) { + var x +} + +module.exports = function calcLabels (trace, axis) { + var i; + + var labels = axis._labels = []; + var gridlines = axis._gridlines; + + for (i = 0; i < gridlines.length; i++) { + var gridline = gridlines[i]; + + labels.push({ + text: gridline.value.toFixed(3), + endAnchor: true, + xy: gridline.xy(0), + dxy: gridline.dxy(0, 0), + axis: gridline.axis, + length: gridline.crossAxis.length + }); + + labels.push({ + text: gridline.value.toFixed(3), + endAnchor: false, + xy: gridline.xy(gridline.crossLength - 1), + dxy: gridline.dxy(gridline.crossLength - 2, 1), + axis: gridline.axis, + length: gridline.crossAxis.length + }); + } +}; diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index 055e1052b45..b4511ce11d2 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -61,10 +61,18 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov = 1 - v; for (k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0][j0 ] + u * (2 - 3 * u) * ak[i0][j0 ] + u2 * ak[i0][j0 ]); - f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0][j0 + 1] + u * (2 - 3 * u) * ak[i0][j0 + 1] + u2 * ak[i0][j0 + 1]); - out[k] = ov * f0 + v * f1; + f0 = ak[i0 ][j0 + 1] - ak[i0 ][j0]; + f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; + f2 = ak[i0 + 2][j0 + 1] - ak[i0 + 2][j0]; + f3 = ak[i0 + 3][j0 + 1] - ak[i0 + 3][j0]; + + out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; + + // mathematically equivalent: + //f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; + //f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; + //out[k] = f1 - f0; } return out; }; @@ -95,8 +103,8 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov = 1 - v; for (k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0 + 1][j0] - ak[i0][j0]; - f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; + f0 = ak[i0][j0 + 1] - ak[i0][j0]; + f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; out[k] = ov * f0 + v * f1; } diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 522d6cfab79..81ef9868641 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -60,6 +60,9 @@ function plotOne(gd, plotinfo, cd) { // in order for them to get cleaned up without a full redraw drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); + + drawAxisLabels(xa, ya, trace, labelLayer, aax._labels, 'a-label'); + drawAxisLabels(xa, ya, trace, labelLayer, bax._labels, 'b-label'); } function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { @@ -90,56 +93,15 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { .exit().remove(); } -function drawAxisLabels(xaxis, yaxis, layer, gridLines, side) { - gridjoin.enter().append('path') - .classed(lineClass, true) - .style('vector-effect', 'non-scaling-stroke'); - - var width = isMajor ? axis.gridwidth : axis.minorgridwidth; - var color = isMajor ? axis.gridcolor : axis.minorgridcolor; - - gridjoin.each(function(d, i) { - var gridline = d; - var axis = gridline.axis; - var x = gridline.x; - var y = gridline.y; - - var xp = map1dArray([], x, xaxis.c2p); - var yp = map1dArray([], y, yaxis.c2p); - - var d = makepath(xp, yp, gridline.smoothing); - - var el = d3.select(this); - el.attr('d', d) - .style('stroke-width', width) - .style('stroke', color) - .style('fill', 'none'); - }) - .exit().remove(); - - - - return; - // We'll differentiate the screen-space coordinates to get the orientation of labels: - var ax = ls.ax; - - // Check whether labels are active for this set: - var active = [side === 'lo' ? 'start' : 'end', 'both'].indexOf(ax.showlabels) !== -1; - - // *Always* do the join. Otherwise labels can't disappear without a total replot - var joinData = active ? ax._majorIndices : []; - - var labelClass = ls.baseClass + '-axis-labels-' + side; - var labelJoin = layer.selectAll('text.' + labelClass).data(joinData); +function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { + var labelJoin = layer.selectAll('text.' + labelClass).data(labels); labelJoin.enter().append('text') .classed(labelClass, true); - var idx0 = side === 'lo' ? 0 : ls.n - 1; - var idx1 = side === 'lo' ? 1 : ls.n - 2; - labelJoin.each(function(d, i) { - var lineIdx = ax._majorIndices[i]; + var label = d; + var ax = d.axis; var el = d3.select(this); @@ -169,19 +131,12 @@ function drawAxisLabels(xaxis, yaxis, layer, gridLines, side) { break; } - var x0 = ls.xc(lineIdx, idx0); - var x1 = ls.xc(lineIdx, idx1); - var y0 = ls.yc(lineIdx, idx0); - var y1 = ls.yc(lineIdx, idx1); - var dx = x1 - x0; - var dy = y1 - y0; - var l = Math.sqrt(dx * dx + dy * dy); - dx /= l; - dy /= l; + var dx = label.dxy[0] * trace.dpdx(xaxis); + var dy = label.dxy[1] * trace.dpdy(yaxis); var angle = Math.atan2(dy, dx) * 180 / Math.PI; - var endAnchor = true; + var endAnchor = label.endAnchor; if(angle < -90) { angle += 180; endAnchor = !endAnchor; @@ -190,12 +145,15 @@ function drawAxisLabels(xaxis, yaxis, layer, gridLines, side) { endAnchor = !endAnchor; } + // Convert coordinates to pixel coordinates: + var xy = trace.c2p(label.xy, xaxis, yaxis); + // XXX: Use existing text functions - el.attr('x', x0 + ax.labelpadding * (endAnchor ? -1 : 1)) // These are pre-transform offsets - .attr('y', y0 + 5) // Shift down to hackily vertically center + el.attr('x', xy[0] + ax.labelpadding * (endAnchor ? -1 : 1)) // These are pre-transform offsets + .attr('y', xy[1] + 5) // Shift down to hackily vertically center .attr('text-anchor', endAnchor ? 'end' : 'start') - .text(prefix + ls.data[lineIdx].toFixed(3) + suffix) - .attr('transform', 'rotate(' + angle + ' ' + x0 + ',' + y0 + ')') + .text(prefix + label.text + suffix) + .attr('transform', 'rotate(' + angle + ' ' + xy[0] + ',' + xy[1] + ')') .call(Drawing.font, ax.labelfont.family, ax.labelfont.size, ax.labelfont.color); }); diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 5bab4796e1b..6ebaa2a2223 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -23,7 +23,7 @@ var createJDerivativeEvaluator = require('./create_j_derivative_evaluator'); * c: cartesian x-y coordinates * p: screen-space pixel coordinates */ -module.exports = function setConvert(trace, xa, ya) { +module.exports = function setConvert(trace) { var a = trace.a; var b = trace.b; var na = trace.a.length; @@ -105,11 +105,11 @@ module.exports = function setConvert(trace, xa, ya) { return trace._evalxy([], i0, j0, ti, tj); }; - trace.c2p = function (xy) { + trace.c2p = function (xy, xa, ya) { return [xa.c2p(xy[0]), ya.c2p(xy[1])]; }; - trace.p2x = function (p) { + trace.p2x = function (p, xa, ya) { return [xa.p2c(p[0]), ya.p2c(p[1])]; }; @@ -156,11 +156,11 @@ module.exports = function setConvert(trace, xa, ya) { return [dxydj[0] / dbdj, dxydj[1] / dbdj]; }; - trace.dpdx = function () { + trace.dpdx = function (xa) { return xa._m; }; - trace.dpdy = function () { + trace.dpdy = function (ya) { return ya._m; }; } From 4e4c093f73177515e246e443a8c97b7640db6500 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 15 Dec 2016 09:56:59 -0500 Subject: [PATCH 010/132] Fix carpet label details --- src/traces/carpet/axis_attributes.js | 5 +++- src/traces/carpet/calc_gridlines.js | 8 ++---- src/traces/carpet/calc_labels.js | 40 ++++++++++++++++------------ src/traces/carpet/defaults.js | 10 ++++++- src/traces/carpet/plot.js | 2 +- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index f4fdecbbdeb..2f2e20d1891 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -84,7 +84,10 @@ module.exports = { role: 'style', description: 'Same as `showtickprefix` but for tick suffixes.' }, - labelfont: extendFlat({}, fontAttrs, { + startlabelfont: extendFlat({}, fontAttrs, { + description: 'Sets the label font.' + }), + endlabelfont: extendFlat({}, fontAttrs, { description: 'Sets the label font.' }), tick0: { diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 8e75f882166..745505e67cb 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -165,9 +165,7 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { return trace._evalxy([], i0, j0, ti, tj); }; - ret.dxy = function (i) { - var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); - var ti = Math.min(1, Math.max(0, i - i0)); + ret.dxy = function (i0, ti) { return trace.dxydi([], i0, j0, ti, tj); }; @@ -188,9 +186,7 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { } - ret.dxy = function (j) { - var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); - var tj = Math.min(1, Math.max(0, j - j0)); + ret.dxy = function (j0, tj) { return trace.dxydj([], i0, j0, ti, tj); }; diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index aabf7b2d8a6..7f16e2bc896 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -34,22 +34,28 @@ module.exports = function calcLabels (trace, axis) { for (i = 0; i < gridlines.length; i++) { var gridline = gridlines[i]; - labels.push({ - text: gridline.value.toFixed(3), - endAnchor: true, - xy: gridline.xy(0), - dxy: gridline.dxy(0, 0), - axis: gridline.axis, - length: gridline.crossAxis.length - }); - - labels.push({ - text: gridline.value.toFixed(3), - endAnchor: false, - xy: gridline.xy(gridline.crossLength - 1), - dxy: gridline.dxy(gridline.crossLength - 2, 1), - axis: gridline.axis, - length: gridline.crossAxis.length - }); + if (['start', 'both'].indexOf(axis.showlabels) !== -1) { + labels.push({ + text: gridline.value.toFixed(3), + endAnchor: true, + xy: gridline.xy(0), + dxy: gridline.dxy(0, 0), + axis: gridline.axis, + length: gridline.crossAxis.length, + font: gridline.crossAxis.startlabelfont + }); + } + + if (['end', 'both'].indexOf(axis.showlabels) !== -1) { + labels.push({ + text: gridline.value.toFixed(3), + endAnchor: false, + xy: gridline.xy(gridline.crossLength - 1), + dxy: gridline.dxy(gridline.crossLength - 2, 1), + axis: gridline.axis, + length: gridline.crossAxis.length, + font: gridline.crossAxis.endlabelfont + }); + } } }; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 9cab51f51cd..67b9274ee40 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -94,7 +94,15 @@ function handleAxisDefaults(traceIn, traceOut, axis) { coerce('labelpadding'); - Lib.coerceFont(coerce, 'labelfont', {size: 12}); + Lib.coerceFont(coerce, 'startlabelfont', { + size: 12, + color: ax.startlinecolor + }); + + Lib.coerceFont(coerce, 'endlabelfont', { + size: 12, + color: ax.endlinecolor + }); // Compute which labels to show. In a sense this is sort of a data computation // that should go in calc.js, but it's so minimal for any conceivable case that diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 81ef9868641..f95c2b7b7ee 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -154,7 +154,7 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { .attr('text-anchor', endAnchor ? 'end' : 'start') .text(prefix + label.text + suffix) .attr('transform', 'rotate(' + angle + ' ' + xy[0] + ',' + xy[1] + ')') - .call(Drawing.font, ax.labelfont.family, ax.labelfont.size, ax.labelfont.color); + .call(Drawing.font, label.font.family, label.font.size, label.font.color); }); labelJoin.exit().remove(); From 5aa52ce216443ba6dc2880c21ff2c9bbe2325525 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 15 Dec 2016 11:38:05 -0500 Subject: [PATCH 011/132] Very rough carpet scatter trace --- src/plots/cartesian/index.js | 5 +- src/traces/carpet/set_convert.js | 18 +++++++ src/traces/scattercarpet/attributes.js | 17 ++++--- src/traces/scattercarpet/calc.js | 65 +++++++------------------- src/traces/scattercarpet/defaults.js | 28 ++++------- src/traces/scattercarpet/plot.js | 26 +++++------ 6 files changed, 68 insertions(+), 91 deletions(-) diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index dd94ae04073..97b2b522cb5 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -62,8 +62,11 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { // Skip trace if whitelist provided and it's not whitelisted: // if (Array.isArray(traces) && traces.indexOf(i) === -1) continue; if(trace.xaxis + trace.yaxis === subplot) { + // XXX: Should trace carpet dependencies. Only replot all carpet plots if the carpet + // axis has actually changed: + // // If this trace is specifically requested, add it to the list: - if(traces.indexOf(trace.index) !== -1) { + if(traces.indexOf(trace.index) !== -1 || trace.carpetid) { // Okay, so example: traces 0, 1, and 2 have fill = tonext. You animate // traces 0 and 2. Trace 1 also needs to be updated, otherwise its fill // is outdated. So this retroactively adds the previous trace if the diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 6ebaa2a2223..f4e65f31cb9 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -105,6 +105,24 @@ module.exports = function setConvert(trace) { return trace._evalxy([], i0, j0, ti, tj); }; + trace.ab2xy = function (aval, bval) { + if (aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1]) { + return [false, false]; + } + var i = trace.a2i(aval); + var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); + var ti = i - i0; + + var j = trace.b2j(bval); + var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); + var tj = j - j0; + + if (tj < 0 || tj > 1 || ti < 0 || ti > 1) { + return [false, false]; + } + return trace._evalxy([], i0, j0, ti, tj); + } + trace.c2p = function (xy, xa, ya) { return [xa.c2p(xy[0]), ya.c2p(xy[1])]; }; diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index c973948f59e..daadb633f6c 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -20,17 +20,16 @@ var scatterMarkerAttrs = scatterAttrs.marker, scatterMarkerLineAttrs = scatterMarkerAttrs.line; module.exports = { - a: { - valType: 'data_array', + carpetid: { + valType: 'string', + role: 'info', description: [ - 'Sets the quantity of component `a` in each data point.', - 'If `a`, `b`, and `c` are all provided, they need not be', - 'normalized, only the relative values matter. If only two', - 'arrays are provided they must be normalized to match', - '`ternary.sum`.' + 'An identifier for this carpet, so that `scattercarpet` and', + '`scattercontour` traces can specify a carpet plot on which', + 'they lie' ].join(' ') }, - b: { + a: { valType: 'data_array', description: [ 'Sets the quantity of component `a` in each data point.', @@ -40,7 +39,7 @@ module.exports = { '`ternary.sum`.' ].join(' ') }, - c: { + b: { valType: 'data_array', description: [ 'Sets the quantity of component `a` in each data point.', diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 1ef85810a3c..0882f71005a 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -17,74 +17,45 @@ var Lib = require('../../lib'); var subTypes = require('../scatter/subtypes'); var calcColorscale = require('../scatter/colorscale_calc'); -var dataArrays = ['a', 'b', 'c']; -var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; - +var dataArrays = ['a', 'b']; module.exports = function calc(gd, trace) { - var i, j, dataArray, newArray, fillArray1, fillArray2, trace; + var i, j, dataArray, newArray, fillArray1, fillArray2, carpet; - for (i = 0; i < gd.traces.length; i++) { - if (gd.traces[i].carpetid === trace.carpetid) { - trace = gd.traces[i]; + for (i = 0; i < gd._fullData.length; i++) { + if (gd._fullData[i].carpetid === trace.carpetid && gd._fullData[i].type === 'carpet') { + carpet = gd._fullData[i]; break; } } - if (!trace) return; - - console.log('trace:', trace); + if (!carpet) return; - return; + // Transfer this over from carpet before plotting since this is a necessary + // condition in order for cartesian to actually plot this trace: + trace.xaxis = carpet.xaxis; + trace.yaxis = carpet.yaxis; - var ternary = gd._fullLayout[trace.subplot], - displaySum = ternary.sum, + var displaySum = carpet.sum, normSum = trace.sum || displaySum; - // fill in one missing component - for(i = 0; i < dataArrays.length; i++) { - dataArray = dataArrays[i]; - if(trace[dataArray]) continue; - - fillArray1 = trace[arraysToFill[dataArray][0]]; - fillArray2 = trace[arraysToFill[dataArray][1]]; - newArray = new Array(fillArray1.length); - for(j = 0; j < fillArray1.length; j++) { - newArray[j] = normSum - fillArray1[j] - fillArray2[j]; - } - trace[dataArray] = newArray; - } - // make the calcdata array var serieslen = trace.a.length; var cd = new Array(serieslen); - var a, b, c, norm, x, y; + var a, b, norm, x, y; for(i = 0; i < serieslen; i++) { a = trace.a[i]; b = trace.b[i]; - c = trace.c[i]; - if(isNumeric(a) && isNumeric(b) && isNumeric(c)) { - a = +a; - b = +b; - c = +c; - norm = displaySum / (a + b + c); - if(norm !== 1) { - a *= norm; - b *= norm; - c *= norm; - } - // map a, b, c onto x and y where the full scale of y - // is [0, sum], and x is [-sum, sum] - // TODO: this makes `a` always the top, `b` the bottom left, - // and `c` the bottom right. Do we want options to rearrange - // these? - y = a; - x = c - b; - cd[i] = {x: x, y: y, a: a, b: b, c: c}; + if(isNumeric(a) && isNumeric(b)) { + var xy = carpet.ab2xy(+a, +b); + cd[i] = {x: xy[0], y: xy[1], a: a, b: b}; } else cd[i] = {x: false, y: false}; } + cd[0].carpet = carpet; + cd[0].trace = trace; + // fill in some extras var marker, s; if(subTypes.hasMarkers(trace)) { diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index 34c5d345e49..f9c7a3fbfca 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -27,28 +27,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + coerce('carpetid'); + + // XXX: Don't hard code this + traceOut.xaxis = 'x'; + traceOut.yaxis = 'y'; + var a = coerce('a'), b = coerce('b'), - c = coerce('c'), len; - // allow any one array to be missing, len is the minimum length of those - // present. Note that after coerce data_array's are either Arrays (which - // are truthy even if empty) or undefined. As in scatter, an empty array - // is different from undefined, because it can signify that this data is - // not known yet but expected in the future - if(a) { - len = a.length; - if(b) { - len = Math.min(len, b.length); - if(c) len = Math.min(len, c.length); - } - else if(c) len = Math.min(len, c.length); - else len = 0; - } - else if(b && c) { - len = Math.min(b.length, c.length); - } + len = Math.min(a.length, b.length); if(!len) { traceOut.visible = false; @@ -58,7 +47,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // cut all data arrays down to same length if(a && len < a.length) traceOut.a = a.slice(0, len); if(b && len < b.length) traceOut.b = b.slice(0, len); - if(c && len < c.length) traceOut.c = c.slice(0, len); coerce('sum'); @@ -94,7 +82,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); } - coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined); + coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+text' : undefined); if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') { dfltHoverOn.push('fills'); diff --git a/src/traces/scattercarpet/plot.js b/src/traces/scattercarpet/plot.js index 742eced81e3..557604c9118 100644 --- a/src/traces/scattercarpet/plot.js +++ b/src/traces/scattercarpet/plot.js @@ -10,23 +10,21 @@ 'use strict'; var scatterPlot = require('../scatter/plot'); +var Axes = require('../../plots/cartesian/axes'); +module.exports = function plot(gd, plotinfoproxy, data) { -module.exports = function plot(ternary, data) { - var plotContainer = ternary.plotContainer; - - // remove all nodes inside the scatter layer - plotContainer.select('.scatterlayer').selectAll('*').remove(); + var carpet = data[0][0].carpet; // mimic cartesian plotinfo var plotinfo = { - xaxis: ternary.xaxis, - yaxis: ternary.yaxis, - plot: plotContainer + xaxis: Axes.getFromId(gd, carpet.xaxis || 'x'), + yaxis: Axes.getFromId(gd, carpet.yaxis || 'y'), + plot: plotinfoproxy.plot }; - var calcdata = new Array(data.length), - fullCalcdata = ternary.graphDiv.calcdata; + /*var calcdata = new Array(data.length), + fullCalcdata = gd.calcdata; for(var i = 0; i < fullCalcdata.length; i++) { var j = data.indexOf(fullCalcdata[i][0].trace); @@ -35,10 +33,10 @@ module.exports = function plot(ternary, data) { calcdata[j] = fullCalcdata[i]; - // while we're here and have references to both the Ternary object + // while we're here and have references to both the Carpet object // and fullData, connect the two (for use by hover) - data[j]._ternary = ternary; - } + data[j]._carpet = plotinfo; + }*/ - scatterPlot(ternary.graphDiv, plotinfo, calcdata); + scatterPlot(plotinfo.graphDiv, plotinfo, data); }; From 288306859467c293c928477e42dfd029d353677c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 15 Dec 2016 16:45:14 -0500 Subject: [PATCH 012/132] Add cheater mock --- src/traces/carpet/axis_attributes.js | 2 +- test/image/mocks/cheater.json | 605 +++++++++++++++++++++++++++ 2 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 test/image/mocks/cheater.json diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 2f2e20d1891..3906ee27215 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -38,7 +38,7 @@ module.exports = { description: [ 'Determines whether axis labels are drawn on the low side,', 'the high side, both, or neither side of the axis.' - ] + ].join(' ') }, labelpadding: { valType: 'integer', diff --git a/test/image/mocks/cheater.json b/test/image/mocks/cheater.json new file mode 100644 index 00000000000..a4343897a13 --- /dev/null +++ b/test/image/mocks/cheater.json @@ -0,0 +1,605 @@ +{ + "data": [ + { + "na":7, + "nb":7, + "apower":2, + "bpower":2, + "carpetid":"mycarpetplot", + "cheaterslope":1, + "type":"carpet", + "aaxis":{ + "tickmode":"linear", + "tick0":0.2, + "dtick":1, + "arraytick0":0, + "arraydtick":1, + "smoothing":true, + "cheatertype":"value", + "showlabels":"both", + "showlabelprefix":"first", + "labelpadding":10, + "labelsuffix":"", + "labelfont":{ + "color":"#c53", + "size":12, + "family":"sans-serif" + }, + "startline":true, + "endline":true, + "startlinecolor":"#3333bb", + "endlinecolor":"#3333bb", + "gridoffset":0, + "gridstep":1, + "gridwidth":1, + "startlinewidth":1.5, + "endlinewidth":1.5, + "gridcolor":"#aaaaaa", + "minorgridcount":3, + "minorgridwidth":1, + "minorgridcolor":"#eeeeee" + }, + "baxis":{ + "tickmode":"linear", + "tick0":0.2, + "dtick":1, + "arraytick0":0, + "arraydtick":1, + "smoothing":true, + "cheatertype":"value", + "showlabels":"both", + "showlabelprefix":"all", + "labelpadding":10, + "labelsuffix":"m", + "labelfont":{ + "color":"#35c", + "size":12, + "family":"sans-serif" + }, + "startline":true, + "endline":true, + "startlinecolor":"#bb3333", + "endlinecolor":"#bb3333", + "gridoffset":0, + "gridstep":1, + "gridwidth":1, + "gridcolor":"#aaaaaa", + "startlinewidth":1.5, + "endlinewidth":1.5, + "minorgridcount":3, + "minorgridwidth":1, + "minorgridcolor":"#eeeeee", + "showstartlabel":true, + "showendlabel":false + }, + "a":[ + 0.2, + 0.44699699699699696, + 1.061861861861862, + 2.044594594594595, + 3.3951951951951957, + 5.113663663663665, + 7.2 + ], + "b":[ + 0.2, + 0.3323529411764705, + 0.6117647058823528, + 1.0382352941176467, + 1.6117647058823528, + 2.3323529411764703, + 3.2 + ], + "y":[ + [ + 1.071599562910793, + 1.1056643965530542, + 1.1605672651223444, + 1.217512515023184, + 1.2697068368119195, + 1.3163253288127295, + 1.3580160561518513 + ], + [ + 1.1306379427088766, + 1.1507886541512502, + 1.1909917237025123, + 1.2398504081629311, + 1.2883843506259547, + 1.3334075552793436, + 1.3744318398547064 + ], + [ + 1.220086516635068, + 1.231396000997655, + 1.258023789415197, + 1.2964823759581592, + 1.3393975096077138, + 1.3818805265561276, + 1.4219727522026016 + ], + [ + 1.2994464793201583, + 1.3082723950290278, + 1.3302386024452697, + 1.3643476366941656, + 1.404784011385966, + 1.446421874011667, + 1.4866434488491957 + ], + [ + 1.3659967488521796, + 1.3741784751599952, + 1.3949299510208388, + 1.4280304002806903, + 1.4682498701077298, + 1.510386823713694, + 1.5515307826639397 + ], + [ + 1.4226785829614574, + 1.4307498681967172, + 1.451377075219899, + 1.484647705872809, + 1.525504349621373, + 1.5686385550747566, + 1.6109623176754218 + ], + [ + 1.471999262687472, + 1.4801361241953204, + 1.5010041734554476, + 1.5348399159892836, + 1.5766012541906476, + 1.6208548037635522, + 1.6643807301424949 + ] + ], + "cheatersope":1 + }, + { + "carpetid":"mycarpetplot", + "type":"scattercarpet", + "a":[ + 0.2, + 0.27070707070707073, + 0.34141414141414145, + 0.4121212121212121, + 0.4828282828282829, + 0.5535353535353535, + 0.6242424242424243, + 0.694949494949495, + 0.7656565656565657, + 0.8363636363636364, + 0.9070707070707071, + 0.9777777777777776, + 1.0484848484848486, + 1.1191919191919193, + 1.1898989898989898, + 1.2606060606060605, + 1.3313131313131314, + 1.402020202020202, + 1.4727272727272727, + 1.5434343434343434, + 1.614141414141414, + 1.6848484848484848, + 1.7555555555555553, + 1.8262626262626263, + 1.896969696969697, + 1.9676767676767677, + 2.038383838383839, + 2.109090909090909, + 2.17979797979798, + 2.250505050505051, + 2.3212121212121213, + 2.3919191919191922, + 2.462626262626263, + 2.533333333333333, + 2.604040404040404, + 2.674747474747475, + 2.7454545454545456, + 2.8161616161616165, + 2.886868686868687, + 2.9575757575757575, + 3.0282828282828285, + 3.098989898989899, + 3.16969696969697, + 3.240404040404041, + 3.311111111111111, + 3.381818181818182, + 3.4525252525252528, + 3.5232323232323233, + 3.593939393939394, + 3.664646464646465, + 3.7353535353535356, + 3.806060606060606, + 3.8767676767676775, + 3.9474747474747476, + 4.018181818181818, + 4.0888888888888895, + 4.1595959595959595, + 4.2303030303030305, + 4.301010101010101, + 4.3717171717171714, + 4.442424242424242, + 4.513131313131313, + 4.583838383838384, + 4.654545454545454, + 4.725252525252526, + 4.795959595959596, + 4.866666666666666, + 4.937373737373738, + 5.008080808080808, + 5.078787878787879, + 5.14949494949495, + 5.22020202020202, + 5.290909090909091, + 5.361616161616162, + 5.432323232323233, + 5.503030303030303, + 5.573737373737374, + 5.644444444444445, + 5.715151515151515, + 5.785858585858587, + 5.856565656565657, + 5.927272727272728, + 5.997979797979798, + 6.068686868686869, + 6.13939393939394, + 6.210101010101011, + 6.2808080808080815, + 6.351515151515152, + 6.422222222222222, + 6.492929292929293, + 6.5636363636363635, + 6.634343434343434, + 6.705050505050505, + 6.775757575757576, + 6.846464646464646, + 6.917171717171717, + 6.987878787878788, + 7.058585858585858, + 7.12929292929293, + 7.2 + ], + "b":[ + 10, + 9.580757065605551, + 9.17151311090705, + 8.7722681359045, + 8.383022140597898, + 8.003775124987246, + 7.634527089072545, + 7.275278032853791, + 6.926027956330986, + 6.586776859504133, + 6.257524742373229, + 5.938271604938272, + 5.6290174471992644, + 5.329762269156209, + 5.040506070809102, + 4.761248852157944, + 4.491990613202734, + 4.2327313539434765, + 3.9834710743801662, + 3.7442097745128056, + 3.5149474543413946, + 3.2956841138659327, + 3.086419753086421, + 2.8871543720028576, + 2.697887970615244, + 2.51862054892358, + 2.3493521069278636, + 2.1900826446280997, + 2.0408121620242836, + 1.9015406591164163, + 1.7722681359044996, + 1.6529945923885316, + 1.5437200285685129, + 1.4444444444444449, + 1.355167840016325, + 1.2758902152841545, + 1.2066115702479339, + 1.1473319049076625, + 1.0980512192633405, + 1.058769513314968, + 1.0294867870625446, + 1.010203040506071, + 1.0009182736455464, + 1.0016324864809714, + 1.0123456790123455, + 1.0330578512396693, + 1.0637690031629425, + 1.104479134782165, + 1.155188246097337, + 1.2158963371084586, + 1.2866034078155293, + 1.367309458218549, + 1.4580144883175195, + 1.5587184981124373, + 1.6694214876033047, + 1.7901234567901243, + 1.92082440567289, + 2.061524334251607, + 2.2122232425262736, + 2.372921130496887, + 2.543617998163452, + 2.724313845525967, + 2.915008672584431, + 3.1157024793388417, + 3.3263952657892073, + 3.5470870319355168, + 3.777777777777776, + 4.01846750331599, + 4.2691562085501475, + 4.529843893480257, + 4.800530558106317, + 5.0812162024283225, + 5.37190082644628, + 5.672584430160188, + 5.983267013570045, + 6.3039485766758485, + 6.634629119477604, + 6.975308641975309, + 7.32598714416896, + 7.686664626058569, + 8.057341087644119, + 8.43801652892562, + 8.82869094990307, + 9.229364350576471, + 9.640036730945821, + 10.060708091011122, + 10.491378430772373, + 10.932047750229568, + 11.382716049382712, + 11.843383328231814, + 12.314049586776857, + 12.794714825017854, + 13.285379042954801, + 13.786042240587697, + 14.296704417916537, + 14.817365574941332, + 15.348025711662077, + 15.888684828078764, + 16.439342924191415, + 17 + ] + }, + { + "carpetid":"mycarpetplot", + "type":"scattercarpet", + "a":[ + 1.9771678356568936, + 1.7281677091353975, + 1.6451461648505092, + 1.7652820598110595, + 1.5179417916142373, + 2.099819189564812, + 1.8024322915995699, + 1.6683193117135444, + 1.1813891438521875, + 1.578247897765319, + 1.6314609420147512, + 1.8458871588188073, + 1.7181384141641556, + 2.2643673033453267, + 1.6376456285923953, + 1.6953881767473784, + 1.7745328938864666, + 1.5711398875234057, + 1.799620526880252, + 2.342986216987497, + 1.7633240331174955, + 1.4833952975931757, + 1.9747651253133203, + 1.4365390954305588, + 1.3911643074250069, + 1.7144456182372474, + 0.9611812192667324, + 2.125702115701399, + 1.7832077696756277, + 1.7678583784846384, + 1.1966026976818127, + 1.5266718096293928, + 1.6438662784411688, + 1.375351724472806, + 1.539528691650646, + 1.9343656055663472, + 1.6678985329789258, + 1.8838779397314183, + 1.8057610847488919, + 1.9867795658155845, + 1.7902207920753248, + 2.2428388139932585, + 1.769166870828669, + 1.7928805285667517, + 1.8961203808797211, + 1.655201962808252, + 1.748110448020544, + 2.1609390713338605, + 1.8582250531025166, + 2.260551220106619, + 2.174229914643124, + 1.899108040698505, + 1.6714200978666203, + 1.3422274118132156, + 1.5125353784890294, + 2.048671995637749, + 2.117472920743815, + 2.436665335120428, + 2.0428458099599434, + 1.888246012915576, + 2.3299074870741365, + 1.2183411433436957, + 1.5914325446683042, + 1.7982601061861565, + 1.5257107713313518, + 1.5944916088712748, + 1.9401436701458648, + 1.7439340903395262, + 1.5560646315993507, + 1.7629838531353392, + 1.5206893377929624, + 1.814475330486396, + 1.7380392383272825, + 1.8190668853737035, + 1.6863475372995764, + 2.1378300153137526, + 1.2178578603105077, + 2.110235458301153, + 2.0366896632400917, + 1.8698443512650469, + 1.442457218557926, + 1.7167433663934857, + 1.8963353433369978, + 1.343741972344295, + 2.0063463496455816, + 2.0295829326718207, + 2.3203724465670805, + 1.227492829769501, + 1.471026977200653, + 1.4617876360662163, + 1.5815843506558118, + 1.7580195616737262, + 1.5299129609761897, + 1.367460804568379, + 2.1174350194343776, + 1.8480754969735693, + 2.285146095070552, + 2.574443838453115, + 1.992071069597503, + 1.689951318926721 + ], + "b":[ + 1.3074519875714097, + 1.4531685621591914, + 2.1633966841963534, + 1.481073227909216, + 1.6799060485798523, + 2.274808308193735, + 1.7371033453009164, + 2.298620713452353, + 1.4214451222999855, + 1.5455663580766836, + 1.6164243290199265, + 1.9682442393920063, + 2.1427944629388787, + 1.4569045884037142, + 1.4281163749466375, + 1.7146767197928359, + 1.0755933816684946, + 0.8784094762145397, + 1.93282129111832, + 1.6448513383492502, + 1.5506155032993147, + 1.764263941854984, + 0.7032357405818286, + 1.616711421892543, + 1.5805996613997118, + 1.4725783403464852, + 1.2622550584210828, + 2.1434272945907717, + 1.8510120143131545, + 1.7208807703301083, + 2.292141617988955, + 1.6002564281676446, + 1.478085831795318, + 1.6796275144762962, + 1.5505730929373653, + 1.8149322552575557, + 1.5948050623614736, + 1.3932724557421987, + 1.780168826955217, + 1.664742252577551, + 1.850786769461731, + 1.653363910092044, + 0.7745920245470629, + 1.6329582257562196, + 1.2773964616345046, + 1.6237896986913494, + 1.7962206908187406, + 2.0890616549036434, + 1.914164477898335, + 1.150998812106018, + 1.5850923232263203, + 2.1528601952062716, + 1.7930066020059647, + 0.7711287657022957, + 1.8862596171137413, + 1.5020315442105905, + 1.5197809399661615, + 1.7113280011848166, + 1.9283144401619667, + 1.9789179945446944, + 1.7793246989822848, + 1.6167238328699642, + 1.9384044700452088, + 1.5660843637915092, + 1.7683028061308124, + 1.5244731497168458, + 1.102008322790224, + 1.6718157162563336, + 2.165694994302381, + 1.8190475051727986, + 1.5789080001223392, + 2.5057162064716687, + 1.2863412085249366, + 1.4843877421001979, + 2.333751888139704, + 1.3530084019232373, + 1.9143452926050923, + 1.507162154023091, + 1.8106757522755341, + 1.3652123999397956, + 1.6776974988628168, + 1.4932496818472836, + 1.590352820803672, + 1.4262241995107394, + 1.633121441166829, + 1.7717278114683015, + 1.6461264733739263, + 1.5974539598821782, + 1.1749746099921552, + 1.7222969846461536, + 1.5254755644233537, + 1.636011940215802, + 1.8344532843883337, + 1.4112779279811438, + 1.7188670382828906, + 1.3291969146673037, + 1.8487719803200113, + 1.9453457313487563, + 1.6248245939430164, + 1.5247741572596762 + ], + "mode":"markers" + } + ], + "layout": { + "xaxis":{ + "showgrid":false, + "showline":false, + "zeroline":false, + "showticklabels":false, + "range":[ + -1.470625, + 1.470625 + ], + "autorange":true + }, + "margin":{ + "t":20, + "r":20, + "b":20, + "l":40 + }, + "dragmode":"pan", + "yaxis":{ + "range":[ + 0.9398704146370815, + 1.7961098784162064 + ], + "autorange":true + } + } +} From c29fdf7a8f1815530b30a04b6c2944a8ea19cdc1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 16 Dec 2016 15:34:05 -0500 Subject: [PATCH 013/132] Add quick hover labels --- src/traces/carpet/calc_labels.js | 8 ++++++-- src/traces/carpet/defaults.js | 2 ++ src/traces/carpet/plot.js | 8 ++++---- src/traces/scattercarpet/calc.js | 2 ++ src/traces/scattercarpet/hover.js | 22 +++++++++++++++------- src/traces/scattercarpet/index.js | 2 +- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index 7f16e2bc896..fc64663b945 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -42,7 +42,9 @@ module.exports = function calcLabels (trace, axis) { dxy: gridline.dxy(0, 0), axis: gridline.axis, length: gridline.crossAxis.length, - font: gridline.crossAxis.startlabelfont + font: gridline.crossAxis.startlabelfont, + isFirst: i === 0, + isLast: i === gridlines.length - 1 }); } @@ -54,7 +56,9 @@ module.exports = function calcLabels (trace, axis) { dxy: gridline.dxy(gridline.crossLength - 2, 1), axis: gridline.axis, length: gridline.crossAxis.length, - font: gridline.crossAxis.endlabelfont + font: gridline.crossAxis.endlabelfont, + isFirst: i === 0, + isLast: i === gridlines.length - 1 }); } } diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 67b9274ee40..2bc203eda0c 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -57,6 +57,8 @@ function handleAxisDefaults(traceIn, traceOut, axis) { return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); } + ax._hovertitle = axis; + coerce('smoothing'); traceOut.smoothing = traceOut.smoothing ? 1 : 0; coerce('cheatertype'); diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index f95c2b7b7ee..413826de4e2 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -108,26 +108,26 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { var prefix; switch(ax.showlabelprefix) { case 'first': - prefix = i === 0 ? ax.labelprefix : ''; + prefix = label.isFirst ? ax.labelprefix : ''; break; case 'all': prefix = ax.labelprefix; break; case 'last': - prefix = i === ax._majorIndices.length - 1 ? ax.labelprefix : ''; + prefix = label.isLast ? ax.labelprefix : ''; break; } var suffix; switch(ax.showlabelsuffix) { case 'first': - suffix = i === 0 ? ax.labelsuffix : ''; + suffix = label.isFirst ? ax.labelsuffix : ''; break; case 'all': suffix = ax.labelsuffix; break; case 'last': - suffix = i === ax._majorIndices.length - 1 ? ax.labelsuffix : ''; + suffix = label.isLast ? ax.labelsuffix : ''; break; } diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 0882f71005a..923dd36007b 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -31,6 +31,8 @@ module.exports = function calc(gd, trace) { if (!carpet) return; + trace._carpet = carpet; + // Transfer this over from carpet before plotting since this is a necessary // condition in order for cartesian to actually plot this trace: trace.xaxis = carpet.xaxis; diff --git a/src/traces/scattercarpet/hover.js b/src/traces/scattercarpet/hover.js index 176786ed050..505678dba6f 100644 --- a/src/traces/scattercarpet/hover.js +++ b/src/traces/scattercarpet/hover.js @@ -43,25 +43,33 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { newPointData.a = cdi.a; newPointData.b = cdi.b; - newPointData.c = cdi.c; newPointData.xLabelVal = undefined; newPointData.yLabelVal = undefined; // TODO: nice formatting, and label by axis title, for a, b, and c? var trace = newPointData.trace, - ternary = trace._ternary, + carpet = trace._carpet, hoverinfo = trace.hoverinfo.split('+'), text = []; function textPart(ax, val) { - text.push(ax._hovertitle + ': ' + Axes.tickText(ax, val, 'hover').text); + text.push(((ax.labelprefix && ax.labelprefix.length > 0) ? ax.labelprefix : (ax._hovertitle + ': ')) + val.toFixed(3) + ax.labelsuffix); } - if(hoverinfo.indexOf('all') !== -1) hoverinfo = ['a', 'b', 'c']; - if(hoverinfo.indexOf('a') !== -1) textPart(ternary.aaxis, cdi.a); - if(hoverinfo.indexOf('b') !== -1) textPart(ternary.baxis, cdi.b); - if(hoverinfo.indexOf('c') !== -1) textPart(ternary.caxis, cdi.c); + if(hoverinfo.indexOf('all') !== -1) hoverinfo = ['a', 'b']; + if(hoverinfo.indexOf('a') !== -1) textPart(carpet.aaxis, cdi.a); + if(hoverinfo.indexOf('b') !== -1) textPart(carpet.baxis, cdi.b); + + var ij = carpet.ab2ij([cdi.a, cdi.b]); + var i0 = Math.floor(ij[0]); + var ti = ij[0] - i0; + + var j0 = Math.floor(ij[1]); + var tj = ij[1] - j0; + + var xy = carpet._evalxy([], i0, j0, ti, tj); + text.push('y: ' + xy[1].toFixed(3)); newPointData.extraText = text.join('
'); diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index 67103f90f10..934cc9dc598 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -16,7 +16,7 @@ ScatterCarpet.supplyDefaults = require('./defaults'); ScatterCarpet.calc = require('./calc'); ScatterCarpet.plot = require('./plot'); //ScatterCarpet.style = require('./style'); -//ScatterCarpet.hoverPoints = require('./hover'); +ScatterCarpet.hoverPoints = require('./hover'); //ScatterCarpet.selectPoints = require('./select'); ScatterCarpet.moduleType = 'trace'; From e384d80567eb682834304eebe03e4618ba979029 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 10 Jan 2017 10:48:29 -0500 Subject: [PATCH 014/132] Continue on carpets --- src/traces/carpet/ab_defaults.js | 40 ++- src/traces/carpet/axis_attributes.js | 415 ++++++++++++++++++++++ src/traces/carpet/axis_defaults.js | 298 ++++++++++++++++ src/traces/carpet/calc.js | 16 +- src/traces/carpet/cheater_basis.js | 12 +- src/traces/carpet/defaults.js | 124 +------ src/traces/carpet/smooth-fill-2d-array.js | 119 +++++++ src/traces/carpet/xy_defaults.js | 20 +- src/traces/heatmap/calc.js | 4 +- src/traces/heatmap/convert_column_xyz.js | 69 ++-- 10 files changed, 954 insertions(+), 163 deletions(-) create mode 100644 src/traces/carpet/axis_defaults.js create mode 100644 src/traces/carpet/smooth-fill-2d-array.js diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index 8806402f5a2..cd735265547 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -6,12 +6,14 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; // var Lib = require('../../lib'); // var isNumeric = require('fast-isnumeric'); +var extendFlat = require('../../lib/extend').extendFlat; +var handleAxisDefaults = require('./axis_defaults'); +var attributes = require('./attributes'); module.exports = function handleABDefaults(traceIn, traceOut, coerce) { var a = coerce('a'); @@ -28,5 +30,41 @@ module.exports = function handleABDefaults(traceIn, traceOut, coerce) { coerce('b0'); } + mimickAxisDefaults(traceIn, traceOut); + return; }; + +function mimickAxisDefaults (traceIn, traceOut) { + var axesList = ['aaxis', 'baxis']; + + axesList.forEach(function(axName) { + var axLetter = axName.charAt(0), + axIn = traceIn[axName] || {}, + axOut = {}, + defaultOptions = { + id: axLetter + 'axis', + letter: axLetter, + font: traceOut.font, + name: axName, + data: traceIn[axLetter], + calendar: traceOut.calendar + }; + + function coerce(attr, dflt) { + return Lib.coerce(axIn, axOut, attributes, attr, dflt); + } + + handleAxisDefaults(axIn, axOut, coerce, defaultOptions, traceOut); + + axOut._categories = axOut._categories || []; + + traceOut[axName] = axOut; + + // so we don't have to repeat autotype unnecessarily, + // copy an autotype back to traceIn + if(!traceIn[axName] && axIn.type !== '-') { + traceIn[axName] = {type: axIn.type}; + } + }); +} diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 3906ee27215..ad31a0fd55a 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -13,11 +13,103 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); module.exports = { + color: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: [ + 'Sets default for all colors associated with this axis', + 'all at once: line, font, tick, and grid colors.', + 'Grid color is lightened by blending this with the plot background', + 'Individual pieces can override this.' + ].join(' ') + }, smoothing: { valType: 'boolean', dflt: true, role: 'info' }, + title: { + valType: 'string', + role: 'info', + description: 'Sets the title of this axis.' + }, + titlefont: extendFlat({}, fontAttrs, { + description: [ + 'Sets this axis\' title font.' + ].join(' ') + }), + type: { + valType: 'enumerated', + // '-' means we haven't yet run autotype or couldn't find any data + // it gets turned into linear in gd._fullLayout but not copied back + // to gd.data like the others are. + values: ['-', 'linear', 'log', 'date', 'category'], + dflt: '-', + role: 'info', + description: [ + 'Sets the axis type.', + 'By default, plotly attempts to determined the axis type', + 'by looking into the data of the traces that referenced', + 'the axis in question.' + ].join(' ') + }, + autorange: { + valType: 'enumerated', + values: [true, false, 'reversed'], + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the range of this axis is', + 'computed in relation to the input data.', + 'See `rangemode` for more info.', + 'If `range` is provided, then `autorange` is set to *false*.' + ].join(' ') + }, + rangemode: { + valType: 'enumerated', + values: ['normal', 'tozero', 'nonnegative'], + dflt: 'normal', + role: 'style', + description: [ + 'If *normal*, the range is computed in relation to the extrema', + 'of the input data.', + 'If *tozero*`, the range extends to 0,', + 'regardless of the input data', + 'If *nonnegative*, the range is non-negative,', + 'regardless of the input data.' + ].join(' ') + }, + range: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'any'}, + {valType: 'any'} + ], + description: [ + 'Sets the range of this axis.', + 'If the axis `type` is *log*, then you must take the log of your', + 'desired range (e.g. to set the range from 1 to 100,', + 'set the range from 0 to 2).', + 'If the axis `type` is *date*, it should be date strings,', + 'like date data, though Date objects and unix milliseconds', + 'will be accepted and converted to strings.', + 'If the axis `type` is *category*, it should be numbers,', + 'using the scale where each category is assigned a serial', + 'number from zero in the order it appears.' + ].join(' ') + }, + + fixedrange: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'Determines whether or not this axis is zoom-able.', + 'If true, then zoom is disabled.' + ].join(' ') + }, cheatertype: { valType: 'enumerated', values: ['index', 'value'], @@ -30,6 +122,250 @@ module.exports = { dflt: 'array', role: 'info', }, + nticks: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'style', + description: [ + 'Specifies the maximum number of ticks for the particular axis.', + 'The actual number of ticks will be chosen automatically to be', + 'less than or equal to `nticks`.', + 'Has an effect only if `tickmode` is set to *auto*.' + ].join(' ') + }, + tick0: { + valType: 'any', + role: 'style', + description: [ + 'Sets the placement of the first tick on this axis.', + 'Use with `dtick`.', + 'If the axis `type` is *log*, then you must take the log of your starting tick', + '(e.g. to set the starting tick to 100, set the `tick0` to 2)', + 'except when `dtick`=*L* (see `dtick` for more info).', + 'If the axis `type` is *date*, it should be a date string, like date data.', + 'If the axis `type` is *category*, it should be a number, using the scale where', + 'each category is assigned a serial number from zero in the order it appears.' + ].join(' ') + }, + dtick: { + valType: 'any', + role: 'style', + description: [ + 'Sets the step in-between ticks on this axis. Use with `tick0`.', + 'Must be a positive number, or special strings available to *log* and *date* axes.', + 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', + 'is the tick number. For example,', + 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', + 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', + 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', + '*log* has several special values; *L*, where `f` is a positive number,', + 'gives ticks linearly spaced in value (but not position).', + 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', + 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', + '`tick0` is ignored for *D1* and *D2*.', + 'If the axis `type` is *date*, then you must convert the time to milliseconds.', + 'For example, to set the interval between ticks to one day,', + 'set `dtick` to 86400000.0.', + '*date* also has special values *M* gives ticks spaced by a number of months.', + '`n` must be a positive integer.', + 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', + 'To set ticks every 4 years, set `dtick` to *M48*' + ].join(' ') + }, + tickvals: { + valType: 'data_array', + description: [ + 'Sets the values at which ticks on this axis appear.', + 'Only has an effect if `tickmode` is set to *array*.', + 'Used with `ticktext`.' + ].join(' ') + }, + ticktext: { + valType: 'data_array', + description: [ + 'Sets the text displayed at the ticks position via `tickvals`.', + 'Only has an effect if `tickmode` is set to *array*.', + 'Used with `tickvals`.' + ].join(' ') + }, + ticks: { + valType: 'enumerated', + values: ['outside', 'inside', ''], + role: 'style', + description: [ + 'Determines whether ticks are drawn or not.', + 'If **, this axis\' ticks are not drawn.', + 'If *outside* (*inside*), this axis\' are drawn outside (inside)', + 'the axis lines.' + ].join(' ') + }, + mirror: { + valType: 'enumerated', + values: [true, 'ticks', false, 'all', 'allticks'], + dflt: false, + role: 'style', + description: [ + 'Determines if the axis lines or/and ticks are mirrored to', + 'the opposite side of the plotting area.', + 'If *true*, the axis lines are mirrored.', + 'If *ticks*, the axis lines and ticks are mirrored.', + 'If *false*, mirroring is disable.', + 'If *all*, axis lines are mirrored on all shared-axes subplots.', + 'If *allticks*, axis lines and ticks are mirrored', + 'on all shared-axes subplots.' + ].join(' ') + }, + ticklen: { + valType: 'number', + min: 0, + dflt: 5, + role: 'style', + description: 'Sets the tick length (in px).' + }, + tickwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the tick width (in px).' + }, + tickcolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the tick color.' + }, + showticklabels: { + valType: 'boolean', + dflt: true, + role: 'style', + description: 'Determines whether or not the tick labels are drawn.' + }, + tickfont: extendFlat({}, fontAttrs, { + description: 'Sets the tick font.' + }), + tickangle: { + valType: 'angle', + dflt: 'auto', + role: 'style', + description: [ + 'Sets the angle of the tick labels with respect to the horizontal.', + 'For example, a `tickangle` of -90 draws the tick labels', + 'vertically.' + ].join(' ') + }, + tickprefix: { + valType: 'string', + dflt: '', + role: 'style', + description: 'Sets a tick label prefix.' + }, + showtickprefix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: [ + 'If *all*, all tick labels are displayed with a prefix.', + 'If *first*, only the first tick is displayed with a prefix.', + 'If *last*, only the last tick is displayed with a suffix.', + 'If *none*, tick prefixes are hidden.' + ].join(' ') + }, + ticksuffix: { + valType: 'string', + dflt: '', + role: 'style', + description: 'Sets a tick label suffix.' + }, + showticksuffix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: 'Same as `showtickprefix` but for tick suffixes.' + }, + showexponent: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: [ + 'If *all*, all exponents are shown besides their significands.', + 'If *first*, only the exponent of the first tick is shown.', + 'If *last*, only the exponent of the last tick is shown.', + 'If *none*, no exponents appear.' + ].join(' ') + }, + exponentformat: { + valType: 'enumerated', + values: ['none', 'e', 'E', 'power', 'SI', 'B'], + dflt: 'B', + role: 'style', + description: [ + 'Determines a formatting rule for the tick exponents.', + 'For example, consider the number 1,000,000,000.', + 'If *none*, it appears as 1,000,000,000.', + 'If *e*, 1e+9.', + 'If *E*, 1E+9.', + 'If *power*, 1x10^9 (with 9 in a super script).', + 'If *SI*, 1G.', + 'If *B*, 1B.' + ].join(' ') + }, + separatethousands: { + valType: 'boolean', + dflt: false, + role: 'style', + description: [ + 'If "true", even 4-digit integers are separated' + ].join(' ') + }, + tickformat: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'Sets the tick label formatting rule using d3 formatting mini-languages', + 'which are very similar to those in Python. For numbers, see:', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', + 'And for dates see:', + 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', + 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', + 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', + '*%H~%M~%S.%2f* would display *09~15~23.46*' + ].join(' ') + }, + categoryorder: { + valType: 'enumerated', + values: [ + 'trace', 'category ascending', 'category descending', 'array' + /* , 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later + ], + dflt: 'trace', + role: 'info', + description: [ + 'Specifies the ordering logic for the case of categorical variables.', + 'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.', + 'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by', + 'the alphanumerical order of the category names.', + /* 'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the', + 'numerical order of the values.',*/ // // value ascending / descending to be implemented later + 'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category', + 'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to', + 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.' + ].join(' ') + }, + categoryarray: { + valType: 'data_array', + role: 'info', + description: [ + 'Sets the order in which categories on this axis appear.', + 'Only has an effect if `categoryorder` is set to *array*.', + 'Used with `categoryorder`.' + ].join(' ') + }, showlabels: { valType: 'enumerated', values: ['start', 'end', 'both', 'none'], @@ -90,6 +426,70 @@ module.exports = { endlabelfont: extendFlat({}, fontAttrs, { description: 'Sets the label font.' }), + // lines and grids + showline: { + valType: 'boolean', + dflt: false, + role: 'style', + description: [ + 'Determines whether or not a line bounding this axis is drawn.' + ].join(' ') + }, + linecolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the axis line color.' + }, + linewidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the axis line.' + }, + showgrid: { + valType: 'boolean', + role: 'style', + description: [ + 'Determines whether or not grid lines are drawn.', + 'If *true*, the grid lines are drawn at every tick mark.' + ].join(' ') + }, + gridcolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, + gridwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + zeroline: { + valType: 'boolean', + role: 'style', + description: [ + 'Determines whether or not a line is drawn at along the 0 value', + 'of this axis.', + 'If *true*, the zero line is drawn on top of the grid lines.' + ].join(' ') + }, + zerolinecolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the line color of the zero line.' + }, + zerolinewidth: { + valType: 'number', + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the zero line.' + }, tick0: { valType: 'any', min: 0, @@ -187,4 +587,19 @@ module.exports = { role: 'style', description: 'Sets the color of the grid lines.' }, + hoverformat: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'Sets the hover text formatting rule using d3 formatting mini-languages', + 'which are very similar to those in Python. For numbers, see:', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', + 'And for dates see:', + 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', + 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', + 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', + '*%H~%M~%S.%2f* would display *09~15~23.46*' + ].join(' ') + }, }; diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js new file mode 100644 index 00000000000..4c9b3463830 --- /dev/null +++ b/src/traces/carpet/axis_defaults.js @@ -0,0 +1,298 @@ +/** +* 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 axisAttributes = require('./attributes'); +var extendFlat = require('../../lib/extend').extendFlat; +var setConvert = require('../../plots/cartesian/set_convert'); +var Lib = require('../../lib'); +var handleCartesianAxisDefaults = require('../../plots/cartesian/axis_defaults'); + +var isNumeric = require('fast-isnumeric'); +var colorMix = require('tinycolor2').mix; + +var Registry = require('../../registry'); +var Lib = require('../../lib'); +var lightFraction = require('../../components/color/attributes').lightFraction; + +var layoutAttributes = require('../../plots/cartesian/layout_attributes'); +var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); +var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); +var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); +var handleCategoryOrderDefaults = require('../../plots/cartesian/category_order_defaults'); +var setConvert = require('../../plots/cartesian/set_convert'); +var orderedCategories = require('../../plots/cartesian/ordered_categories'); +var axisIds = require('../../plots/cartesian/axis_ids'); +var autoType = require('../../plots/cartesian/axis_autotype'); + + +/*module.exports = function handleAxisDefaults(traceIn, traceOut, axis) { + var i; + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); + } + + coerce('type'); + coerce('smoothing'); + traceOut.smoothing = traceOut.smoothing ? 1 : 0; + coerce('cheatertype'); + + coerce('showlabels'); + coerce('labelprefix', axis + ' = '); + coerce('labelsuffix'); + coerce('showlabelprefix'); + coerce('showlabelsuffix'); + + coerce('tickmode'); + coerce('tick0'); + coerce('dtick'); + coerce('arraytick0'); + coerce('arraydtick'); + //coerce('gridoffset'); + //coerce('gridstep'); + + coerce('gridwidth'); + coerce('gridcolor'); + + coerce('startline'); + coerce('startlinewidth', traceOut.gridwidth); + coerce('startlinecolor', traceOut.gridcolor); + coerce('endline'); + coerce('endlinewidth', traceOut.gridwidth); + coerce('endlinecolor', traceOut.gridwidth); + + coerce('minorgridcount'); + coerce('minorgridwidth'); + coerce('minorgridcolor'); + + coerce('showstartlabel'); + coerce('showendlabel'); + + coerce('labelpadding'); + + // We'll never draw this. We just need a couple category management functions. + var ax = traceOut[axis + 'axis'] = extendFlat(traceOut[axis + 'axis'] || {}, { + range: [], + domain: [], + _id: axis, + }); + setConvert(traceOut[axis + 'axis']); + + Lib.coerceFont(coerce, 'startlabelfont', { + size: 12, + color: ax.startlinecolor + }); + + Lib.coerceFont(coerce, 'endlabelfont', { + size: 12, + color: ax.endlinecolor + }); + + ax._hovertitle = axis; +}*/ + + +/** + * options: object containing: + * + * letter: 'x' or 'y' + * title: name of the axis (ie 'Colorbar') to go in default title + * name: axis object name (ie 'xaxis') if one should be stored + * font: the default font to inherit + * outerTicks: boolean, should ticks default to outside? + * showGrid: boolean, should gridlines be shown by default? + * noHover: boolean, this axis doesn't support hover effects? + * data: the plot data to use in choosing auto type + * bgColor: the plot background color, to calculate default gridline colors + */ +module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) { + var letter = options.letter, + font = options.font || {}, + attributes = axisAttributes[letter + 'axis'], + defaultTitle = 'Click to enter ' + + (options.title || (letter.toUpperCase() + ' axis')) + + ' title'; + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } + + // I don't know what this does: + function coerce2(attr, dflt) { + return Lib.coerce2(containerIn, containerOut, attributes, attr, dflt); + } + + // set up some private properties + if(options.name) { + containerOut._name = options.name; + containerOut._id = options.name; + } + + // now figure out type and do some more initialization + var axType = coerce('type'); + if(axType === '-') { + setAutoType(containerOut, options.data); + + if(containerOut.type === '-') { + containerOut.type = 'linear'; + } + else { + // copy autoType back to input axis + // note that if this object didn't exist + // in the input layout, we have to put it in + // this happens in the main supplyDefaults function + axType = containerIn.type = containerOut.type; + } + } + + coerce('smoothing'); + containerOut.smoothing = containerOut.smoothing ? 1 : 0; + coerce('cheatertype'); + + coerce('showlabels'); + coerce('labelprefix', letter + ' = '); + coerce('labelsuffix'); + coerce('showlabelprefix'); + coerce('showlabelsuffix'); + + coerce('tickmode'); + coerce('tick0'); + coerce('dtick'); + coerce('arraytick0'); + coerce('arraydtick'); + //coerce('gridoffset'); + //coerce('gridstep'); + + coerce('gridwidth'); + coerce('gridcolor'); + + coerce('startline'); + coerce('startlinewidth', containerOut.gridwidth); + coerce('startlinecolor', containerOut.gridcolor); + coerce('endline'); + coerce('endlinewidth', containerOut.gridwidth); + coerce('endlinecolor', containerOut.gridwidth); + + coerce('minorgridcount'); + coerce('minorgridwidth'); + coerce('minorgridcolor'); + + coerce('showstartlabel'); + coerce('showendlabel'); + + coerce('labelpadding'); + + // We'll never draw this. We just need a couple category management functions. + Lib.coerceFont(coerce, 'startlabelfont', { + size: 12, + color: containerOut.startlinecolor + }); + + Lib.coerceFont(coerce, 'endlabelfont', { + size: 12, + color: containerOut.endlinecolor + }); + + containerOut._hovertitle = letter; + + + + + + + + if(axType === 'date') { + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); + handleCalendarDefaults(containerIn, containerOut, 'calendar', options.calendar); + } + + setConvert(containerOut); + + var dfltColor = coerce('color'); + // if axis.color was provided, use it for fonts too; otherwise, + // inherit from global font color in case that was provided. + var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color; + + coerce('title', defaultTitle); + Lib.coerceFont(coerce, 'titlefont', { + family: font.family, + size: Math.round(font.size * 1.2), + color: dfltFontColor + }); + + var validRange = ( + (containerIn.range || []).length === 2 && + isNumeric(containerOut.r2l(containerIn.range[0])) && + isNumeric(containerOut.r2l(containerIn.range[1])) + ); + var autoRange = coerce('autorange', !validRange); + + if(autoRange) coerce('rangemode'); + + coerce('range'); + containerOut.cleanRange(); + + coerce('fixedrange'); + + handleTickValueDefaults(containerIn, containerOut, coerce, axType); + handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); + handleTickMarkDefaults(containerIn, containerOut, coerce, options); + handleCategoryOrderDefaults(containerIn, containerOut, coerce); + + var lineColor = coerce2('linecolor', dfltColor), + lineWidth = coerce2('linewidth'), + showLine = coerce('showline', !!lineColor || !!lineWidth); + + if(!showLine) { + delete containerOut.linecolor; + delete containerOut.linewidth; + } + + if(showLine || containerOut.ticks) coerce('mirror'); + + var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, lightFraction).toRgbString()), + gridWidth = coerce2('gridwidth'), + showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth); + + if(!showGridLines) { + delete containerOut.gridcolor; + delete containerOut.gridwidth; + } + + var zeroLineColor = coerce2('zerolinecolor', dfltColor), + zeroLineWidth = coerce2('zerolinewidth'), + showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth); + + if(!showZeroLine) { + delete containerOut.zerolinecolor; + delete containerOut.zerolinewidth; + } + + // fill in categories + containerOut._initialCategories = axType === 'category' ? + orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : + []; + + return containerOut; +}; + +function setAutoType(ax, data) { + // new logic: let people specify any type they want, + // only autotype if type is '-' + if(ax.type !== '-') return; + + var id = ax._id, + axLetter = id.charAt(0); + + var calAttr = axLetter + 'calendar', + calendar = ax[calAttr]; + + ax.type = autoType(data, calendar); +} diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index c0033133665..615a4427b18 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -12,12 +12,13 @@ var Axes = require('../../plots/cartesian/axes'); var cheaterBasis = require('./cheater_basis'); var arrayMinmax = require('./array_minmax'); var search = require('../../lib/search').findBin; -var computeControlPoints = require('./compute_control_points'); var map2dArray = require('./map_2d_array'); -var createSplineEvaluator = require('./create_spline_evaluator'); var setConvert = require('./set_convert'); var calcGridlines = require('./calc_gridlines'); var calcLabels = require('./calc_labels'); +var clean2dArray = require('../heatmap/clean_2d_array'); +var isNumeric = require('fast-isnumeric'); +var smoothFill2dArray = require('./smooth-fill-2d-array'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), @@ -63,6 +64,15 @@ module.exports = function calc(gd, trace) { // Convert cartesian-space x/y coordinates to screen space pixel coordinates: trace.xp = map2dArray(trace.xp, x, xa.c2p); trace.yp = map2dArray(trace.yp, y, ya.c2p); + x = clean2dArray(x, true); + y = clean2dArray(y, true); + + // Fill in any undefined values with elliptic smoothing. This doesn't take + // into account the spacing of the values. That is, the derivatives should + // be modified to use a and b values. It's not that hard, but this is already + // moderate overkill for just filling in missing values. + smoothFill2dArray(x); + smoothFill2dArray(y); // Create conversions from one coordinate system to another: setConvert(trace, xa, ya); diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 66cfd2fae6b..26e054a04e1 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -35,12 +35,12 @@ module.exports = function(a, b, cheaterslope) { var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } - for(i = 0; i < na; i++) { - data[i] = []; - var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); - for(j = 0; j < nb; j++) { - var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); - data[i][j] = aval - bval * cheaterslope; + for(j = 0; j < nb; j++) { + var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); + data[j] = []; + for(i = 0; i < na; i++) { + var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); + data[j][i] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 2bc203eda0c..f381ba45401 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -11,34 +11,22 @@ var Lib = require('../../lib'); -var attributes = require('./attributes'); var handleXYDefaults = require('./xy_defaults'); var handleABDefaults = require('./ab_defaults'); +var attributes = require('./attributes'); module.exports = function supplyDefaults(traceIn, traceOut) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - traceIn.cheatersope = parseFloat(traceIn.cheaterslope); - coerce('carpetid'); coerce('cheaterslope'); traceOut.cheaterslope = parseFloat(traceIn.cheaterslope); - var len = handleXYDefaults(traceIn, traceOut, coerce); - - if(!len) { - traceOut.visible = false; - return; - } - handleABDefaults(traceIn, traceOut, coerce); - handleAxisDefaults(traceIn, traceOut, 'a'); - handleAxisDefaults(traceIn, traceOut, 'b'); - if (traceOut.a.length < 3) { traceOut.aaxis.smoothing = 0; } @@ -47,110 +35,12 @@ module.exports = function supplyDefaults(traceIn, traceOut) { traceOut.baxis.smoothing = 0; } -}; - -function handleAxisDefaults(traceIn, traceOut, axis) { - var ax = traceOut[axis + 'axis'] = traceOut[axis + 'axis'] || {}; - var i; - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); - } - - ax._hovertitle = axis; - - coerce('smoothing'); - traceOut.smoothing = traceOut.smoothing ? 1 : 0; - coerce('cheatertype'); - - coerce('showlabels'); - coerce('labelprefix', axis + ' = '); - coerce('labelsuffix'); - coerce('showlabelprefix'); - coerce('showlabelsuffix'); - - coerce('tickmode'); - coerce('tick0'); - coerce('dtick'); - coerce('arraytick0'); - coerce('arraydtick'); - //coerce('gridoffset'); - //coerce('gridstep'); - - coerce('gridwidth'); - coerce('gridcolor'); - - coerce('startline'); - coerce('startlinewidth', traceOut.gridwidth); - coerce('startlinecolor', traceOut.gridcolor); - coerce('endline'); - coerce('endlinewidth', traceOut.gridwidth); - coerce('endlinecolor', traceOut.gridwidth); - - coerce('minorgridcount'); - coerce('minorgridwidth'); - coerce('minorgridcolor'); - - coerce('showstartlabel'); - coerce('showendlabel'); - - coerce('labelpadding'); - - Lib.coerceFont(coerce, 'startlabelfont', { - size: 12, - color: ax.startlinecolor - }); - - Lib.coerceFont(coerce, 'endlabelfont', { - size: 12, - color: ax.endlinecolor - }); - - // Compute which labels to show. In a sense this is sort of a data computation - // that should go in calc.js, but it's so minimal for any conceivable case that - // I'll write it here for now: - - /* - ax._gridIndices = []; - for(i = ax.gridoffset; i < traceIn[axis].length; i += ax.gridstep) { - ax._gridIndices.push(i); - } - - // Ensure the first grid line shows up: - if(ax._gridIndices[0] !== 0) { - ax._gridIndices.unshift(0); - } - - // Ensure the final grid line shows up: - if(ax._gridIndices[ax._gridIndices.length - 1] !== traceIn[axis].length - 1) { - ax._gridIndices.push(traceIn[axis].length - 1); - } - - // Labels don't require first and last, so just use user-provided offset + step: - ax._majorIndices = []; - for(i = ax.gridoffset; i < traceIn[axis].length; i += ax.gridstep) { - ax._majorIndices.push(i); - } - - if(ax.showstartlabel) { - // Ensure the first grid line shows up: - if(ax._majorIndices[0] !== 0) { - ax._majorIndices.unshift(0); - } - } + var len = handleXYDefaults(traceIn, traceOut, coerce); - if(ax.showendlabel) { - // Ensure the final grid line shows up: - if(ax._majorIndices[ax._majorIndices.length - 1] !== traceIn[axis].length - 1) { - ax._majorIndices.push(traceIn[axis].length - 1); - } + if(!len) { + //traceOut.visible = false; + //return; } - ax._minorGridIndices = []; - for(i = ax.minorgridoffset; i < traceIn[axis].length; i += ax.minorgridstep) { - if(ax._gridIndices.indexOf(i) === -1) { - ax._minorGridIndices.push(i); - } - } - */ -} + console.log('traceOut.visible'); +}; diff --git a/src/traces/carpet/smooth-fill-2d-array.js b/src/traces/carpet/smooth-fill-2d-array.js new file mode 100644 index 00000000000..9ae2962ca4e --- /dev/null +++ b/src/traces/carpet/smooth-fill-2d-array.js @@ -0,0 +1,119 @@ +/** +* 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'; + +module.exports = function smoothFill2dArray (data) { + var i, j, k, n; + var ip = []; + var jp = []; + + var ni = data.length; + var nj = data[0].length; + + function avgSurrounding (i, j) { + var sum = 0.0; + var val; + var cnt = 0; + if (i > 0 && (val = data[i - 1][j]) !== undefined) { + cnt++; + sum += val; + } + if (i < ni - 1 && (val = data[i + 1][j]) !== undefined) { + cnt++; + sum += val; + } + if (j > 0 && (val = data[i][j - 1]) !== undefined) { + cnt++; + sum += val; + } + if (j < nj - 1 && (val = data[i][j + 1]) !== undefined) { + cnt++; + sum += val; + } + return sum / Math.max(1, cnt); + } + + // Track the maximum magnitude so that we can track error relative + // to the maximum: + var dmax = 0.0; + for (i = 0; i < ni; i++) { + for (j = 0; j < nj; j++) { + if (data[i][j] === undefined) { + ip.push(i); + jp.push(j); + data[i][j] = avgSurrounding(i, j); + } + dmax = Math.max(dmax, Math.abs(data[i][j])); + } + } + + // The tolerance doesn't need to be too low. It's just for display positioning + var tol = 1e-6; + var resid = 0; + var itermax = 100; + var iter = 0; + var n = ip.length; + do { + resid = 0; + for(k = 0; k < n; k++) { + i = ip[k]; + j = jp[k]; + + // Note the second-derivative = 0 condition at the edges. This may seem + // like overkill, except this is for layout so that pure Neumann conditions + // (deriv = 0) will just dump all the points on top of one another. + var boundaryCnt = 0; + var newVal = 0; + + if (i === 0) { + newVal += 2.0 * data[1][j] - data[2][j]; + boundaryCnt++; + } else if (i === ni - 1) { + newVal += 2.0 * data[ni - 2][j] - data[ni - 3][j]; + boundaryCnt++; + } + + if (j === 0) { + newVal += 2.0 * data[i][1] - data[i][2]; + boundaryCnt++; + } else if (j === ni - 1) { + newVal += 2.0 * data[i][nj - 2] - data[i][nj - 3]; + boundaryCnt++; + } + + if (!boundaryCnt) { + // interior point, so simply average: + newVal = 0.25 * ( + data[i - 1][j] + + data[i + 1][j] + + data[i][j - 1] + + data[i][j + 1] + ); + } else { + newVal /= boundaryCnt; + } + + var diff = newVal - data[i][j]; + var reldiff = diff / dmax; + resid += reldiff * reldiff; + + // Gauss-Seidel-ish iteration, omega chosen based on heuristics and some + // quick tests. + // + // NB: Don't overrelax the boundaries + data[i][j] += diff * (1 + (boundaryCnt ? 0 : 0.8)); + } + + resid = Math.sqrt(resid); + } while (iter++ < itermax && resid > tol); + + //console.log('residual is', resid, 'after', iter, 'iterations'); + + return data; +} diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index cfccf2ec1d5..926d454b7d3 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -14,23 +14,27 @@ // var isNumeric = require('fast-isnumeric'); var hasColumns = require('./has_columns'); - +var convertColumnData = require('../heatmap/convert_column_xyz'); module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { + var hasxcols = true; + var hasycols = true; + var cols = []; var x = coerce('x'); - if(x && !hasColumns(x)) { - // x absent is valid, but x present is only valid - // if x has columns - return 0; - } + var hasxcols = !!(x && !hasColumns(x)); + if (hasxcols) cols.push('x'); traceOut._cheater = !x; var y = coerce('y'); - // y must be both present *and* must have columns - if(!y || !hasColumns(y)) { + var hasycols = !!(y && !hasColumns(y)); + if (hasycols) cols.push('y'); + + if (cols.length) { + convertColumnData(traceOut, traceOut.aaxis, traceOut.baxis, 'a', 'b', cols); + } else { return 0; } diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index ae307000e5b..1f48f11eed4 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -16,7 +16,7 @@ var Axes = require('../../plots/cartesian/axes'); var histogram2dCalc = require('../histogram2d/calc'); var colorscaleCalc = require('../../components/colorscale/calc'); var hasColumns = require('./has_columns'); -var convertColumnXYZ = require('./convert_column_xyz'); +var convertColumnData = require('./convert_column_xyz'); var maxRowLength = require('./max_row_length'); var clean2dArray = require('./clean_2d_array'); var interp2d = require('./interp2d'); @@ -57,7 +57,7 @@ module.exports = function calc(gd, trace) { z = binned.z; } else { - if(hasColumns(trace)) convertColumnXYZ(trace, xa, ya); + if(hasColumns(trace)) convertColumnData(trace, xa, ya, 'x', 'y', ['z']); x = trace.x ? xa.makeCalcdata(trace, 'x') : []; y = trace.y ? ya.makeCalcdata(trace, 'y') : []; diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 1798b4e93b8..e92b2942abf 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -12,46 +12,63 @@ var Lib = require('../../lib'); -module.exports = function convertColumnXYZ(trace, xa, ya) { - var xCol = trace.x.slice(), - yCol = trace.y.slice(), - zCol = trace.z, +module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, arrayVarNames) { + console.log('arrayVarNames:', arrayVarNames); + var col1 = trace[var1Name].slice(), + col2 = trace[var2Name].slice(), textCol = trace.text, - colLen = Math.min(xCol.length, yCol.length, zCol.length), + colLen = Math.min(col1.length, col2.length), hasColumnText = (textCol !== undefined && !Array.isArray(textCol[0])), - xcalendar = trace.xcalendar, - ycalendar = trace.ycalendar; + col1Calendar = trace[var1Name + 'calendar'], + col2Calendar = trace[var2Name + 'calendar']; - var i; + var i, j, arrayVar, newArrays, newArray, arrayVarName; - if(colLen < xCol.length) xCol = xCol.slice(0, colLen); - if(colLen < yCol.length) yCol = yCol.slice(0, colLen); + for (i = 0; i < arrayVarNames.length; i++) { + arrayVar = trace[arrayVarNames[i]]; + if (arrayVar) colLen = Math.min(colLen, arrayVar.length); + } + + if(colLen < col1.length) col1 = col1.slice(0, colLen); + if(colLen < col2.length) col2 = col2.slice(0, colLen); for(i = 0; i < colLen; i++) { - xCol[i] = xa.d2c(xCol[i], 0, xcalendar); - yCol[i] = ya.d2c(yCol[i], 0, ycalendar); + col1[i] = ax1.d2c(col1[i], 0, col1Calendar); + col2[i] = ax2.d2c(col2[i], 0, col2Calendar); } - var xColdv = Lib.distinctVals(xCol), - x = xColdv.vals, - yColdv = Lib.distinctVals(yCol), - y = yColdv.vals, - z = Lib.init2dArray(y.length, x.length); + var col1dv = Lib.distinctVals(col1), + col1vals = col1dv.vals, + col2dv = Lib.distinctVals(col2), + col2vals = col2dv.vals, + newArrays = []; + + for (i = 0; i < arrayVarNames.length; i++) { + newArrays[i] = Lib.init2dArray(col2vals.length, col1vals.length); + } - var ix, iy, text; + var i1, i2, text; - if(hasColumnText) text = Lib.init2dArray(y.length, x.length); + if(hasColumnText) text = Lib.init2dArray(col2vals.length, col1vals.length); for(i = 0; i < colLen; i++) { - ix = Lib.findBin(xCol[i] + xColdv.minDiff / 2, x); - iy = Lib.findBin(yCol[i] + yColdv.minDiff / 2, y); + i1 = Lib.findBin(col1[i] + col1dv.minDiff / 2, col1vals); + i2 = Lib.findBin(col2[i] + col2dv.minDiff / 2, col2vals); + + for(j = 0; j < arrayVarNames.length; j++) { + arrayVarName = arrayVarNames[j]; + arrayVar = trace[arrayVarName]; + newArray = newArrays[j]; + newArray[i2][i1] = arrayVar[i]; + } - z[iy][ix] = zCol[i]; - if(hasColumnText) text[iy][ix] = textCol[i]; + if(hasColumnText) text[i2][i1] = textCol[i]; } - trace.x = x; - trace.y = y; - trace.z = z; + trace[var1Name] = col1vals; + trace[var2Name] = col2vals; + for(j = 0; j < arrayVarNames.length; j++) { + trace[arrayVarNames[j]] = newArrays[j]; + } if(hasColumnText) trace.text = text; }; From e731152d7af596f5683d93ddb7b574ef1aee9f8b Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 16 Jan 2017 07:51:50 -0500 Subject: [PATCH 015/132] Supply default reorganization for carpet plots --- src/plots/cartesian/axes.js | 2 + src/traces/carpet/ab_defaults.js | 19 +++-- src/traces/carpet/attributes.js | 13 ++++ src/traces/carpet/axis_defaults.js | 10 ++- src/traces/carpet/calc.js | 45 +++++------ src/traces/carpet/calc_gridlines.js | 1 - src/traces/carpet/calc_labels.js | 16 ++-- src/traces/carpet/cheater_basis.js | 12 +-- src/traces/carpet/defaults.js | 16 ++-- src/traces/carpet/smooth-fill-2d-array.js | 93 ++++++++++++++++------- src/traces/carpet/xy_defaults.js | 16 ++-- src/traces/heatmap/convert_column_xyz.js | 2 - 12 files changed, 161 insertions(+), 84 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 64dc21e4b71..13bedfee732 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1124,6 +1124,8 @@ axes.tickText = function(ax, x, hover) { if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text; if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix; + console.log('out:', out); + return out; }; diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index cd735265547..d45d6f03df3 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -15,7 +15,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var handleAxisDefaults = require('./axis_defaults'); var attributes = require('./attributes'); -module.exports = function handleABDefaults(traceIn, traceOut, coerce) { +module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce) { var a = coerce('a'); if(!a) { @@ -30,32 +30,39 @@ module.exports = function handleABDefaults(traceIn, traceOut, coerce) { coerce('b0'); } - mimickAxisDefaults(traceIn, traceOut); + mimickAxisDefaults(traceIn, traceOut, fullLayout); return; }; -function mimickAxisDefaults (traceIn, traceOut) { +function mimickAxisDefaults (traceIn, traceOut, fullLayout) { var axesList = ['aaxis', 'baxis']; axesList.forEach(function(axName) { var axLetter = axName.charAt(0), axIn = traceIn[axName] || {}, - axOut = {}, + axOut = { + _gd: { + _fullLayout: { + separators: fullLayout.separators + } + } + }, defaultOptions = { + tickfont: 'x', id: axLetter + 'axis', letter: axLetter, font: traceOut.font, name: axName, data: traceIn[axLetter], - calendar: traceOut.calendar + calendar: traceOut.calendar, }; function coerce(attr, dflt) { return Lib.coerce(axIn, axOut, attributes, attr, dflt); } - handleAxisDefaults(axIn, axOut, coerce, defaultOptions, traceOut); + handleAxisDefaults(axIn, axOut, coerce, defaultOptions); axOut._categories = axOut._categories || []; diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index ee955fcc0db..a10c340aeff 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -9,7 +9,9 @@ 'use strict'; var extendFlat = require('../../lib/extend').extendFlat; +var fontAttrs = require('../../plots/font_attributes'); var axisAttrs = require('./axis_attributes'); +var colorAttrs = require('../../components/color/attributes'); module.exports = { carpetid: { @@ -93,4 +95,15 @@ module.exports = { }, aaxis: extendFlat({}, axisAttrs), baxis: extendFlat({}, axisAttrs), + font: { + family: extendFlat({}, fontAttrs.family, { + dflt: '"Open Sans", verdana, arial, sans-serif' + }), + size: extendFlat({}, fontAttrs.size, { + dflt: 12 + }), + color: extendFlat({}, fontAttrs.color, { + dflt: colorAttrs.defaultLine + }), + }, }; diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 4c9b3463830..296180f7742 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -11,7 +11,6 @@ var axisAttributes = require('./attributes'); var extendFlat = require('../../lib/extend').extendFlat; var setConvert = require('../../plots/cartesian/set_convert'); -var Lib = require('../../lib'); var handleCartesianAxisDefaults = require('../../plots/cartesian/axis_defaults'); var isNumeric = require('fast-isnumeric'); @@ -227,6 +226,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, color: dfltFontColor }); + Lib.coerceFont(coerce, 'tickfont', { + family: font.family, + size: Math.round(font.size * 1.2), + color: dfltFontColor + }); + var validRange = ( (containerIn.range || []).length === 2 && isNumeric(containerOut.r2l(containerIn.range[0])) && @@ -280,6 +285,9 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : []; + // Something above overrides this deep in the axis code, but no, we actually want to coerce this. + coerce('tickmode'); + return containerOut; }; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 615a4427b18..a87c0e7246e 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -39,10 +39,31 @@ module.exports = function calc(gd, trace) { xdata = trace.x; } + // Precompute the cartesian-space coordinates of the grid lines: + var x = xdata; + var y = trace.y; + + trace.x = x = clean2dArray(x); + trace.y = y = clean2dArray(y); + + // Fill in any undefined values with elliptic smoothing. This doesn't take + // into account the spacing of the values. That is, the derivatives should + // be modified to use a and b values. It's not that hard, but this is already + // moderate overkill for just filling in missing values. + smoothFill2dArray(x, a ,b); + smoothFill2dArray(y, a, b); + + // Create conversions from one coordinate system to another: + setConvert(trace, xa, ya); + + // Convert cartesian-space x/y coordinates to screen space pixel coordinates: + trace.xp = map2dArray(trace.xp, x, xa.c2p); + trace.yp = map2dArray(trace.yp, y, ya.c2p); + // This is a rather expensive scan. Nothing guarantees monotonicity, // so we need to scan through all data to get proper ranges: - var xrange = arrayMinmax(xdata); - var yrange = arrayMinmax(ydata); + var xrange = arrayMinmax(x); + var yrange = arrayMinmax(y); var dx = 0.5 * (xrange[1] - xrange[0]); var xc = 0.5 * (xrange[1] + xrange[0]); @@ -57,26 +78,6 @@ module.exports = function calc(gd, trace) { Axes.expand(xa, xrange, {padded: true}); Axes.expand(ya, yrange, {padded: true}); - // Precompute the cartesian-space coordinates of the grid lines: - var x = xdata; - var y = trace.y; - - // Convert cartesian-space x/y coordinates to screen space pixel coordinates: - trace.xp = map2dArray(trace.xp, x, xa.c2p); - trace.yp = map2dArray(trace.yp, y, ya.c2p); - x = clean2dArray(x, true); - y = clean2dArray(y, true); - - // Fill in any undefined values with elliptic smoothing. This doesn't take - // into account the spacing of the values. That is, the derivatives should - // be modified to use a and b values. It's not that hard, but this is already - // moderate overkill for just filling in missing values. - smoothFill2dArray(x); - smoothFill2dArray(y); - - // Create conversions from one coordinate system to another: - setConvert(trace, xa, ya); - calcGridlines(trace, 'a', 'b'); calcGridlines(trace, 'b', 'a'); diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 745505e67cb..1944147ba44 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -211,7 +211,6 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { return ret; }; - if (axis.tickmode === 'array') { var j0, j1; //var j0 = axis.startline ? 1 : 0; diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index fc64663b945..f80632433d1 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -26,7 +26,7 @@ function normalize (x) { } module.exports = function calcLabels (trace, axis) { - var i; + var i, tobj; var labels = axis._labels = []; var gridlines = axis._gridlines; @@ -35,8 +35,9 @@ module.exports = function calcLabels (trace, axis) { var gridline = gridlines[i]; if (['start', 'both'].indexOf(axis.showlabels) !== -1) { - labels.push({ - text: gridline.value.toFixed(3), + tobj = Axes.tickText(axis, gridline.value); + + extendFlat(tobj, { endAnchor: true, xy: gridline.xy(0), dxy: gridline.dxy(0, 0), @@ -46,11 +47,14 @@ module.exports = function calcLabels (trace, axis) { isFirst: i === 0, isLast: i === gridlines.length - 1 }); + + labels.push(tobj); } if (['end', 'both'].indexOf(axis.showlabels) !== -1) { - labels.push({ - text: gridline.value.toFixed(3), + tobj = Axes.tickText(axis, gridline.value); + + extendFlat(tobj, { endAnchor: false, xy: gridline.xy(gridline.crossLength - 1), dxy: gridline.dxy(gridline.crossLength - 2, 1), @@ -60,6 +64,8 @@ module.exports = function calcLabels (trace, axis) { isFirst: i === 0, isLast: i === gridlines.length - 1 }); + + labels.push(tobj); } } }; diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 26e054a04e1..66cfd2fae6b 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -35,12 +35,12 @@ module.exports = function(a, b, cheaterslope) { var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } - for(j = 0; j < nb; j++) { - var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); - data[j] = []; - for(i = 0; i < na; i++) { - var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); - data[j][i] = aval - bval * cheaterslope; + for(i = 0; i < na; i++) { + data[i] = []; + var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); + for(j = 0; j < nb; j++) { + var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); + data[i][j] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index f381ba45401..2f7a3eeccad 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -10,22 +10,26 @@ 'use strict'; var Lib = require('../../lib'); - var handleXYDefaults = require('./xy_defaults'); var handleABDefaults = require('./ab_defaults'); var attributes = require('./attributes'); -module.exports = function supplyDefaults(traceIn, traceOut) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, fullLayout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + console.log('fullLayout:', fullLayout); + coerce('carpetid'); coerce('cheaterslope'); + Lib.coerceFont(coerce, 'font'); + console.log('traceOut.font:', fullLayout.font); + traceOut.cheaterslope = parseFloat(traceIn.cheaterslope); - handleABDefaults(traceIn, traceOut, coerce); + handleABDefaults(traceIn, traceOut, fullLayout, coerce); if (traceOut.a.length < 3) { traceOut.aaxis.smoothing = 0; @@ -38,9 +42,7 @@ module.exports = function supplyDefaults(traceIn, traceOut) { var len = handleXYDefaults(traceIn, traceOut, coerce); if(!len) { - //traceOut.visible = false; - //return; + traceOut.visible = false; + return; } - - console.log('traceOut.visible'); }; diff --git a/src/traces/carpet/smooth-fill-2d-array.js b/src/traces/carpet/smooth-fill-2d-array.js index 9ae2962ca4e..c9f96c2d7d2 100644 --- a/src/traces/carpet/smooth-fill-2d-array.js +++ b/src/traces/carpet/smooth-fill-2d-array.js @@ -8,7 +8,7 @@ 'use strict'; -module.exports = function smoothFill2dArray (data) { +module.exports = function smoothFill2dArray (data, a, b) { var i, j, k, n; var ip = []; var jp = []; @@ -53,8 +53,10 @@ module.exports = function smoothFill2dArray (data) { } } - // The tolerance doesn't need to be too low. It's just for display positioning - var tol = 1e-6; + if (!ip.length) return data; + + // The tolerance doesn't need to be excessive. It's just for display positioning + var tol = 1e-5; var resid = 0; var itermax = 100; var iter = 0; @@ -65,38 +67,77 @@ module.exports = function smoothFill2dArray (data) { i = ip[k]; j = jp[k]; - // Note the second-derivative = 0 condition at the edges. This may seem - // like overkill, except this is for layout so that pure Neumann conditions - // (deriv = 0) will just dump all the points on top of one another. - var boundaryCnt = 0; + // Note the second-derivative = 0 condition at the edges. + var contribCnt = 0; var newVal = 0; + var d0, d1, x0, x1, i0, j0; if (i === 0) { - newVal += 2.0 * data[1][j] - data[2][j]; - boundaryCnt++; + i0 = Math.min(ni - 1, 2); + x0 = a[i0] + x1 = a[1]; + d0 = data[i0][j]; + d1 = data[1][j]; + newVal += d1 + (d1 - d0) * (a[0] - x1) / (x1 - x0); + contribCnt++; } else if (i === ni - 1) { - newVal += 2.0 * data[ni - 2][j] - data[ni - 3][j]; - boundaryCnt++; + i0 = Math.max(0, ni - 3); + x0 = a[i0]; + x1 = a[ni - 2]; + d0 = data[i0][j]; + d1 = data[ni - 2][j]; + newVal += d1 + (d1 - d0) * (a[ni - 1] - x1) / (x1 - x0); + contribCnt++; + } + + if ((i === 0 || i === ni - 1) && (j > 0 && j < nj - 1)) { + var dxp = b[j + 1] - b[j]; + var dxm = b[j] - b[j - 1]; + newVal += (dxm * data[i][j + 1] + dxp * data[i][j - 1]) / (dxm + dxp); + contribCnt++; } if (j === 0) { - newVal += 2.0 * data[i][1] - data[i][2]; - boundaryCnt++; - } else if (j === ni - 1) { - newVal += 2.0 * data[i][nj - 2] - data[i][nj - 3]; - boundaryCnt++; + j0 = Math.min(nj - 1, 2); + x0 = b[j0]; + x1 = b[1]; + d0 = data[i][j0]; + d1 = data[i][1]; + newVal += d1 + (d1 - d0) * (b[0] - x1) / (x1 - x0); + contribCnt++; + } else if (j === nj - 1) { + j0 = Math.max(0, nj - 3); + x0 = b[j0]; + x1 = b[nj - 2]; + d0 = data[i][j0]; + d1 = data[i][nj - 2]; + newVal += d1 + (d1 - d0) * (b[ni - 1] - x1) / (x1 - x0); + contribCnt++; } - if (!boundaryCnt) { + if ((j === 0 || j === nj - 1) && (i > 0 && i < ni - 1)) { + var dxp = a[i + 1] - a[i]; + var dxm = a[i] - a[i - 1]; + newVal += (dxm * data[i + 1][j] + dxp * data[i - 1][j]) / (dxm + dxp); + contribCnt++; + } + + if (!contribCnt) { // interior point, so simply average: - newVal = 0.25 * ( - data[i - 1][j] + - data[i + 1][j] + - data[i][j - 1] + - data[i][j + 1] - ); + // Get the grid spacing on either side: + var dap = a[i + 1] - a[i]; + var dam = a[i] - a[i - 1]; + var dbp = b[j + 1] - b[j]; + var dbm = b[j] - b[j - 1]; + // Some useful constants: + var c = dap * dam * (dap + dam); + var d = dbp * dbm * (dbp + dbm); + + newVal = (c * (dbm * data[i][j + 1] + dbp * data[i][j - 1]) + d * (dam * data[i + 1][j] + dap * data[i - 1][j])) / + (d * (dam + dap) + c * (dbm + dbp)); + } else { - newVal /= boundaryCnt; + newVal /= contribCnt; } var diff = newVal - data[i][j]; @@ -107,13 +148,13 @@ module.exports = function smoothFill2dArray (data) { // quick tests. // // NB: Don't overrelax the boundaries - data[i][j] += diff * (1 + (boundaryCnt ? 0 : 0.8)); + data[i][j] += diff * (1 + (contribCnt ? 0 : 0.8)); } resid = Math.sqrt(resid); } while (iter++ < itermax && resid > tol); - //console.log('residual is', resid, 'after', iter, 'iterations'); + console.log('Smoother converged to', resid, 'after', iter, 'iterations'); return data; } diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 926d454b7d3..7faf5c4c307 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -22,23 +22,23 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var cols = []; var x = coerce('x'); - var hasxcols = !!(x && !hasColumns(x)); - if (hasxcols) cols.push('x'); + var needsXTransform = x && !hasColumns(x); + if (needsXTransform) cols.push('x'); traceOut._cheater = !x; var y = coerce('y'); - var hasycols = !!(y && !hasColumns(y)); - if (hasycols) cols.push('y'); + var needsYTransform = y && !hasColumns(y); + if (needsYTransform) cols.push('y'); + + if (!x && !y) return; if (cols.length) { - convertColumnData(traceOut, traceOut.aaxis, traceOut.baxis, 'a', 'b', cols); - } else { - return 0; + convertColumnData(traceOut, traceOut.baxis, traceOut.aaxis, 'b', 'a', cols); } - return y.length; + return true; }; /* diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 9133e6f7f74..1d307a65890 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -11,9 +11,7 @@ var Lib = require('../../lib'); - module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, arrayVarNames) { - console.log('arrayVarNames:', arrayVarNames); var col1 = trace[var1Name].slice(), col2 = trace[var2Name].slice(), textCol = trace.text, From fb002c8c29dd7533d26c90e09df14952c70701c8 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 16 Jan 2017 18:58:32 -0500 Subject: [PATCH 016/132] Major refactor of carpet axes --- src/components/drawing/spline_evaluator.js | 35 ++-- src/plots/cartesian/axes.js | 4 +- src/plots/cartesian/axis_defaults.js | 2 + src/plots/cartesian/layout_attributes.js | 8 + src/plots/cartesian/layout_defaults.js | 13 +- src/traces/carpet/ab_defaults.js | 44 ++--- src/traces/carpet/attributes.js | 11 ++ src/traces/carpet/axis_attributes.js | 166 ++++++----------- src/traces/carpet/axis_defaults.js | 168 ++++-------------- src/traces/carpet/calc.js | 6 +- src/traces/carpet/calc_gridlines.js | 100 +++++------ src/traces/carpet/calc_labels.js | 20 ++- src/traces/carpet/catmull_rom.js | 25 ++- src/traces/carpet/cheater_basis.js | 4 +- src/traces/carpet/compute_control_points.js | 38 ++-- .../carpet/create_i_derivative_evaluator.js | 38 ++-- .../carpet/create_j_derivative_evaluator.js | 46 ++--- src/traces/carpet/create_spline_evaluator.js | 42 ++--- src/traces/carpet/defaults.js | 13 +- src/traces/carpet/ensure_array.js | 6 +- src/traces/carpet/index.js | 2 +- src/traces/carpet/interpolate.js | 28 +-- src/traces/carpet/makepath.js | 10 +- src/traces/carpet/map_1d_array.js | 12 +- src/traces/carpet/map_2d_array.js | 18 +- src/traces/carpet/plot.js | 30 +--- src/traces/carpet/set_convert.js | 42 ++--- src/traces/carpet/smooth-fill-2d-array.js | 40 ++--- src/traces/carpet/xy_defaults.js | 8 +- src/traces/heatmap/convert_column_xyz.js | 6 +- src/traces/scattercarpet/calc.js | 6 +- src/traces/scattercarpet/index.js | 6 +- src/traces/scattercarpet/plot.js | 2 +- 33 files changed, 430 insertions(+), 569 deletions(-) diff --git a/src/components/drawing/spline_evaluator.js b/src/components/drawing/spline_evaluator.js index 050c5890430..da68d8278cc 100644 --- a/src/components/drawing/spline_evaluator.js +++ b/src/components/drawing/spline_evaluator.js @@ -13,22 +13,22 @@ // http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf var CatmullRomExp = 0.5; function makeControlPoints(p0, p1, p2, smoothness) { - var d1x = p0[0] - p1[0], - d1y = p0[1] - p1[1], - d2x = p2[0] - p1[0], - d2y = p2[1] - p1[1], - d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), - d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), - numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, - numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, - denom1 = 3 * d2a * (d1a + d2a), - denom2 = 3 * d1a * (d1a + d2a); + var d1x = p0[0] - p1[0], + d1y = p0[1] - p1[1], + d2x = p2[0] - p1[0], + d2y = p2[1] - p1[1], + d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), + d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), + numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, + numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, + denom1 = 3 * d2a * (d1a + d2a), + denom2 = 3 * d1a * (d1a + d2a); var dxL = p1[0] + (denom1 && numx / denom1); var dyL = p1[1] + (denom1 && numy / denom1); var dxU = p1[0] - (denom2 && numx / denom2); var dyU = p1[1] - (denom2 && numy / denom2); - return [[dxL, dyL], [dxU, dyU]]; + return [[dxL, dyL], [dxU, dyU]]; } /* @@ -41,17 +41,17 @@ function makeControlPoints(p0, p1, p2, smoothness) { * In other words, it really doesn't make too much of a difference whether we * work with tangents or control points. */ -module.exports.computeControlPoints = function (pts, smoothness) { +module.exports.computeControlPoints = function(pts, smoothness) { var tangents = []; for(i = 1; i < pts.length - 1; i++) { tangents.push(makeControlPoints(pts[i - 1], pts[i], pts[i + 1], smoothness)); } return tangents; -} +}; -module.exports.evaluateSpline = function evaluateSpline (t, pts, controlpts, smoothness) { +module.exports.evaluateSpline = function evaluateSpline(t, pts, controlpts, smoothness) { var n = pts.length; - if (n < 3) { + if(n < 3) { var x = (1 - t) * pts[0][0] + t * pts[1][0]; var y = (1 - t) * pts[0][1] + t * pts[1][1]; return [x, y]; @@ -66,7 +66,7 @@ module.exports.evaluateSpline = function evaluateSpline (t, pts, controlpts, smo p0 = pts[i]; p3 = pts[i + 1]; - if (i === 0 || i === n - 2) { + if(i === 0 || i === n - 2) { // Evaluate the quadratic first and last segments; p1 = i === 0 ? controlpts[i][0] : controlpts[i - 1][1]; a = ot * ot; @@ -93,5 +93,4 @@ module.exports.evaluateSpline = function evaluateSpline (t, pts, controlpts, smo ]; } } -} - +}; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 13bedfee732..6580c0b1ec8 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1124,8 +1124,6 @@ axes.tickText = function(ax, x, hover) { if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text; if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix; - console.log('out:', out); - return out; }; @@ -1662,6 +1660,8 @@ axes.doTicks = function(gd, axid, skipTitle) { ticksign = ticksign.map(function(v) { return -v; }); } + if(!ax.visible) return; + // remove zero lines, grid lines, and inside ticks if they're within // 1 pixel of the end // The key case here is removing zero lines when the axis bound is zero. diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 4a4cb04295c..0d641495436 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -57,6 +57,8 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, containerOut._id = axisIds.name2id(options.name); } + coerce('visible', !options.cheateronly); + // now figure out type and do some more initialization var axType = coerce('type'); if(axType === '-') { diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b917c4cf8e1..134dd183b1e 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -16,6 +16,14 @@ var constants = require('./constants'); module.exports = { + visible: { + valType: 'boolean', + description: [ + 'A single toggle to hide the axis while preserving interaction like dragging.', + 'Default is true when a cheater plot is present on the axis, otherwise', + 'false' + ].join(' ') + }, color: { valType: 'color', dflt: colorAttrs.defaultLine, diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 56c5d008ae6..2ed33956055 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -27,6 +27,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { yaListCartesian = [], xaListGl2d = [], yaListGl2d = [], + xaListNotCheater = [], outerTicks = {}, noGrids = {}, i; @@ -49,6 +50,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var xaName = axisIds.id2name(trace.xaxis), yaName = axisIds.id2name(trace.yaxis); + // Note that we track the *opposite* of whether it's a cheater plot + // because that makes it straightforward to check that any trace on + // this axis that's *not* a cheater will make it visible + if((!Registry.traceIs(trace, 'carpet') || !trace._cheater) && xaListNotCheater.indexOf(xaName) === -1) { + xaListNotCheater.push(xaName); + } + // add axes implied by traces if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName); if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName); @@ -65,6 +73,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } } + console.log('xaListNotCheater:', xaListNotCheater); + // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) // if gl3d or geo is present on graph. This is retain backward compatible. // @@ -127,7 +137,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { name: axName, data: fullData, bgColor: bgColor, - calendar: layoutOut.calendar + calendar: layoutOut.calendar, + cheateronly: axLetter === 'x' && xaListNotCheater.indexOf(axName) === -1 }, positioningOptions = { letter: axLetter, diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index d45d6f03df3..fbd3d91700b 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -15,7 +15,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var handleAxisDefaults = require('./axis_defaults'); var attributes = require('./attributes'); -module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce) { +module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce, dfltColor) { var a = coerce('a'); if(!a) { @@ -30,33 +30,37 @@ module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce coerce('b0'); } - mimickAxisDefaults(traceIn, traceOut, fullLayout); + mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor); return; }; -function mimickAxisDefaults (traceIn, traceOut, fullLayout) { +function mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor) { var axesList = ['aaxis', 'baxis']; axesList.forEach(function(axName) { - var axLetter = axName.charAt(0), - axIn = traceIn[axName] || {}, - axOut = { - _gd: { - _fullLayout: { - separators: fullLayout.separators - } + var axLetter = axName.charAt(0); + var axIn = traceIn[axName] || {}; + + var axOut = { + _gd: { + _fullLayout: { + separators: fullLayout.separators } - }, - defaultOptions = { - tickfont: 'x', - id: axLetter + 'axis', - letter: axLetter, - font: traceOut.font, - name: axName, - data: traceIn[axLetter], - calendar: traceOut.calendar, - }; + } + }; + + var defaultOptions = { + tickfont: 'x', + id: axLetter + 'axis', + letter: axLetter, + font: traceOut.font, + name: axName, + data: traceIn[axLetter], + calendar: traceOut.calendar, + dfltColor: dfltColor, + bgColor: fullLayout.paper_bgcolor, + }; function coerce(attr, dflt) { return Lib.coerce(axIn, axOut, attributes, attr, dflt); diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index a10c340aeff..7556f3efa8f 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -106,4 +106,15 @@ module.exports = { dflt: colorAttrs.defaultLine }), }, + color: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: [ + 'Sets default for all colors associated with this axis', + 'all at once: line, font, tick, and grid colors.', + 'Grid color is lightened by blending this with the plot background', + 'Individual pieces can override this.' + ].join(' ') + }, }; diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index ad31a0fd55a..ebc00705fa9 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -15,7 +15,6 @@ var colorAttrs = require('../../components/color/attributes'); module.exports = { color: { valType: 'color', - dflt: colorAttrs.defaultLine, role: 'style', description: [ 'Sets default for all colors associated with this axis', @@ -366,10 +365,10 @@ module.exports = { 'Used with `categoryorder`.' ].join(' ') }, - showlabels: { + showticklabels: { valType: 'enumerated', values: ['start', 'end', 'both', 'none'], - dflt: 'end', + dflt: 'start', role: 'style', description: [ 'Determines whether axis labels are drawn on the low side,', @@ -401,29 +400,7 @@ module.exports = { valType: 'boolean', dflt: true, }, - showlabelprefix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: [ - 'If *all*, all tick labels are displayed with a prefix.', - 'If *first*, only the first tick is displayed with a prefix.', - 'If *last*, only the last tick is displayed with a suffix.', - 'If *none*, tick prefixes are hidden.' - ].join(' ') - }, - showlabelsuffix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: 'Same as `showtickprefix` but for tick suffixes.' - }, - startlabelfont: extendFlat({}, fontAttrs, { - description: 'Sets the label font.' - }), - endlabelfont: extendFlat({}, fontAttrs, { + labelfont: extendFlat({}, fontAttrs, { description: 'Sets the label font.' }), // lines and grids @@ -448,47 +425,86 @@ module.exports = { role: 'style', description: 'Sets the width (in px) of the axis line.' }, + gridcolor: { + valType: 'color', + role: 'style', + description: 'Sets the axis line color.' + }, + gridwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the axis line.' + }, showgrid: { valType: 'boolean', role: 'style', + dflt: true, description: [ 'Determines whether or not grid lines are drawn.', 'If *true*, the grid lines are drawn at every tick mark.' ].join(' ') }, - gridcolor: { - valType: 'color', - dflt: colorAttrs.lightLine, - role: 'style', - description: 'Sets the color of the grid lines.' + minorgridcount: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info', + description: 'Sets the number of minor grid ticks per major grid tick' }, - gridwidth: { + minorgridwidth: { valType: 'number', min: 0, dflt: 1, role: 'style', description: 'Sets the width (in px) of the grid lines.' }, - zeroline: { + minorgridcolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, + startline: { valType: 'boolean', role: 'style', description: [ - 'Determines whether or not a line is drawn at along the 0 value', + 'Determines whether or not a line is drawn at along the starting value', 'of this axis.', - 'If *true*, the zero line is drawn on top of the grid lines.' + 'If *true*, the start line is drawn on top of the grid lines.' ].join(' ') }, - zerolinecolor: { + startlinecolor: { valType: 'color', - dflt: colorAttrs.defaultLine, role: 'style', - description: 'Sets the line color of the zero line.' + description: 'Sets the line color of the start line.' }, - zerolinewidth: { + startlinewidth: { valType: 'number', dflt: 1, role: 'style', - description: 'Sets the width (in px) of the zero line.' + description: 'Sets the width (in px) of the start line.' + }, + endline: { + valType: 'boolean', + role: 'style', + description: [ + 'Determines whether or not a line is drawn at along the final value', + 'of this axis.', + 'If *true*, the end line is drawn on top of the grid lines.' + ].join(' ') + }, + endlinewidth: { + valType: 'number', + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the end line.' + }, + endlinecolor: { + valType: 'color', + role: 'style', + description: 'Sets the line color of the end line.' }, tick0: { valType: 'any', @@ -518,75 +534,6 @@ module.exports = { role: 'info', description: 'The stride between grid lines along the axis' }, - gridwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - gridcolor: { - valType: 'color', - dflt: '#aaa', - role: 'style', - description: 'Sets the color of the grid lines.' - }, - startlinewidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - startline: { - valType: 'boolean', - role: 'info', - dflt: true - }, - endline: { - valType: 'boolean', - role: 'info', - dflt: true - }, - startlinecolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the color of the grid lines.' - }, - endlinewidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - endlinecolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - role: 'style', - description: 'Sets the color of the grid lines.' - }, - minorgridcount: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'info', - description: 'Sets the number of minor grid ticks per major grid tick' - }, - minorgridwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - minorgridcolor: { - valType: 'color', - dflt: colorAttrs.lightLine, - role: 'style', - description: 'Sets the color of the grid lines.' - }, hoverformat: { valType: 'string', dflt: '', @@ -603,3 +550,4 @@ module.exports = { ].join(' ') }, }; + diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 296180f7742..d7b2d64fd1f 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -8,7 +8,7 @@ 'use strict'; -var axisAttributes = require('./attributes'); +var carpetAttrs = require('./attributes'); var extendFlat = require('../../lib/extend').extendFlat; var setConvert = require('../../plots/cartesian/set_convert'); var handleCartesianAxisDefaults = require('../../plots/cartesian/axis_defaults'); @@ -18,7 +18,6 @@ var colorMix = require('tinycolor2').mix; var Registry = require('../../registry'); var Lib = require('../../lib'); -var lightFraction = require('../../components/color/attributes').lightFraction; var layoutAttributes = require('../../plots/cartesian/layout_attributes'); var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); @@ -30,74 +29,6 @@ var orderedCategories = require('../../plots/cartesian/ordered_categories'); var axisIds = require('../../plots/cartesian/axis_ids'); var autoType = require('../../plots/cartesian/axis_autotype'); - -/*module.exports = function handleAxisDefaults(traceIn, traceOut, axis) { - var i; - - function coerce(attr, dflt) { - return Lib.coerce(traceIn, traceOut, attributes, axis + 'axis.' + attr, dflt); - } - - coerce('type'); - coerce('smoothing'); - traceOut.smoothing = traceOut.smoothing ? 1 : 0; - coerce('cheatertype'); - - coerce('showlabels'); - coerce('labelprefix', axis + ' = '); - coerce('labelsuffix'); - coerce('showlabelprefix'); - coerce('showlabelsuffix'); - - coerce('tickmode'); - coerce('tick0'); - coerce('dtick'); - coerce('arraytick0'); - coerce('arraydtick'); - //coerce('gridoffset'); - //coerce('gridstep'); - - coerce('gridwidth'); - coerce('gridcolor'); - - coerce('startline'); - coerce('startlinewidth', traceOut.gridwidth); - coerce('startlinecolor', traceOut.gridcolor); - coerce('endline'); - coerce('endlinewidth', traceOut.gridwidth); - coerce('endlinecolor', traceOut.gridwidth); - - coerce('minorgridcount'); - coerce('minorgridwidth'); - coerce('minorgridcolor'); - - coerce('showstartlabel'); - coerce('showendlabel'); - - coerce('labelpadding'); - - // We'll never draw this. We just need a couple category management functions. - var ax = traceOut[axis + 'axis'] = extendFlat(traceOut[axis + 'axis'] || {}, { - range: [], - domain: [], - _id: axis, - }); - setConvert(traceOut[axis + 'axis']); - - Lib.coerceFont(coerce, 'startlabelfont', { - size: 12, - color: ax.startlinecolor - }); - - Lib.coerceFont(coerce, 'endlabelfont', { - size: 12, - color: ax.endlinecolor - }); - - ax._hovertitle = axis; -}*/ - - /** * options: object containing: * @@ -114,7 +45,7 @@ var autoType = require('../../plots/cartesian/axis_autotype'); module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) { var letter = options.letter, font = options.font || {}, - attributes = axisAttributes[letter + 'axis'], + attributes = carpetAttrs[letter + 'axis'], defaultTitle = 'Click to enter ' + (options.title || (letter.toUpperCase() + ' axis')) + ' title'; @@ -155,58 +86,28 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, containerOut.smoothing = containerOut.smoothing ? 1 : 0; coerce('cheatertype'); - coerce('showlabels'); + coerce('showticklabels'); coerce('labelprefix', letter + ' = '); coerce('labelsuffix'); - coerce('showlabelprefix'); - coerce('showlabelsuffix'); + coerce('showtickprefix'); + coerce('showticksuffix'); coerce('tickmode'); coerce('tick0'); coerce('dtick'); coerce('arraytick0'); coerce('arraydtick'); - //coerce('gridoffset'); - //coerce('gridstep'); - - coerce('gridwidth'); - coerce('gridcolor'); - - coerce('startline'); - coerce('startlinewidth', containerOut.gridwidth); - coerce('startlinecolor', containerOut.gridcolor); - coerce('endline'); - coerce('endlinewidth', containerOut.gridwidth); - coerce('endlinecolor', containerOut.gridwidth); - - coerce('minorgridcount'); - coerce('minorgridwidth'); - coerce('minorgridcolor'); + // coerce('gridoffset'); + // coerce('gridstep'); coerce('showstartlabel'); coerce('showendlabel'); coerce('labelpadding'); - // We'll never draw this. We just need a couple category management functions. - Lib.coerceFont(coerce, 'startlabelfont', { - size: 12, - color: containerOut.startlinecolor - }); - - Lib.coerceFont(coerce, 'endlabelfont', { - size: 12, - color: containerOut.endlinecolor - }); - containerOut._hovertitle = letter; - - - - - if(axType === 'date') { var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults'); handleCalendarDefaults(containerIn, containerOut, 'calendar', options.calendar); @@ -214,7 +115,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, setConvert(containerOut); - var dfltColor = coerce('color'); + var dfltColor = coerce('color', options.dfltColor); // if axis.color was provided, use it for fonts too; otherwise, // inherit from global font color in case that was provided. var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color; @@ -251,34 +152,37 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, handleTickMarkDefaults(containerIn, containerOut, coerce, options); handleCategoryOrderDefaults(containerIn, containerOut, coerce); - var lineColor = coerce2('linecolor', dfltColor), - lineWidth = coerce2('linewidth'), - showLine = coerce('showline', !!lineColor || !!lineWidth); + var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, 70).toRgbString()); + var gridWidth = coerce2('gridwidth'); + var showGrid = coerce('showgrid'); - if(!showLine) { - delete containerOut.linecolor; - delete containerOut.linewidth; - } + if(!showGrid) { + delete containerOut.gridcolor; + delete containerOut.gridWidth; + } else { + var startLineColor = coerce2('startlinecolor', dfltColor); + var startLineWidth = coerce2('startlinewidth'); + var showStartLine = coerce('startline', containerOut.showgrid || !!startLineColor || !!startLineWidth); + + if(!showStartLine) { + delete containerOut.startlinecolor; + delete containerOut.startlinewidth; + } - if(showLine || containerOut.ticks) coerce('mirror'); + var endLineColor = coerce2('endlinecolor', dfltColor); + var endLineWidth = coerce2('endlinewidth'); + var showStartLine = coerce('endline', containerOut.showgrid || !!endLineColor || !!endLineWidth); - var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, lightFraction).toRgbString()), - gridWidth = coerce2('gridwidth'), - showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth); + if(!showStartLine) { + delete containerOut.endlinecolor; + delete containerOut.endlinewidth; + } - if(!showGridLines) { - delete containerOut.gridcolor; - delete containerOut.gridwidth; + coerce('minorgridcount'); + coerce('minorgridwidth'); + coerce('minorgridcolor', colorMix(gridColor, options.bgColor, 95).toRgbString()); } - var zeroLineColor = coerce2('zerolinecolor', dfltColor), - zeroLineWidth = coerce2('zerolinewidth'), - showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth); - - if(!showZeroLine) { - delete containerOut.zerolinecolor; - delete containerOut.zerolinewidth; - } // fill in categories containerOut._initialCategories = axType === 'category' ? @@ -288,6 +192,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, // Something above overrides this deep in the axis code, but no, we actually want to coerce this. coerce('tickmode'); + // We'll never draw this. We just need a couple category management functions. + Lib.coerceFont(coerce, 'labelfont', { + size: 12, + color: containerOut.startlinecolor + }); + return containerOut; }; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index a87c0e7246e..54c6de64d5c 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -32,7 +32,7 @@ module.exports = function calc(gd, trace) { var ydata = trace.y; if(trace._cheater) { - var avals = aax.cheatertype === 'index' ? a.length : a + var avals = aax.cheatertype === 'index' ? a.length : a; var bvals = bax.cheatertype === 'index' ? b.length : b; trace.x = xdata = cheaterBasis(avals, bvals, trace.cheaterslope); } else { @@ -50,7 +50,7 @@ module.exports = function calc(gd, trace) { // into account the spacing of the values. That is, the derivatives should // be modified to use a and b values. It's not that hard, but this is already // moderate overkill for just filling in missing values. - smoothFill2dArray(x, a ,b); + smoothFill2dArray(x, a, b); smoothFill2dArray(y, a, b); // Create conversions from one coordinate system to another: @@ -96,6 +96,6 @@ module.exports = function calc(gd, trace) { * Given a data range from starting at x1, this function computes the first * point distributed along x0 + n * dx that lies within the range. */ -function getLinspaceStartingPoint (xlow, x0, dx) { +function getLinspaceStartingPoint(xlow, x0, dx) { return x0 + dx * Math.ceil((xlow - x0) / dx); } diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 1944147ba44..f9ea445e0f8 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -21,7 +21,7 @@ var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); var extendFlat = require('../../lib/extend').extendFlat; -module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { +module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var i, j, gridline, j0, i0; var data = trace[axisLetter]; @@ -49,35 +49,35 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { // lines fall once every three expanded grid row/cols: var stride = axis.smoothing ? 3 : 1; - function constructValueGridline (value) { + function constructValueGridline(value) { var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; var xpoints = []; var ypoints = []; var ret = {}; // Search for the fractional grid index giving this line: - if (axisLetter === 'b') { + if(axisLetter === 'b') { j = trace.b2j(value); j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); tj = j - j0; ret.length = nb; ret.crossLength = na; - ret.xy = function (i) { + ret.xy = function(i) { var i0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(i))); var ti = i - i0; return trace._evalxy([], i0, j0, ti, tj); - } + }; - ret.dxy = function (i0, ti) { + ret.dxy = function(i0, ti) { return trace.dxydi([], i0, j0, ti, tj); }; - for (var i = 0; i < na; i++) { + for(var i = 0; i < na; i++) { i0 = Math.min(na - 2, i); ti = i - i0; xy = trace._evalxy([], i0, j0, ti, tj); - if (crossAxis.smoothing && i > 0) { + if(crossAxis.smoothing && i > 0) { // First control point: dxydi0 = trace.dxydi([], i - 1, j0, 0, tj); xpoints.push(pxy[0] + dxydi0[0] / 3); @@ -89,8 +89,8 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { ypoints.push(xy[1] - dxydi1[1] / 3); } - xpoints.push(xy[0]) - ypoints.push(xy[1]) + xpoints.push(xy[0]); + ypoints.push(xy[1]); pxy = xy; } @@ -101,22 +101,22 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { ret.length = na; ret.crossLength = nb; - ret.xy = function (j) { + ret.xy = function(j) { var j0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(j))); var tj = j - j0; return trace._evalxy([], i0, j0, ti, tj); - } + }; - ret.dxy = function (j0, tj) { + ret.dxy = function(j0, tj) { return trace.dxydj([], i0, j0, ti, tj); }; - for (var j = 0; j < nb; j++) { + for(var j = 0; j < nb; j++) { j0 = Math.min(nb - 2, j); tj = j - j0; xy = trace._evalxy([], i0, j0, ti, tj); - if (crossAxis.smoothing && j > 0) { + if(crossAxis.smoothing && j > 0) { // First control point: dxydj0 = trace.dxydj([], i0, j - 1, ti, 0); xpoints.push(pxy[0] + dxydj0[0] / 3); @@ -128,8 +128,8 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { ypoints.push(xy[1] - dxydj1[1] / 3); } - xpoints.push(xy[0]) - ypoints.push(xy[1]) + xpoints.push(xy[0]); + ypoints.push(xy[1]); pxy = xy; } @@ -148,30 +148,30 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { return ret; } - function constructArrayGridline (idx) { + function constructArrayGridline(idx) { var xpoints = []; var ypoints = []; var ret = {}; ret.length = data.length; ret.crossLength = crossData.length; - if (axisLetter === 'b') { + if(axisLetter === 'b') { var j0 = Math.max(0, Math.min(nb - 2, idx)); var tj = Math.min(1, Math.max(0, idx - j0)); - ret.xy = function (i) { + ret.xy = function(i) { var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); var ti = Math.min(1, Math.max(0, i - i0)); return trace._evalxy([], i0, j0, ti, tj); }; - ret.dxy = function (i0, ti) { + ret.dxy = function(i0, ti) { return trace.dxydi([], i0, j0, ti, tj); }; // In the tickmode: array case, this operation is a simple // transfer of data: - for (i = 0; i < nea; i++) { + for(i = 0; i < nea; i++) { xpoints[i] = xcp[i][idx * stride]; ypoints[i] = ycp[i][idx * stride]; } @@ -179,20 +179,20 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { var i0 = Math.max(0, Math.min(na - 2, idx)); var ti = Math.min(1, Math.max(0, idx - i0)); - ret.xy = function (j) { + ret.xy = function(j) { var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); var tj = Math.min(1, Math.max(0, j - j0)); return trace._evalxy([], i0, j0, ti, tj); - } + }; - ret.dxy = function (j0, tj) { + ret.dxy = function(j0, tj) { return trace.dxydj([], i0, j0, ti, tj); }; // In the tickmode: array case, this operation is a simple // transfer of data: - for (i = 0; i < neb; i++) { + for(i = 0; i < neb; i++) { xpoints[i] = xcp[idx * stride][i]; ypoints[i] = ycp[idx * stride][i]; } @@ -209,18 +209,18 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { ret.smoothing = crossAxis.smoothing; return ret; - }; + } - if (axis.tickmode === 'array') { + if(axis.tickmode === 'array') { var j0, j1; - //var j0 = axis.startline ? 1 : 0; - //var j1 = data.length - (axis.endline ? 1 : 0); + // var j0 = axis.startline ? 1 : 0; + // var j1 = data.length - (axis.endline ? 1 : 0); var eps = 5e-15; var bounds = [ Math.floor(((data.length - 1) - axis.arraytick0) / axis.arraydtick * (1 + eps)), Math.ceil((- axis.arraytick0) / axis.arraydtick / (1 + eps)) - ].sort(function (a, b) {return a - b}); + ].sort(function(a, b) {return a - b;}); // Unpack sorted values so we can be sure to avoid infinite loops if something // is backwards: @@ -229,40 +229,40 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { // If the axes fall along array lines, then this is a much simpler process since // we already have all the control points we need - for (n = n1; n < n2; n++) { + for(n = n1; n < n2; n++) { j = axis.arraytick0 + axis.arraydtick * n; - if (j < 0 || j > data.length - 1) continue; + if(j < 0 || j > data.length - 1) continue; gridlines.push(extendFlat(constructArrayGridline(j), { color: axis.gridcolor, width: axis.gridwidth })); } - for (n = n1; n < n2; n++) { + for(n = n1; n < n2; n++) { j0 = axis.arraytick0 + axis.arraydtick * n; j1 = Math.min(j0 + axis.arraydtick, data.length - 1); // TODO: fix the bounds computation so we don't have to do a large range and then throw // out unneeded numbers - if (j0 < 0 || j0 > data.length - 1) continue; - if (j1 < 0 || j1 > data.length - 1) continue; + if(j0 < 0 || j0 > data.length - 1) continue; + if(j1 < 0 || j1 > data.length - 1) continue; var v0 = data[j0]; var v1 = data[j1]; - for (i = 0; i < axis.minorgridcount; i++) { + for(i = 0; i < axis.minorgridcount; i++) { var d = j1 - j0; // TODO: fix the bounds computation so we don't have to do a large range and then throw // out unneeded numbers - if (d <= 0) continue; + if(d <= 0) continue; // XXX: This calculation isn't quite right. Off by one somewhere? var v = v0 + (v1 - v0) * (i + 1) / (axis.minorgridcount + 1) * (axis.arraydtick / d); // TODO: fix the bounds computation so we don't have to do a large range and then throw // out unneeded numbers - if (v < data[0] || v > data[data.length - 1]) continue; + if(v < data[0] || v > data[data.length - 1]) continue; minorgridlines.push(extendFlat(constructValueGridline(v), { color: axis.minorgridcolor, width: axis.minorgridwidth @@ -270,14 +270,14 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { } } - if (axis.startline) { + if(axis.startline) { boundarylines.push(extendFlat(constructArrayGridline(0), { color: axis.startlinecolor, width: axis.startlinewidth })); } - if (axis.endline) { + if(axis.endline) { boundarylines.push(extendFlat(constructArrayGridline(data.length - 1), { color: axis.endlinecolor, width: axis.endlinewidth @@ -296,14 +296,14 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { var bounds = [ Math.floor((data[data.length - 1] - axis.tick0) / axis.dtick * (1 + eps)), Math.ceil((data[0] - axis.tick0) / axis.dtick / (1 + eps)) - ].sort(function (a, b) {return a - b}); + ].sort(function(a, b) {return a - b;}); // Unpack sorted values so we can be sure to avoid infinite loops if something // is backwards: var n1 = bounds[0]; var n2 = bounds[1]; - for (var n = n1; n <= n2; n++) { + for(var n = n1; n <= n2; n++) { var value = axis.tick0 + axis.dtick * n; gridlines.push(extendFlat(constructValueGridline(value), { @@ -312,12 +312,12 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { })); } - for (n = n1 - 1; n < n2 + 1; n++) { + for(n = n1 - 1; n < n2 + 1; n++) { value = axis.tick0 + axis.dtick * n; - for (i = 0; i < axis.minorgridcount; i++) { - var v = value + axis.dtick * (i + 1) / (axis.minorgridcount + 1) - if (v < data[0] || v > data[data.length - 1]) continue; + for(i = 0; i < axis.minorgridcount; i++) { + var v = value + axis.dtick * (i + 1) / (axis.minorgridcount + 1); + if(v < data[0] || v > data[data.length - 1]) continue; minorgridlines.push(extendFlat(constructValueGridline(v), { color: axis.minorgridcolor, width: axis.minorgridwidth @@ -325,18 +325,18 @@ module.exports = function calcGridlines (trace, axisLetter, crossAxisLetter) { } } - if (axis.startline) { + if(axis.startline) { boundarylines.push(extendFlat(constructValueGridline(data[0]), { color: axis.startlinecolor, width: axis.startlinewidth })); } - if (axis.endline) { + if(axis.endline) { boundarylines.push(extendFlat(constructValueGridline(data[data.length - 1]), { color: axis.endlinecolor, width: axis.endlinewidth })); } } -} +}; diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index f80632433d1..5ec9a066016 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -21,29 +21,31 @@ var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); var extendFlat = require('../../lib/extend').extendFlat; -function normalize (x) { - var x +function normalize(x) { + var x; } -module.exports = function calcLabels (trace, axis) { - var i, tobj; +module.exports = function calcLabels(trace, axis) { + var i, tobj, prefix, suffix; var labels = axis._labels = []; var gridlines = axis._gridlines; - for (i = 0; i < gridlines.length; i++) { + for(i = 0; i < gridlines.length; i++) { var gridline = gridlines[i]; - if (['start', 'both'].indexOf(axis.showlabels) !== -1) { + if(['start', 'both'].indexOf(axis.showticklabels) !== -1) { tobj = Axes.tickText(axis, gridline.value); extendFlat(tobj, { + prefix: prefix, + suffix: suffix, endAnchor: true, xy: gridline.xy(0), dxy: gridline.dxy(0, 0), axis: gridline.axis, length: gridline.crossAxis.length, - font: gridline.crossAxis.startlabelfont, + font: gridline.crossAxis.labelfont, isFirst: i === 0, isLast: i === gridlines.length - 1 }); @@ -51,7 +53,7 @@ module.exports = function calcLabels (trace, axis) { labels.push(tobj); } - if (['end', 'both'].indexOf(axis.showlabels) !== -1) { + if(['end', 'both'].indexOf(axis.showticklabels) !== -1) { tobj = Axes.tickText(axis, gridline.value); extendFlat(tobj, { @@ -60,7 +62,7 @@ module.exports = function calcLabels (trace, axis) { dxy: gridline.dxy(gridline.crossLength - 2, 1), axis: gridline.axis, length: gridline.crossAxis.length, - font: gridline.crossAxis.endlabelfont, + font: gridline.crossAxis.labelfont, isFirst: i === 0, isLast: i === gridlines.length - 1 }); diff --git a/src/traces/carpet/catmull_rom.js b/src/traces/carpet/catmull_rom.js index b7561e216b8..261a2a6d1a1 100644 --- a/src/traces/carpet/catmull_rom.js +++ b/src/traces/carpet/catmull_rom.js @@ -20,16 +20,16 @@ */ var CatmullRomExp = 0.5; module.exports = function makeControlPoints(p0, p1, p2, smoothness) { - var d1x = p0[0] - p1[0], - d1y = p0[1] - p1[1], - d2x = p2[0] - p1[0], - d2y = p2[1] - p1[1], - d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), - d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), - numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, - numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, - denom1 = d2a * (d1a + d2a) * 3, - denom2 = d1a * (d1a + d2a) * 3; + var d1x = p0[0] - p1[0], + d1y = p0[1] - p1[1], + d2x = p2[0] - p1[0], + d2y = p2[1] - p1[1], + d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), + d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), + numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, + numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, + denom1 = d2a * (d1a + d2a) * 3, + denom2 = d1a * (d1a + d2a) * 3; return [[ p1[0] + (denom1 && numx / denom1), p1[1] + (denom1 && numy / denom1) @@ -38,6 +38,5 @@ module.exports = function makeControlPoints(p0, p1, p2, smoothness) { p1[1] - (denom2 && numy / denom2) ]]; - return [[dxL, dyL], [dxU, dyU]]; -} - + return [[dxL, dyL], [dxU, dyU]]; +}; diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 66cfd2fae6b..fd175ba8dc4 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -27,11 +27,11 @@ module.exports = function(a, b, cheaterslope) { // not evenly spaced, the switch to value-based indexing is continuous. // This means evenly spaced data should look the same whether value // or index cheatertype. - if (adata) { + if(adata) { var ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]) / (na - 1); } - if (bdata) { + if(bdata) { var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } diff --git a/src/traces/carpet/compute_control_points.js b/src/traces/carpet/compute_control_points.js index 37ebfda6cc8..5974e7f6176 100644 --- a/src/traces/carpet/compute_control_points.js +++ b/src/traces/carpet/compute_control_points.js @@ -73,7 +73,6 @@ var ensureArray = require('./ensure_array'); */ - /* * Catmull-rom is biased at the boundaries toward the interior and we actually * can't use catmull-rom to compute the control point closest to (but inside) @@ -116,7 +115,7 @@ var ensureArray = require('./ensure_array'); * Of course it works whichever way it's oriented; you just need to interpret the * input/output accordingly. */ -function inferCubicControlPoint (p0, p2, p3) { +function inferCubicControlPoint(p0, p2, p3) { // Extend p1 away from p0 by 50%. This is the equivalent quadratic point that // would give the same slope as catmull rom at p0. var p2e0 = -0.5 * p3[0] + 1.5 * p2[0]; @@ -128,7 +127,7 @@ function inferCubicControlPoint (p0, p2, p3) { ]; } -module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoothing) { +module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmoothing) { var i, j, ie, je, xei, yei, xi, yi, qx, qy, cp, p1; // At this point, we know these dimensions are correct and representative of // the whole 2D arrays: @@ -142,7 +141,7 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot xe = ensureArray(xe, nea); ye = ensureArray(ye, nea); - for (ie = 0; ie < nea; ie++) { + for(ie = 0; ie < nea; ie++) { xe[ie] = ensureArray(xe[ie], neb); ye[ie] = ensureArray(ye[ie], neb); } @@ -161,20 +160,20 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot // // // ie = (i) (e)xpanded: - for (i = 0, ie = 0; i < na; i++, ie += asmoothing ? 3 : 1) { + for(i = 0, ie = 0; i < na; i++, ie += asmoothing ? 3 : 1) { xei = xe[ie]; yei = ye[ie]; xi = x[i]; yi = y[i]; // je = (j) (e)xpanded: - for (j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { + for(j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { xei[je] = xi[j]; yei[je] = yi[j]; } } - if (asmoothing) { + if(asmoothing) { // If there's a-smoothing, this loop fills in the X'd points with catmull-rom // control points computed along the a-axis: // . . . . @@ -193,12 +192,12 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot // ------> // a // - for (j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { + for(j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { // Fill in the points marked X for this a-row: - for (i = 1, ie = 3; i < na - 1; i++, ie += 3) { + for(i = 1, ie = 3; i < na - 1; i++, ie += 3) { cp = makeControlPoints( [x[i - 1][j], y[i - 1][j]], - [x[i ][j], y[i ][j]], + [x[i ][j], y[i ][j]], [x[i + 1][j], y[i + 1][j]], asmoothing ); @@ -234,7 +233,7 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot } } - if (bsmoothing) { + if(bsmoothing) { // If there's a-smoothing, this loop fills in the X'd points with catmull-rom // control points computed along the b-axis: // . . . . @@ -253,11 +252,11 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot // ------> // a // - for (ie = 0; ie < nea; ie++) { - for (je = 3; je < neb - 3; je += 3) { + for(ie = 0; ie < nea; ie++) { + for(je = 3; je < neb - 3; je += 3) { cp = makeControlPoints( [xe[ie][je - 3], ye[ie][je - 3]], - [xe[ie][je ], ye[ie][je ]], + [xe[ie][je ], ye[ie][je ]], [xe[ie][je + 3], ye[ie][je + 3]], bsmoothing ); @@ -286,7 +285,7 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot } } - if (asmoothing && bsmoothing) { + if(asmoothing && bsmoothing) { // Do one more pass, this time recomputing exactly what we just computed. // It's overdetermined since we're peforming catmull-rom in two directions, // so we'll just average the overdetermined. These points don't lie along the @@ -311,12 +310,12 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot // ------> // a // - for (je = 1; je < neb; je += (je + 1) % 3 === 0 ? 2 : 1) { + for(je = 1; je < neb; je += (je + 1) % 3 === 0 ? 2 : 1) { // Fill in the points marked X for this a-row: - for (ie = 3; ie < nea - 3; ie += 3) { + for(ie = 3; ie < nea - 3; ie += 3) { cp = makeControlPoints( [xe[ie - 3][je], ye[ie - 3][je]], - [xe[ie ][je], ye[ie ][je]], + [xe[ie ][je], ye[ie ][je]], [xe[ie + 3][je], ye[ie + 3][je]], asmoothing ); @@ -348,5 +347,4 @@ module.exports = function computeControlPoints (xe, ye, x, y, asmoothing, bsmoot } return [xe, ye]; -} - +}; diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js index dec3e6e9ff9..d18d5c10613 100644 --- a/src/traces/carpet/create_i_derivative_evaluator.js +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -38,10 +38,10 @@ * NB: It's presumed that at this point all data has been sanitized and is valid numerical data arrays * of the correct dimension. */ -module.exports = function (arrays, asmoothing, bsmoothing) { - if (asmoothing && bsmoothing) { - return function (out, i0, j0, u, v) { - if (!out) out = []; +module.exports = function(arrays, asmoothing, bsmoothing) { + if(asmoothing && bsmoothing) { + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, ak, k; // Since it's a grid of control points, the actual indices are * 3: @@ -59,10 +59,10 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov2 = ov * ov; var ov3 = ov2 * ov; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); + f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); f2 = 3 * ((u2 - 1) * ak[i0][j0 + 2] + ou2 * ak[i0 + 1][j0 + 2] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 2] + u2 * ak[i0 + 3][j0 + 2]); f3 = 3 * ((u2 - 1) * ak[i0][j0 + 3] + ou2 * ak[i0 + 1][j0 + 3] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 3] + u2 * ak[i0 + 3][j0 + 3]); @@ -73,11 +73,11 @@ module.exports = function (arrays, asmoothing, bsmoothing) { return out; }; - } else if (asmoothing) { + } else if(asmoothing) { // Handle smooth in the a-direction but linear in the b-direction by performing four // linear interpolations followed by one cubic interpolation of the result - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; i0 *= 3; var u2 = u * u; @@ -86,19 +86,19 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ou2 = ou * ou; var ou3 = ou2 * ou; var ov = 1 - v; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); + f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); out[k] = ov * f0 + v * f1; } return out; }; - } else if (bsmoothing) { + } else if(bsmoothing) { // Same as the above case, except reversed: - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; j0 *= 3; var v2 = v * v; @@ -106,7 +106,7 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov = 1 - v; var ov2 = ov * ov; var ov3 = ov2 * ov; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; f0 = ak[i0 + 1][j0] - ak[i0][j0]; f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; @@ -119,11 +119,11 @@ module.exports = function (arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, k, ak; var ov = 1 - v; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; f0 = ak[i0 + 1][j0] - ak[i0][j0]; f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; @@ -133,4 +133,4 @@ module.exports = function (arrays, asmoothing, bsmoothing) { return out; }; } -} +}; diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index b4511ce11d2..d8bf978a908 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -9,10 +9,10 @@ 'use strict'; -module.exports = function (arrays, asmoothing, bsmoothing) { - if (asmoothing && bsmoothing) { - return function (out, i0, j0, u, v) { - if (!out) out = []; +module.exports = function(arrays, asmoothing, bsmoothing) { + if(asmoothing && bsmoothing) { + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, ak, k; // Since it's a grid of control points, the actual indices are * 3: @@ -32,10 +32,10 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov2 = ov * ov; var ov3 = ov2 * ov; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); + f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); f2 = 3 * ((v2 - 1) * ak[i0 + 2][j0] + ov2 * ak[i0 + 2][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 2][j0 + 2] + v2 * ak[i0 + 2][j0 + 3]); f3 = 3 * ((v2 - 1) * ak[i0 + 3][j0] + ov2 * ak[i0 + 3][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 3][j0 + 2] + v2 * ak[i0 + 3][j0 + 3]); @@ -46,11 +46,11 @@ module.exports = function (arrays, asmoothing, bsmoothing) { return out; }; - } else if (asmoothing) { + } else if(asmoothing) { // Handle smooth in the a-direction but linear in the b-direction by performing four // linear interpolations followed by one cubic interpolation of the result - return function (out, i0, j0, v, u) { - if (!out) out = []; + return function(out, i0, j0, v, u) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; i0 *= 3; var u2 = u * u; @@ -59,10 +59,10 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ou2 = ou * ou; var ou3 = ou2 * ou; var ov = 1 - v; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0 ][j0 + 1] - ak[i0 ][j0]; + f0 = ak[i0 ][j0 + 1] - ak[i0 ][j0]; f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; f2 = ak[i0 + 2][j0 + 1] - ak[i0 + 2][j0]; f3 = ak[i0 + 3][j0 + 1] - ak[i0 + 3][j0]; @@ -70,25 +70,25 @@ module.exports = function (arrays, asmoothing, bsmoothing) { out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; // mathematically equivalent: - //f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; - //f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; - //out[k] = f1 - f0; + // f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; + // f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; + // out[k] = f1 - f0; } return out; }; - } else if (bsmoothing) { + } else if(bsmoothing) { // Same as the above case, except reversed: - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; j0 *= 3; var v2 = v * v; var ov = 1 - v; var ov2 = ov * ov; var ou = 1 - u; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); + f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); out[k] = ou * f0 + u * f1; @@ -97,11 +97,11 @@ module.exports = function (arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: - return function (out, i0, j0, v, u) { - if (!out) out = []; + return function(out, i0, j0, v, u) { + if(!out) out = []; var f0, f1, k, ak; var ov = 1 - v; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; f0 = ak[i0][j0 + 1] - ak[i0][j0]; f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; @@ -112,4 +112,4 @@ module.exports = function (arrays, asmoothing, bsmoothing) { }; } -} +}; diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js index e7f2c8ce730..de8b5975991 100644 --- a/src/traces/carpet/create_spline_evaluator.js +++ b/src/traces/carpet/create_spline_evaluator.js @@ -21,10 +21,10 @@ * to be able control whether the derivative at a cell boundary is approached * from one side or the other. */ -module.exports = function (arrays, asmoothing, bsmoothing) { - if (asmoothing && bsmoothing) { - return function (out, i0, j0, u, v) { - if (!out) out = []; +module.exports = function(arrays, asmoothing, bsmoothing) { + if(asmoothing && bsmoothing) { + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, ak, k; // Since it's a grid of control points, the actual indices are * 3: @@ -44,9 +44,9 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov2 = ov * ov; var ov3 = ov2 * ov; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; + f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; f2 = ou3 * ak[i0][j0 + 2] + 3 * (ou2 * u * ak[i0 + 1][j0 + 2] + ou * u2 * ak[i0 + 2][j0 + 2]) + u3 * ak[i0 + 3][j0 + 2]; f3 = ou3 * ak[i0][j0 + 3] + 3 * (ou2 * u * ak[i0 + 1][j0 + 3] + ou * u2 * ak[i0 + 2][j0 + 3]) + u3 * ak[i0 + 3][j0 + 3]; @@ -55,11 +55,11 @@ module.exports = function (arrays, asmoothing, bsmoothing) { return out; }; - } else if (asmoothing) { + } else if(asmoothing) { // Handle smooth in the a-direction but linear in the b-direction by performing four // linear interpolations followed by one cubic interpolation of the result - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; i0 *= 3; var u2 = u * u; @@ -68,9 +68,9 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ou2 = ou * ou; var ou3 = ou2 * ou; var ov = 1 - v; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ov * ak[i0 ][j0] + v * ak[i0 ][j0 + 1]; + f0 = ov * ak[i0 ][j0] + v * ak[i0 ][j0 + 1]; f1 = ov * ak[i0 + 1][j0] + v * ak[i0 + 1][j0 + 1]; f2 = ov * ak[i0 + 2][j0] + v * ak[i0 + 2][j0 + 1]; f3 = ov * ak[i0 + 3][j0] + v * ak[i0 + 3][j0 + 1]; @@ -79,10 +79,10 @@ module.exports = function (arrays, asmoothing, bsmoothing) { } return out; }; - } else if (bsmoothing) { + } else if(bsmoothing) { // Same as the above case, except reversed: - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, f2, f3, k, ak; j0 *= 3; var v2 = v * v; @@ -91,9 +91,9 @@ module.exports = function (arrays, asmoothing, bsmoothing) { var ov2 = ov * ov; var ov3 = ov2 * ov; var ou = 1 - u; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; f2 = ou * ak[i0][j0 + 2] + u * ak[i0 + 1][j0 + 2]; f3 = ou * ak[i0][j0 + 3] + u * ak[i0 + 1][j0 + 3]; @@ -104,14 +104,14 @@ module.exports = function (arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: - return function (out, i0, j0, u, v) { - if (!out) out = []; + return function(out, i0, j0, u, v) { + if(!out) out = []; var f0, f1, k, ak; var ov = 1 - v; var ou = 1 - u; - for (k = 0; k < arrays.length; k++) { + for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; out[k] = ov * f0 + v * f1; @@ -120,4 +120,4 @@ module.exports = function (arrays, asmoothing, bsmoothing) { }; } -} +}; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 2f7a3eeccad..b8b7bfc1ed2 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -13,29 +13,28 @@ var Lib = require('../../lib'); var handleXYDefaults = require('./xy_defaults'); var handleABDefaults = require('./ab_defaults'); var attributes = require('./attributes'); +var colorAttrs = require('../../components/color/attributes'); -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, fullLayout) { +module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - console.log('fullLayout:', fullLayout); - coerce('carpetid'); coerce('cheaterslope'); + var defaultColor = coerce('color', colorAttrs.defaultLine); Lib.coerceFont(coerce, 'font'); - console.log('traceOut.font:', fullLayout.font); traceOut.cheaterslope = parseFloat(traceIn.cheaterslope); - handleABDefaults(traceIn, traceOut, fullLayout, coerce); + handleABDefaults(traceIn, traceOut, fullLayout, coerce, defaultColor); - if (traceOut.a.length < 3) { + if(traceOut.a.length < 3) { traceOut.aaxis.smoothing = 0; } - if (traceOut.b.length < 3) { + if(traceOut.b.length < 3) { traceOut.baxis.smoothing = 0; } diff --git a/src/traces/carpet/ensure_array.js b/src/traces/carpet/ensure_array.js index 34e01fda2e1..667f221813c 100644 --- a/src/traces/carpet/ensure_array.js +++ b/src/traces/carpet/ensure_array.js @@ -17,13 +17,11 @@ * collection. */ module.exports = function ensureArray(out, n) { - if (!Array.isArray(out)) out = []; + if(!Array.isArray(out)) out = []; // If too long, truncate. (If too short, it will grow // automatically so we don't care about that case) out.length = n; return out; -} - - +}; diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index e701bd88f48..eed45ee824c 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -20,7 +20,7 @@ Carpet.animatable = true; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; Carpet.basePlotModule = require('../../plots/cartesian'); -Carpet.categories = ['cartesian', 'symbols', 'markerColorscale']; +Carpet.categories = ['cartesian', 'symbols', 'markerColorscale', 'carpet']; Carpet.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', diff --git a/src/traces/carpet/interpolate.js b/src/traces/carpet/interpolate.js index 7b9b0bc7d32..f26e4831333 100644 --- a/src/traces/carpet/interpolate.js +++ b/src/traces/carpet/interpolate.js @@ -8,22 +8,22 @@ 'use strict'; -module.exports = function (f, t) { +module.exports = function(f, t) { var ot2, ot3, t2, t3; var ot = 1 - t; switch(f.length) { - case 4: - ot2 = ot * ot; - ot3 = ot2 * ot; - t2 = t * t; - t3 = t2 * t; - return f[0] * ot3 + 3 * (f[1] * ot2 * t + f[2] * ot * t2) + t3 * f[3]; - case 3: - ot2 = ot * ot; - t2 = t * t; - return f[0] * ot2 + 2 * f[1] * ot * t + t2 * f[2]; - case 2: - return ot * f[0] + t * f[1]; + case 4: + ot2 = ot * ot; + ot3 = ot2 * ot; + t2 = t * t; + t3 = t2 * t; + return f[0] * ot3 + 3 * (f[1] * ot2 * t + f[2] * ot * t2) + t3 * f[3]; + case 3: + ot2 = ot * ot; + t2 = t * t; + return f[0] * ot2 + 2 * f[1] * ot * t + t2 * f[2]; + case 2: + return ot * f[0] + t * f[1]; } -} +}; diff --git a/src/traces/carpet/makepath.js b/src/traces/carpet/makepath.js index 67d22da2c48..9dea70a8aa4 100644 --- a/src/traces/carpet/makepath.js +++ b/src/traces/carpet/makepath.js @@ -8,16 +8,16 @@ 'use strict'; -module.exports = function makePath (xp, yp, isBicubic) { +module.exports = function makePath(xp, yp, isBicubic) { // Prevent d3 errors that would result otherwise: - if (xp.length === 0) return ''; + if(xp.length === 0) return ''; var i, path = []; var stride = isBicubic ? 3 : 1; - for (i = 0; i < xp.length; i += stride) { + for(i = 0; i < xp.length; i += stride) { path.push(xp[i] + ',' + yp[i]); - if (isBicubic && i < xp.length - stride) { + if(isBicubic && i < xp.length - stride) { path.push('C'); path.push([ xp[i + 1] + ',' + yp[i + 1], @@ -26,4 +26,4 @@ module.exports = function makePath (xp, yp, isBicubic) { } } return 'M' + path.join(isBicubic ? '' : 'L'); -} +}; diff --git a/src/traces/carpet/map_1d_array.js b/src/traces/carpet/map_1d_array.js index f6f5c144443..7a8dca1c284 100644 --- a/src/traces/carpet/map_1d_array.js +++ b/src/traces/carpet/map_1d_array.js @@ -13,21 +13,21 @@ * The output array is optional, but if provided, it will be reused without * reallocation to the extent possible. */ -module.exports = function mapArray (out, data, func) { +module.exports = function mapArray(out, data, func) { var i, j; - if (!Array.isArray(out)) { + if(!Array.isArray(out)) { // If not an array, make it an array: out = []; - } else if (out.length > data.length) { + } else if(out.length > data.length) { // If too long, truncate. (If too short, it will grow // automatically so we don't care about that case) - out = out.slice(0, data.length) + out = out.slice(0, data.length); } - for (i = 0; i < data.length; i++) { + for(i = 0; i < data.length; i++) { out[i] = func(data[i]); } return out; -} +}; diff --git a/src/traces/carpet/map_2d_array.js b/src/traces/carpet/map_2d_array.js index 13627597050..9edd832350d 100644 --- a/src/traces/carpet/map_2d_array.js +++ b/src/traces/carpet/map_2d_array.js @@ -13,31 +13,31 @@ * The output array is optional, but if provided, it will be reused without * reallocation to the extent possible. */ -module.exports = function mapArray (out, data, func) { +module.exports = function mapArray(out, data, func) { var i, j; - if (!Array.isArray(out)) { + if(!Array.isArray(out)) { // If not an array, make it an array: out = []; - } else if (out.length > data.length) { + } else if(out.length > data.length) { // If too long, truncate. (If too short, it will grow // automatically so we don't care about that case) - out = out.slice(0, data.length) + out = out.slice(0, data.length); } - for (i = 0; i < data.length; i++) { - if (!Array.isArray(out[i])) { + for(i = 0; i < data.length; i++) { + if(!Array.isArray(out[i])) { // If not an array, make it an array: out[i] = []; - } else if (out[i].length > data.length) { + } else if(out[i].length > data.length) { // If too long, truncate. (If too short, it will grow // automatically so we don't care about[i] that case) out[i] = out[i].slice(0, data.length); } - for (j = 0; j < data[1].length; j++) { + for(j = 0; j < data[1].length; j++) { out[i][j] = func(data[i][j]); } } return out; -} +}; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 413826de4e2..4148f913739 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -105,34 +105,6 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { var el = d3.select(this); - var prefix; - switch(ax.showlabelprefix) { - case 'first': - prefix = label.isFirst ? ax.labelprefix : ''; - break; - case 'all': - prefix = ax.labelprefix; - break; - case 'last': - prefix = label.isLast ? ax.labelprefix : ''; - break; - } - - var suffix; - switch(ax.showlabelsuffix) { - case 'first': - suffix = label.isFirst ? ax.labelsuffix : ''; - break; - case 'all': - suffix = ax.labelsuffix; - break; - case 'last': - suffix = label.isLast ? ax.labelsuffix : ''; - break; - } - - - var dx = label.dxy[0] * trace.dpdx(xaxis); var dy = label.dxy[1] * trace.dpdy(yaxis); var angle = Math.atan2(dy, dx) * 180 / Math.PI; @@ -152,7 +124,7 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { el.attr('x', xy[0] + ax.labelpadding * (endAnchor ? -1 : 1)) // These are pre-transform offsets .attr('y', xy[1] + 5) // Shift down to hackily vertically center .attr('text-anchor', endAnchor ? 'end' : 'start') - .text(prefix + label.text + suffix) + .text(label.text) .attr('transform', 'rotate(' + angle + ' ' + xy[0] + ',' + xy[1] + ')') .call(Drawing.font, label.font.family, label.font.size, label.font.color); }); diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index f4e65f31cb9..a05fe589410 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -54,19 +54,19 @@ module.exports = function setConvert(trace) { * Convert from i/j data grid coordinates to a/b values. Note in particular that this * is *linear* interpolation, even if the data is interpolated bicubically. */ - trace.i2a = function (i) { + trace.i2a = function(i) { var i0 = Math.max(0, Math.floor(i[0]), na - 2); var ti = i[0] - i0; return (1 - ti) * a[i0] + ti * a[i0 + 1]; }; - trace.j2b = function (j) { + trace.j2b = function(j) { var j0 = Math.max(0, Math.floor(j[1]), na - 2); var tj = j[1] - j0; return (1 - tj) * b[j0] + tj * b[j0 + 1]; }; - trace.ij2ab = function (ij) { + trace.ij2ab = function(ij) { return [trace.i2a(ij[0]), trace.j2b(ij[1])]; }; @@ -75,29 +75,29 @@ module.exports = function setConvert(trace) { * through the a/b data arrays and assumes they are monotonic, which is presumed to have * been enforced already. */ - trace.a2i = function (aval) { + trace.a2i = function(aval) { var i0 = Math.max(0, Math.min(search(aval, a), na - 2)); var a0 = a[i0]; var a1 = a[i0 + 1]; return Math.max(0, Math.min(na - 1, i0 + (aval - a0) / (a1 - a0))); }; - trace.b2j = function (bval) { + trace.b2j = function(bval) { var j0 = Math.max(0, Math.min(search(bval, b), nb - 2)); var b0 = b[j0]; var b1 = b[j0 + 1]; return Math.max(0, Math.min(nb - 1, j0 + (bval - b0) / (b1 - b0))); }; - trace.ab2ij = function (ab) { + trace.ab2ij = function(ab) { return [trace.a2i(ab[0]), trace.b2j(ab[1])]; - } + }; /* * Convert from i/j coordinates to x/y caretesian coordinates. This means either bilinear * or bicubic spline evaluation, but the hard part is already done at this point. */ - trace.i2c = function (ij) { + trace.i2c = function(ij) { var i0 = Math.max(0, Math.min(na - 2, Math.floor(ij[0]))); var ti = ij[0] - i0; var j0 = Math.max(0, Math.min(nb - 2, Math.floor(ij[1]))); @@ -105,8 +105,8 @@ module.exports = function setConvert(trace) { return trace._evalxy([], i0, j0, ti, tj); }; - trace.ab2xy = function (aval, bval) { - if (aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1]) { + trace.ab2xy = function(aval, bval) { + if(aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1]) { return [false, false]; } var i = trace.a2i(aval); @@ -117,21 +117,21 @@ module.exports = function setConvert(trace) { var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); var tj = j - j0; - if (tj < 0 || tj > 1 || ti < 0 || ti > 1) { + if(tj < 0 || tj > 1 || ti < 0 || ti > 1) { return [false, false]; } return trace._evalxy([], i0, j0, ti, tj); - } + }; - trace.c2p = function (xy, xa, ya) { + trace.c2p = function(xy, xa, ya) { return [xa.c2p(xy[0]), ya.c2p(xy[1])]; }; - trace.p2x = function (p, xa, ya) { + trace.p2x = function(p, xa, ya) { return [xa.p2c(p[0]), ya.p2c(p[1])]; }; - trace.dadi = function (i, u) { + trace.dadi = function(i, u) { // Right now only a piecewise linear a or b basis is permitted since smoother interpolation // would cause monotonicity problems. As a retult, u is entirely disregarded in this // computation, though we'll specify it as a parameter for the sake of completeness and @@ -147,7 +147,7 @@ module.exports = function setConvert(trace) { return a[i0 + 1] - a[i0]; }; - trace.dbdj = function (j, v) { + trace.dbdj = function(j, v) { // See above caveats for dadi which also apply here var j0 = Math.max(0, Math.min(b.length - 2, j)); @@ -160,25 +160,25 @@ module.exports = function setConvert(trace) { // // NB: separate grid cell + fractional grid cell coordinate format is due to the discontinuous // derivative, as described better in create_i_derivative_evaluator.js - trace.dxyda = function (i0, j0, u, v) { + trace.dxyda = function(i0, j0, u, v) { var dxydi = trace.dxydi(null, i0, j0, u, v); var dadi = trace.dadi(i0, u); return [dxydi[0] / dadi, dxydi[1] / dbdj]; }; - trace.dxydb = function (i0, j0, u, v) { + trace.dxydb = function(i0, j0, u, v) { var dxydj = trace.dxydj(null, i0, j0, u, v); var dbdj = trace.dbdj(j0, v); return [dxydj[0] / dbdj, dxydj[1] / dbdj]; }; - trace.dpdx = function (xa) { + trace.dpdx = function(xa) { return xa._m; }; - trace.dpdy = function (ya) { + trace.dpdy = function(ya) { return ya._m; }; -} +}; diff --git a/src/traces/carpet/smooth-fill-2d-array.js b/src/traces/carpet/smooth-fill-2d-array.js index c9f96c2d7d2..e3479744d0b 100644 --- a/src/traces/carpet/smooth-fill-2d-array.js +++ b/src/traces/carpet/smooth-fill-2d-array.js @@ -8,7 +8,7 @@ 'use strict'; -module.exports = function smoothFill2dArray (data, a, b) { +module.exports = function smoothFill2dArray(data, a, b) { var i, j, k, n; var ip = []; var jp = []; @@ -16,23 +16,23 @@ module.exports = function smoothFill2dArray (data, a, b) { var ni = data.length; var nj = data[0].length; - function avgSurrounding (i, j) { + function avgSurrounding(i, j) { var sum = 0.0; var val; var cnt = 0; - if (i > 0 && (val = data[i - 1][j]) !== undefined) { + if(i > 0 && (val = data[i - 1][j]) !== undefined) { cnt++; sum += val; } - if (i < ni - 1 && (val = data[i + 1][j]) !== undefined) { + if(i < ni - 1 && (val = data[i + 1][j]) !== undefined) { cnt++; sum += val; } - if (j > 0 && (val = data[i][j - 1]) !== undefined) { + if(j > 0 && (val = data[i][j - 1]) !== undefined) { cnt++; sum += val; } - if (j < nj - 1 && (val = data[i][j + 1]) !== undefined) { + if(j < nj - 1 && (val = data[i][j + 1]) !== undefined) { cnt++; sum += val; } @@ -42,9 +42,9 @@ module.exports = function smoothFill2dArray (data, a, b) { // Track the maximum magnitude so that we can track error relative // to the maximum: var dmax = 0.0; - for (i = 0; i < ni; i++) { - for (j = 0; j < nj; j++) { - if (data[i][j] === undefined) { + for(i = 0; i < ni; i++) { + for(j = 0; j < nj; j++) { + if(data[i][j] === undefined) { ip.push(i); jp.push(j); data[i][j] = avgSurrounding(i, j); @@ -53,7 +53,7 @@ module.exports = function smoothFill2dArray (data, a, b) { } } - if (!ip.length) return data; + if(!ip.length) return data; // The tolerance doesn't need to be excessive. It's just for display positioning var tol = 1e-5; @@ -72,15 +72,15 @@ module.exports = function smoothFill2dArray (data, a, b) { var newVal = 0; var d0, d1, x0, x1, i0, j0; - if (i === 0) { + if(i === 0) { i0 = Math.min(ni - 1, 2); - x0 = a[i0] + x0 = a[i0]; x1 = a[1]; d0 = data[i0][j]; d1 = data[1][j]; newVal += d1 + (d1 - d0) * (a[0] - x1) / (x1 - x0); contribCnt++; - } else if (i === ni - 1) { + } else if(i === ni - 1) { i0 = Math.max(0, ni - 3); x0 = a[i0]; x1 = a[ni - 2]; @@ -90,14 +90,14 @@ module.exports = function smoothFill2dArray (data, a, b) { contribCnt++; } - if ((i === 0 || i === ni - 1) && (j > 0 && j < nj - 1)) { + if((i === 0 || i === ni - 1) && (j > 0 && j < nj - 1)) { var dxp = b[j + 1] - b[j]; var dxm = b[j] - b[j - 1]; newVal += (dxm * data[i][j + 1] + dxp * data[i][j - 1]) / (dxm + dxp); contribCnt++; } - if (j === 0) { + if(j === 0) { j0 = Math.min(nj - 1, 2); x0 = b[j0]; x1 = b[1]; @@ -105,7 +105,7 @@ module.exports = function smoothFill2dArray (data, a, b) { d1 = data[i][1]; newVal += d1 + (d1 - d0) * (b[0] - x1) / (x1 - x0); contribCnt++; - } else if (j === nj - 1) { + } else if(j === nj - 1) { j0 = Math.max(0, nj - 3); x0 = b[j0]; x1 = b[nj - 2]; @@ -115,14 +115,14 @@ module.exports = function smoothFill2dArray (data, a, b) { contribCnt++; } - if ((j === 0 || j === nj - 1) && (i > 0 && i < ni - 1)) { + if((j === 0 || j === nj - 1) && (i > 0 && i < ni - 1)) { var dxp = a[i + 1] - a[i]; var dxm = a[i] - a[i - 1]; newVal += (dxm * data[i + 1][j] + dxp * data[i - 1][j]) / (dxm + dxp); contribCnt++; } - if (!contribCnt) { + if(!contribCnt) { // interior point, so simply average: // Get the grid spacing on either side: var dap = a[i + 1] - a[i]; @@ -152,9 +152,9 @@ module.exports = function smoothFill2dArray (data, a, b) { } resid = Math.sqrt(resid); - } while (iter++ < itermax && resid > tol); + } while(iter++ < itermax && resid > tol); console.log('Smoother converged to', resid, 'after', iter, 'iterations'); return data; -} +}; diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 7faf5c4c307..dcbe45995b3 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -23,18 +23,18 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var x = coerce('x'); var needsXTransform = x && !hasColumns(x); - if (needsXTransform) cols.push('x'); + if(needsXTransform) cols.push('x'); traceOut._cheater = !x; var y = coerce('y'); var needsYTransform = y && !hasColumns(y); - if (needsYTransform) cols.push('y'); + if(needsYTransform) cols.push('y'); - if (!x && !y) return; + if(!x && !y) return; - if (cols.length) { + if(cols.length) { convertColumnData(traceOut, traceOut.baxis, traceOut.aaxis, 'b', 'a', cols); } diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 1d307a65890..d185c7f8f22 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -22,9 +22,9 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, var i, j, arrayVar, newArrays, newArray, arrayVarName; - for (i = 0; i < arrayVarNames.length; i++) { + for(i = 0; i < arrayVarNames.length; i++) { arrayVar = trace[arrayVarNames[i]]; - if (arrayVar) colLen = Math.min(colLen, arrayVar.length); + if(arrayVar) colLen = Math.min(colLen, arrayVar.length); } if(colLen < col1.length) col1 = col1.slice(0, colLen); @@ -41,7 +41,7 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, col2vals = col2dv.vals, newArrays = []; - for (i = 0; i < arrayVarNames.length; i++) { + for(i = 0; i < arrayVarNames.length; i++) { newArrays[i] = Lib.init2dArray(col2vals.length, col1vals.length); } diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 923dd36007b..5d1a2224c8b 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -22,14 +22,14 @@ var dataArrays = ['a', 'b']; module.exports = function calc(gd, trace) { var i, j, dataArray, newArray, fillArray1, fillArray2, carpet; - for (i = 0; i < gd._fullData.length; i++) { - if (gd._fullData[i].carpetid === trace.carpetid && gd._fullData[i].type === 'carpet') { + for(i = 0; i < gd._fullData.length; i++) { + if(gd._fullData[i].carpetid === trace.carpetid && gd._fullData[i].type === 'carpet') { carpet = gd._fullData[i]; break; } } - if (!carpet) return; + if(!carpet) return; trace._carpet = carpet; diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index 934cc9dc598..acc2939cb35 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -12,12 +12,12 @@ var ScatterCarpet = {}; ScatterCarpet.attributes = require('./attributes'); ScatterCarpet.supplyDefaults = require('./defaults'); -//ScatterCarpet.colorbar = require('../scatter/colorbar'); +// ScatterCarpet.colorbar = require('../scatter/colorbar'); ScatterCarpet.calc = require('./calc'); ScatterCarpet.plot = require('./plot'); -//ScatterCarpet.style = require('./style'); +// ScatterCarpet.style = require('./style'); ScatterCarpet.hoverPoints = require('./hover'); -//ScatterCarpet.selectPoints = require('./select'); +// ScatterCarpet.selectPoints = require('./select'); ScatterCarpet.moduleType = 'trace'; ScatterCarpet.name = 'scattercarpet'; diff --git a/src/traces/scattercarpet/plot.js b/src/traces/scattercarpet/plot.js index 557604c9118..96a7b147dae 100644 --- a/src/traces/scattercarpet/plot.js +++ b/src/traces/scattercarpet/plot.js @@ -23,7 +23,7 @@ module.exports = function plot(gd, plotinfoproxy, data) { plot: plotinfoproxy.plot }; - /*var calcdata = new Array(data.length), + /* var calcdata = new Array(data.length), fullCalcdata = gd.calcdata; for(var i = 0; i < fullCalcdata.length; i++) { From 714db5a57982c548372e79921fe6a2ac6542afad Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 11:50:21 -0500 Subject: [PATCH 017/132] Large lint-fixing pass --- src/components/drawing/spline_evaluator.js | 96 ------------------- src/plots/cartesian/layout_defaults.js | 2 - src/traces/carpet/ab_defaults.js | 8 +- src/traces/carpet/axis_attributes.js | 35 ++----- src/traces/carpet/axis_defaults.js | 21 ++-- src/traces/carpet/calc.js | 20 ++-- src/traces/carpet/calc_gridlines.js | 69 ++++++------- src/traces/carpet/calc_labels.js | 18 +--- src/traces/carpet/catmull_rom.js | 2 - src/traces/carpet/cheater_basis.js | 10 +- src/traces/carpet/compute_bounds.js | 13 --- src/traces/carpet/compute_control_points.js | 2 +- src/traces/carpet/construct_cheater.js | 16 ---- .../carpet/create_i_derivative_evaluator.js | 12 ++- .../carpet/create_j_derivative_evaluator.js | 9 +- src/traces/carpet/interpolate.js | 29 ------ src/traces/carpet/map_1d_array.js | 2 +- src/traces/carpet/plot.js | 18 ++-- src/traces/carpet/set_convert.js | 6 +- ...ll-2d-array.js => smooth_fill_2d_array.js} | 31 +++--- src/traces/carpet/xy_defaults.js | 2 - src/traces/heatmap/convert_column_xyz.js | 2 +- src/traces/scattercarpet/calc.js | 9 +- src/traces/scattercarpet/hover.js | 2 - 24 files changed, 111 insertions(+), 323 deletions(-) delete mode 100644 src/components/drawing/spline_evaluator.js delete mode 100644 src/traces/carpet/compute_bounds.js delete mode 100644 src/traces/carpet/construct_cheater.js delete mode 100644 src/traces/carpet/interpolate.js rename src/traces/carpet/{smooth-fill-2d-array.js => smooth_fill_2d_array.js} (86%) diff --git a/src/components/drawing/spline_evaluator.js b/src/components/drawing/spline_evaluator.js deleted file mode 100644 index da68d8278cc..00000000000 --- a/src/components/drawing/spline_evaluator.js +++ /dev/null @@ -1,96 +0,0 @@ -/** -* 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'; - -// generalized Catmull-Rom splines, per -// http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf -var CatmullRomExp = 0.5; -function makeControlPoints(p0, p1, p2, smoothness) { - var d1x = p0[0] - p1[0], - d1y = p0[1] - p1[1], - d2x = p2[0] - p1[0], - d2y = p2[1] - p1[1], - d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2), - d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2), - numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness, - numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness, - denom1 = 3 * d2a * (d1a + d2a), - denom2 = 3 * d1a * (d1a + d2a); - var dxL = p1[0] + (denom1 && numx / denom1); - var dyL = p1[1] + (denom1 && numy / denom1); - var dxU = p1[0] - (denom2 && numx / denom2); - var dyU = p1[1] - (denom2 && numy / denom2); - - return [[dxL, dyL], [dxU, dyU]]; -} - -/* - * Computes centripetal catmull-rom control points. These shouldn't be confused - * with tangents, which are slightly different. Specifically, the length of the - * tangent vector is - * - * degree * (control point - reference point) - * - * In other words, it really doesn't make too much of a difference whether we - * work with tangents or control points. - */ -module.exports.computeControlPoints = function(pts, smoothness) { - var tangents = []; - for(i = 1; i < pts.length - 1; i++) { - tangents.push(makeControlPoints(pts[i - 1], pts[i], pts[i + 1], smoothness)); - } - return tangents; -}; - -module.exports.evaluateSpline = function evaluateSpline(t, pts, controlpts, smoothness) { - var n = pts.length; - if(n < 3) { - var x = (1 - t) * pts[0][0] + t * pts[1][0]; - var y = (1 - t) * pts[0][1] + t * pts[1][1]; - return [x, y]; - } else { - // Compute the controlpts *once* at the beginning, and store them: - var a, b, c, d, p0, p1, p2, p3, i, t, ot; - param = Math.max(0, Math.min(n - 1, param)); - i = Math.min(Math.floor(param), n - 2); - t = param - i; - ot = 1 - t; - - p0 = pts[i]; - p3 = pts[i + 1]; - - if(i === 0 || i === n - 2) { - // Evaluate the quadratic first and last segments; - p1 = i === 0 ? controlpts[i][0] : controlpts[i - 1][1]; - a = ot * ot; - b = 2 * ot * t; - c = t * t; - return [ - a * p0[0] + b * p1[0] + c * p3[0], - a * p0[1] + b * p1[1] + c * p3[1] - ]; - } else { - // Evaluate internal cubic spline segments: - p1 = controlpts[i - 1][1]; - p2 = controlpts[i][0]; - p3 = pts[i + 1]; - - a = ot * ot * ot; - b = 3 * ot * ot * t; - c = 3 * ot * t * t; - d = t * t * t; - - return [ - a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], - a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1] - ]; - } - } -}; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 2ed33956055..aec495433bf 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -73,8 +73,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } } - console.log('xaListNotCheater:', xaListNotCheater); - // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) // if gl3d or geo is present on graph. This is retain backward compatible. // diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index fbd3d91700b..8d6cb660f50 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -11,9 +11,7 @@ // var Lib = require('../../lib'); // var isNumeric = require('fast-isnumeric'); -var extendFlat = require('../../lib/extend').extendFlat; var handleAxisDefaults = require('./axis_defaults'); -var attributes = require('./attributes'); module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce, dfltColor) { var a = coerce('a'); @@ -62,11 +60,7 @@ function mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor) { bgColor: fullLayout.paper_bgcolor, }; - function coerce(attr, dflt) { - return Lib.coerce(axIn, axOut, attributes, attr, dflt); - } - - handleAxisDefaults(axIn, axOut, coerce, defaultOptions); + handleAxisDefaults(axIn, axOut, defaultOptions); axOut._categories = axOut._categories || []; diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index ebc00705fa9..e80465cca66 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -236,10 +236,14 @@ module.exports = { description: 'Sets the tick color.' }, showticklabels: { - valType: 'boolean', - dflt: true, + valType: 'enumerated', + values: ['start', 'end', 'both', 'none'], + dflt: 'start', role: 'style', - description: 'Determines whether or not the tick labels are drawn.' + description: [ + 'Determines whether axis labels are drawn on the low side,', + 'the high side, both, or neither side of the axis.' + ].join(' ') }, tickfont: extendFlat({}, fontAttrs, { description: 'Sets the tick font.' @@ -365,16 +369,6 @@ module.exports = { 'Used with `categoryorder`.' ].join(' ') }, - showticklabels: { - valType: 'enumerated', - values: ['start', 'end', 'both', 'none'], - dflt: 'start', - role: 'style', - description: [ - 'Determines whether axis labels are drawn on the low side,', - 'the high side, both, or neither side of the axis.' - ].join(' ') - }, labelpadding: { valType: 'integer', role: 'style', @@ -506,20 +500,6 @@ module.exports = { role: 'style', description: 'Sets the line color of the end line.' }, - tick0: { - valType: 'any', - min: 0, - dflt: 0, - role: 'info', - description: 'The starting index of grid lines along the axis' - }, - dtick: { - valType: 'any', - min: 1, - dflt: 1, - role: 'info', - description: 'The stride between grid lines along the axis' - }, arraytick0: { valType: 'integer', min: 0, @@ -550,4 +530,3 @@ module.exports = { ].join(' ') }, }; - diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index d7b2d64fd1f..e58f6176998 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -9,24 +9,17 @@ 'use strict'; var carpetAttrs = require('./attributes'); -var extendFlat = require('../../lib/extend').extendFlat; -var setConvert = require('../../plots/cartesian/set_convert'); -var handleCartesianAxisDefaults = require('../../plots/cartesian/axis_defaults'); var isNumeric = require('fast-isnumeric'); var colorMix = require('tinycolor2').mix; - var Registry = require('../../registry'); var Lib = require('../../lib'); - -var layoutAttributes = require('../../plots/cartesian/layout_attributes'); var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); var handleCategoryOrderDefaults = require('../../plots/cartesian/category_order_defaults'); var setConvert = require('../../plots/cartesian/set_convert'); var orderedCategories = require('../../plots/cartesian/ordered_categories'); -var axisIds = require('../../plots/cartesian/axis_ids'); var autoType = require('../../plots/cartesian/axis_autotype'); /** @@ -42,7 +35,7 @@ var autoType = require('../../plots/cartesian/axis_autotype'); * data: the plot data to use in choosing auto type * bgColor: the plot background color, to calculate default gridline colors */ -module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) { +module.exports = function handleAxisDefaults(containerIn, containerOut, options) { var letter = options.letter, font = options.font || {}, attributes = carpetAttrs[letter + 'axis'], @@ -93,6 +86,8 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('showticksuffix'); coerce('tickmode'); + coerce('tickvals'); + coerce('ticktext'); coerce('tick0'); coerce('dtick'); coerce('arraytick0'); @@ -161,7 +156,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, delete containerOut.gridWidth; } else { var startLineColor = coerce2('startlinecolor', dfltColor); - var startLineWidth = coerce2('startlinewidth'); + var startLineWidth = coerce2('startlinewidth', gridWidth); var showStartLine = coerce('startline', containerOut.showgrid || !!startLineColor || !!startLineWidth); if(!showStartLine) { @@ -170,16 +165,16 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } var endLineColor = coerce2('endlinecolor', dfltColor); - var endLineWidth = coerce2('endlinewidth'); - var showStartLine = coerce('endline', containerOut.showgrid || !!endLineColor || !!endLineWidth); + var endLineWidth = coerce2('endlinewidth', gridWidth); + var showEndLine = coerce('endline', containerOut.showgrid || !!endLineColor || !!endLineWidth); - if(!showStartLine) { + if(!showEndLine) { delete containerOut.endlinecolor; delete containerOut.endlinewidth; } coerce('minorgridcount'); - coerce('minorgridwidth'); + coerce('minorgridwidth', gridWidth); coerce('minorgridcolor', colorMix(gridColor, options.bgColor, 95).toRgbString()); } diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 54c6de64d5c..d94f573e242 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -11,14 +11,12 @@ var Axes = require('../../plots/cartesian/axes'); var cheaterBasis = require('./cheater_basis'); var arrayMinmax = require('./array_minmax'); -var search = require('../../lib/search').findBin; var map2dArray = require('./map_2d_array'); var setConvert = require('./set_convert'); var calcGridlines = require('./calc_gridlines'); var calcLabels = require('./calc_labels'); var clean2dArray = require('../heatmap/clean_2d_array'); -var isNumeric = require('fast-isnumeric'); -var smoothFill2dArray = require('./smooth-fill-2d-array'); +var smoothFill2dArray = require('./smooth_fill_2d_array'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), @@ -28,21 +26,17 @@ module.exports = function calc(gd, trace) { a = trace.a, b = trace.b; - var xdata; - var ydata = trace.y; + var x; + var y = trace.y; if(trace._cheater) { var avals = aax.cheatertype === 'index' ? a.length : a; var bvals = bax.cheatertype === 'index' ? b.length : b; - trace.x = xdata = cheaterBasis(avals, bvals, trace.cheaterslope); + trace.x = x = cheaterBasis(avals, bvals, trace.cheaterslope); } else { - xdata = trace.x; + x = trace.x; } - // Precompute the cartesian-space coordinates of the grid lines: - var x = xdata; - var y = trace.y; - trace.x = x = clean2dArray(x); trace.y = y = clean2dArray(y); @@ -96,6 +90,6 @@ module.exports = function calc(gd, trace) { * Given a data range from starting at x1, this function computes the first * point distributed along x0 + n * dx that lies within the range. */ -function getLinspaceStartingPoint(xlow, x0, dx) { +/* function getLinspaceStartingPoint(xlow, x0, dx) { return x0 + dx * Math.ceil((xlow - x0) / dx); -} +}*/ diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index f9ea445e0f8..47cdeb2fa12 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -9,20 +9,12 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); -var cheaterBasis = require('./cheater_basis'); -var arrayMinmax = require('./array_minmax'); -var search = require('../../lib/search').findBin; -var computeControlPoints = require('./compute_control_points'); -var map2dArray = require('./map_2d_array'); -var createSplineEvaluator = require('./create_spline_evaluator'); -var setConvert = require('./set_convert'); -var map2dArray = require('./map_2d_array'); -var map1dArray = require('./map_1d_array'); -var makepath = require('./makepath'); var extendFlat = require('../../lib/extend').extendFlat; module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { - var i, j, gridline, j0, i0; + var i, j, j0; + var eps, bounds, n1, n2, n, value, v; + var j1, v0, v1, d; var data = trace[axisLetter]; var axis = trace[axisLetter + 'axis']; @@ -34,6 +26,13 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var crossData = trace[crossAxisLetter]; var crossAxis = trace[crossAxisLetter + 'axis']; + if(axis.tickmode === 'array') { + axis.tickvals = []; + for(i = 0; i < data.length; i++) { + axis.tickvals.push(data[i]); + } + } + var xcp = trace._xctrl; var ycp = trace._yctrl; var nea = xcp.length; @@ -41,6 +40,8 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var na = trace.a.length; var nb = trace.b.length; + Axes.calcTicks(axis); + // The default is an empty array that will cause the join to remove the gridline if // it's just disappeared: axis._startline = axis._endline = []; @@ -50,7 +51,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var stride = axis.smoothing ? 3 : 1; function constructValueGridline(value) { - var j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, i, dxydj0, dxydj1; + var i, j, j0, tj, pxy, i0, ti, xy, dxydi0, dxydi1, dxydj0, dxydj1; var xpoints = []; var ypoints = []; var ret = {}; @@ -72,7 +73,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { return trace.dxydi([], i0, j0, ti, tj); }; - for(var i = 0; i < na; i++) { + for(i = 0; i < na; i++) { i0 = Math.min(na - 2, i); ti = i - i0; xy = trace._evalxy([], i0, j0, ti, tj); @@ -111,7 +112,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { return trace.dxydj([], i0, j0, ti, tj); }; - for(var j = 0; j < nb; j++) { + for(j = 0; j < nb; j++) { j0 = Math.min(nb - 2, j); tj = j - j0; xy = trace._evalxy([], i0, j0, ti, tj); @@ -149,6 +150,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { } function constructArrayGridline(idx) { + var i0, j0, ti, tj; var xpoints = []; var ypoints = []; var ret = {}; @@ -156,8 +158,8 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { ret.crossLength = crossData.length; if(axisLetter === 'b') { - var j0 = Math.max(0, Math.min(nb - 2, idx)); - var tj = Math.min(1, Math.max(0, idx - j0)); + j0 = Math.max(0, Math.min(nb - 2, idx)); + tj = Math.min(1, Math.max(0, idx - j0)); ret.xy = function(i) { var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); @@ -176,8 +178,8 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { ypoints[i] = ycp[i][idx * stride]; } } else { - var i0 = Math.max(0, Math.min(na - 2, idx)); - var ti = Math.min(1, Math.max(0, idx - i0)); + i0 = Math.max(0, Math.min(na - 2, idx)); + ti = Math.min(1, Math.max(0, idx - i0)); ret.xy = function(j) { var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); @@ -212,20 +214,19 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { } if(axis.tickmode === 'array') { - var j0, j1; // var j0 = axis.startline ? 1 : 0; // var j1 = data.length - (axis.endline ? 1 : 0); - var eps = 5e-15; - var bounds = [ + eps = 5e-15; + bounds = [ Math.floor(((data.length - 1) - axis.arraytick0) / axis.arraydtick * (1 + eps)), Math.ceil((- axis.arraytick0) / axis.arraydtick / (1 + eps)) ].sort(function(a, b) {return a - b;}); // Unpack sorted values so we can be sure to avoid infinite loops if something // is backwards: - var n1 = bounds[0] - 1; - var n2 = bounds[1] + 1; + n1 = bounds[0] - 1; + n2 = bounds[1] + 1; // If the axes fall along array lines, then this is a much simpler process since // we already have all the control points we need @@ -247,18 +248,18 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { if(j0 < 0 || j0 > data.length - 1) continue; if(j1 < 0 || j1 > data.length - 1) continue; - var v0 = data[j0]; - var v1 = data[j1]; + v0 = data[j0]; + v1 = data[j1]; for(i = 0; i < axis.minorgridcount; i++) { - var d = j1 - j0; + d = j1 - j0; // TODO: fix the bounds computation so we don't have to do a large range and then throw // out unneeded numbers if(d <= 0) continue; // XXX: This calculation isn't quite right. Off by one somewhere? - var v = v0 + (v1 - v0) * (i + 1) / (axis.minorgridcount + 1) * (axis.arraydtick / d); + v = v0 + (v1 - v0) * (i + 1) / (axis.minorgridcount + 1) * (axis.arraydtick / d); // TODO: fix the bounds computation so we don't have to do a large range and then throw // out unneeded numbers @@ -292,19 +293,19 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { // (roughly speaking): // Give this a nice generous epsilon. We use at as * (1 + eps) in order to make // inequalities a little tolerant in a more or less correct manner: - var eps = 5e-15; - var bounds = [ + eps = 5e-15; + bounds = [ Math.floor((data[data.length - 1] - axis.tick0) / axis.dtick * (1 + eps)), Math.ceil((data[0] - axis.tick0) / axis.dtick / (1 + eps)) ].sort(function(a, b) {return a - b;}); // Unpack sorted values so we can be sure to avoid infinite loops if something // is backwards: - var n1 = bounds[0]; - var n2 = bounds[1]; + n1 = bounds[0]; + n2 = bounds[1]; - for(var n = n1; n <= n2; n++) { - var value = axis.tick0 + axis.dtick * n; + for(n = n1; n <= n2; n++) { + value = axis.tick0 + axis.dtick * n; gridlines.push(extendFlat(constructValueGridline(value), { color: axis.gridcolor, @@ -316,7 +317,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { value = axis.tick0 + axis.dtick * n; for(i = 0; i < axis.minorgridcount; i++) { - var v = value + axis.dtick * (i + 1) / (axis.minorgridcount + 1); + v = value + axis.dtick * (i + 1) / (axis.minorgridcount + 1); if(v < data[0] || v > data[data.length - 1]) continue; minorgridlines.push(extendFlat(constructValueGridline(v), { color: axis.minorgridcolor, diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index 5ec9a066016..59a13b93d18 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -9,30 +9,16 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); -var cheaterBasis = require('./cheater_basis'); -var arrayMinmax = require('./array_minmax'); -var search = require('../../lib/search').findBin; -var computeControlPoints = require('./compute_control_points'); -var map2dArray = require('./map_2d_array'); -var createSplineEvaluator = require('./create_spline_evaluator'); -var setConvert = require('./set_convert'); -var map2dArray = require('./map_2d_array'); -var map1dArray = require('./map_1d_array'); -var makepath = require('./makepath'); var extendFlat = require('../../lib/extend').extendFlat; -function normalize(x) { - var x; -} - module.exports = function calcLabels(trace, axis) { - var i, tobj, prefix, suffix; + var i, tobj, prefix, suffix, gridline; var labels = axis._labels = []; var gridlines = axis._gridlines; for(i = 0; i < gridlines.length; i++) { - var gridline = gridlines[i]; + gridline = gridlines[i]; if(['start', 'both'].indexOf(axis.showticklabels) !== -1) { tobj = Axes.tickText(axis, gridline.value); diff --git a/src/traces/carpet/catmull_rom.js b/src/traces/carpet/catmull_rom.js index 261a2a6d1a1..d7bd2d99fcf 100644 --- a/src/traces/carpet/catmull_rom.js +++ b/src/traces/carpet/catmull_rom.js @@ -37,6 +37,4 @@ module.exports = function makeControlPoints(p0, p1, p2, smoothness) { p1[0] - (denom2 && numx / denom2), p1[1] - (denom2 && numy / denom2) ]]; - - return [[dxL, dyL], [dxU, dyU]]; }; diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index fd175ba8dc4..c9e2b647d83 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -15,7 +15,7 @@ var isArray = require('../../lib').isArray; * If */ module.exports = function(a, b, cheaterslope) { - var i, j; + var i, j, ascal, bscal, aval, bval; var data = []; var na = isArray(a) ? a.length : a; @@ -28,18 +28,18 @@ module.exports = function(a, b, cheaterslope) { // This means evenly spaced data should look the same whether value // or index cheatertype. if(adata) { - var ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]) / (na - 1); + ascal = (adata.length - 1) / (adata[adata.length - 1] - adata[0]) / (na - 1); } if(bdata) { - var bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); + bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } for(i = 0; i < na; i++) { data[i] = []; - var aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); + aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); for(j = 0; j < nb; j++) { - var bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); + bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); data[i][j] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/compute_bounds.js b/src/traces/carpet/compute_bounds.js deleted file mode 100644 index 8491e24e01a..00000000000 --- a/src/traces/carpet/compute_bounds.js +++ /dev/null @@ -1,13 +0,0 @@ -/** -* 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 computeBounds() { - -}; diff --git a/src/traces/carpet/compute_control_points.js b/src/traces/carpet/compute_control_points.js index 5974e7f6176..a302a57bdff 100644 --- a/src/traces/carpet/compute_control_points.js +++ b/src/traces/carpet/compute_control_points.js @@ -128,7 +128,7 @@ function inferCubicControlPoint(p0, p2, p3) { } module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmoothing) { - var i, j, ie, je, xei, yei, xi, yi, qx, qy, cp, p1; + var i, j, ie, je, xei, yei, xi, yi, cp, p1; // At this point, we know these dimensions are correct and representative of // the whole 2D arrays: var na = x.length; diff --git a/src/traces/carpet/construct_cheater.js b/src/traces/carpet/construct_cheater.js deleted file mode 100644 index f96629093c6..00000000000 --- a/src/traces/carpet/construct_cheater.js +++ /dev/null @@ -1,16 +0,0 @@ -/** -* 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'; - -/* this function constructs an array of arrays with offset per row in the - * style of a cheater plot. - */ -module.exports = function constructCheater() { -}; diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js index d18d5c10613..2bfbe22af70 100644 --- a/src/traces/carpet/create_i_derivative_evaluator.js +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -78,13 +78,11 @@ module.exports = function(arrays, asmoothing, bsmoothing) { // linear interpolations followed by one cubic interpolation of the result return function(out, i0, j0, u, v) { if(!out) out = []; - var f0, f1, f2, f3, k, ak; + var f0, f1, k, ak; i0 *= 3; var u2 = u * u; - var u3 = u2 * u; var ou = 1 - u; var ou2 = ou * ou; - var ou3 = ou2 * ou; var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; @@ -96,8 +94,12 @@ module.exports = function(arrays, asmoothing, bsmoothing) { return out; }; } else if(bsmoothing) { - // Same as the above case, except reversed: + // Same as the above case, except reversed. I've disabled the no-unused vars rule + // so that this function is fully interpolation-agnostic. Otherwise it would need + // to be called differently in different cases. Which wouldn't be the worst, but + /* eslint-disable no-unused-vars */ return function(out, i0, j0, u, v) { + /* eslint-enable no-unused-vars */ if(!out) out = []; var f0, f1, f2, f3, k, ak; j0 *= 3; @@ -119,7 +121,9 @@ module.exports = function(arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: + /* eslint-disable no-unused-vars */ return function(out, i0, j0, u, v) { + /* eslint-enable no-unused-vars */ if(!out) out = []; var f0, f1, k, ak; var ov = 1 - v; diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index d8bf978a908..2b406ed78c3 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -27,10 +27,8 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ou3 = ou2 * ou; var v2 = v * v; - var v3 = v2 * v; var ov = 1 - v; var ov2 = ov * ov; - var ov3 = ov2 * ov; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; @@ -58,7 +56,6 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ou = 1 - u; var ou2 = ou * ou; var ou3 = ou2 * ou; - var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; @@ -78,9 +75,11 @@ module.exports = function(arrays, asmoothing, bsmoothing) { }; } else if(bsmoothing) { // Same as the above case, except reversed: + /* eslint-disable no-unused-vars */ return function(out, i0, j0, u, v) { + /* eslint-enable no-unused-vars */ if(!out) out = []; - var f0, f1, f2, f3, k, ak; + var f0, f1, k, ak; j0 *= 3; var v2 = v * v; var ov = 1 - v; @@ -97,7 +96,9 @@ module.exports = function(arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: + /* eslint-disable no-unused-vars */ return function(out, i0, j0, v, u) { + /* eslint-enable no-unused-vars */ if(!out) out = []; var f0, f1, k, ak; var ov = 1 - v; diff --git a/src/traces/carpet/interpolate.js b/src/traces/carpet/interpolate.js deleted file mode 100644 index f26e4831333..00000000000 --- a/src/traces/carpet/interpolate.js +++ /dev/null @@ -1,29 +0,0 @@ -/** -* 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(f, t) { - var ot2, ot3, t2, t3; - var ot = 1 - t; - - switch(f.length) { - case 4: - ot2 = ot * ot; - ot3 = ot2 * ot; - t2 = t * t; - t3 = t2 * t; - return f[0] * ot3 + 3 * (f[1] * ot2 * t + f[2] * ot * t2) + t3 * f[3]; - case 3: - ot2 = ot * ot; - t2 = t * t; - return f[0] * ot2 + 2 * f[1] * ot * t + t2 * f[2]; - case 2: - return ot * f[0] + t * f[1]; - } -}; diff --git a/src/traces/carpet/map_1d_array.js b/src/traces/carpet/map_1d_array.js index 7a8dca1c284..89d67358150 100644 --- a/src/traces/carpet/map_1d_array.js +++ b/src/traces/carpet/map_1d_array.js @@ -14,7 +14,7 @@ * reallocation to the extent possible. */ module.exports = function mapArray(out, data, func) { - var i, j; + var i; if(!Array.isArray(out)) { // If not an array, make it an array: diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 4148f913739..f62ad4bffb8 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -36,10 +36,10 @@ function plotOne(gd, plotinfo, cd) { bax = trace.baxis; // id = 'carpet' + uid; - var x = cd[0].x; - var y = cd[0].y; - var a = cd[0].a; - var b = cd[0].b; + // var x = cd[0].x; + // var y = cd[0].y; + // var a = cd[0].a; + // var b = cd[0].b; // XXX: Layer choice?? var gridLayer = plotinfo.plot.selectAll('.maplayer'); @@ -47,7 +47,6 @@ function plotOne(gd, plotinfo, cd) { var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); var majorLayer = makeg(gridLayer, 'g', 'majorlayer'); var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); - var boundaryLayer = makeg(gridLayer, 'g', 'boundarylayer'); var labelLayer = makeg(gridLayer, 'g', 'labellayer'); drawGridLines(xa, ya, majorLayer, aax, 'a', aax._gridlines, true); @@ -73,19 +72,18 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { .classed(lineClass, true) .style('vector-effect', 'non-scaling-stroke'); - gridjoin.each(function(d, i) { + gridjoin.each(function(d) { var gridline = d; - var axis = gridline.axis; var x = gridline.x; var y = gridline.y; var xp = map1dArray([], x, xaxis.c2p); var yp = map1dArray([], y, yaxis.c2p); - var d = makepath(xp, yp, gridline.smoothing); + var path = makepath(xp, yp, gridline.smoothing); var el = d3.select(this); - el.attr('d', d) + el.attr('d', path) .style('stroke-width', gridline.width) .style('stroke', gridline.color) .style('fill', 'none'); @@ -99,7 +97,7 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { labelJoin.enter().append('text') .classed(labelClass, true); - labelJoin.each(function(d, i) { + labelJoin.each(function(d) { var label = d; var ax = d.axis; diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index a05fe589410..593cd720695 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -131,7 +131,7 @@ module.exports = function setConvert(trace) { return [xa.p2c(p[0]), ya.p2c(p[1])]; }; - trace.dadi = function(i, u) { + trace.dadi = function(i /* , u*/) { // Right now only a piecewise linear a or b basis is permitted since smoother interpolation // would cause monotonicity problems. As a retult, u is entirely disregarded in this // computation, though we'll specify it as a parameter for the sake of completeness and @@ -147,7 +147,7 @@ module.exports = function setConvert(trace) { return a[i0 + 1] - a[i0]; }; - trace.dbdj = function(j, v) { + trace.dbdj = function(j /* , v*/) { // See above caveats for dadi which also apply here var j0 = Math.max(0, Math.min(b.length - 2, j)); @@ -164,7 +164,7 @@ module.exports = function setConvert(trace) { var dxydi = trace.dxydi(null, i0, j0, u, v); var dadi = trace.dadi(i0, u); - return [dxydi[0] / dadi, dxydi[1] / dbdj]; + return [dxydi[0] / dadi, dxydi[1] / dadi]; }; trace.dxydb = function(i0, j0, u, v) { diff --git a/src/traces/carpet/smooth-fill-2d-array.js b/src/traces/carpet/smooth_fill_2d_array.js similarity index 86% rename from src/traces/carpet/smooth-fill-2d-array.js rename to src/traces/carpet/smooth_fill_2d_array.js index e3479744d0b..2ec717a8f26 100644 --- a/src/traces/carpet/smooth-fill-2d-array.js +++ b/src/traces/carpet/smooth_fill_2d_array.js @@ -8,8 +8,10 @@ 'use strict'; +var Lib = require('../../lib'); + module.exports = function smoothFill2dArray(data, a, b) { - var i, j, k, n; + var i, j, k; var ip = []; var jp = []; @@ -56,6 +58,7 @@ module.exports = function smoothFill2dArray(data, a, b) { if(!ip.length) return data; // The tolerance doesn't need to be excessive. It's just for display positioning + var dxp, dxm, dap, dam, dbp, dbm, c, d, diff, reldiff; var tol = 1e-5; var resid = 0; var itermax = 100; @@ -91,8 +94,8 @@ module.exports = function smoothFill2dArray(data, a, b) { } if((i === 0 || i === ni - 1) && (j > 0 && j < nj - 1)) { - var dxp = b[j + 1] - b[j]; - var dxm = b[j] - b[j - 1]; + dxp = b[j + 1] - b[j]; + dxm = b[j] - b[j - 1]; newVal += (dxm * data[i][j + 1] + dxp * data[i][j - 1]) / (dxm + dxp); contribCnt++; } @@ -116,8 +119,8 @@ module.exports = function smoothFill2dArray(data, a, b) { } if((j === 0 || j === nj - 1) && (i > 0 && i < ni - 1)) { - var dxp = a[i + 1] - a[i]; - var dxm = a[i] - a[i - 1]; + dxp = a[i + 1] - a[i]; + dxm = a[i] - a[i - 1]; newVal += (dxm * data[i + 1][j] + dxp * data[i - 1][j]) / (dxm + dxp); contribCnt++; } @@ -125,13 +128,13 @@ module.exports = function smoothFill2dArray(data, a, b) { if(!contribCnt) { // interior point, so simply average: // Get the grid spacing on either side: - var dap = a[i + 1] - a[i]; - var dam = a[i] - a[i - 1]; - var dbp = b[j + 1] - b[j]; - var dbm = b[j] - b[j - 1]; + dap = a[i + 1] - a[i]; + dam = a[i] - a[i - 1]; + dbp = b[j + 1] - b[j]; + dbm = b[j] - b[j - 1]; // Some useful constants: - var c = dap * dam * (dap + dam); - var d = dbp * dbm * (dbp + dbm); + c = dap * dam * (dap + dam); + d = dbp * dbm * (dbp + dbm); newVal = (c * (dbm * data[i][j + 1] + dbp * data[i][j - 1]) + d * (dam * data[i + 1][j] + dap * data[i - 1][j])) / (d * (dam + dap) + c * (dbm + dbp)); @@ -140,8 +143,8 @@ module.exports = function smoothFill2dArray(data, a, b) { newVal /= contribCnt; } - var diff = newVal - data[i][j]; - var reldiff = diff / dmax; + diff = newVal - data[i][j]; + reldiff = diff / dmax; resid += reldiff * reldiff; // Gauss-Seidel-ish iteration, omega chosen based on heuristics and some @@ -154,7 +157,7 @@ module.exports = function smoothFill2dArray(data, a, b) { resid = Math.sqrt(resid); } while(iter++ < itermax && resid > tol); - console.log('Smoother converged to', resid, 'after', iter, 'iterations'); + Lib.log('Smoother converged to', resid, 'after', iter, 'iterations'); return data; }; diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index dcbe45995b3..33ee0f93631 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -17,8 +17,6 @@ var hasColumns = require('./has_columns'); var convertColumnData = require('../heatmap/convert_column_xyz'); module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { - var hasxcols = true; - var hasycols = true; var cols = []; var x = coerce('x'); diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index d185c7f8f22..325d3941391 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -20,7 +20,7 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, col1Calendar = trace[var1Name + 'calendar'], col2Calendar = trace[var2Name + 'calendar']; - var i, j, arrayVar, newArrays, newArray, arrayVarName; + var i, j, arrayVar, newArray, arrayVarName; for(i = 0; i < arrayVarNames.length; i++) { arrayVar = trace[arrayVarNames[i]]; diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 5d1a2224c8b..c4c741eeb57 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -17,10 +17,8 @@ var Lib = require('../../lib'); var subTypes = require('../scatter/subtypes'); var calcColorscale = require('../scatter/colorscale_calc'); -var dataArrays = ['a', 'b']; - module.exports = function calc(gd, trace) { - var i, j, dataArray, newArray, fillArray1, fillArray2, carpet; + var i, carpet; for(i = 0; i < gd._fullData.length; i++) { if(gd._fullData[i].carpetid === trace.carpetid && gd._fullData[i].type === 'carpet') { @@ -38,13 +36,10 @@ module.exports = function calc(gd, trace) { trace.xaxis = carpet.xaxis; trace.yaxis = carpet.yaxis; - var displaySum = carpet.sum, - normSum = trace.sum || displaySum; - // make the calcdata array var serieslen = trace.a.length; var cd = new Array(serieslen); - var a, b, norm, x, y; + var a, b; for(i = 0; i < serieslen; i++) { a = trace.a[i]; b = trace.b[i]; diff --git a/src/traces/scattercarpet/hover.js b/src/traces/scattercarpet/hover.js index 505678dba6f..c10aeb0f993 100644 --- a/src/traces/scattercarpet/hover.js +++ b/src/traces/scattercarpet/hover.js @@ -10,8 +10,6 @@ 'use strict'; var scatterHover = require('../scatter/hover'); -var Axes = require('../../plots/cartesian/axes'); - module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var scatterPointData = scatterHover(pointData, xval, yval, hovermode); From 2e5fbe6b0c2502d245808e821862d3e62f3ca434 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 11:53:45 -0500 Subject: [PATCH 018/132] Fix bad attribute defaults --- src/traces/carpet/axis_attributes.js | 53 ++++++++-------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index e80465cca66..33fdbb9adf4 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -133,45 +133,6 @@ module.exports = { 'Has an effect only if `tickmode` is set to *auto*.' ].join(' ') }, - tick0: { - valType: 'any', - role: 'style', - description: [ - 'Sets the placement of the first tick on this axis.', - 'Use with `dtick`.', - 'If the axis `type` is *log*, then you must take the log of your starting tick', - '(e.g. to set the starting tick to 100, set the `tick0` to 2)', - 'except when `dtick`=*L* (see `dtick` for more info).', - 'If the axis `type` is *date*, it should be a date string, like date data.', - 'If the axis `type` is *category*, it should be a number, using the scale where', - 'each category is assigned a serial number from zero in the order it appears.' - ].join(' ') - }, - dtick: { - valType: 'any', - role: 'style', - description: [ - 'Sets the step in-between ticks on this axis. Use with `tick0`.', - 'Must be a positive number, or special strings available to *log* and *date* axes.', - 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', - 'is the tick number. For example,', - 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', - 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', - 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', - '*log* has several special values; *L*, where `f` is a positive number,', - 'gives ticks linearly spaced in value (but not position).', - 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', - 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', - '`tick0` is ignored for *D1* and *D2*.', - 'If the axis `type` is *date*, then you must convert the time to milliseconds.', - 'For example, to set the interval between ticks to one day,', - 'set `dtick` to 86400000.0.', - '*date* also has special values *M* gives ticks spaced by a number of months.', - '`n` must be a positive integer.', - 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', - 'To set ticks every 4 years, set `dtick` to *M48*' - ].join(' ') - }, tickvals: { valType: 'data_array', description: [ @@ -500,6 +461,20 @@ module.exports = { role: 'style', description: 'Sets the line color of the end line.' }, + tick0: { + valType: 'any', + min: 0, + dflt: 0, + role: 'info', + description: 'The starting index of grid lines along the axis' + }, + dtick: { + valType: 'any', + min: 1, + dflt: 1, + role: 'info', + description: 'The stride between grid lines along the axis' + }, arraytick0: { valType: 'integer', min: 0, From 502f89ad0349b38c3475a9ef26e59a016a6b2f8c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 11:59:57 -0500 Subject: [PATCH 019/132] Choose better defaults --- src/traces/carpet/axis_attributes.js | 2 +- src/traces/carpet/defaults.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 33fdbb9adf4..4b251963e24 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -112,7 +112,7 @@ module.exports = { cheatertype: { valType: 'enumerated', values: ['index', 'value'], - dflt: 'index', + dflt: 'value', role: 'info' }, tickmode: { diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index b8b7bfc1ed2..7718f7a72e8 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -20,13 +20,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayou return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - coerce('carpetid'); - coerce('cheaterslope'); var defaultColor = coerce('color', colorAttrs.defaultLine); - Lib.coerceFont(coerce, 'font'); - traceOut.cheaterslope = parseFloat(traceIn.cheaterslope); + coerce('carpetid'); + coerce('cheaterslope'); handleABDefaults(traceIn, traceOut, fullLayout, coerce, defaultColor); From 98a660fb016f5da707815cb077668b529437b5ae Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 12:05:30 -0500 Subject: [PATCH 020/132] Remove unused require --- src/components/drawing/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index ccb6431ed24..368365db908 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -24,8 +24,6 @@ var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); var drawing = module.exports = {}; -drawing.splineEvaluator = require('./spline_evaluator'); - // ----------------------------------------------------- // styling functions for plot elements // ----------------------------------------------------- From 656a8db523ce15aebca6faae5aba80e23286f7aa Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 12:21:00 -0500 Subject: [PATCH 021/132] CLean up the carpet plot function --- src/traces/carpet/plot.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index f62ad4bffb8..cc1d631e778 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -29,11 +29,11 @@ function makeg(el, type, klass) { function plotOne(gd, plotinfo, cd) { var trace = cd[0].trace, - // uid = trace.uid, xa = plotinfo.xaxis, ya = plotinfo.yaxis, aax = trace.aaxis, bax = trace.baxis; + // uid = trace.uid, // id = 'carpet' + uid; // var x = cd[0].x; @@ -51,7 +51,6 @@ function plotOne(gd, plotinfo, cd) { drawGridLines(xa, ya, majorLayer, aax, 'a', aax._gridlines, true); drawGridLines(xa, ya, majorLayer, bax, 'b', bax._gridlines, true); - drawGridLines(xa, ya, minorLayer, aax, 'a', aax._minorgridlines, true); drawGridLines(xa, ya, minorLayer, bax, 'b', bax._minorgridlines, true); @@ -66,13 +65,13 @@ function plotOne(gd, plotinfo, cd) { function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { var lineClass = 'const-' + axisLetter + '-lines'; - var gridjoin = layer.selectAll('.' + lineClass).data(gridlines); + var gridJoin = layer.selectAll('.' + lineClass).data(gridlines); - gridjoin.enter().append('path') + gridJoin.enter().append('path') .classed(lineClass, true) .style('vector-effect', 'non-scaling-stroke'); - gridjoin.each(function(d) { + gridJoin.each(function(d) { var gridline = d; var x = gridline.x; var y = gridline.y; @@ -83,12 +82,14 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { var path = makepath(xp, yp, gridline.smoothing); var el = d3.select(this); + el.attr('d', path) .style('stroke-width', gridline.width) .style('stroke', gridline.color) .style('fill', 'none'); - }) - .exit().remove(); + }); + + gridJoin.exit().remove(); } function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { @@ -97,14 +98,16 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { labelJoin.enter().append('text') .classed(labelClass, true); - labelJoin.each(function(d) { - var label = d; - var ax = d.axis; - - var el = d3.select(this); - + labelJoin.each(function(label) { + // The rest of the calculation is in calc_labels. Only the parts that depend upon + // the screen space representation of the x and y axes are here: + // + // Compute the direction of the labels in pixel coordinates: var dx = label.dxy[0] * trace.dpdx(xaxis); var dy = label.dxy[1] * trace.dpdy(yaxis); + + // Compute the angle and adjust so that the labels are always upright + // and the anchor is on the correct side: var angle = Math.atan2(dy, dx) * 180 / Math.PI; var endAnchor = label.endAnchor; if(angle < -90) { @@ -115,12 +118,11 @@ function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { endAnchor = !endAnchor; } - // Convert coordinates to pixel coordinates: + // Compute the position in pixel coordinates var xy = trace.c2p(label.xy, xaxis, yaxis); - // XXX: Use existing text functions - el.attr('x', xy[0] + ax.labelpadding * (endAnchor ? -1 : 1)) // These are pre-transform offsets - .attr('y', xy[1] + 5) // Shift down to hackily vertically center + d3.select(this).attr('x', xy[0] + label.axis.labelpadding * (endAnchor ? -1 : 1)) + .attr('y', xy[1] + label.font.size * 0.3) .attr('text-anchor', endAnchor ? 'end' : 'start') .text(label.text) .attr('transform', 'rotate(' + angle + ' ' + xy[0] + ',' + xy[1] + ')') From 56f96625bef7293e495668ea12d55d9d23096444 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 13:37:59 -0500 Subject: [PATCH 022/132] Carpet clip path --- src/traces/carpet/axis_attributes.js | 6 ++-- src/traces/carpet/axis_defaults.js | 1 - src/traces/carpet/calc.js | 20 ++++++----- src/traces/carpet/calc_clippath.js | 54 ++++++++++++++++++++++++++++ src/traces/carpet/makepath.js | 2 +- src/traces/carpet/plot.js | 36 +++++++++++++++++-- 6 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 src/traces/carpet/calc_clippath.js diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 4b251963e24..69f46edbed6 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -24,8 +24,10 @@ module.exports = { ].join(' ') }, smoothing: { - valType: 'boolean', - dflt: true, + valType: 'number', + dflt: 1, + min: 0, + max: 1, role: 'info' }, title: { diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index e58f6176998..0b9117f3a20 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -76,7 +76,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) } coerce('smoothing'); - containerOut.smoothing = containerOut.smoothing ? 1 : 0; coerce('cheatertype'); coerce('showticklabels'); diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index d94f573e242..4288171a26f 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -15,6 +15,7 @@ var map2dArray = require('./map_2d_array'); var setConvert = require('./set_convert'); var calcGridlines = require('./calc_gridlines'); var calcLabels = require('./calc_labels'); +var calcClipPath = require('./calc_clippath'); var clean2dArray = require('../heatmap/clean_2d_array'); var smoothFill2dArray = require('./smooth_fill_2d_array'); @@ -65,6 +66,9 @@ module.exports = function calc(gd, trace) { var dy = 0.5 * (yrange[1] - yrange[0]); var yc = 0.5 * (yrange[1] + yrange[0]); + // Expand the axes to fit the plot, except just grow it by a factor of 1.3 + // because the labels should be taken into account except that's difficult + // hence 1.3. var grow = 1.3; xrange = [xc - dx * grow, xc + dx * grow]; yrange = [yc - dy * grow, yc + dy * grow]; @@ -72,12 +76,20 @@ module.exports = function calc(gd, trace) { Axes.expand(xa, xrange, {padded: true}); Axes.expand(ya, yrange, {padded: true}); + // Enumerate the gridlines, both major and minor, and store them on the trace + // object: calcGridlines(trace, 'a', 'b'); calcGridlines(trace, 'b', 'a'); + // Calculate the text labels for each major gridline and store them on the + // trace object: calcLabels(trace, aax); calcLabels(trace, bax); + // Tabulate points for the four segments that bound the axes so that we can + // map to pixel coordinates in the plot function and create a clip rect: + trace._clipsegments = calcClipPath(trace._xctrl, trace._yctrl, aax, bax); + return [{ x: x, y: y, @@ -85,11 +97,3 @@ module.exports = function calc(gd, trace) { b: b }]; }; - -/* - * Given a data range from starting at x1, this function computes the first - * point distributed along x0 + n * dx that lies within the range. - */ -/* function getLinspaceStartingPoint(xlow, x0, dx) { - return x0 + dx * Math.ceil((xlow - x0) / dx); -}*/ diff --git a/src/traces/carpet/calc_clippath.js b/src/traces/carpet/calc_clippath.js new file mode 100644 index 00000000000..9911f554de1 --- /dev/null +++ b/src/traces/carpet/calc_clippath.js @@ -0,0 +1,54 @@ +/** +* 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 makeClipPath(xctrl, yctrl, aax, bax) { + var i, xc, yc, x, y; + var segments = []; + + var asmoothing = !!aax.smoothing; + var bsmoothing = !!bax.smoothing; + var nea1 = xctrl.length - 1; + var neb1 = xctrl[0].length - 1; + + // Along the lower a axis: + for(i = 0, x = [], y = []; i <= nea1; i++) { + x[i] = xctrl[i][0]; + y[i] = yctrl[i][0]; + } + segments.push({x: x, y: y, bicubic: asmoothing}); + + // Along the upper b axis: + xc = xctrl[nea1]; + yc = yctrl[nea1]; + for(i = 0, x = [], y = []; i <= neb1; i++) { + x[i] = xc[i]; + y[i] = yc[i]; + } + segments.push({x: x, y: y, bicubic: bsmoothing}); + + // Backwards along the upper a axis: + for(i = nea1, x = [], y = []; i >= 0; i--) { + x[nea1 - i] = xctrl[i][neb1]; + y[nea1 - i] = yctrl[i][neb1]; + } + segments.push({x: x, y: y, bicubic: asmoothing}); + + // Backwards along the lower b axis: + xc = xctrl[0]; + yc = yctrl[0]; + for(i = neb1, x = [], y = []; i >= 0; i--) { + x[neb1 - i] = xc[i]; + y[neb1 - i] = yc[i]; + } + segments.push({x: x, y: y, bicubic: bsmoothing}); + + return segments; +}; diff --git a/src/traces/carpet/makepath.js b/src/traces/carpet/makepath.js index 9dea70a8aa4..fd507859a3e 100644 --- a/src/traces/carpet/makepath.js +++ b/src/traces/carpet/makepath.js @@ -25,5 +25,5 @@ module.exports = function makePath(xp, yp, isBicubic) { ].join(' ')); } } - return 'M' + path.join(isBicubic ? '' : 'L'); + return path.join(isBicubic ? '' : 'L'); }; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index cc1d631e778..4d34f194e90 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -32,7 +32,8 @@ function plotOne(gd, plotinfo, cd) { xa = plotinfo.xaxis, ya = plotinfo.yaxis, aax = trace.aaxis, - bax = trace.baxis; + bax = trace.baxis, + fullLayout = gd._fullLayout; // uid = trace.uid, // id = 'carpet' + uid; @@ -43,6 +44,7 @@ function plotOne(gd, plotinfo, cd) { // XXX: Layer choice?? var gridLayer = plotinfo.plot.selectAll('.maplayer'); + var clipLayer = makeg(fullLayout._defs, 'g', 'clips'); var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); var majorLayer = makeg(gridLayer, 'g', 'majorlayer'); @@ -61,6 +63,36 @@ function plotOne(gd, plotinfo, cd) { drawAxisLabels(xa, ya, trace, labelLayer, aax._labels, 'a-label'); drawAxisLabels(xa, ya, trace, labelLayer, bax._labels, 'b-label'); + + // Swap for debugging in order to draw directly: + // drawClipPath(trace, gridLayer, xa, ya); + drawClipPath(trace, clipLayer, xa, ya); +} + +function drawClipPath(trace, layer, xaxis, yaxis) { + var seg, xp, yp, i; + // var clip = makeg(layer, 'g', 'carpetclip'); + var clip = makeg(layer, 'clipPath', 'carpetclip'); + var path = makeg(clip, 'path', 'carpetboundary'); + var segments = trace._clipsegments; + var segs = []; + + for(i = 0; i < segments.length; i++) { + seg = segments[i]; + xp = map1dArray([], seg.x, xaxis.c2p); + yp = map1dArray([], seg.y, yaxis.c2p); + segs.push(makepath(xp, yp, seg.bicubic)); + } + + // This could be optimized ever so slightly to avoid no-op L segments + // at the corners, but it's so negligible that I don't think it's worth + // the extra complexity + path.attr('id', 'clip' + trace.uid + 'carpet') + .attr('d', 'M' + segs.join('L') + 'Z'); + // .style('vector-effect', 'non-scaling-stroke') + // .style('stroke-width', 2) + // .style('stroke', 'black') + // .style('fill', 'rgba(0, 0, 0, 0.1)'); } function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { @@ -79,7 +111,7 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { var xp = map1dArray([], x, xaxis.c2p); var yp = map1dArray([], y, yaxis.c2p); - var path = makepath(xp, yp, gridline.smoothing); + var path = 'M' + makepath(xp, yp, gridline.smoothing); var el = d3.select(this); From 7fd160d42c565e974b0dd6dc4e8d6c83e52bdace Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 17 Jan 2017 17:03:52 -0500 Subject: [PATCH 023/132] Fix header info --- src/traces/carpet/ab_defaults.js | 2 +- src/traces/carpet/array_minmax.js | 2 +- src/traces/carpet/attributes.js | 2 +- src/traces/carpet/axis_attributes.js | 2 +- src/traces/carpet/axis_defaults.js | 2 +- src/traces/carpet/calc_clippath.js | 2 +- src/traces/carpet/calc_gridlines.js | 2 +- src/traces/carpet/calc_labels.js | 2 +- src/traces/carpet/catmull_rom.js | 2 +- src/traces/carpet/cheater_basis.js | 2 +- src/traces/carpet/compute_control_points.js | 2 +- src/traces/carpet/constants.js | 2 +- .../carpet/create_i_derivative_evaluator.js | 2 +- .../carpet/create_j_derivative_evaluator.js | 3 +- src/traces/carpet/create_spline_evaluator.js | 2 +- src/traces/carpet/defaults.js | 2 +- src/traces/carpet/ensure_array.js | 2 +- src/traces/carpet/has_columns.js | 2 +- src/traces/carpet/index.js | 9 +---- src/traces/carpet/makepath.js | 2 +- src/traces/carpet/map_1d_array.js | 2 +- src/traces/carpet/map_2d_array.js | 2 +- src/traces/carpet/plot.js | 2 +- src/traces/carpet/set_convert.js | 2 +- src/traces/carpet/xy_defaults.js | 39 +------------------ 25 files changed, 26 insertions(+), 69 deletions(-) diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index 8d6cb660f50..5f8183d3ce4 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/array_minmax.js b/src/traces/carpet/array_minmax.js index b3e00499075..d1b74e94343 100644 --- a/src/traces/carpet/array_minmax.js +++ b/src/traces/carpet/array_minmax.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index 7556f3efa8f..54dc153d603 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 69f46edbed6..a3a515f8087 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 0b9117f3a20..bead56102b0 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/calc_clippath.js b/src/traces/carpet/calc_clippath.js index 9911f554de1..98dab9294af 100644 --- a/src/traces/carpet/calc_clippath.js +++ b/src/traces/carpet/calc_clippath.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 47cdeb2fa12..c21f4a26415 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/calc_labels.js b/src/traces/carpet/calc_labels.js index 59a13b93d18..1e21373f274 100644 --- a/src/traces/carpet/calc_labels.js +++ b/src/traces/carpet/calc_labels.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/catmull_rom.js b/src/traces/carpet/catmull_rom.js index d7bd2d99fcf..389784934b5 100644 --- a/src/traces/carpet/catmull_rom.js +++ b/src/traces/carpet/catmull_rom.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index c9e2b647d83..00812beaeb8 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/compute_control_points.js b/src/traces/carpet/compute_control_points.js index a302a57bdff..628f264bc38 100644 --- a/src/traces/carpet/compute_control_points.js +++ b/src/traces/carpet/compute_control_points.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/constants.js b/src/traces/carpet/constants.js index 5c701c8f88c..34ced31a564 100644 --- a/src/traces/carpet/constants.js +++ b/src/traces/carpet/constants.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js index 2bfbe22af70..f4d9ecba107 100644 --- a/src/traces/carpet/create_i_derivative_evaluator.js +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index 2b406ed78c3..e3e72b51819 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -1,6 +1,5 @@ - /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js index de8b5975991..bdd1e59026c 100644 --- a/src/traces/carpet/create_spline_evaluator.js +++ b/src/traces/carpet/create_spline_evaluator.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 7718f7a72e8..3cc3ea02c79 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/ensure_array.js b/src/traces/carpet/ensure_array.js index 667f221813c..222b4dc2aae 100644 --- a/src/traces/carpet/ensure_array.js +++ b/src/traces/carpet/ensure_array.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/has_columns.js b/src/traces/carpet/has_columns.js index a472f0f15f0..66e1ef74c89 100644 --- a/src/traces/carpet/has_columns.js +++ b/src/traces/carpet/has_columns.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index eed45ee824c..ef3797449ed 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -20,14 +20,9 @@ Carpet.animatable = true; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; Carpet.basePlotModule = require('../../plots/cartesian'); -Carpet.categories = ['cartesian', 'symbols', 'markerColorscale', 'carpet']; +Carpet.categories = ['cartesian', 'carpet']; Carpet.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`.', - 'Text (appearing either on the chart or on hover only) is via `text`.', - 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'to numerical arrays.' ].join(' ') }; diff --git a/src/traces/carpet/makepath.js b/src/traces/carpet/makepath.js index fd507859a3e..4966eab4506 100644 --- a/src/traces/carpet/makepath.js +++ b/src/traces/carpet/makepath.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/map_1d_array.js b/src/traces/carpet/map_1d_array.js index 89d67358150..907618f10e2 100644 --- a/src/traces/carpet/map_1d_array.js +++ b/src/traces/carpet/map_1d_array.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/map_2d_array.js b/src/traces/carpet/map_2d_array.js index 9edd832350d..cac5e8626f6 100644 --- a/src/traces/carpet/map_2d_array.js +++ b/src/traces/carpet/map_2d_array.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 4d34f194e90..10634b23739 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 593cd720695..01690e43d56 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 33ee0f93631..3fc69a72cd8 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -9,10 +9,6 @@ 'use strict'; -// var Lib = require('../../lib'); - -// var isNumeric = require('fast-isnumeric'); - var hasColumns = require('./has_columns'); var convertColumnData = require('../heatmap/convert_column_xyz'); @@ -38,36 +34,3 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { return true; }; - -/* -function isValidZ(z) { - var allRowsAreArrays = true, - oneRowIsFilled = false, - hasOneNumber = false, - zi; - - - // Without this step: - // - // hasOneNumber = false breaks contour but not heatmap - // allRowsAreArrays = false breaks contour but not heatmap - // oneRowIsFilled = false breaks both - - for(var i = 0; i < z.length; i++) { - zi = z[i]; - if(!Array.isArray(zi)) { - allRowsAreArrays = false; - break; - } - if(zi.length > 0) oneRowIsFilled = true; - for(var j = 0; j < zi.length; j++) { - if(isNumeric(zi[j])) { - hasOneNumber = true; - break; - } - } - } - - return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); -} -*/ From 6651fdf3e0a729954f25c27b472d236581f86f0b Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 18 Jan 2017 12:14:28 -0500 Subject: [PATCH 024/132] Correct bad internal API decisions for spline bounds --- src/traces/carpet/calc_gridlines.js | 27 ++++++------ src/traces/carpet/create_spline_evaluator.js | 44 ++++++++++++++++---- src/traces/carpet/set_convert.js | 19 ++------- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index c21f4a26415..c92c0d5dec6 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -57,16 +57,20 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var ret = {}; // Search for the fractional grid index giving this line: if(axisLetter === 'b') { + // For the position we use just the i-j coordinates: j = trace.b2j(value); + + // The derivatives for catmull-rom splines are discontinuous across cell + // boundaries though, so we need to provide both the cell and the position + // within the cell separately: j0 = Math.floor(Math.max(0, Math.min(nb - 2, j))); tj = j - j0; + ret.length = nb; ret.crossLength = na; ret.xy = function(i) { - var i0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(i))); - var ti = i - i0; - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], i, j); }; ret.dxy = function(i0, ti) { @@ -76,7 +80,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { for(i = 0; i < na; i++) { i0 = Math.min(na - 2, i); ti = i - i0; - xy = trace._evalxy([], i0, j0, ti, tj); + xy = trace._evalxy([], i, j); if(crossAxis.smoothing && i > 0) { // First control point: @@ -103,9 +107,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { ret.crossLength = nb; ret.xy = function(j) { - var j0 = Math.max(0, Math.min(crossData.length - 2, Math.floor(j))); - var tj = j - j0; - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], i, j); }; ret.dxy = function(j0, tj) { @@ -115,7 +117,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { for(j = 0; j < nb; j++) { j0 = Math.min(nb - 2, j); tj = j - j0; - xy = trace._evalxy([], i0, j0, ti, tj); + xy = trace._evalxy([], i, j); if(crossAxis.smoothing && j > 0) { // First control point: @@ -162,9 +164,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { tj = Math.min(1, Math.max(0, idx - j0)); ret.xy = function(i) { - var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); - var ti = Math.min(1, Math.max(0, i - i0)); - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], i, idx); }; ret.dxy = function(i0, ti) { @@ -182,12 +182,9 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { ti = Math.min(1, Math.max(0, idx - i0)); ret.xy = function(j) { - var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); - var tj = Math.min(1, Math.max(0, j - j0)); - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], idx, j); }; - ret.dxy = function(j0, tj) { return trace.dxydj([], i0, j0, ti, tj); }; diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js index bdd1e59026c..b709150cacd 100644 --- a/src/traces/carpet/create_spline_evaluator.js +++ b/src/traces/carpet/create_spline_evaluator.js @@ -21,12 +21,20 @@ * to be able control whether the derivative at a cell boundary is approached * from one side or the other. */ -module.exports = function(arrays, asmoothing, bsmoothing) { +module.exports = function(arrays, na, nb, asmoothing, bsmoothing) { + var imax = na - 2; + var jmax = nb - 2; + if(asmoothing && bsmoothing) { - return function(out, i0, j0, u, v) { + return function(out, i, j) { if(!out) out = []; var f0, f1, f2, f3, ak, k; + var i0 = Math.max(0, Math.min(Math.floor(i), imax)); + var j0 = Math.max(0, Math.min(Math.floor(j), jmax)); + var u = Math.max(0, Math.min(1, i - i0)); + var v = Math.max(0, Math.min(1, j - j0)); + // Since it's a grid of control points, the actual indices are * 3: i0 *= 3; j0 *= 3; @@ -46,7 +54,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; + f0 = ou3 * ak[i0][j0] + 3 * (ou2 * u * ak[i0 + 1][j0] + ou * u2 * ak[i0 + 2][j0]) + u3 * ak[i0 + 3][j0]; f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; f2 = ou3 * ak[i0][j0 + 2] + 3 * (ou2 * u * ak[i0 + 1][j0 + 2] + ou * u2 * ak[i0 + 2][j0 + 2]) + u3 * ak[i0 + 3][j0 + 2]; f3 = ou3 * ak[i0][j0 + 3] + 3 * (ou2 * u * ak[i0 + 1][j0 + 3] + ou * u2 * ak[i0 + 2][j0 + 3]) + u3 * ak[i0 + 3][j0 + 3]; @@ -58,8 +66,14 @@ module.exports = function(arrays, asmoothing, bsmoothing) { } else if(asmoothing) { // Handle smooth in the a-direction but linear in the b-direction by performing four // linear interpolations followed by one cubic interpolation of the result - return function(out, i0, j0, u, v) { + return function(out, i, j) { if(!out) out = []; + + var i0 = Math.max(0, Math.min(Math.floor(i), imax)); + var j0 = Math.max(0, Math.min(Math.floor(j), jmax)); + var u = Math.max(0, Math.min(1, i - i0)); + var v = Math.max(0, Math.min(1, j - j0)); + var f0, f1, f2, f3, k, ak; i0 *= 3; var u2 = u * u; @@ -70,7 +84,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ov * ak[i0 ][j0] + v * ak[i0 ][j0 + 1]; + f0 = ov * ak[i0][j0] + v * ak[i0][j0 + 1]; f1 = ov * ak[i0 + 1][j0] + v * ak[i0 + 1][j0 + 1]; f2 = ov * ak[i0 + 2][j0] + v * ak[i0 + 2][j0 + 1]; f3 = ov * ak[i0 + 3][j0] + v * ak[i0 + 3][j0 + 1]; @@ -81,8 +95,14 @@ module.exports = function(arrays, asmoothing, bsmoothing) { }; } else if(bsmoothing) { // Same as the above case, except reversed: - return function(out, i0, j0, u, v) { + return function(out, i, j) { if(!out) out = []; + + var i0 = Math.max(0, Math.min(Math.floor(i), imax)); + var j0 = Math.max(0, Math.min(Math.floor(j), jmax)); + var u = Math.max(0, Math.min(1, i - i0)); + var v = Math.max(0, Math.min(1, j - j0)); + var f0, f1, f2, f3, k, ak; j0 *= 3; var v2 = v * v; @@ -93,7 +113,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f0 = ou * ak[i0][j0] + u * ak[i0 + 1][j0]; f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; f2 = ou * ak[i0][j0 + 2] + u * ak[i0 + 1][j0 + 2]; f3 = ou * ak[i0][j0 + 3] + u * ak[i0 + 1][j0 + 3]; @@ -104,14 +124,20 @@ module.exports = function(arrays, asmoothing, bsmoothing) { }; } else { // Finally, both directions are linear: - return function(out, i0, j0, u, v) { + return function(out, i, j) { if(!out) out = []; + + var i0 = Math.max(0, Math.min(Math.floor(i), imax)); + var j0 = Math.max(0, Math.min(Math.floor(j), jmax)); + var u = Math.max(0, Math.min(1, i - i0)); + var v = Math.max(0, Math.min(1, j - j0)); + var f0, f1, k, ak; var ov = 1 - v; var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0 ] + u * ak[i0 + 1][j0 ]; + f0 = ou * ak[i0][j0] + u * ak[i0 + 1][j0]; f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; out[k] = ov * f0 + v * f1; diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 01690e43d56..ad1844f7e56 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -45,7 +45,7 @@ module.exports = function setConvert(trace) { // some logic since it would be unnecessarily expensive to compute both interpolations // nearly identically but separately and to include a bunch of linear vs. bicubic logic in // every single call. - trace._evalxy = createSplineEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); + trace._evalxy = createSplineEvaluator([trace._xctrl, trace._yctrl], na, nb, aax.smoothing, bax.smoothing); trace.dxydi = createIDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); trace.dxydj = createJDerivativeEvaluator([trace._xctrl, trace._yctrl], aax.smoothing, bax.smoothing); @@ -98,11 +98,7 @@ module.exports = function setConvert(trace) { * or bicubic spline evaluation, but the hard part is already done at this point. */ trace.i2c = function(ij) { - var i0 = Math.max(0, Math.min(na - 2, Math.floor(ij[0]))); - var ti = ij[0] - i0; - var j0 = Math.max(0, Math.min(nb - 2, Math.floor(ij[1]))); - var tj = ij[1] - j0; - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], ij[0], ij[1]); }; trace.ab2xy = function(aval, bval) { @@ -110,17 +106,8 @@ module.exports = function setConvert(trace) { return [false, false]; } var i = trace.a2i(aval); - var i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); - var ti = i - i0; - var j = trace.b2j(bval); - var j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); - var tj = j - j0; - - if(tj < 0 || tj > 1 || ti < 0 || ti > 1) { - return [false, false]; - } - return trace._evalxy([], i0, j0, ti, tj); + return trace._evalxy([], i, j); }; trace.c2p = function(xy, xa, ya) { From ef8207443ad1f1046499ed0a520755107bcf321d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 18 Jan 2017 16:45:46 -0500 Subject: [PATCH 025/132] Start fixing setconvert --- src/traces/carpet/set_convert.js | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index ad1844f7e56..9fe920c7516 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -101,13 +101,39 @@ module.exports = function setConvert(trace) { return trace._evalxy([], ij[0], ij[1]); }; - trace.ab2xy = function(aval, bval) { - if(aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1]) { + trace.ab2xy = function(aval, bval, extrapolate) { + if(!extrapolate && (aval < a[0] || aval > a[na - 1] | bval < b[0] || bval > b[nb - 1])) { return [false, false]; } var i = trace.a2i(aval); var j = trace.b2j(bval); - return trace._evalxy([], i, j); + + var pt = trace._evalxy([], i, j); + + if (extrapolate) { + var i0, ti, j0, tj, iex, bex; + if (aval < a[0]) { + i0 = 0; + ti = 0; + iex = (aval - a[0]) / (a[1] - a[0]); + } else if (aval > a[na - 1]) { + i0 = na - 2; + //ti = + iex = (aval - a[na - 1]) / (a[na - 1] - a[na - 2]); + } + + if (bval < b[0]) { + j0 = 0; + bex = (bval - b[0]) / (b[1] - b[0]); + } else if (bval > b[na - 1]) { + j0 = na - 2; + bex = (bval - b[na - 1]) / (b[na - 1] - b[na - 2]); + } + + + } + + return pt; }; trace.c2p = function(xy, xa, ya) { From dbf269faa7ed7cf73a173c5a1c657b619472131d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 19 Jan 2017 18:19:43 -0500 Subject: [PATCH 026/132] scattercarpet extrapolation and visibility culling --- src/traces/carpet/constants.js | 1 + src/traces/carpet/plot.js | 7 ++-- src/traces/carpet/set_convert.js | 71 +++++++++++++++++++++++++++----- src/traces/scatter/plot.js | 4 +- src/traces/scattercarpet/calc.js | 9 +++- src/traces/scattercarpet/plot.js | 26 ++++++------ 6 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/traces/carpet/constants.js b/src/traces/carpet/constants.js index 34ced31a564..7c9465e0a00 100644 --- a/src/traces/carpet/constants.js +++ b/src/traces/carpet/constants.js @@ -10,4 +10,5 @@ 'use strict'; module.exports = { + RELATIVE_CULL_TOLERANCE: 1e-6 }; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 10634b23739..c2bd24d052f 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -87,10 +87,11 @@ function drawClipPath(trace, layer, xaxis, yaxis) { // This could be optimized ever so slightly to avoid no-op L segments // at the corners, but it's so negligible that I don't think it's worth // the extra complexity - path.attr('id', 'clip' + trace.uid + 'carpet') - .attr('d', 'M' + segs.join('L') + 'Z'); + trace.clipPathId = 'clip' + trace.uid + 'carpet'; + clip.attr('id', trace.clipPathId); + path.attr('d', 'M' + segs.join('L') + 'Z'); + // .style('stroke-width', 20) // .style('vector-effect', 'non-scaling-stroke') - // .style('stroke-width', 2) // .style('stroke', 'black') // .style('fill', 'rgba(0, 0, 0, 0.1)'); } diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 9fe920c7516..18cf8fe229c 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -8,6 +8,7 @@ 'use strict'; +var constants = require('./constants'); var search = require('../../lib/search').findBin; var computeControlPoints = require('./compute_control_points'); var createSplineEvaluator = require('./create_spline_evaluator'); @@ -110,27 +111,50 @@ module.exports = function setConvert(trace) { var pt = trace._evalxy([], i, j); - if (extrapolate) { - var i0, ti, j0, tj, iex, bex; - if (aval < a[0]) { + if(extrapolate) { + // The amount by which to extrapolate: + var iex = 0; + var jex = 0; + var der = []; + + var i0, ti, j0, tj; + if(aval < a[0]) { i0 = 0; ti = 0; iex = (aval - a[0]) / (a[1] - a[0]); - } else if (aval > a[na - 1]) { + } else if(aval > a[na - 1]) { i0 = na - 2; - //ti = + ti = 1; iex = (aval - a[na - 1]) / (a[na - 1] - a[na - 2]); + } else { + i0 = Math.max(0, Math.min(na - 2, Math.floor(i))); + ti = i - i0; } - if (bval < b[0]) { + if(bval < b[0]) { j0 = 0; - bex = (bval - b[0]) / (b[1] - b[0]); - } else if (bval > b[na - 1]) { - j0 = na - 2; - bex = (bval - b[na - 1]) / (b[na - 1] - b[na - 2]); + tj = 0; + jex = (bval - b[0]) / (b[1] - b[0]); + } else if(bval > b[nb - 1]) { + j0 = nb - 2; + tj = 1; + jex = (bval - b[nb - 1]) / (b[nb - 1] - b[nb - 2]); + } else { + j0 = Math.max(0, Math.min(nb - 2, Math.floor(j))); + tj = j - j0; } + if(iex) { + trace.dxydi(der, i0, j0, ti, tj); + pt[0] += der[0] * iex; + pt[1] += der[1] * iex; + } + if(jex) { + trace.dxydj(der, i0, j0, ti, tj); + pt[0] += der[0] * jex; + pt[1] += der[1] * jex; + } } return pt; @@ -194,4 +218,31 @@ module.exports = function setConvert(trace) { trace.dpdy = function(ya) { return ya._m; }; + + + // Grab the limits once rather than recomputing the bounds for every point + // independently: + var amin = a[0]; + var amax = a[na - 1]; + var bmin = b[0]; + var bmax = b[nb - 1]; + + // Compute the tolerance so that points are visible slightly outside the + // defined carpet axis: + var atol = (amax - amin) * constants.RELATIVE_CULL_TOLERANCE; + var btol = (bmax - bmin) * constants.RELATIVE_CULL_TOLERANCE; + + // Expand the limits to include the relative tolerance: + amin -= atol; + amax += atol; + bmin -= btol; + bmax += btol; + + trace.isVisible = function(a, b) { + return a > amin && a < amax && b > bmin && b < bmax; + }; + + trace.isOccluded = function(a, b) { + return a < amin || a > amax || b < bmin || b > bmax; + }; }; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index b9dde96cd7a..8b3e8f177c4 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -403,11 +403,11 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition textFilter = hideFilter; if(showMarkers) { - markerFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; + markerFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity; } if(showText) { - textFilter = trace.marker.maxdisplayed ? visFilter : Lib.identity; + textFilter = (trace.marker.maxdisplayed || trace._needsCull) ? visFilter : Lib.identity; } // marker points diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index c4c741eeb57..1d516ba1749 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -40,16 +40,21 @@ module.exports = function calc(gd, trace) { var serieslen = trace.a.length; var cd = new Array(serieslen); var a, b; + var needsCull = false; for(i = 0; i < serieslen; i++) { a = trace.a[i]; b = trace.b[i]; if(isNumeric(a) && isNumeric(b)) { - var xy = carpet.ab2xy(+a, +b); - cd[i] = {x: xy[0], y: xy[1], a: a, b: b}; + var xy = carpet.ab2xy(+a, +b, true); + var visible = carpet.isVisible(+a, +b); + if(!visible) needsCull = true; + cd[i] = {x: xy[0], y: xy[1], a: a, b: b, vis: visible}; } else cd[i] = {x: false, y: false}; } + trace._needsCull = needsCull; + cd[0].carpet = carpet; cd[0].trace = trace; diff --git a/src/traces/scattercarpet/plot.js b/src/traces/scattercarpet/plot.js index 96a7b147dae..3b6b2ba0ad0 100644 --- a/src/traces/scattercarpet/plot.js +++ b/src/traces/scattercarpet/plot.js @@ -13,6 +13,7 @@ var scatterPlot = require('../scatter/plot'); var Axes = require('../../plots/cartesian/axes'); module.exports = function plot(gd, plotinfoproxy, data) { + var i, trace, node; var carpet = data[0][0].carpet; @@ -23,20 +24,19 @@ module.exports = function plot(gd, plotinfoproxy, data) { plot: plotinfoproxy.plot }; - /* var calcdata = new Array(data.length), - fullCalcdata = gd.calcdata; - - for(var i = 0; i < fullCalcdata.length; i++) { - var j = data.indexOf(fullCalcdata[i][0].trace); - - if(j === -1) continue; + scatterPlot(plotinfo.graphDiv, plotinfo, data); - calcdata[j] = fullCalcdata[i]; + for(i = 0; i < data.length; i++) { + trace = data[i][0].trace; - // while we're here and have references to both the Carpet object - // and fullData, connect the two (for use by hover) - data[j]._carpet = plotinfo; - }*/ + // Note: .select is adequate but seems to mutate the node data, + // which is at least a bit suprising and causes problems elsewhere + node = plotinfo.plot.selectAll('g.trace' + trace.uid + ' .js-line'); - scatterPlot(plotinfo.graphDiv, plotinfo, data); + // Note: it would be more efficient if this didn't need to be applied + // separately to all scattercarpet traces, but that would require + // lots of reorganization of scatter traces that is otherwise not + // necessary. That makes this a potential optimization. + node.attr('clip-path', 'url(#clip' + carpet.uid + 'carpet)'); + } }; From 17a6b521c29e834c2739cefe97c574789f9368c8 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 20 Jan 2017 11:05:38 -0500 Subject: [PATCH 027/132] Add calc prioritization --- lib/contourcarpet.js | 11 +++++++++ lib/index.js | 1 + src/plots/plots.js | 22 +++++++++++++++--- src/traces/carpet/index.js | 1 + src/traces/contourcarpet/attributes.js | 32 ++++++++++++++++++++++++++ src/traces/contourcarpet/defaults.js | 32 ++++++++++++++++++++++++++ src/traces/contourcarpet/index.js | 31 +++++++++++++++++++++++++ src/traces/contourcarpet/plot.js | 16 +++++++++++++ 8 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 lib/contourcarpet.js create mode 100644 src/traces/contourcarpet/attributes.js create mode 100644 src/traces/contourcarpet/defaults.js create mode 100644 src/traces/contourcarpet/index.js create mode 100644 src/traces/contourcarpet/plot.js diff --git a/lib/contourcarpet.js b/lib/contourcarpet.js new file mode 100644 index 00000000000..43cb6831955 --- /dev/null +++ b/lib/contourcarpet.js @@ -0,0 +1,11 @@ +/** +* 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'; + +module.exports = require('../src/traces/contourcarpet'); diff --git a/lib/index.js b/lib/index.js index 96bea5695f1..ec9177ca59c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,6 +37,7 @@ Plotly.register([ require('./carpet'), require('./scattercarpet'), + require('./contourcarpet'), require('./ohlc'), require('./candlestick') diff --git a/src/plots/plots.js b/src/plots/plots.js index a1d853d40ef..954a57d28d4 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1996,11 +1996,27 @@ plots.doCalcdata = function(gd, traces) { } } - // 'regular' loop + // Look up priorities + var priority; + var prioritizedFullDataLookup = []; + var hasPriorities = false; for(i = 0; i < fullData.length; i++) { + trace = fullData[i]; + _module = trace._module; + priority = (_module && _module.calcPriority) ? _module.calcPriority : 0; + hasPriorities = hasPriorities || priority; + prioritizedFullDataLookup.push({index: i, priority: priority}); + } + + // Sort by priority descending so that higher priority is computed first: + if(hasPriorities) prioritizedFullDataLookup.sort(function(a, b) { return b.priority - a.priority; }); + + // 'regular' loop + for(i = 0; i < prioritizedFullDataLookup.length; i++) { + var index = prioritizedFullDataLookup[i].index; var cd = []; - trace = fullData[i]; + trace = fullData[index]; if(trace.visible === true) { _module = trace._module; @@ -2024,6 +2040,6 @@ plots.doCalcdata = function(gd, traces) { if(!cd[0].t) cd[0].t = {}; cd[0].trace = trace; - calcdata[i] = cd; + calcdata[index] = cd; } }; diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index ef3797449ed..eb1987b12c1 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -16,6 +16,7 @@ Carpet.supplyDefaults = require('./defaults'); Carpet.plot = require('./plot'); Carpet.calc = require('./calc'); Carpet.animatable = true; +Carpet.calcPriority = 1; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js new file mode 100644 index 00000000000..f4ad329d274 --- /dev/null +++ b/src/traces/contourcarpet/attributes.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 scatterAttrs = require('../scatter/attributes'); +var plotAttrs = require('../../plots/attributes'); +var colorAttributes = require('../../components/colorscale/color_attributes'); +var colorbarAttrs = require('../../components/colorbar/attributes'); + +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterMarkerAttrs = scatterAttrs.marker, + scatterLineAttrs = scatterAttrs.line, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; + +module.exports = { + carpetid: { + valType: 'string', + role: 'info', + description: [ + 'An identifier for this carpet, so that `scattercarpet` and', + '`scattercontour` traces can specify a carpet plot on which', + 'they lie' + ].join(' ') + }, +}; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js new file mode 100644 index 00000000000..7fa76b65f44 --- /dev/null +++ b/src/traces/contourcarpet/defaults.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 Lib = require('../../lib'); + +var constants = require('../scatter/constants'); +var subTypes = require('../scatter/subtypes'); +var handleMarkerDefaults = require('../scatter/marker_defaults'); +var handleLineDefaults = require('../scatter/line_defaults'); +var handleLineShapeDefaults = require('../scatter/line_shape_defaults'); +var handleTextDefaults = require('../scatter/text_defaults'); +var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); + +var attributes = require('./attributes'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + coerce('carpetid'); + +}; diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js new file mode 100644 index 00000000000..af1b190315e --- /dev/null +++ b/src/traces/contourcarpet/index.js @@ -0,0 +1,31 @@ +/** +* 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 ContourCarpet = {}; + +ContourCarpet.attributes = require('./attributes'); +ContourCarpet.supplyDefaults = require('./defaults'); +// ContourCarpet.colorbar = require('../scatter/colorbar'); +// ContourCarpet.calc = require('./calc'); +ContourCarpet.plot = require('./plot'); +// ContourCarpet.style = require('./style'); +// ContourCarpet.hoverPoints = require('./hover'); +// ContourCarpet.selectPoints = require('./select'); + +ContourCarpet.moduleType = 'trace'; +ContourCarpet.name = 'contourcarpet'; +ContourCarpet.basePlotModule = require('../../plots/cartesian'); +ContourCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend']; +ContourCarpet.meta = { + hrName: 'contour_carpet', + description: [].join(' ') +}; + +module.exports = ContourCarpet; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js new file mode 100644 index 00000000000..c47e94c72eb --- /dev/null +++ b/src/traces/contourcarpet/plot.js @@ -0,0 +1,16 @@ +/** +* 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 scatterPlot = require('../scatter/plot'); +var Axes = require('../../plots/cartesian/axes'); + +module.exports = function plot(gd, plotinfoproxy, data) { + var i, trace, node; +}; From dc2d76e3cf6d36dd73bee69d4b8835d5eae408a5 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 20 Jan 2017 11:33:01 -0500 Subject: [PATCH 028/132] Prioritize module.calc + module.transform and make ordering stable --- src/plots/plots.js | 50 +++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 954a57d28d4..ed913394e53 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1923,7 +1923,34 @@ plots.doCalcdata = function(gd, traces) { fullData = gd._fullData, fullLayout = gd._fullLayout; - var trace, _module, i, j; + var trace, _module, i, j, index; + + // Look up priorities. The carpet trace is currently + var priority; + var prioritizedFullDataLookup = []; + var hasPriorities = false; + for(i = 0; i < fullData.length; i++) { + trace = fullData[i]; + _module = trace._module; + + // Get the priority from the module definition, otherwise default to zero: + priority = (_module && _module.calcPriority) ? _module.calcPriority : 0; + + // Note if a non-zero priority was found so that we only need to sort if + // there's anything to sort: + hasPriorities = hasPriorities || priority; + + prioritizedFullDataLookup.push({index: i, priority: priority}); + } + + // Sort by priority descending so that higher priority is computed first. If they share the same priority + // (which has already been default to zero), then they will be compared by trace index so that the original + // order is preserved within priority groups: + if(hasPriorities) { + prioritizedFullDataLookup.sort(function(a, b) { + return (b.priority - a.priority) || (a.index - b.index); + }); + } // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without // *all* needing doCalcdata: @@ -1965,7 +1992,9 @@ plots.doCalcdata = function(gd, traces) { // transform loop for(i = 0; i < fullData.length; i++) { - trace = fullData[i]; + index = prioritizedFullDataLookup[i].index; + + trace = fullData[index]; if(trace.visible === true && trace.transforms) { _module = trace._module; @@ -1996,24 +2025,9 @@ plots.doCalcdata = function(gd, traces) { } } - // Look up priorities - var priority; - var prioritizedFullDataLookup = []; - var hasPriorities = false; - for(i = 0; i < fullData.length; i++) { - trace = fullData[i]; - _module = trace._module; - priority = (_module && _module.calcPriority) ? _module.calcPriority : 0; - hasPriorities = hasPriorities || priority; - prioritizedFullDataLookup.push({index: i, priority: priority}); - } - - // Sort by priority descending so that higher priority is computed first: - if(hasPriorities) prioritizedFullDataLookup.sort(function(a, b) { return b.priority - a.priority; }); - // 'regular' loop for(i = 0; i < prioritizedFullDataLookup.length; i++) { - var index = prioritizedFullDataLookup[i].index; + index = prioritizedFullDataLookup[i].index; var cd = []; trace = fullData[index]; From 136f43cac3bda6007cb1b94a5e3767cceb8fa699 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 24 Jan 2017 10:21:31 -0500 Subject: [PATCH 029/132] Basic contourcarpet --- src/traces/carpet/set_convert.js | 5 + src/traces/contour/find_all_paths.js | 23 +- src/traces/contourcarpet/attributes.js | 123 ++++++++- src/traces/contourcarpet/calc.js | 211 +++++++++++++++ src/traces/contourcarpet/defaults.js | 38 ++- src/traces/contourcarpet/index.js | 4 +- src/traces/contourcarpet/plot.js | 349 ++++++++++++++++++++++++- src/traces/heatmap/xyz_defaults.js | 16 +- 8 files changed, 734 insertions(+), 35 deletions(-) create mode 100644 src/traces/contourcarpet/calc.js diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 18cf8fe229c..22bd458a77c 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -34,6 +34,10 @@ module.exports = function setConvert(trace) { var x = trace.x; var y = trace.y; + // XXX: ONLY PASSTHRU. ONLY. No, ONLY. + aax.c2p = function (v) { return v; }; + bax.c2p = function (v) { return v; }; + // This is potentially a very expensive step! It does the bulk of the work of constructing // an expanded basis of control points. Note in particular that it overwrites the existing // basis without creating a new array since that would potentially thrash the garbage @@ -160,6 +164,7 @@ module.exports = function setConvert(trace) { return pt; }; + trace.c2p = function(xy, xa, ya) { return [xa.c2p(xy[0]), ya.c2p(xy[1])]; }; diff --git a/src/traces/contour/find_all_paths.js b/src/traces/contour/find_all_paths.js index 5826932509e..c9f163c28f9 100644 --- a/src/traces/contour/find_all_paths.js +++ b/src/traces/contour/find_all_paths.js @@ -18,6 +18,8 @@ module.exports = function findAllPaths(pathinfo) { pi, j; + console.log('pathinfo:', pathinfo); + for(i = 0; i < pathinfo.length; i++) { pi = pathinfo[i]; @@ -48,16 +50,16 @@ function ptDist(pt1, pt2) { } function makePath(pi, loc, edgeflag) { - var startLocStr = loc.join(','), - locStr = startLocStr, - mi = pi.crossings[locStr], - marchStep = startStep(mi, edgeflag, loc), - // start by going backward a half step and finding the crossing point - pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], - startStepStr = marchStep.join(','), - m = pi.z.length, - n = pi.z[0].length, - cnt; + var startLocStr = loc.join(','); + var locStr = startLocStr; + var mi = pi.crossings[locStr]; + var marchStep = startStep(mi, edgeflag, loc); + // start by going backward a half step and finding the crossing point + var pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])]; + var startStepStr = marchStep.join(','); + var m = pi.z.length; + var n = pi.z[0].length; + var cnt; // now follow the path for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops @@ -257,6 +259,7 @@ function getInterpPx(pi, loc, step) { if(step[1]) { var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); + return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), ya.c2p(pi.y[locy], true)]; } diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index f4ad329d274..3b870d58050 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -8,8 +8,10 @@ 'use strict'; +var heatmapAttrs = require('../heatmap/attributes'); var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); +var colorscaleAttrs = require('../../components/colorscale/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); @@ -19,14 +21,125 @@ var scatterMarkerAttrs = scatterAttrs.marker, scatterLineAttrs = scatterAttrs.line, scatterMarkerLineAttrs = scatterMarkerAttrs.line; -module.exports = { +module.exports = extendFlat({}, { carpetid: { valType: 'string', role: 'info', description: [ - 'An identifier for this carpet, so that `scattercarpet` and', - '`scattercontour` traces can specify a carpet plot on which', - 'they lie' + 'The `carpetid` of the carpet axes on which this contour trace lies' ].join(' ') }, -}; + z: heatmapAttrs.z, + a: heatmapAttrs.x, + a0: heatmapAttrs.x0, + da: heatmapAttrs.dx, + b: heatmapAttrs.y, + b0: heatmapAttrs.y0, + db: heatmapAttrs.dy, + text: heatmapAttrs.text, + transpose: heatmapAttrs.transpose, + atype: heatmapAttrs.xtype, + btype: heatmapAttrs.ytype, + + connectgaps: heatmapAttrs.connectgaps, + + autocontour: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the contour level attributes are', + 'picked by an algorithm.', + 'If *true*, the number of contour levels can be set in `ncontours`.', + 'If *false*, set the contour level attributes in `contours`.' + ].join(' ') + }, + ncontours: { + valType: 'integer', + dflt: 15, + min: 1, + role: 'style', + description: [ + 'Sets the maximum number of contour levels. The actual number', + 'of contours will be chosen automatically to be less than or', + 'equal to the value of `ncontours`.', + 'Has an effect only if `autocontour` is *true* or if', + '`contours.size` is missing.' + ].join(' ') + }, + + contours: { + start: { + valType: 'number', + dflt: null, + role: 'style', + description: [ + 'Sets the starting contour level value.', + 'Must be less than `contours.end`' + ].join(' ') + }, + end: { + valType: 'number', + dflt: null, + role: 'style', + description: [ + 'Sets the end contour level value.', + 'Must be more than `contours.start`' + ].join(' ') + }, + size: { + valType: 'number', + dflt: null, + min: 0, + role: 'style', + description: [ + 'Sets the step between each contour level.', + 'Must be positive.' + ].join(' ') + }, + coloring: { + valType: 'enumerated', + values: ['fill', 'heatmap', 'lines', 'none'], + dflt: 'fill', + role: 'style', + description: [ + 'Determines the coloring method showing the contour values.', + 'If *fill*, coloring is done evenly between each contour level', + 'If *heatmap*, a heatmap gradient coloring is applied', + 'between each contour level.', + 'If *lines*, coloring is done on the contour lines.', + 'If *none*, no coloring is applied on this trace.' + ].join(' ') + }, + showlines: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the contour lines are drawn.', + 'Has only an effect if `contours.coloring` is set to *fill*.' + ].join(' ') + } + }, + + line: { + color: extendFlat({}, scatterLineAttrs.color, { + description: [ + 'Sets the color of the contour level.', + 'Has no if `contours.coloring` is set to *lines*.' + ].join(' ') + }), + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + smoothing: extendFlat({}, scatterLineAttrs.smoothing, { + description: [ + 'Sets the amount of smoothing for the contour lines,', + 'where *0* corresponds to no smoothing.' + ].join(' ') + }) + } +}, + colorscaleAttrs, + { autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}) }, + { colorbar: colorbarAttrs } +); diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js new file mode 100644 index 00000000000..d79eac20e0a --- /dev/null +++ b/src/traces/contourcarpet/calc.js @@ -0,0 +1,211 @@ +/** +* 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'; + +var Axes = require('../../plots/cartesian/axes'); +var extendFlat = require('../../lib').extendFlat; +var Registry = require('../../registry'); +var colorscaleCalc = require('../../components/colorscale/calc'); +var hasColumns = require('../heatmap/has_columns'); +var convertColumnData = require('../heatmap/convert_column_xyz'); +var clean2dArray = require('../heatmap/clean_2d_array'); +var maxRowLength = require('../heatmap/max_row_length'); +var interp2d = require('../heatmap/interp2d'); +var findEmpties = require('../heatmap/find_empties'); +var makeBoundArray = require('../heatmap/make_bound_array'); + + +// most is the same as heatmap calc, then adjust it +// though a few things inside heatmap calc still look for +// contour maps, because the makeBoundArray calls are too entangled +module.exports = function calc(gd, trace) { + var i, carpet; + + for(i = 0; i < gd._fullData.length; i++) { + if(gd._fullData[i].carpetid === trace.carpetid && gd._fullData[i].type === 'carpet') { + carpet = gd._fullData[i]; + break; + } + } + + if(!carpet) return; + trace._carpet = carpet; + + var cd = heatmappishCalc(gd, trace), + contours = trace.contours; + + // check if we need to auto-choose contour levels + if(trace.autocontour !== false) { + var dummyAx = autoContours(trace.zmin, trace.zmax, trace.ncontours); + + contours.size = dummyAx.dtick; + + contours.start = Axes.tickFirst(dummyAx); + dummyAx.range.reverse(); + contours.end = Axes.tickFirst(dummyAx); + + if(contours.start === trace.zmin) contours.start += contours.size; + if(contours.end === trace.zmax) contours.end -= contours.size; + + // if you set a small ncontours, *and* the ends are exactly on zmin/zmax + // there's an edge case where start > end now. Make sure there's at least + // one meaningful contour, put it midway between the crossed values + if(contours.start > contours.end) { + contours.start = contours.end = (contours.start + contours.end) / 2; + } + + // copy auto-contour info back to the source data. + trace._input.contours = extendFlat({}, contours); + } + else { + // sanity checks on manually-supplied start/end/size + var start = contours.start, + end = contours.end, + inputContours = trace._input.contours; + + if(start > end) { + contours.start = inputContours.start = end; + end = contours.end = inputContours.end = start; + start = contours.start; + } + + if(!(contours.size > 0)) { + var sizeOut; + if(start === end) sizeOut = 1; + else sizeOut = autoContours(start, end, trace.ncontours).dtick; + + inputContours.size = contours.size = sizeOut; + } + } + + return cd; +}; + +/* + * autoContours: make a dummy axis object with dtick we can use + * as contours.size, and if needed we can use Axes.tickFirst + * with this axis object to calculate the start and end too + * + * start: the value to start the contours at + * end: the value to end at (must be > start) + * ncontours: max number of contours to make, like roughDTick + * + * returns: an axis object + */ +function autoContours(start, end, ncontours) { + var dummyAx = { + type: 'linear', + range: [start, end] + }; + + Axes.autoTicks( + dummyAx, + (end - start) / (ncontours || 15) + ); + + return dummyAx; +} + +function heatmappishCalc (gd, trace) { + // prepare the raw data + // run makeCalcdata on x and y even for heatmaps, in case of category mappings + var carpet = trace._carpet; + var aax = carpet.aaxis, + bax = carpet.baxis, + isContour = Registry.traceIs(trace, 'contour'), + isGL2D = Registry.traceIs(trace, 'gl2d'), + zsmooth = isContour ? 'best' : trace.zsmooth, + a, + a0, + da, + b, + b0, + db, + z, + i; + + // cancel minimum tick spacings (only applies to bars and boxes) + aax._minDtick = 0; + bax._minDtick = 0; + + if(hasColumns(trace)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']); + + a = trace.a ? aax.makeCalcdata(trace, 'a') : []; + b = trace.b ? bax.makeCalcdata(trace, 'b') : []; + a0 = trace.a0 || 0; + da = trace.da || 1; + b0 = trace.b0 || 0; + db = trace.db || 1; + + z = clean2dArray(trace.z, trace.transpose); + + if(isContour || trace.connectgaps) { + trace._emptypoints = findEmpties(z); + trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); + } + + function noZsmooth(msg) { + zsmooth = trace._input.zsmooth = trace.zsmooth = false; + Lib.notifier('cannot fast-zsmooth: ' + msg); + } + + // check whether we really can smooth (ie all boxes are about the same size) + if(zsmooth === 'fast') { + if(aax.type === 'log' || bax.type === 'log') { + noZsmooth('log axis found'); + } + else { + if(a.length) { + var avgda = (a[a.length - 1] - a[0]) / (a.length - 1), + maxErrX = Math.abs(avgda / 100); + for(i = 0; i < a.length - 1; i++) { + if(Math.abs(a[i + 1] - a[i] - avgda) > maxErrX) { + noZsmooth('a scale is not linear'); + break; + } + } + } + if(b.length && zsmooth === 'fast') { + var avgdy = (b[b.length - 1] - b[0]) / (b.length - 1), + maxErrY = Math.abs(avgdy / 100); + for(i = 0; i < b.length - 1; i++) { + if(Math.abs(b[i + 1] - b[i] - avgdy) > maxErrY) { + noZsmooth('b scale is not linear'); + break; + } + } + } + } + } + + // create arrays of brick boundaries, to be used by autorange and heatmap.plot + var xlen = maxRowLength(z), + xIn = trace.xtype === 'scaled' ? '' : a, + xArray = makeBoundArray(trace, xIn, a0, da, xlen, aax), + yIn = trace.ytype === 'scaled' ? '' : b, + yArray = makeBoundArray(trace, yIn, b0, db, z.length, bax); + + var cd0 = {a: xArray, b: yArray, z: z}; + + // auto-z and autocolorscale if applicable + colorscaleCalc(trace, z, '', 'z'); + + if(isContour && trace.contours && trace.contours.coloring === 'heatmap') { + var dummyTrace = { + type: trace.type === 'contour' ? 'heatmap' : 'histogram2d', + xcalendar: trace.xcalendar, + ycalendar: trace.ycalendar + }; + cd0.xfill = makeBoundArray(dummyTrace, xIn, a0, da, xlen, aax); + cd0.yfill = makeBoundArray(dummyTrace, yIn, b0, db, z.length, bax); + } + + return [cd0]; +} diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 7fa76b65f44..f812cbd27af 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -11,16 +11,10 @@ var Lib = require('../../lib'); -var constants = require('../scatter/constants'); -var subTypes = require('../scatter/subtypes'); -var handleMarkerDefaults = require('../scatter/marker_defaults'); -var handleLineDefaults = require('../scatter/line_defaults'); -var handleLineShapeDefaults = require('../scatter/line_shape_defaults'); -var handleTextDefaults = require('../scatter/text_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); - +var handleXYZDefaults = require('../heatmap/xyz_defaults'); var attributes = require('./attributes'); - +var hasColumns = require('../heatmap/has_columns'); +var handleStyleDefaults = require('../contour/style_defaults'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -29,4 +23,30 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('carpetid'); + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); + if(!len) { + traceOut.visible = false; + return; + } + + coerce('text'); + coerce('connectgaps', hasColumns(traceOut)); + + var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), + contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), + missingEnd = (contourStart === false) || (contourEnd === false), + + // normally we only need size if autocontour is off. But contour.calc + // pushes its calculated contour size back to the input trace, so for + // things like restyle that can call supplyDefaults without calc + // after the initial draw, we can just reuse the previous calculation + contourSize = coerce('contours.size'), + autoContour; + + if(missingEnd) autoContour = traceOut.autocontour = true; + else autoContour = coerce('autocontour', false); + + if(autoContour || !contourSize) coerce('ncontours'); + + handleStyleDefaults(traceIn, traceOut, coerce, layout); }; diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index af1b190315e..6b5523c5fe6 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -13,7 +13,7 @@ var ContourCarpet = {}; ContourCarpet.attributes = require('./attributes'); ContourCarpet.supplyDefaults = require('./defaults'); // ContourCarpet.colorbar = require('../scatter/colorbar'); -// ContourCarpet.calc = require('./calc'); +ContourCarpet.calc = require('./calc'); ContourCarpet.plot = require('./plot'); // ContourCarpet.style = require('./style'); // ContourCarpet.hoverPoints = require('./hover'); @@ -22,7 +22,7 @@ ContourCarpet.plot = require('./plot'); ContourCarpet.moduleType = 'trace'; ContourCarpet.name = 'contourcarpet'; ContourCarpet.basePlotModule = require('../../plots/cartesian'); -ContourCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend']; +ContourCarpet.categories = ['cartesian', 'carpet', 'symbols', 'markerColorscale', 'showLegend']; ContourCarpet.meta = { hrName: 'contour_carpet', description: [].join(' ') diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index c47e94c72eb..2a925f5da4d 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -10,7 +10,352 @@ var scatterPlot = require('../scatter/plot'); var Axes = require('../../plots/cartesian/axes'); +var Drawing = require('../../components/drawing'); -module.exports = function plot(gd, plotinfoproxy, data) { - var i, trace, node; +var makeCrossings = require('../contour/make_crossings'); +var findAllPaths = require('../contour/find_all_paths'); + +module.exports = function plot(gd, plotinfo, cdcontours) { + for(var i = 0; i < cdcontours.length; i++) { + plotOne(gd, plotinfo, cdcontours[i]); + } }; + +function plotOne(gd, plotinfo, cd) { + var i, j, k; + var trace = cd[0].trace; + var carpet = trace._carpet; + var a = cd[0].a; + var b = cd[0].b; + var aa = carpet.aaxis; + var ba = carpet.baxis; + var contours = trace.contours; + var uid = trace.uid; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + var fullLayout = gd._fullLayout; + var id = 'contour' + uid; + var pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + + if(trace.visible !== true) { + fullLayout._paper.selectAll('.' + id + ',.hm' + uid).remove(); + fullLayout._infolayer.selectAll('.cb' + uid).remove(); + return; + } + + fullLayout._paper.selectAll('.hm' + uid).remove(); + + makeCrossings(pathinfo); + findAllPaths(pathinfo); + + for (i = 0; i < pathinfo.length; i++) { + var pi = pathinfo[i]; + for (j = 0; j < pi.edgepaths.length; j++) { + var foo = pi.edgepaths[j]; + for (k = 0; k < foo.length; k++) { + var ep = foo[k]; + var pt = carpet.ab2xy(ep[0], ep[1], true); + ep[0] = xa.c2p(pt[0]); + ep[1] = ya.c2p(pt[1]); + } + } + } + + console.log('pathinfo:', pathinfo); + + /*var leftedge = xa.c2p(a[0], true), + rightedge = xa.c2p(a[a.length - 1], true), + bottomedge = ya.c2p(b[0], true), + topedge = ya.c2p(b[b.length - 1], true), + perimeter = [ + [leftedge, topedge], + [rightedge, topedge], + [rightedge, bottomedge], + [leftedge, bottomedge] + ];*/ + + // draw everything + var plotGroup = makeContourGroup(plotinfo, cd, id); + //makeBackground(plotGroup, perimeter, contours); + //makeFills(plotGroup, pathinfo, perimeter, contours); + makeLines(plotGroup, pathinfo, contours); + //clipGaps(plotGroup, plotinfo, cd[0], perimeter); +} + +function emptyPathinfo(contours, plotinfo, cd0) { + var cs = contours.size; + var pathinfo = []; + + var carpet = cd0.trace._carpet; + + for(var ci = contours.start; ci < contours.end + cs / 10; ci += cs) { + pathinfo.push({ + level: ci, + // all the cells with nontrivial marching index + crossings: {}, + // starting points on the edges of the lattice for each contour + starts: [], + // all unclosed paths (may have less items than starts, + // if a path is closed by rounding) + edgepaths: [], + // all closed paths + paths: [], + // store axes so we can convert to px + xaxis: carpet.aaxis, + yaxis: carpet.baxis, + // full data arrays to use for interpolation + x: cd0.a, + y: cd0.b, + z: cd0.z, + smoothing: cd0.trace.line.smoothing + }); + + if(pathinfo.length > 1000) { + Lib.warn('Too many contours, clipping at 1000', contours); + break; + } + } + return pathinfo; +} +function makeContourGroup(plotinfo, cd, id) { + var plotgroup = plotinfo.plot.select('.maplayer') + .selectAll('g.contour.' + id) + .data(cd); + + plotgroup.enter().append('g') + .classed('contour', true) + .classed(id, true); + + plotgroup.exit().remove(); + + return plotgroup; +} + +function makeLines(plotgroup, pathinfo, contours) { + var smoothing = pathinfo[0].smoothing; + + var linegroup = plotgroup.selectAll('g.contourlevel') + .data(contours.showlines === false ? [] : pathinfo); + linegroup.enter().append('g') + .classed('contourlevel', true); + linegroup.exit().remove(); + + var opencontourlines = linegroup.selectAll('path.openline') + .data(function(d) { return d.edgepaths; }); + opencontourlines.enter().append('path') + .classed('openline', true); + opencontourlines.exit().remove(); + opencontourlines + .attr('d', function(d) { + return Drawing.smoothopen(d, smoothing); + }) + .style('stroke-miterlimit', 1) + .style('stroke-width', 1) + .style('stroke', 'black') + .style('fill', 'none'); + + var closedcontourlines = linegroup.selectAll('path.closedline') + .data(function(d) { return d.paths; }); + closedcontourlines.enter().append('path') + .classed('closedline', true); + closedcontourlines.exit().remove(); + closedcontourlines + .attr('d', function(d) { + return Drawing.smoothclosed(d, smoothing); + }) + .style('stroke-miterlimit', 1); +} + +/*function makeBackground(plotgroup, perimeter, contours) { + var bggroup = plotgroup.selectAll('g.contourbg').data([0]); + bggroup.enter().append('g').classed('contourbg', true); + + var bgfill = bggroup.selectAll('path') + .data(contours.coloring === 'fill' ? [0] : []); + bgfill.enter().append('path'); + bgfill.exit().remove(); + bgfill + .attr('d', 'M' + perimeter.join('L') + 'Z') + .style('stroke', 'none'); +} + +function makeFills(plotgroup, pathinfo, perimeter, contours) { + var fillgroup = plotgroup.selectAll('g.contourfill') + .data([0]); + fillgroup.enter().append('g') + .classed('contourfill', true); + + var fillitems = fillgroup.selectAll('path') + .data(contours.coloring === 'fill' ? pathinfo : []); + fillitems.enter().append('path'); + fillitems.exit().remove(); + fillitems.each(function(pi) { + // join all paths for this level together into a single path + // first follow clockwise around the perimeter to close any open paths + // if the whole perimeter is above this level, start with a path + // enclosing the whole thing. With all that, the parity should mean + // that we always fill everything above the contour, nothing below + var fullpath = joinAllPaths(pi, perimeter); + + if(!fullpath) d3.select(this).remove(); + else d3.select(this).attr('d', fullpath).style('stroke', 'none'); + }); +}*/ + +/*function joinAllPaths(pi, perimeter) { + var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? + '' : ('M' + perimeter.join('L') + 'Z'), + i = 0, + startsleft = pi.edgepaths.map(function(v, i) { return i; }), + newloop = true, + endpt, + newendpt, + cnt, + nexti, + possiblei, + addpath; + + function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < 0.01; } + function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < 0.01; } + function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < 0.01; } + function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } + + while(startsleft.length) { + addpath = Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); + fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); + startsleft.splice(startsleft.indexOf(i), 1); + endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; + nexti = -1; + + // now loop through sides, moving our endpoint until we find a new start + for(cnt = 0; cnt < 4; cnt++) { // just to prevent infinite loops + if(!endpt) { + Lib.log('Missing end?', i, pi); + break; + } + + if(istop(endpt) && !isright(endpt)) newendpt = perimeter[1]; // right top + else if(isleft(endpt)) newendpt = perimeter[0]; // left top + else if(isbottom(endpt)) newendpt = perimeter[3]; // right bottom + else if(isright(endpt)) newendpt = perimeter[2]; // left bottom + + for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { + var ptNew = pi.edgepaths[possiblei][0]; + // is ptNew on the (horz. or vert.) segment from endpt to newendpt? + if(Math.abs(endpt[0] - newendpt[0]) < 0.01) { + if(Math.abs(endpt[0] - ptNew[0]) < 0.01 && + (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } + else if(Math.abs(endpt[1] - newendpt[1]) < 0.01) { + if(Math.abs(endpt[1] - ptNew[1]) < 0.01 && + (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } + else { + Lib.log('endpt to newendpt is not vert. or horz.', + endpt, newendpt, ptNew); + } + } + + endpt = newendpt; + + if(nexti >= 0) break; + fullpath += 'L' + newendpt; + } + + if(nexti === pi.edgepaths.length) { + Lib.log('unclosed perimeter path'); + break; + } + + i = nexti; + + // if we closed back on a loop we already included, + // close it and start a new loop + newloop = (startsleft.indexOf(i) === -1); + if(newloop) { + i = startsleft[0]; + fullpath += 'Z'; + } + } + + // finally add the interior paths + for(i = 0; i < pi.paths.length; i++) { + fullpath += Drawing.smoothclosed(pi.paths[i], pi.smoothing); + } + + return fullpath; +}*/ + +/*function clipGaps(plotGroup, plotinfo, cd0, perimeter) { + var clipId = 'clip' + cd0.trace.uid; + + var defs = plotinfo.plot.selectAll('defs') + .data([0]); + defs.enter().append('defs'); + + var clipPath = defs.selectAll('#' + clipId) + .data(cd0.trace.connectgaps ? [] : [0]); + clipPath.enter().append('clipPath').attr('id', clipId); + clipPath.exit().remove(); + + if(cd0.trace.connectgaps === false) { + var clipPathInfo = { + // fraction of the way from missing to present point + // to draw the boundary. + // if you make this 1 (or 1-epsilon) then a point in + // a sea of missing data will disappear entirely. + level: 0.9, + crossings: {}, + starts: [], + edgepaths: [], + paths: [], + xaxis: plotinfo.xaxis, + yaxis: plotinfo.yaxis, + x: cd0.x, + y: cd0.y, + // 0 = no data, 1 = data + z: makeClipMask(cd0), + smoothing: 0 + }; + + makeCrossings([clipPathInfo]); + findAllPaths([clipPathInfo]); + var fullpath = joinAllPaths(clipPathInfo, perimeter); + + var path = clipPath.selectAll('path') + .data([0]); + path.enter().append('path'); + path.attr('d', fullpath); + } + else clipId = null; + + plotGroup.call(Drawing.setClipUrl, clipId); + plotinfo.plot.selectAll('.hm' + cd0.trace.uid) + .call(Drawing.setClipUrl, clipId); +}*/ + +/*function makeClipMask(cd0) { + var empties = cd0.trace._emptypoints, + z = [], + m = cd0.z.length, + n = cd0.z[0].length, + i, + row = [], + emptyPoint; + + for(i = 0; i < n; i++) row.push(1); + for(i = 0; i < m; i++) z.push(row.slice()); + for(i = 0; i < empties.length; i++) { + emptyPoint = empties[i]; + z[emptyPoint[0]][emptyPoint[1]] = 0; + } + // save this mask to determine whether to show this data in hover + cd0.zmask = z; + return z; +}*/ diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 33d13eb8dd5..435fc9a9eb5 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -15,22 +15,24 @@ var Registry = require('../../registry'); var hasColumns = require('./has_columns'); -module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout) { +module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, xName, yName) { var z = coerce('z'); + xName = xName || 'x'; + yName = yName || 'y'; var x, y; if(z === undefined || !z.length) return 0; if(hasColumns(traceIn)) { - x = coerce('x'); - y = coerce('y'); + x = coerce(xName); + y = coerce(yName); - // column z must be accompanied by 'x' and 'y' arrays + // column z must be accompanied by xName and yName arrays if(!x || !y) return 0; } else { - x = coordDefaults('x', coerce); - y = coordDefaults('y', coerce); + x = coordDefaults(xName, coerce); + y = coordDefaults(yName, coerce); // TODO put z validation elsewhere if(!isValidZ(z)) return 0; @@ -39,7 +41,7 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout) { } var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); - handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); + handleCalendarDefaults(traceIn, traceOut, [xName, yName], layout); return traceOut.z.length; }; From 36e2100c2c368c03d392d6d9277fc4a60ca601dc Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 27 Jan 2017 11:18:54 -0500 Subject: [PATCH 030/132] Make a big input transpose fix --- src/traces/carpet/calc_gridlines.js | 18 +-- src/traces/carpet/cheater_basis.js | 12 +- .../carpet/create_i_derivative_evaluator.js | 24 ++-- .../carpet/create_j_derivative_evaluator.js | 28 ++-- src/traces/carpet/create_spline_evaluator.js | 28 ++-- src/traces/carpet/set_convert.js | 4 +- src/traces/carpet/smooth_fill_2d_array.js | 131 ++++++++++++------ src/traces/contour/find_all_paths.js | 2 - src/traces/contourcarpet/calc.js | 2 +- src/traces/contourcarpet/plot.js | 22 +-- 10 files changed, 158 insertions(+), 113 deletions(-) diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index c92c0d5dec6..48b91e95451 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -35,8 +35,8 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { var xcp = trace._xctrl; var ycp = trace._yctrl; - var nea = xcp.length; - var neb = xcp[0].length; + var nea = xcp[0].length; + var neb = xcp.length; var na = trace.a.length; var nb = trace.b.length; @@ -152,7 +152,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { } function constructArrayGridline(idx) { - var i0, j0, ti, tj; + var j, i0, j0, ti, tj; var xpoints = []; var ypoints = []; var ret = {}; @@ -173,9 +173,9 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { // In the tickmode: array case, this operation is a simple // transfer of data: - for(i = 0; i < nea; i++) { - xpoints[i] = xcp[i][idx * stride]; - ypoints[i] = ycp[i][idx * stride]; + for(j = 0; j < nea; j++) { + xpoints[j] = xcp[idx * stride][j]; + ypoints[j] = ycp[idx * stride][j]; } } else { i0 = Math.max(0, Math.min(na - 2, idx)); @@ -191,9 +191,9 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { // In the tickmode: array case, this operation is a simple // transfer of data: - for(i = 0; i < neb; i++) { - xpoints[i] = xcp[idx * stride][i]; - ypoints[i] = ycp[idx * stride][i]; + for(j = 0; j < neb; j++) { + xpoints[j] = xcp[j][idx * stride]; + ypoints[j] = ycp[j][idx * stride]; } } diff --git a/src/traces/carpet/cheater_basis.js b/src/traces/carpet/cheater_basis.js index 00812beaeb8..9f663a2fec4 100644 --- a/src/traces/carpet/cheater_basis.js +++ b/src/traces/carpet/cheater_basis.js @@ -35,12 +35,12 @@ module.exports = function(a, b, cheaterslope) { bscal = (bdata.length - 1) / (bdata[bdata.length - 1] - bdata[0]) / (nb - 1); } - for(i = 0; i < na; i++) { - data[i] = []; - aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); - for(j = 0; j < nb; j++) { - bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); - data[i][j] = aval - bval * cheaterslope; + for(j = 0; j < nb; j++) { + data[j] = []; + bval = bdata ? (bdata[j] - bdata[0]) * bscal : j / (nb - 1); + for(i = 0; i < na; i++) { + aval = adata ? (adata[i] - adata[0]) * ascal : i / (na - 1); + data[j][i] = aval - bval * cheaterslope; } } diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js index f4d9ecba107..5dc4b40ef88 100644 --- a/src/traces/carpet/create_i_derivative_evaluator.js +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -62,10 +62,10 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); - f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); - f2 = 3 * ((u2 - 1) * ak[i0][j0 + 2] + ou2 * ak[i0 + 1][j0 + 2] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 2] + u2 * ak[i0 + 3][j0 + 2]); - f3 = 3 * ((u2 - 1) * ak[i0][j0 + 3] + ou2 * ak[i0 + 1][j0 + 3] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 3] + u2 * ak[i0 + 3][j0 + 3]); + f0 = 3 * ((u2 - 1) * ak[j0 ][i0] + ou2 * ak[j0 ][i0 + 1] + u * (2 - 3 * u) * ak[j0 ][i0 + 2] + u2 * ak[j0 ][i0 + 3]); + f1 = 3 * ((u2 - 1) * ak[j0 + 1][i0] + ou2 * ak[j0 + 1][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 1][i0 + 2] + u2 * ak[j0 + 1][i0 + 3]); + f2 = 3 * ((u2 - 1) * ak[j0 + 2][i0] + ou2 * ak[j0 + 2][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 2][i0 + 2] + u2 * ak[j0 + 2][i0 + 3]); + f3 = 3 * ((u2 - 1) * ak[j0 + 3][i0] + ou2 * ak[j0 + 3][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 3][i0 + 2] + u2 * ak[j0 + 3][i0 + 3]); // Now just interpolate in the v-direction since it's all separable: out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; @@ -86,8 +86,8 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((u2 - 1) * ak[i0][j0 ] + ou2 * ak[i0 + 1][j0 ] + u * (2 - 3 * u) * ak[i0 + 2][j0 ] + u2 * ak[i0 + 3][j0 ]); - f1 = 3 * ((u2 - 1) * ak[i0][j0 + 1] + ou2 * ak[i0 + 1][j0 + 1] + u * (2 - 3 * u) * ak[i0 + 2][j0 + 1] + u2 * ak[i0 + 3][j0 + 1]); + f0 = 3 * ((u2 - 1) * ak[j0 ][i0] + ou2 * ak[j0 ][i0 + 1] + u * (2 - 3 * u) * ak[j0 ][i0 + 2] + u2 * ak[j0 ][i0 + 3]); + f1 = 3 * ((u2 - 1) * ak[j0 + 1][i0] + ou2 * ak[j0 + 1][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 1][i0 + 2] + u2 * ak[j0 + 1][i0 + 3]); out[k] = ov * f0 + v * f1; } @@ -110,10 +110,10 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ov3 = ov2 * ov; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0 + 1][j0] - ak[i0][j0]; - f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; - f2 = ak[i0 + 1][j0 + 2] - ak[i0][j0 + 2]; - f3 = ak[i0 + 1][j0 + 3] - ak[i0][j0 + 3]; + f0 = ak[j0][i0 + 1] - ak[j0][i0]; + f1 = ak[j0 + 1][i0 + 1] - ak[j0 + 1][i0]; + f2 = ak[j0 + 2][i0 + 1] - ak[j0 + 2][i0]; + f3 = ak[j0 + 3][i0 + 1] - ak[j0 + 3][i0]; out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; } @@ -129,8 +129,8 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0 + 1][j0] - ak[i0][j0]; - f1 = ak[i0 + 1][j0 + 1] - ak[i0][j0 + 1]; + f0 = ak[j0][i0 + 1] - ak[j0][i0]; + f1 = ak[j0 + 1][i0 + 1] - ak[j0 + 1][i0]; out[k] = ov * f0 + v * f1; } diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index e3e72b51819..3aeb6a3fb0a 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -32,10 +32,10 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); - f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); - f2 = 3 * ((v2 - 1) * ak[i0 + 2][j0] + ov2 * ak[i0 + 2][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 2][j0 + 2] + v2 * ak[i0 + 2][j0 + 3]); - f3 = 3 * ((v2 - 1) * ak[i0 + 3][j0] + ov2 * ak[i0 + 3][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 3][j0 + 2] + v2 * ak[i0 + 3][j0 + 3]); + f0 = 3 * ((v2 - 1) * ak[j0][i0 ] + ov2 * ak[j0 + 1][i0 ] + v * (2 - 3 * v) * ak[j0 + 2][i0 ] + v2 * ak[j0 + 3][i0 ]); + f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); + f2 = 3 * ((v2 - 1) * ak[j0][i0 + 2] + ov2 * ak[j0 + 1][i0 + 2] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 2] + v2 * ak[j0 + 3][i0 + 2]); + f3 = 3 * ((v2 - 1) * ak[j0][i0 + 3] + ov2 * ak[j0 + 1][i0 + 3] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 3] + v2 * ak[j0 + 3][i0 + 3]); // Now just interpolate in the v-direction since it's all separable: out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; @@ -58,16 +58,16 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0 ][j0 + 1] - ak[i0 ][j0]; - f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; - f2 = ak[i0 + 2][j0 + 1] - ak[i0 + 2][j0]; - f3 = ak[i0 + 3][j0 + 1] - ak[i0 + 3][j0]; + f0 = ak[j0 + 1][i0 ] - ak[j0][i0 ]; + f1 = ak[j0 + 1][i0 + 1] - ak[j0][i0 + 1]; + f2 = ak[j0 + 1][i0 + 2] - ak[j0][i0 + 2]; + f3 = ak[j0 + 1][i0 + 3] - ak[j0][i0 + 3]; out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; // mathematically equivalent: - // f0 = ou3 * ak[i0][j0 ] + 3 * (ou2 * u * ak[i0 + 1][j0 ] + ou * u2 * ak[i0 + 2][j0 ]) + u3 * ak[i0 + 3][j0 ]; - // f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; + // f0 = ou3 * ak[j0 ][i0] + 3 * (ou2 * u * ak[j0 ][i0 + 1] + ou * u2 * ak[j0 ][i0 + 2]) + u3 * ak[j0 ][i0 + 3]; + // f1 = ou3 * ak[j0 + 1][i0] + 3 * (ou2 * u * ak[j0 + 1][i0 + 1] + ou * u2 * ak[j0 + 1][i0 + 2]) + u3 * ak[j0 + 1][i0 + 3]; // out[k] = f1 - f0; } return out; @@ -86,8 +86,8 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((v2 - 1) * ak[i0 ][j0] + ov2 * ak[i0 ][j0 + 1] + v * (2 - 3 * v) * ak[i0 ][j0 + 2] + v2 * ak[i0 ][j0 + 3]); - f1 = 3 * ((v2 - 1) * ak[i0 + 1][j0] + ov2 * ak[i0 + 1][j0 + 1] + v * (2 - 3 * v) * ak[i0 + 1][j0 + 2] + v2 * ak[i0 + 1][j0 + 3]); + f0 = 3 * ((v2 - 1) * ak[j0][i0 ] + ov2 * ak[j0 + 1][i0 ] + v * (2 - 3 * v) * ak[j0 + 2][i0 ] + v2 * ak[j0 + 3][i0 ]); + f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); out[k] = ou * f0 + u * f1; } @@ -103,8 +103,8 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[i0][j0 + 1] - ak[i0][j0]; - f1 = ak[i0 + 1][j0 + 1] - ak[i0 + 1][j0]; + f0 = ak[j0 + 1][i0] - ak[j0][i0]; + f1 = ak[j0 + 1][i0 + 1] - ak[j0][i0 + 1]; out[k] = ov * f0 + v * f1; } diff --git a/src/traces/carpet/create_spline_evaluator.js b/src/traces/carpet/create_spline_evaluator.js index b709150cacd..b5870ccbdd0 100644 --- a/src/traces/carpet/create_spline_evaluator.js +++ b/src/traces/carpet/create_spline_evaluator.js @@ -54,10 +54,10 @@ module.exports = function(arrays, na, nb, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou3 * ak[i0][j0] + 3 * (ou2 * u * ak[i0 + 1][j0] + ou * u2 * ak[i0 + 2][j0]) + u3 * ak[i0 + 3][j0]; - f1 = ou3 * ak[i0][j0 + 1] + 3 * (ou2 * u * ak[i0 + 1][j0 + 1] + ou * u2 * ak[i0 + 2][j0 + 1]) + u3 * ak[i0 + 3][j0 + 1]; - f2 = ou3 * ak[i0][j0 + 2] + 3 * (ou2 * u * ak[i0 + 1][j0 + 2] + ou * u2 * ak[i0 + 2][j0 + 2]) + u3 * ak[i0 + 3][j0 + 2]; - f3 = ou3 * ak[i0][j0 + 3] + 3 * (ou2 * u * ak[i0 + 1][j0 + 3] + ou * u2 * ak[i0 + 2][j0 + 3]) + u3 * ak[i0 + 3][j0 + 3]; + f0 = ou3 * ak[j0][i0] + 3 * (ou2 * u * ak[j0][i0 + 1] + ou * u2 * ak[j0][i0 + 2]) + u3 * ak[j0][i0 + 3]; + f1 = ou3 * ak[j0 + 1][i0] + 3 * (ou2 * u * ak[j0 + 1][i0 + 1] + ou * u2 * ak[j0 + 1][i0 + 2]) + u3 * ak[j0 + 1][i0 + 3]; + f2 = ou3 * ak[j0 + 2][i0] + 3 * (ou2 * u * ak[j0 + 2][i0 + 1] + ou * u2 * ak[j0 + 2][i0 + 2]) + u3 * ak[j0 + 2][i0 + 3]; + f3 = ou3 * ak[j0 + 3][i0] + 3 * (ou2 * u * ak[j0 + 3][i0 + 1] + ou * u2 * ak[j0 + 3][i0 + 2]) + u3 * ak[j0 + 3][i0 + 3]; out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; } @@ -84,10 +84,10 @@ module.exports = function(arrays, na, nb, asmoothing, bsmoothing) { var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ov * ak[i0][j0] + v * ak[i0][j0 + 1]; - f1 = ov * ak[i0 + 1][j0] + v * ak[i0 + 1][j0 + 1]; - f2 = ov * ak[i0 + 2][j0] + v * ak[i0 + 2][j0 + 1]; - f3 = ov * ak[i0 + 3][j0] + v * ak[i0 + 3][j0 + 1]; + f0 = ov * ak[j0][i0] + v * ak[j0 + 1][i0]; + f1 = ov * ak[j0][i0 + 1] + v * ak[j0 + 1][i0 + 1]; + f2 = ov * ak[j0][i0 + 2] + v * ak[j0 + 1][i0 + 1]; + f3 = ov * ak[j0][i0 + 3] + v * ak[j0 + 1][i0 + 1]; out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; } @@ -113,10 +113,10 @@ module.exports = function(arrays, na, nb, asmoothing, bsmoothing) { var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0] + u * ak[i0 + 1][j0]; - f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; - f2 = ou * ak[i0][j0 + 2] + u * ak[i0 + 1][j0 + 2]; - f3 = ou * ak[i0][j0 + 3] + u * ak[i0 + 1][j0 + 3]; + f0 = ou * ak[j0][i0] + u * ak[j0][i0 + 1]; + f1 = ou * ak[j0 + 1][i0] + u * ak[j0 + 1][i0 + 1]; + f2 = ou * ak[j0 + 2][i0] + u * ak[j0 + 2][i0 + 1]; + f3 = ou * ak[j0 + 3][i0] + u * ak[j0 + 3][i0 + 1]; out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; } @@ -137,8 +137,8 @@ module.exports = function(arrays, na, nb, asmoothing, bsmoothing) { var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ou * ak[i0][j0] + u * ak[i0 + 1][j0]; - f1 = ou * ak[i0][j0 + 1] + u * ak[i0 + 1][j0 + 1]; + f0 = ou * ak[j0][i0] + u * ak[j0][i0 + 1]; + f1 = ou * ak[j0 + 1][i0] + u * ak[j0 + 1][i0 + 1]; out[k] = ov * f0 + v * f1; } diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 22bd458a77c..3b0482a733b 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -35,8 +35,8 @@ module.exports = function setConvert(trace) { var y = trace.y; // XXX: ONLY PASSTHRU. ONLY. No, ONLY. - aax.c2p = function (v) { return v; }; - bax.c2p = function (v) { return v; }; + aax.c2p = function(v) { return v; }; + bax.c2p = function(v) { return v; }; // This is potentially a very expensive step! It does the bulk of the work of constructing // an expanded basis of control points. Note in particular that it overwrites the existing diff --git a/src/traces/carpet/smooth_fill_2d_array.js b/src/traces/carpet/smooth_fill_2d_array.js index 2ec717a8f26..7550690f998 100644 --- a/src/traces/carpet/smooth_fill_2d_array.js +++ b/src/traces/carpet/smooth_fill_2d_array.js @@ -14,51 +14,61 @@ module.exports = function smoothFill2dArray(data, a, b) { var i, j, k; var ip = []; var jp = []; + // var neighborCnts = []; - var ni = data.length; - var nj = data[0].length; + var ni = data[0].length; + var nj = data.length; function avgSurrounding(i, j) { + // As a low-quality start, we can simply average surrounding points (in a not + // non-uniform grid aware manner): var sum = 0.0; var val; var cnt = 0; - if(i > 0 && (val = data[i - 1][j]) !== undefined) { + if(i > 0 && (val = data[j][i - 1]) !== undefined) { cnt++; sum += val; } - if(i < ni - 1 && (val = data[i + 1][j]) !== undefined) { + if(i < ni - 1 && (val = data[j][i + 1]) !== undefined) { cnt++; sum += val; } - if(j > 0 && (val = data[i][j - 1]) !== undefined) { + if(j > 0 && (val = data[j - 1][i]) !== undefined) { cnt++; sum += val; } - if(j < nj - 1 && (val = data[i][j + 1]) !== undefined) { + if(j < nj - 1 && (val = data[j + 1][i]) !== undefined) { cnt++; sum += val; } return sum / Math.max(1, cnt); + } - // Track the maximum magnitude so that we can track error relative - // to the maximum: + // This loop iterates over all cells. Any cells that are null will be noted and those + // are the only points we will loop over and update via laplace's equation. Points with + // any neighbors will receive the average. If there are no neighboring points, then they + // will be set to zero. Also as we go, track the maximum magnitude so that we can scale + // our tolerance accordingly. var dmax = 0.0; for(i = 0; i < ni; i++) { for(j = 0; j < nj; j++) { - if(data[i][j] === undefined) { + if(data[j][i] === undefined) { ip.push(i); jp.push(j); - data[i][j] = avgSurrounding(i, j); + + data[j][i] = avgSurrounding(i, j); + // neighborCnts.push(result.neighbors); } - dmax = Math.max(dmax, Math.abs(data[i][j])); + dmax = Math.max(dmax, Math.abs(data[j][i])); } } + if(!ip.length) return data; // The tolerance doesn't need to be excessive. It's just for display positioning - var dxp, dxm, dap, dam, dbp, dbm, c, d, diff, reldiff; + var dxp, dxm, dap, dam, dbp, dbm, c, d, diff, reldiff, overrelaxation; var tol = 1e-5; var resid = 0; var itermax = 100; @@ -66,92 +76,129 @@ module.exports = function smoothFill2dArray(data, a, b) { var n = ip.length; do { resid = 0; + // Normally we'd loop in two dimensions, but not all points are blank and need + // an update, so we instead loop only over the points that were tabulated above for(k = 0; k < n; k++) { i = ip[k]; j = jp[k]; + // neighborCnt = neighborCnts[k]; - // Note the second-derivative = 0 condition at the edges. - var contribCnt = 0; + // Track a counter for how many contributions there are. We'll use this counter + // to average at the end, which reduces to laplace's equation with neumann boundary + // conditions on the first derivative (second derivative is zero so that we get + // a nice linear extrapolation at the boundaries). + var boundaryCnt = 0; var newVal = 0; var d0, d1, x0, x1, i0, j0; if(i === 0) { + // If this lies along the i = 0 boundary, extrapolate from the two points + // to the right of this point. Note that the finite differences take into + // account non-uniform grid spacing: i0 = Math.min(ni - 1, 2); x0 = a[i0]; x1 = a[1]; - d0 = data[i0][j]; - d1 = data[1][j]; + d0 = data[j][i0]; + d1 = data[j][1]; newVal += d1 + (d1 - d0) * (a[0] - x1) / (x1 - x0); - contribCnt++; + boundaryCnt++; } else if(i === ni - 1) { + // If along the high i boundary, extrapolate from the two points to the + // left of this point i0 = Math.max(0, ni - 3); x0 = a[i0]; x1 = a[ni - 2]; - d0 = data[i0][j]; - d1 = data[ni - 2][j]; + d0 = data[j][i0]; + d1 = data[j][ni - 2]; newVal += d1 + (d1 - d0) * (a[ni - 1] - x1) / (x1 - x0); - contribCnt++; + boundaryCnt++; } if((i === 0 || i === ni - 1) && (j > 0 && j < nj - 1)) { + // If along the min(i) or max(i) boundaries, also smooth vertically as long + // as we're not in a corner. Note that the finite differences used here + // are also aware of nonuniform grid spacing: dxp = b[j + 1] - b[j]; dxm = b[j] - b[j - 1]; - newVal += (dxm * data[i][j + 1] + dxp * data[i][j - 1]) / (dxm + dxp); - contribCnt++; + newVal += (dxm * data[j + 1][i] + dxp * data[j - 1][i]) / (dxm + dxp); + boundaryCnt++; } if(j === 0) { + // If along the j = 0 boundary, extrpolate this point from the two points + // above it j0 = Math.min(nj - 1, 2); x0 = b[j0]; x1 = b[1]; - d0 = data[i][j0]; - d1 = data[i][1]; + d0 = data[j0][i]; + d1 = data[1][i]; newVal += d1 + (d1 - d0) * (b[0] - x1) / (x1 - x0); - contribCnt++; + boundaryCnt++; } else if(j === nj - 1) { + // Same for the max j boundary from the cells below it: j0 = Math.max(0, nj - 3); x0 = b[j0]; x1 = b[nj - 2]; - d0 = data[i][j0]; - d1 = data[i][nj - 2]; - newVal += d1 + (d1 - d0) * (b[ni - 1] - x1) / (x1 - x0); - contribCnt++; + d0 = data[j0][i]; + d1 = data[nj - 2][i]; + newVal += d1 + (d1 - d0) * (b[nj - 1] - x1) / (x1 - x0); + boundaryCnt++; } if((j === 0 || j === nj - 1) && (i > 0 && i < ni - 1)) { + // Now average points to the left/right as long as not in a corner: dxp = a[i + 1] - a[i]; dxm = a[i] - a[i - 1]; - newVal += (dxm * data[i + 1][j] + dxp * data[i - 1][j]) / (dxm + dxp); - contribCnt++; + newVal += (dxm * data[j][i + 1] + dxp * data[j][i - 1]) / (dxm + dxp); + boundaryCnt++; } - if(!contribCnt) { - // interior point, so simply average: - // Get the grid spacing on either side: + if(!boundaryCnt) { + // If none of the above conditions were triggered, then this is an interior + // point and we can just do a laplace equation update. As above, these differences + // are aware of nonuniform grid spacing: dap = a[i + 1] - a[i]; dam = a[i] - a[i - 1]; dbp = b[j + 1] - b[j]; dbm = b[j] - b[j - 1]; - // Some useful constants: + + // These are just some useful constants for the iteration, which is perfectly + // straightforward but a little long to derive from f_xx + f_yy = 0. c = dap * dam * (dap + dam); d = dbp * dbm * (dbp + dbm); - newVal = (c * (dbm * data[i][j + 1] + dbp * data[i][j - 1]) + d * (dam * data[i + 1][j] + dap * data[i - 1][j])) / - (d * (dam + dap) + c * (dbm + dbp)); - + newVal = (c * (dbm * data[j + 1][i] + dbp * data[j - 1][i]) + + d * (dam * data[j][i + 1] + dap * data[j][i - 1])) / + (d * (dam + dap) + c * (dbm + dbp)); } else { - newVal /= contribCnt; + // If we did have contributions from the boundary conditions, then average + // the result from the various contributions: + newVal /= boundaryCnt; } - diff = newVal - data[i][j]; + // Jacobi updates are ridiculously slow to converge, so this approach uses a + // Gauss-seidel iteration which is dramatically faster. + diff = newVal - data[j][i]; reldiff = diff / dmax; resid += reldiff * reldiff; // Gauss-Seidel-ish iteration, omega chosen based on heuristics and some // quick tests. // - // NB: Don't overrelax the boundaries - data[i][j] += diff * (1 + (contribCnt ? 0 : 0.8)); + // NB: Don't overrelax the boundarie. Otherwise set an overrelaxation factor + // which is a little low but safely optimal-ish: + overrelaxation = boundaryCnt ? 0 : 0.85; + + // If there are four non-null neighbors, then we want a simple average without + // overrelaxation. If all the surrouding points are null, then we want the full + // overrelaxation + // + // Based on experiments, this actually seems to slow down convergence just a bit. + // I'll leave it here for reference in case this needs to be revisited, but + // it seems to work just fine without this. + // if (overrelaxation) overrelaxation *= (4 - neighborCnt) / 4; + + data[j][i] += diff * (1 + overrelaxation); } resid = Math.sqrt(resid); diff --git a/src/traces/contour/find_all_paths.js b/src/traces/contour/find_all_paths.js index c9f163c28f9..ab4916b9114 100644 --- a/src/traces/contour/find_all_paths.js +++ b/src/traces/contour/find_all_paths.js @@ -18,8 +18,6 @@ module.exports = function findAllPaths(pathinfo) { pi, j; - console.log('pathinfo:', pathinfo); - for(i = 0; i < pathinfo.length; i++) { pi = pathinfo[i]; diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index d79eac20e0a..e93d36bd6b6 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -113,7 +113,7 @@ function autoContours(start, end, ncontours) { return dummyAx; } -function heatmappishCalc (gd, trace) { +function heatmappishCalc(gd, trace) { // prepare the raw data // run makeCalcdata on x and y even for heatmaps, in case of category mappings var carpet = trace._carpet; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 2a925f5da4d..52a5d2d05c6 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -48,11 +48,11 @@ function plotOne(gd, plotinfo, cd) { makeCrossings(pathinfo); findAllPaths(pathinfo); - for (i = 0; i < pathinfo.length; i++) { + for(i = 0; i < pathinfo.length; i++) { var pi = pathinfo[i]; - for (j = 0; j < pi.edgepaths.length; j++) { + for(j = 0; j < pi.edgepaths.length; j++) { var foo = pi.edgepaths[j]; - for (k = 0; k < foo.length; k++) { + for(k = 0; k < foo.length; k++) { var ep = foo[k]; var pt = carpet.ab2xy(ep[0], ep[1], true); ep[0] = xa.c2p(pt[0]); @@ -63,7 +63,7 @@ function plotOne(gd, plotinfo, cd) { console.log('pathinfo:', pathinfo); - /*var leftedge = xa.c2p(a[0], true), + /* var leftedge = xa.c2p(a[0], true), rightedge = xa.c2p(a[a.length - 1], true), bottomedge = ya.c2p(b[0], true), topedge = ya.c2p(b[b.length - 1], true), @@ -76,10 +76,10 @@ function plotOne(gd, plotinfo, cd) { // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - //makeBackground(plotGroup, perimeter, contours); - //makeFills(plotGroup, pathinfo, perimeter, contours); + // makeBackground(plotGroup, perimeter, contours); + // makeFills(plotGroup, pathinfo, perimeter, contours); makeLines(plotGroup, pathinfo, contours); - //clipGaps(plotGroup, plotinfo, cd[0], perimeter); + // clipGaps(plotGroup, plotinfo, cd[0], perimeter); } function emptyPathinfo(contours, plotinfo, cd0) { @@ -166,7 +166,7 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -/*function makeBackground(plotgroup, perimeter, contours) { +/* function makeBackground(plotgroup, perimeter, contours) { var bggroup = plotgroup.selectAll('g.contourbg').data([0]); bggroup.enter().append('g').classed('contourbg', true); @@ -202,7 +202,7 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { }); }*/ -/*function joinAllPaths(pi, perimeter) { +/* function joinAllPaths(pi, perimeter) { var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? '' : ('M' + perimeter.join('L') + 'Z'), i = 0, @@ -292,7 +292,7 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { return fullpath; }*/ -/*function clipGaps(plotGroup, plotinfo, cd0, perimeter) { +/* function clipGaps(plotGroup, plotinfo, cd0, perimeter) { var clipId = 'clip' + cd0.trace.uid; var defs = plotinfo.plot.selectAll('defs') @@ -340,7 +340,7 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { .call(Drawing.setClipUrl, clipId); }*/ -/*function makeClipMask(cd0) { +/* function makeClipMask(cd0) { var empties = cd0.trace._emptypoints, z = [], m = cd0.z.length, From 3c1ca75c228c635d88c596075440cb27345f83e2 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 27 Jan 2017 13:26:55 -0500 Subject: [PATCH 031/132] Style contourcarpet plots --- src/plots/cartesian/layout_defaults.js | 10 ++++- src/traces/contour/plot.js | 6 ++- src/traces/contourcarpet/attributes.js | 6 +-- src/traces/contourcarpet/calc.js | 18 ++------- src/traces/contourcarpet/index.js | 4 +- src/traces/contourcarpet/plot.js | 42 ++++++++++--------- src/traces/contourcarpet/style.js | 56 ++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 44 deletions(-) create mode 100644 src/traces/contourcarpet/style.js diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index aec495433bf..7db34fac080 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -53,7 +53,15 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // Note that we track the *opposite* of whether it's a cheater plot // because that makes it straightforward to check that any trace on // this axis that's *not* a cheater will make it visible - if((!Registry.traceIs(trace, 'carpet') || !trace._cheater) && xaListNotCheater.indexOf(xaName) === -1) { + // + // There are three conditions that cause an axis to get marked as non + // cheater (which is to say, visible by default): + // 1. It's not in the carpet category at all + // 2. Or if it is, then it's not a non-cheater carpet axis + // 3. And the axis isn't already marked non-cheater + if((!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) && + xaListNotCheater.indexOf(xaName) === -1 + ) { xaListNotCheater.push(xaName); } diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index 2c0830cefd1..a4c61e17a11 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -270,7 +270,8 @@ function makeLines(plotgroup, pathinfo, contours) { .attr('d', function(d) { return Drawing.smoothopen(d, smoothing); }) - .style('stroke-miterlimit', 1); + .style('stroke-miterlimit', 1) + .style('vector-effect', 'non-scaling-stroke'); var closedcontourlines = linegroup.selectAll('path.closedline') .data(function(d) { return d.paths; }); @@ -281,7 +282,8 @@ function makeLines(plotgroup, pathinfo, contours) { .attr('d', function(d) { return Drawing.smoothclosed(d, smoothing); }) - .style('stroke-miterlimit', 1); + .style('stroke-miterlimit', 1) + .style('vector-effect', 'non-scaling-stroke'); } function clipGaps(plotGroup, plotinfo, cd0, perimeter) { diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 3b870d58050..4aa88fd2ca5 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -10,16 +10,12 @@ var heatmapAttrs = require('../heatmap/attributes'); var scatterAttrs = require('../scatter/attributes'); -var plotAttrs = require('../../plots/attributes'); var colorscaleAttrs = require('../../components/colorscale/attributes'); -var colorAttributes = require('../../components/colorscale/color_attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); var extendFlat = require('../../lib/extend').extendFlat; -var scatterMarkerAttrs = scatterAttrs.marker, - scatterLineAttrs = scatterAttrs.line, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; +var scatterLineAttrs = scatterAttrs.line; module.exports = extendFlat({}, { carpetid: { diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index e93d36bd6b6..3742344a490 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -9,6 +9,7 @@ 'use strict'; +var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var extendFlat = require('../../lib').extendFlat; var Registry = require('../../registry'); @@ -120,7 +121,6 @@ function heatmappishCalc(gd, trace) { var aax = carpet.aaxis, bax = carpet.baxis, isContour = Registry.traceIs(trace, 'contour'), - isGL2D = Registry.traceIs(trace, 'gl2d'), zsmooth = isContour ? 'best' : trace.zsmooth, a, a0, @@ -146,10 +146,8 @@ function heatmappishCalc(gd, trace) { z = clean2dArray(trace.z, trace.transpose); - if(isContour || trace.connectgaps) { - trace._emptypoints = findEmpties(z); - trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); - } + trace._emptypoints = findEmpties(z); + trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); function noZsmooth(msg) { zsmooth = trace._input.zsmooth = trace.zsmooth = false; @@ -197,15 +195,5 @@ function heatmappishCalc(gd, trace) { // auto-z and autocolorscale if applicable colorscaleCalc(trace, z, '', 'z'); - if(isContour && trace.contours && trace.contours.coloring === 'heatmap') { - var dummyTrace = { - type: trace.type === 'contour' ? 'heatmap' : 'histogram2d', - xcalendar: trace.xcalendar, - ycalendar: trace.ycalendar - }; - cd0.xfill = makeBoundArray(dummyTrace, xIn, a0, da, xlen, aax); - cd0.yfill = makeBoundArray(dummyTrace, yIn, b0, db, z.length, bax); - } - return [cd0]; } diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index 6b5523c5fe6..2dae8e19c80 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -15,14 +15,14 @@ ContourCarpet.supplyDefaults = require('./defaults'); // ContourCarpet.colorbar = require('../scatter/colorbar'); ContourCarpet.calc = require('./calc'); ContourCarpet.plot = require('./plot'); -// ContourCarpet.style = require('./style'); +ContourCarpet.style = require('./style'); // ContourCarpet.hoverPoints = require('./hover'); // ContourCarpet.selectPoints = require('./select'); ContourCarpet.moduleType = 'trace'; ContourCarpet.name = 'contourcarpet'; ContourCarpet.basePlotModule = require('../../plots/cartesian'); -ContourCarpet.categories = ['cartesian', 'carpet', 'symbols', 'markerColorscale', 'showLegend']; +ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'markerColorscale', 'showLegend']; ContourCarpet.meta = { hrName: 'contour_carpet', description: [].join(' ') diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 52a5d2d05c6..a4b2737069f 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -8,8 +8,7 @@ 'use strict'; -var scatterPlot = require('../scatter/plot'); -var Axes = require('../../plots/cartesian/axes'); +var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); var makeCrossings = require('../contour/make_crossings'); @@ -22,13 +21,13 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { - var i, j, k; + var i, j, k, path, pt, pts; var trace = cd[0].trace; var carpet = trace._carpet; - var a = cd[0].a; - var b = cd[0].b; - var aa = carpet.aaxis; - var ba = carpet.baxis; + // var a = cd[0].a; + // var b = cd[0].b; + // var aa = carpet.aaxis; + // var ba = carpet.baxis; var contours = trace.contours; var uid = trace.uid; var xa = plotinfo.xaxis; @@ -51,18 +50,25 @@ function plotOne(gd, plotinfo, cd) { for(i = 0; i < pathinfo.length; i++) { var pi = pathinfo[i]; for(j = 0; j < pi.edgepaths.length; j++) { - var foo = pi.edgepaths[j]; - for(k = 0; k < foo.length; k++) { - var ep = foo[k]; - var pt = carpet.ab2xy(ep[0], ep[1], true); - ep[0] = xa.c2p(pt[0]); - ep[1] = ya.c2p(pt[1]); + path = pi.edgepaths[j]; + for(k = 0; k < path.length; k++) { + pts = path[k]; + pt = carpet.ab2xy(pts[0], pts[1], true); + pts[0] = xa.c2p(pt[0]); + pts[1] = ya.c2p(pt[1]); + } + } + for(j = 0; j < pi.paths.length; j++) { + path = pi.paths[j]; + for(k = 0; k < path.length; k++) { + pts = path[k]; + pt = carpet.ab2xy(pts[0], pts[1], true); + pts[0] = xa.c2p(pt[0]); + pts[1] = ya.c2p(pt[1]); } } } - console.log('pathinfo:', pathinfo); - /* var leftedge = xa.c2p(a[0], true), rightedge = xa.c2p(a[a.length - 1], true), bottomedge = ya.c2p(b[0], true), @@ -149,10 +155,7 @@ function makeLines(plotgroup, pathinfo, contours) { .attr('d', function(d) { return Drawing.smoothopen(d, smoothing); }) - .style('stroke-miterlimit', 1) - .style('stroke-width', 1) - .style('stroke', 'black') - .style('fill', 'none'); + .style('vector-effect', 'non-scaling-stroke'); var closedcontourlines = linegroup.selectAll('path.closedline') .data(function(d) { return d.paths; }); @@ -163,6 +166,7 @@ function makeLines(plotgroup, pathinfo, contours) { .attr('d', function(d) { return Drawing.smoothclosed(d, smoothing); }) + .style('vector-effect', 'non-scaling-stroke') .style('stroke-miterlimit', 1); } diff --git a/src/traces/contourcarpet/style.js b/src/traces/contourcarpet/style.js new file mode 100644 index 00000000000..8a4b6a776b0 --- /dev/null +++ b/src/traces/contourcarpet/style.js @@ -0,0 +1,56 @@ +/** +* 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'; + +var d3 = require('d3'); + +var Drawing = require('../../components/drawing'); +var heatmapStyle = require('../heatmap/style'); + +// var makeColorMap = require('./make_color_map'); + + +module.exports = function style(gd) { + var contours = d3.select(gd).selectAll('g.contour'); + + contours.style('opacity', function(d) { + return d.trace.opacity; + }); + + contours.each(function(d) { + var c = d3.select(this), + trace = d.trace, + // contours = trace.contours, + line = trace.line; + // cs = contours.size || 1, + // start = contours.start; + + // var colorMap = makeColorMap(trace); + + c.selectAll('g.contourlevel').each(function() { + d3.select(this).selectAll('path') + .call(Drawing.lineGroupStyle, + line.width, + line.color, + // contours.coloring === 'lines' ? colorMap(start + i * cs) : line.color, + line.dash); + }); + + /* c.selectAll('g.contourbg path') + .style('fill', colorMap(start - cs / 2)); + + c.selectAll('g.contourfill path') + .style('fill', function(d, i) { + return colorMap(start + (i + 0.5) * cs); + });*/ + }); + + heatmapStyle(gd); +}; From 82e86f34b707a7a9b10aec7bf7a691eec9d2cd93 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 30 Jan 2017 10:03:18 -0500 Subject: [PATCH 032/132] Carpet contour adjustments --- src/traces/carpet/plot.js | 3 +- src/traces/contourcarpet/plot.js | 86 ++++++++++++++++++------------- src/traces/contourcarpet/style.js | 17 +++--- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index c2bd24d052f..0c7b349d697 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -88,8 +88,9 @@ function drawClipPath(trace, layer, xaxis, yaxis) { // at the corners, but it's so negligible that I don't think it's worth // the extra complexity trace.clipPathId = 'clip' + trace.uid + 'carpet'; + trace.clipPathData = 'M' + segs.join('L') + 'Z'; clip.attr('id', trace.clipPathId); - path.attr('d', 'M' + segs.join('L') + 'Z'); + path.attr('d', trace.clipPathData); // .style('stroke-width', 20) // .style('vector-effect', 'non-scaling-stroke') // .style('stroke', 'black') diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index a4b2737069f..8b15ebd13f6 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -8,6 +8,7 @@ 'use strict'; +var d3 = require('d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); @@ -21,11 +22,11 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { - var i, j, k, path, pt, pts; + var i, j, k, path, pt; var trace = cd[0].trace; var carpet = trace._carpet; - // var a = cd[0].a; - // var b = cd[0].b; + var a = cd[0].a; + var b = cd[0].b; // var aa = carpet.aaxis; // var ba = carpet.baxis; var contours = trace.contours; @@ -47,47 +48,58 @@ function plotOne(gd, plotinfo, cd) { makeCrossings(pathinfo); findAllPaths(pathinfo); + function ab2p(ab) { + var pt = carpet.ab2xy(ab[0], ab[1], true); + return [xa.c2p(pt[0]), ya.c2p(pt[1])]; + } + for(i = 0; i < pathinfo.length; i++) { var pi = pathinfo[i]; + var pedgepaths = pi.pedgepaths = []; + var ppaths = pi.ppaths = []; for(j = 0; j < pi.edgepaths.length; j++) { path = pi.edgepaths[j]; + var pedgepath = []; for(k = 0; k < path.length; k++) { - pts = path[k]; - pt = carpet.ab2xy(pts[0], pts[1], true); - pts[0] = xa.c2p(pt[0]); - pts[1] = ya.c2p(pt[1]); + pedgepath[k] = ab2p(path[k]); } + pedgepaths.push(pedgepath); } for(j = 0; j < pi.paths.length; j++) { path = pi.paths[j]; + var ppath = []; for(k = 0; k < path.length; k++) { - pts = path[k]; - pt = carpet.ab2xy(pts[0], pts[1], true); - pts[0] = xa.c2p(pt[0]); - pts[1] = ya.c2p(pt[1]); + ppath[k] = ab2p(path[k]); } + ppaths.push(ppath); } } - /* var leftedge = xa.c2p(a[0], true), - rightedge = xa.c2p(a[a.length - 1], true), - bottomedge = ya.c2p(b[0], true), - topedge = ya.c2p(b[b.length - 1], true), - perimeter = [ - [leftedge, topedge], - [rightedge, topedge], - [rightedge, bottomedge], - [leftedge, bottomedge] - ];*/ + // Mark the perimeter in a-b coordinates: + var leftedge = a[0]; + var rightedge = a[a.length - 1]; + var bottomedge = b[0]; + var topedge = b[b.length - 1]; + var perimeter = [ + [leftedge, topedge], + [rightedge, topedge], + [rightedge, bottomedge], + [leftedge, bottomedge] + ]; // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - // makeBackground(plotGroup, perimeter, contours); - // makeFills(plotGroup, pathinfo, perimeter, contours); + makeBackground(plotGroup, contours, carpet); + makeFills(plotGroup, pathinfo, perimeter, contours, ab2p); makeLines(plotGroup, pathinfo, contours); + // clipBoundary(plotGroup, carpet); // clipGaps(plotGroup, plotinfo, cd[0], perimeter); } +function clipBoundary(plotGroup, carpet) { + plotGroup.attr('clip-path', 'url(#' + carpet.clipPathId + ')'); +} + function emptyPathinfo(contours, plotinfo, cd0) { var cs = contours.size; var pathinfo = []; @@ -147,7 +159,7 @@ function makeLines(plotgroup, pathinfo, contours) { linegroup.exit().remove(); var opencontourlines = linegroup.selectAll('path.openline') - .data(function(d) { return d.edgepaths; }); + .data(function(d) { return d.pedgepaths; }); opencontourlines.enter().append('path') .classed('openline', true); opencontourlines.exit().remove(); @@ -158,7 +170,7 @@ function makeLines(plotgroup, pathinfo, contours) { .style('vector-effect', 'non-scaling-stroke'); var closedcontourlines = linegroup.selectAll('path.closedline') - .data(function(d) { return d.paths; }); + .data(function(d) { return d.ppaths; }); closedcontourlines.enter().append('path') .classed('closedline', true); closedcontourlines.exit().remove(); @@ -170,7 +182,7 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -/* function makeBackground(plotgroup, perimeter, contours) { +function makeBackground(plotgroup, contours, carpet) { var bggroup = plotgroup.selectAll('g.contourbg').data([0]); bggroup.enter().append('g').classed('contourbg', true); @@ -179,11 +191,11 @@ function makeLines(plotgroup, pathinfo, contours) { bgfill.enter().append('path'); bgfill.exit().remove(); bgfill - .attr('d', 'M' + perimeter.join('L') + 'Z') + .attr('d', carpet.clipPathData) .style('stroke', 'none'); } -function makeFills(plotgroup, pathinfo, perimeter, contours) { +function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p) { var fillgroup = plotgroup.selectAll('g.contourfill') .data([0]); fillgroup.enter().append('g') @@ -199,16 +211,17 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { // if the whole perimeter is above this level, start with a path // enclosing the whole thing. With all that, the parity should mean // that we always fill everything above the contour, nothing below - var fullpath = joinAllPaths(pi, perimeter); + var fullpath = joinAllPaths(pi, perimeter, ab2p); if(!fullpath) d3.select(this).remove(); else d3.select(this).attr('d', fullpath).style('stroke', 'none'); }); -}*/ +} -/* function joinAllPaths(pi, perimeter) { +function joinAllPaths(pi, perimeter, ab2p) { + console.log('pi:', pi); var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? - '' : ('M' + perimeter.join('L') + 'Z'), + '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'), i = 0, startsleft = pi.edgepaths.map(function(v, i) { return i; }), newloop = true, @@ -225,7 +238,7 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } while(startsleft.length) { - addpath = Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); + addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing); fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); startsleft.splice(startsleft.indexOf(i), 1); endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; @@ -269,7 +282,8 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { endpt = newendpt; if(nexti >= 0) break; - fullpath += 'L' + newendpt; + console.log('add new endpoint:', newendpt); + fullpath += 'L' + ab2p(newendpt); } if(nexti === pi.edgepaths.length) { @@ -290,11 +304,11 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { // finally add the interior paths for(i = 0; i < pi.paths.length; i++) { - fullpath += Drawing.smoothclosed(pi.paths[i], pi.smoothing); + fullpath += Drawing.smoothclosed(pi.paths[i].map(ab2p), pi.smoothing); } return fullpath; -}*/ +} /* function clipGaps(plotGroup, plotinfo, cd0, perimeter) { var clipId = 'clip' + cd0.trace.uid; diff --git a/src/traces/contourcarpet/style.js b/src/traces/contourcarpet/style.js index 8a4b6a776b0..f8e6125f06f 100644 --- a/src/traces/contourcarpet/style.js +++ b/src/traces/contourcarpet/style.js @@ -14,8 +14,7 @@ var d3 = require('d3'); var Drawing = require('../../components/drawing'); var heatmapStyle = require('../heatmap/style'); -// var makeColorMap = require('./make_color_map'); - +var makeColorMap = require('../contour/make_color_map'); module.exports = function style(gd) { var contours = d3.select(gd).selectAll('g.contour'); @@ -27,12 +26,12 @@ module.exports = function style(gd) { contours.each(function(d) { var c = d3.select(this), trace = d.trace, - // contours = trace.contours, - line = trace.line; - // cs = contours.size || 1, - // start = contours.start; + contours = trace.contours, + line = trace.line, + cs = contours.size || 1, + start = contours.start; - // var colorMap = makeColorMap(trace); + var colorMap = makeColorMap(trace); c.selectAll('g.contourlevel').each(function() { d3.select(this).selectAll('path') @@ -43,13 +42,13 @@ module.exports = function style(gd) { line.dash); }); - /* c.selectAll('g.contourbg path') + c.selectAll('g.contourbg path') .style('fill', colorMap(start - cs / 2)); c.selectAll('g.contourfill path') .style('fill', function(d, i) { return colorMap(start + (i + 0.5) * cs); - });*/ + }); }); heatmapStyle(gd); From 9ccdcfef5f746625f06dc6f446414bbe63833071 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 30 Jan 2017 12:04:05 -0500 Subject: [PATCH 033/132] Add missing `visible` attribute to gl3d --- src/plots/gl3d/layout/axis_attributes.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index fe1f672b7ba..18f9ca7d006 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -14,6 +14,14 @@ var extendFlat = require('../../../lib/extend').extendFlat; module.exports = { + visible: { + valType: 'boolean', + description: [ + 'A single toggle to hide the axis while preserving interaction like dragging.', + 'Default is true when a cheater plot is present on the axis, otherwise', + 'false' + ].join(' ') + }, showspikes: { valType: 'boolean', role: 'info', From 71efcf903aba9eebc725d88951a3fbc568da044d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 30 Jan 2017 17:12:22 -0500 Subject: [PATCH 034/132] Fix contourcarpet bg path rendering --- src/traces/contourcarpet/plot.js | 35 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 8b15ebd13f6..ec4da85db3c 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -9,12 +9,20 @@ 'use strict'; var d3 = require('d3'); +var map1dArray = require('../carpet/map_1d_array'); +var makepath = require('../carpet/makepath'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); +function makeg(el, type, klass) { + var join = el.selectAll(type + '.' + klass).data([0]); + join.enter().append(type).classed(klass, true); + return join; +} + module.exports = function plot(gd, plotinfo, cdcontours) { for(var i = 0; i < cdcontours.length; i++) { plotOne(gd, plotinfo, cdcontours[i]); @@ -22,7 +30,7 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { - var i, j, k, path, pt; + var i, j, k, path; var trace = cd[0].trace; var carpet = trace._carpet; var a = cd[0].a; @@ -89,10 +97,10 @@ function plotOne(gd, plotinfo, cd) { // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, contours, carpet); + makeBackground(plotGroup, xa, ya, contours, carpet); makeFills(plotGroup, pathinfo, perimeter, contours, ab2p); makeLines(plotGroup, pathinfo, contours); - // clipBoundary(plotGroup, carpet); + clipBoundary(plotGroup, carpet); // clipGaps(plotGroup, plotinfo, cd[0], perimeter); } @@ -182,16 +190,27 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -function makeBackground(plotgroup, contours, carpet) { - var bggroup = plotgroup.selectAll('g.contourbg').data([0]); - bggroup.enter().append('g').classed('contourbg', true); +function makeBackground(plotgroup, xaxis, yaxis, contours, carpet) { + var seg, xp, yp, i; + var bggroup = makeg(plotgroup, 'g', 'contourbg'); var bgfill = bggroup.selectAll('path') .data(contours.coloring === 'fill' ? [0] : []); bgfill.enter().append('path'); bgfill.exit().remove(); + + var segments = carpet._clipsegments; + var segs = []; + + for(i = 0; i < segments.length; i++) { + seg = segments[i]; + xp = map1dArray([], seg.x, xaxis.c2p); + yp = map1dArray([], seg.y, yaxis.c2p); + segs.push(makepath(xp, yp, seg.bicubic)); + } + bgfill - .attr('d', carpet.clipPathData) + .attr('d', 'M' + segs.join('L') + 'Z') .style('stroke', 'none'); } @@ -219,7 +238,6 @@ function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p) { } function joinAllPaths(pi, perimeter, ab2p) { - console.log('pi:', pi); var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'), i = 0, @@ -282,7 +300,6 @@ function joinAllPaths(pi, perimeter, ab2p) { endpt = newendpt; if(nexti >= 0) break; - console.log('add new endpoint:', newendpt); fullpath += 'L' + ab2p(newendpt); } From 946c7515a71ae59d0a8a68e895bcaa8d279fc7d1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 31 Jan 2017 13:17:57 -0500 Subject: [PATCH 035/132] A giant transpose of most of carpet plots --- src/traces/carpet/calc.js | 12 +- src/traces/carpet/calc_clippath.js | 26 ++-- src/traces/carpet/calc_gridlines.js | 2 +- src/traces/carpet/compute_control_points.js | 134 ++++++++-------- src/traces/carpet/set_convert.js | 4 +- src/traces/contourcarpet/plot.js | 164 +++++++++++++++----- 6 files changed, 213 insertions(+), 129 deletions(-) diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 4288171a26f..e616d4a4a92 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -20,12 +20,12 @@ var clean2dArray = require('../heatmap/clean_2d_array'); var smoothFill2dArray = require('./smooth_fill_2d_array'); module.exports = function calc(gd, trace) { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'), - ya = Axes.getFromId(gd, trace.yaxis || 'y'), - aax = trace.aaxis, - bax = trace.baxis, - a = trace.a, - b = trace.b; + var xa = Axes.getFromId(gd, trace.xaxis || 'x'); + var ya = Axes.getFromId(gd, trace.yaxis || 'y'); + var aax = trace.aaxis; + var bax = trace.baxis; + var a = trace.a; + var b = trace.b; var x; var y = trace.y; diff --git a/src/traces/carpet/calc_clippath.js b/src/traces/carpet/calc_clippath.js index 98dab9294af..e77c2280783 100644 --- a/src/traces/carpet/calc_clippath.js +++ b/src/traces/carpet/calc_clippath.js @@ -10,43 +10,39 @@ 'use strict'; module.exports = function makeClipPath(xctrl, yctrl, aax, bax) { - var i, xc, yc, x, y; + var i, x, y; var segments = []; var asmoothing = !!aax.smoothing; var bsmoothing = !!bax.smoothing; - var nea1 = xctrl.length - 1; - var neb1 = xctrl[0].length - 1; + var nea1 = xctrl[0].length - 1; + var neb1 = xctrl.length - 1; // Along the lower a axis: for(i = 0, x = [], y = []; i <= nea1; i++) { - x[i] = xctrl[i][0]; - y[i] = yctrl[i][0]; + x[i] = xctrl[0][i]; + y[i] = yctrl[0][i]; } segments.push({x: x, y: y, bicubic: asmoothing}); // Along the upper b axis: - xc = xctrl[nea1]; - yc = yctrl[nea1]; for(i = 0, x = [], y = []; i <= neb1; i++) { - x[i] = xc[i]; - y[i] = yc[i]; + x[i] = xctrl[i][nea1]; + y[i] = yctrl[i][nea1]; } segments.push({x: x, y: y, bicubic: bsmoothing}); // Backwards along the upper a axis: for(i = nea1, x = [], y = []; i >= 0; i--) { - x[nea1 - i] = xctrl[i][neb1]; - y[nea1 - i] = yctrl[i][neb1]; + x[nea1 - i] = xctrl[neb1][i]; + y[nea1 - i] = yctrl[neb1][i]; } segments.push({x: x, y: y, bicubic: asmoothing}); // Backwards along the lower b axis: - xc = xctrl[0]; - yc = yctrl[0]; for(i = neb1, x = [], y = []; i >= 0; i--) { - x[neb1 - i] = xc[i]; - y[neb1 - i] = yc[i]; + x[neb1 - i] = xctrl[i][0]; + y[neb1 - i] = yctrl[i][0]; } segments.push({x: x, y: y, bicubic: bsmoothing}); diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 48b91e95451..c74e2f35788 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -44,7 +44,7 @@ module.exports = function calcGridlines(trace, axisLetter, crossAxisLetter) { // The default is an empty array that will cause the join to remove the gridline if // it's just disappeared: - axis._startline = axis._endline = []; + // axis._startline = axis._endline = []; // If the cross axis uses bicubic interpolation, then the grid // lines fall once every three expanded grid row/cols: diff --git a/src/traces/carpet/compute_control_points.js b/src/traces/carpet/compute_control_points.js index 628f264bc38..71b96f4b635 100644 --- a/src/traces/carpet/compute_control_points.js +++ b/src/traces/carpet/compute_control_points.js @@ -128,22 +128,22 @@ function inferCubicControlPoint(p0, p2, p3) { } module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmoothing) { - var i, j, ie, je, xei, yei, xi, yi, cp, p1; + var i, j, ie, je, xej, yej, xj, yj, cp, p1; // At this point, we know these dimensions are correct and representative of // the whole 2D arrays: - var na = x.length; - var nb = x[0].length; + var na = x[0].length; + var nb = x.length; // (n)umber of (e)xpanded points: var nea = asmoothing ? 3 * na - 2 : na; var neb = bsmoothing ? 3 * nb - 2 : nb; - xe = ensureArray(xe, nea); - ye = ensureArray(ye, nea); + xe = ensureArray(xe, neb); + ye = ensureArray(ye, neb); - for(ie = 0; ie < nea; ie++) { - xe[ie] = ensureArray(xe[ie], neb); - ye[ie] = ensureArray(ye[ie], neb); + for(ie = 0; ie < neb; ie++) { + xe[ie] = ensureArray(xe[ie], nea); + ye[ie] = ensureArray(ye[ie], nea); } // This loop fills in the X'd points: @@ -160,16 +160,16 @@ module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmooth // // // ie = (i) (e)xpanded: - for(i = 0, ie = 0; i < na; i++, ie += asmoothing ? 3 : 1) { - xei = xe[ie]; - yei = ye[ie]; - xi = x[i]; - yi = y[i]; + for(j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { + xej = xe[je]; + yej = ye[je]; + xj = x[j]; + yj = y[j]; // je = (j) (e)xpanded: - for(j = 0, je = 0; j < nb; j++, je += bsmoothing ? 3 : 1) { - xei[je] = xi[j]; - yei[je] = yi[j]; + for(i = 0, ie = 0; i < na; i++, ie += asmoothing ? 3 : 1) { + xej[ie] = xj[i]; + yej[ie] = yj[i]; } } @@ -196,16 +196,16 @@ module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmooth // Fill in the points marked X for this a-row: for(i = 1, ie = 3; i < na - 1; i++, ie += 3) { cp = makeControlPoints( - [x[i - 1][j], y[i - 1][j]], - [x[i ][j], y[i ][j]], - [x[i + 1][j], y[i + 1][j]], + [x[j][i - 1], y[j][i - 1]], + [x[j][i ], y[j][i]], + [x[j][i + 1], y[j][i + 1]], asmoothing ); - xe[ie - 1][je] = cp[0][0]; - ye[ie - 1][je] = cp[0][1]; - xe[ie + 1][je] = cp[1][0]; - ye[ie + 1][je] = cp[1][1]; + xe[je][ie - 1] = cp[0][0]; + ye[je][ie - 1] = cp[0][1]; + xe[je][ie + 1] = cp[1][0]; + ye[je][ie + 1] = cp[1][1]; } // The very first cubic interpolation point (to the left for i = 1 above) is @@ -215,21 +215,21 @@ module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmooth // tangent by 1/3 and also construct a new cubic spline control point 1/3 from // the original to the i = 0 point. p1 = inferCubicControlPoint( - [xe[0][je], ye[0][je]], - [xe[2][je], ye[2][je]], - [xe[3][je], ye[3][je]] + [xe[je][0], ye[je][0]], + [xe[je][2], ye[je][2]], + [xe[je][3], ye[je][3]] ); - xe[1][je] = p1[0]; - ye[1][je] = p1[1]; + xe[je][1] = p1[0]; + ye[je][1] = p1[1]; // Ditto last points, sans explanation: p1 = inferCubicControlPoint( - [xe[nea - 1][je], ye[nea - 1][je]], - [xe[nea - 3][je], ye[nea - 3][je]], - [xe[nea - 4][je], ye[nea - 4][je]] + [xe[je][nea - 1], ye[je][nea - 1]], + [xe[je][nea - 3], ye[je][nea - 3]], + [xe[je][nea - 4], ye[je][nea - 4]] ); - xe[nea - 2][je] = p1[0]; - ye[nea - 2][je] = p1[1]; + xe[je][nea - 2] = p1[0]; + ye[je][nea - 2] = p1[1]; } } @@ -255,33 +255,33 @@ module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmooth for(ie = 0; ie < nea; ie++) { for(je = 3; je < neb - 3; je += 3) { cp = makeControlPoints( - [xe[ie][je - 3], ye[ie][je - 3]], - [xe[ie][je ], ye[ie][je ]], - [xe[ie][je + 3], ye[ie][je + 3]], + [xe[je - 3][ie], ye[je - 3][ie]], + [xe[je][ie], ye[je][ie]], + [xe[je + 3][ie], ye[je + 3][ie]], bsmoothing ); - xe[ie][je - 1] = cp[0][0]; - ye[ie][je - 1] = cp[0][1]; - xe[ie][je + 1] = cp[1][0]; - ye[ie][je + 1] = cp[1][1]; + xe[je - 1][ie] = cp[0][0]; + ye[je - 1][ie] = cp[0][1]; + xe[je + 1][ie] = cp[1][0]; + ye[je + 1][ie] = cp[1][1]; } // Do the same boundary condition magic for these control points marked Y above: p1 = inferCubicControlPoint( - [xe[ie][0], ye[ie][0]], - [xe[ie][2], ye[ie][2]], - [xe[ie][3], ye[ie][3]] + [xe[0][ie], ye[0][ie]], + [xe[2][ie], ye[2][ie]], + [xe[3][ie], ye[3][ie]] ); - xe[ie][1] = p1[0]; - ye[ie][1] = p1[1]; + xe[1][ie] = p1[0]; + ye[1][ie] = p1[1]; p1 = inferCubicControlPoint( - [xe[ie][neb - 1], ye[ie][neb - 1]], - [xe[ie][neb - 3], ye[ie][neb - 3]], - [xe[ie][neb - 4], ye[ie][neb - 4]] + [xe[neb - 1][ie], ye[neb - 1][ie]], + [xe[neb - 3][ie], ye[neb - 3][ie]], + [xe[neb - 4][ie], ye[neb - 4][ie]] ); - xe[ie][neb - 2] = p1[0]; - ye[ie][neb - 2] = p1[1]; + xe[neb - 2][ie] = p1[0]; + ye[neb - 2][ie] = p1[1]; } } @@ -314,35 +314,35 @@ module.exports = function computeControlPoints(xe, ye, x, y, asmoothing, bsmooth // Fill in the points marked X for this a-row: for(ie = 3; ie < nea - 3; ie += 3) { cp = makeControlPoints( - [xe[ie - 3][je], ye[ie - 3][je]], - [xe[ie ][je], ye[ie ][je]], - [xe[ie + 3][je], ye[ie + 3][je]], + [xe[je][ie - 3], ye[je][ie - 3]], + [xe[je][ie], ye[je][ie]], + [xe[je][ie + 3], ye[je][ie + 3]], asmoothing ); - xe[ie - 1][je] = 0.5 * (xe[ie - 1][je] + cp[0][0]); - ye[ie - 1][je] = 0.5 * (ye[ie - 1][je] + cp[0][1]); - xe[ie + 1][je] = 0.5 * (xe[ie + 1][je] + cp[1][0]); - ye[ie + 1][je] = 0.5 * (ye[ie + 1][je] + cp[1][1]); + xe[je][ie - 1] = 0.5 * (xe[je][ie - 1] + cp[0][0]); + ye[je][ie - 1] = 0.5 * (ye[je][ie - 1] + cp[0][1]); + xe[je][ie + 1] = 0.5 * (xe[je][ie + 1] + cp[1][0]); + ye[je][ie + 1] = 0.5 * (ye[je][ie + 1] + cp[1][1]); } // This case is just slightly different. The computation is the same, // but having computed this, we'll average with the existing result. p1 = inferCubicControlPoint( - [xe[0][je], ye[0][je]], - [xe[2][je], ye[2][je]], - [xe[3][je], ye[3][je]] + [xe[je][0], ye[je][0]], + [xe[je][2], ye[je][2]], + [xe[je][3], ye[je][3]] ); - xe[1][je] = 0.5 * (xe[1][je] + p1[0]); - ye[1][je] = 0.5 * (ye[1][je] + p1[1]); + xe[je][1] = 0.5 * (xe[je][1] + p1[0]); + ye[je][1] = 0.5 * (ye[je][1] + p1[1]); p1 = inferCubicControlPoint( - [xe[nea - 1][je], ye[nea - 1][je]], - [xe[nea - 3][je], ye[nea - 3][je]], - [xe[nea - 4][je], ye[nea - 4][je]] + [xe[je][nea - 1], ye[je][nea - 1]], + [xe[je][nea - 3], ye[je][nea - 3]], + [xe[je][nea - 4], ye[je][nea - 4]] ); - xe[nea - 2][je] = 0.5 * (xe[nea - 2][je] + p1[0]); - ye[nea - 2][je] = 0.5 * (ye[nea - 2][je] + p1[1]); + xe[je][nea - 2] = 0.5 * (xe[je][nea - 2] + p1[0]); + ye[je][nea - 2] = 0.5 * (ye[je][nea - 2] + p1[1]); } } diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index 3b0482a733b..ab54c24ec43 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -102,8 +102,8 @@ module.exports = function setConvert(trace) { * Convert from i/j coordinates to x/y caretesian coordinates. This means either bilinear * or bicubic spline evaluation, but the hard part is already done at this point. */ - trace.i2c = function(ij) { - return trace._evalxy([], ij[0], ij[1]); + trace.i2c = function(i, j) { + return trace._evalxy([], i, j); }; trace.ab2xy = function(aval, bval, extrapolate) { diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index ec4da85db3c..f71a5b794a5 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -25,7 +25,9 @@ function makeg(el, type, klass) { module.exports = function plot(gd, plotinfo, cdcontours) { for(var i = 0; i < cdcontours.length; i++) { + //console.group('plot contourcarpet'); plotOne(gd, plotinfo, cdcontours[i]); + //console.groupEnd(); } }; @@ -98,7 +100,7 @@ function plotOne(gd, plotinfo, cd) { // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); makeBackground(plotGroup, xa, ya, contours, carpet); - makeFills(plotGroup, pathinfo, perimeter, contours, ab2p); + makeFills(plotGroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya); makeLines(plotGroup, pathinfo, contours); clipBoundary(plotGroup, carpet); // clipGaps(plotGroup, plotinfo, cd[0], perimeter); @@ -214,7 +216,7 @@ function makeBackground(plotgroup, xaxis, yaxis, contours, carpet) { .style('stroke', 'none'); } -function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p) { +function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya) { var fillgroup = plotgroup.selectAll('g.contourfill') .data([0]); fillgroup.enter().append('g') @@ -230,32 +232,104 @@ function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p) { // if the whole perimeter is above this level, start with a path // enclosing the whole thing. With all that, the parity should mean // that we always fill everything above the contour, nothing below - var fullpath = joinAllPaths(pi, perimeter, ab2p); + var fullpath = joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya); if(!fullpath) d3.select(this).remove(); else d3.select(this).attr('d', fullpath).style('stroke', 'none'); }); } -function joinAllPaths(pi, perimeter, ab2p) { +function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? - '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'), - i = 0, - startsleft = pi.edgepaths.map(function(v, i) { return i; }), - newloop = true, - endpt, - newendpt, - cnt, - nexti, - possiblei, - addpath; - - function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < 0.01; } - function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < 0.01; } - function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < 0.01; } - function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } + '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'); + var i = 0; + var startsleft = pi.edgepaths.map(function(v, i) { return i; }); + var newloop = true; + var endpt, newendpt, cnt, nexti, possiblei, addpath; + + var atol = Math.abs(perimeter[0][0] - perimeter[2][0]) * 1e-8; + var btol = Math.abs(perimeter[0][1] - perimeter[2][1]) * 1e-8; + + function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < btol; } + function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < btol; } + function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < atol; } + function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < atol; } + + var aax = carpet.aaxis; + var bax = carpet.baxis; + var astride = aax.smoothing ? 3 : 1; + var bstride = bax.smoothing ? 3 : 1; + var a = carpet.a; + var b = carpet.b; + var xctrl = carpet._xctrl; + var yctrl = carpet._yctrl; + // console.log('xctrl, yctrl:', xctrl, yctrl); + + // Draw a path along the edge to a new point: + function pathto(pt0, pt1) { + // console.log('pathto:', pt0, pt1); + var i0, i1, i, j0, j1, j, a0, a1, b0, b1, dir, ie, je; + var path = ''; + if((istop(pt0) && !isright(pt0)) || (isbottom(pt0) && !isleft(pt0))) { + j = isbottom(pt0) ? 0 : b.length - 1; + je = j * bstride; + dir = pt1[0] - pt0[0] > 0 ? 1 : -1; + a0 = carpet.a2i(pt0[0]); + a1 = carpet.a2i(pt1[0]); + + i0 = (dir > 0 ? Math.floor : Math.ceil)(a0); + i1 = (dir > 0 ? Math.floor : Math.ceil)(a1); + + // console.log('leftright'); + // console.log('j, dir, a0, a1, i0, i1:', j, dir, a0, a1, i0, i1); + + for(i = i0 + dir; i * dir <= i1 * dir; i += dir) { + path += aax.smoothing ? 'C' : 'L'; + if(aax.smoothing) { + path += [xa.c2p(xctrl[je][i * astride - 2 * dir]), ya.c2p(yctrl[je][i * astride - 2 * dir])] + ' '; + path += [xa.c2p(xctrl[je][i * astride - 1 * dir]), ya.c2p(yctrl[je][i * astride - 1 * dir])] + ' '; + } + path += [xa.c2p(xctrl[je][i * astride]), ya.c2p(yctrl[je][i * astride])]; + } + } else { + // console.log('isleft(pt0)', pt0, isleft(pt0)); + i = isleft(pt0) ? 0 : a.length - 1; + ie = i * astride; + dir = pt1[1] - pt0[1] > 0 ? 1 : -1; + b0 = carpet.b2j(pt0[1]); + b1 = carpet.b2j(pt1[1]); + + j0 = (dir > 0 ? Math.floor : Math.ceil)(b0); + j1 = (dir > 0 ? Math.floor : Math.ceil)(b1); + + // console.log('j0, j1:', j0, j1); + + // console.log('j0, j1, dir, i:', j0, j1, dir, i); + for(j = j0 + dir; j * dir <= j1 * dir; j += dir) { + // console.log('j:', j); + path += bax.smoothing ? 'C' : 'L'; + // console.log('append', i, j, xctrl[j][i], yctrl[j][i]); + if(bax.smoothing) { + path += [xa.c2p(xctrl[j * bstride - 2 * dir][ie]), ya.c2p(yctrl[j * bstride - 2 * dir][ie])] + ' '; + path += [xa.c2p(xctrl[j * bstride - 1 * dir][ie]), ya.c2p(yctrl[j * bstride - 1 * dir][ie])] + ' '; + } + path += [xa.c2p(xctrl[j * bstride][ie]), ya.c2p(yctrl[j * bstride][ie])]; + } + } + return path; + } + endpt = null; while(startsleft.length) { + var startpt = pi.edgepaths[i][0]; + + if(endpt) { + // console.log('trace a path from the last endpoint to the next point', endpt, startpt); + // console.log('to the next edgepath', endpt, startpt); + fullpath += pathto(endpt, startpt); + } + + // console.log('add edgepath', i, JSON.stringify(pi.edgepaths[i])); addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing); fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); startsleft.splice(startsleft.indexOf(i), 1); @@ -269,38 +343,49 @@ function joinAllPaths(pi, perimeter, ab2p) { break; } - if(istop(endpt) && !isright(endpt)) newendpt = perimeter[1]; // right top - else if(isleft(endpt)) newendpt = perimeter[0]; // left top - else if(isbottom(endpt)) newendpt = perimeter[3]; // right bottom - else if(isright(endpt)) newendpt = perimeter[2]; // left bottom + // console.log('endpt:', endpt); + // console.log('endpt:', endpt); + // console.log('top, right, bottom, left', istop(endpt), isright(endpt), isbottom(endpt), isleft(endpt)); + + if(istop(endpt) && !isright(endpt)) { + // right top + newendpt = perimeter[1]; + } else if(isleft(endpt)) { + // left top + newendpt = perimeter[0]; + } else if(isbottom(endpt)) { + // right bottom + newendpt = perimeter[3]; + } else if(isright(endpt)) { + // left bottom + newendpt = perimeter[2]; + } for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { var ptNew = pi.edgepaths[possiblei][0]; // is ptNew on the (horz. or vert.) segment from endpt to newendpt? - if(Math.abs(endpt[0] - newendpt[0]) < 0.01) { - if(Math.abs(endpt[0] - ptNew[0]) < 0.01 && - (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { + if(Math.abs(endpt[0] - newendpt[0]) < atol) { + if(Math.abs(endpt[0] - ptNew[0]) < atol && (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { newendpt = ptNew; nexti = possiblei; } - } - else if(Math.abs(endpt[1] - newendpt[1]) < 0.01) { - if(Math.abs(endpt[1] - ptNew[1]) < 0.01 && - (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { + } else if(Math.abs(endpt[1] - newendpt[1]) < btol) { + if(Math.abs(endpt[1] - ptNew[1]) < btol && (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { newendpt = ptNew; nexti = possiblei; } - } - else { - Lib.log('endpt to newendpt is not vert. or horz.', - endpt, newendpt, ptNew); + } else { + Lib.log('endpt to newendpt is not vert. or horz.', endpt, newendpt, ptNew); } } - endpt = newendpt; + if(nexti >= 0) { + break; + } - if(nexti >= 0) break; - fullpath += 'L' + ab2p(newendpt); + // console.log('upper loop pathto', endpt, newendpt); + fullpath += pathto(endpt, newendpt); + endpt = newendpt; } if(nexti === pi.edgepaths.length) { @@ -314,8 +399,11 @@ function joinAllPaths(pi, perimeter, ab2p) { // close it and start a new loop newloop = (startsleft.indexOf(i) === -1); if(newloop) { + // console.log('newendpt, startpt:', newendpt, startpt); i = startsleft[0]; - fullpath += 'Z'; + // console.log('newloop pathto', endpt, newendpt); + fullpath += pathto(endpt, newendpt) + 'Z'; + endpt = null; } } From ec7ab187b7bc42781a865e2551eaebc727b35eea Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 31 Jan 2017 16:07:03 -0500 Subject: [PATCH 036/132] Extract axsi-aligned carpet lines --- src/traces/carpet/axis_aligned_line.js | 98 +++++++++++++++++++ .../carpet/create_j_derivative_evaluator.js | 6 +- src/traces/contourcarpet/plot.js | 98 ++++--------------- 3 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 src/traces/carpet/axis_aligned_line.js diff --git a/src/traces/carpet/axis_aligned_line.js b/src/traces/carpet/axis_aligned_line.js new file mode 100644 index 00000000000..d29dfa4dc48 --- /dev/null +++ b/src/traces/carpet/axis_aligned_line.js @@ -0,0 +1,98 @@ +/** +* 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'; + +/* This function retrns a set of control points that define a curve aligned along + * either the a or b axis. Exactly one of a or b must be an array defining the range + * spanned. + * + * Honestly this is the most complicated function I've implemente here so far because + * of the way it handles knot insertion and direction/axis-agnostic slices. + */ +module.exports = function (carpet, a, b, startpoint) { + var idx; + var axis = Array.isArray(a) ? 'a' : 'b'; + var ax = axis === 'a' ? carpet.aaxis : carpet.baxis; + var smoothing = ax.smoothing; + var toIdx = axis === 'a' ? carpet.a2i : carpet.b2j; + var pt = axis === 'a' ? a : b; + var iso = axis === 'a' ? b : a; + var n = axis === 'a' ? carpet.a.length : carpet.b.length; + var m = axis === 'a' ? carpet.b.length : carpet.a.length; + var isoIdx = axis === 'a' ? carpet.b2j(iso) : carpet.a2i(iso); + + var xy = axis === 'a' ? function (value) { + return carpet._evalxy([], value, isoIdx); + } : function (value) { + return carpet._evalxy([], isoIdx, value); + }; + + if (smoothing) { + var tanIsoIdx = Math.max(0, Math.min(m - 2, isoIdx)); + var tanIsoPar = isoIdx - tanIsoIdx; + var tangent = axis === 'a' ? function (i, ti) { + return carpet.dxydi([], i, tanIsoIdx, ti, tanIsoPar); + } : function (j, tj) { + return carpet.dxydj([], tanIsoIdx, j, tanIsoPar, tj); + }; + } + + var vstart = toIdx(pt[0]); + var vend = toIdx(pt[1]); + + // So that we can make this work in two directions, flip all of the + // math functions if the direction is from higher to lower indices: + // + // Note that the tolerance is directional! + var dir = vstart < vend ? 1 : -1; + var tol = (vend - vstart) * 1e-8; + var dirfloor = dir > 0 ? Math.floor : Math.ceil; + var dirceil = dir > 0 ? Math.ceil : Math.floor; + var dirmin = dir > 0 ? Math.min : Math.max; + var dirmax = dir > 0 ? Math.max : Math.min; + + var idx0 = dirfloor(vstart + tol); + var idx1 = dirceil(vend - tol); + + var segments = []; + + var p0, p1, v0, v1, start, end, range; + + p0 = xy(vstart); + segments.push([p0]); + + for(idx = idx0; idx * dir < idx1 * dir; idx += dir) { + var segment = []; + start = dirmax(vstart, idx); + end = dirmin(vend, idx + dir); + range = end - start; + + // In order to figure out which cell we're in for the derivative (remember, + // the derivatives are *not* constant across grid lines), let's just average + // the start and end points. This cuts out just a tiny bit of logic and + // there's really no computational difference: + var refidx = Math.max(0, Math.min(n - 2, Math.floor(0.5 * (start + end)))); + + p1 = xy(end); + if (smoothing) { + v0 = tangent(refidx, start - refidx); + v1 = tangent(refidx, end - refidx); + + segment.push([p0[0] + v0[0] / 3 * range, p0[1] + v0[1] / 3 * range]); + segment.push([p1[0] - v1[0] / 3 * range, p1[1] - v1[1] / 3 * range]); + } + + segment.push(p1); + + segments.push(segment); + p0 = p1; + } + + return segments; +}; diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index 3aeb6a3fb0a..bf7e7a96635 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -32,7 +32,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((v2 - 1) * ak[j0][i0 ] + ov2 * ak[j0 + 1][i0 ] + v * (2 - 3 * v) * ak[j0 + 2][i0 ] + v2 * ak[j0 + 3][i0 ]); + f0 = 3 * ((v2 - 1) * ak[j0][i0] + ov2 * ak[j0 + 1][i0] + v * (2 - 3 * v) * ak[j0 + 2][i0] + v2 * ak[j0 + 3][i0]); f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); f2 = 3 * ((v2 - 1) * ak[j0][i0 + 2] + ov2 * ak[j0 + 1][i0 + 2] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 2] + v2 * ak[j0 + 3][i0 + 2]); f3 = 3 * ((v2 - 1) * ak[j0][i0 + 3] + ov2 * ak[j0 + 1][i0 + 3] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 3] + v2 * ak[j0 + 3][i0 + 3]); @@ -58,7 +58,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = ak[j0 + 1][i0 ] - ak[j0][i0 ]; + f0 = ak[j0 + 1][i0] - ak[j0][i0]; f1 = ak[j0 + 1][i0 + 1] - ak[j0][i0 + 1]; f2 = ak[j0 + 1][i0 + 2] - ak[j0][i0 + 2]; f3 = ak[j0 + 1][i0 + 3] - ak[j0][i0 + 3]; @@ -86,7 +86,7 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var ou = 1 - u; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((v2 - 1) * ak[j0][i0 ] + ov2 * ak[j0 + 1][i0 ] + v * (2 - 3 * v) * ak[j0 + 2][i0 ] + v2 * ak[j0 + 3][i0 ]); + f0 = 3 * ((v2 - 1) * ak[j0][i0] + ov2 * ak[j0 + 1][i0] + v * (2 - 3 * v) * ak[j0 + 2][i0] + v2 * ak[j0 + 3][i0]); f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); out[k] = ou * f0 + u * f1; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index f71a5b794a5..50115f964ed 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -16,6 +16,7 @@ var Drawing = require('../../components/drawing'); var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); +var axisAlignedLine = require('../carpet/axis_aligned_line'); function makeg(el, type, klass) { var join = el.selectAll(type + '.' + klass).data([0]); @@ -25,9 +26,7 @@ function makeg(el, type, klass) { module.exports = function plot(gd, plotinfo, cdcontours) { for(var i = 0; i < cdcontours.length; i++) { - //console.group('plot contourcarpet'); plotOne(gd, plotinfo, cdcontours[i]); - //console.groupEnd(); } }; @@ -255,65 +254,23 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < atol; } function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < atol; } - var aax = carpet.aaxis; - var bax = carpet.baxis; - var astride = aax.smoothing ? 3 : 1; - var bstride = bax.smoothing ? 3 : 1; - var a = carpet.a; - var b = carpet.b; - var xctrl = carpet._xctrl; - var yctrl = carpet._yctrl; - // console.log('xctrl, yctrl:', xctrl, yctrl); - - // Draw a path along the edge to a new point: function pathto(pt0, pt1) { - // console.log('pathto:', pt0, pt1); - var i0, i1, i, j0, j1, j, a0, a1, b0, b1, dir, ie, je; + var i, j, segments, axis; var path = ''; + if((istop(pt0) && !isright(pt0)) || (isbottom(pt0) && !isleft(pt0))) { - j = isbottom(pt0) ? 0 : b.length - 1; - je = j * bstride; - dir = pt1[0] - pt0[0] > 0 ? 1 : -1; - a0 = carpet.a2i(pt0[0]); - a1 = carpet.a2i(pt1[0]); - - i0 = (dir > 0 ? Math.floor : Math.ceil)(a0); - i1 = (dir > 0 ? Math.floor : Math.ceil)(a1); - - // console.log('leftright'); - // console.log('j, dir, a0, a1, i0, i1:', j, dir, a0, a1, i0, i1); - - for(i = i0 + dir; i * dir <= i1 * dir; i += dir) { - path += aax.smoothing ? 'C' : 'L'; - if(aax.smoothing) { - path += [xa.c2p(xctrl[je][i * astride - 2 * dir]), ya.c2p(yctrl[je][i * astride - 2 * dir])] + ' '; - path += [xa.c2p(xctrl[je][i * astride - 1 * dir]), ya.c2p(yctrl[je][i * astride - 1 * dir])] + ' '; - } - path += [xa.c2p(xctrl[je][i * astride]), ya.c2p(yctrl[je][i * astride])]; - } + axis = carpet.aaxis; + segments = axisAlignedLine(carpet, [pt0[0], pt1[0]], 0.5 * (pt0[1] + pt1[1])); } else { - // console.log('isleft(pt0)', pt0, isleft(pt0)); - i = isleft(pt0) ? 0 : a.length - 1; - ie = i * astride; - dir = pt1[1] - pt0[1] > 0 ? 1 : -1; - b0 = carpet.b2j(pt0[1]); - b1 = carpet.b2j(pt1[1]); - - j0 = (dir > 0 ? Math.floor : Math.ceil)(b0); - j1 = (dir > 0 ? Math.floor : Math.ceil)(b1); - - // console.log('j0, j1:', j0, j1); - - // console.log('j0, j1, dir, i:', j0, j1, dir, i); - for(j = j0 + dir; j * dir <= j1 * dir; j += dir) { - // console.log('j:', j); - path += bax.smoothing ? 'C' : 'L'; - // console.log('append', i, j, xctrl[j][i], yctrl[j][i]); - if(bax.smoothing) { - path += [xa.c2p(xctrl[j * bstride - 2 * dir][ie]), ya.c2p(yctrl[j * bstride - 2 * dir][ie])] + ' '; - path += [xa.c2p(xctrl[j * bstride - 1 * dir][ie]), ya.c2p(yctrl[j * bstride - 1 * dir][ie])] + ' '; - } - path += [xa.c2p(xctrl[j * bstride][ie]), ya.c2p(yctrl[j * bstride][ie])]; + axis = carpet.baxis; + segments = axisAlignedLine(carpet, 0.5 * (pt0[0] + pt1[0]), [pt0[1], pt1[1]]); + } + + for (i = 1; i < segments.length; i++) { + path += axis.smoothing ? 'C' : 'L'; + for (j = 0; j < segments[i].length; j++) { + var pt = segments[i][j] + path += [xa.c2p(pt[0]), ya.c2p(pt[1])] + ' '; } } return path; @@ -324,12 +281,9 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { var startpt = pi.edgepaths[i][0]; if(endpt) { - // console.log('trace a path from the last endpoint to the next point', endpt, startpt); - // console.log('to the next edgepath', endpt, startpt); fullpath += pathto(endpt, startpt); } - // console.log('add edgepath', i, JSON.stringify(pi.edgepaths[i])); addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing); fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); startsleft.splice(startsleft.indexOf(i), 1); @@ -343,22 +297,14 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { break; } - // console.log('endpt:', endpt); - // console.log('endpt:', endpt); - // console.log('top, right, bottom, left', istop(endpt), isright(endpt), isbottom(endpt), isleft(endpt)); - if(istop(endpt) && !isright(endpt)) { - // right top - newendpt = perimeter[1]; + newendpt = perimeter[1]; // right top } else if(isleft(endpt)) { - // left top - newendpt = perimeter[0]; + newendpt = perimeter[0]; // left top } else if(isbottom(endpt)) { - // right bottom - newendpt = perimeter[3]; + newendpt = perimeter[3]; // right bottom } else if(isright(endpt)) { - // left bottom - newendpt = perimeter[2]; + newendpt = perimeter[2]; // left bottom } for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { @@ -379,11 +325,7 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { } } - if(nexti >= 0) { - break; - } - - // console.log('upper loop pathto', endpt, newendpt); + if(nexti >= 0) break; fullpath += pathto(endpt, newendpt); endpt = newendpt; } @@ -399,9 +341,7 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { // close it and start a new loop newloop = (startsleft.indexOf(i) === -1); if(newloop) { - // console.log('newendpt, startpt:', newendpt, startpt); i = startsleft[0]; - // console.log('newloop pathto', endpt, newendpt); fullpath += pathto(endpt, newendpt) + 'Z'; endpt = null; } From eed5406e14cc6bb279218ccc03d356e18c924dc3 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 31 Jan 2017 17:52:21 -0500 Subject: [PATCH 037/132] Fix the carpet axis derivatives --- src/traces/carpet/axis_aligned_line.js | 43 +++++++++++-------- .../carpet/create_i_derivative_evaluator.js | 22 +++++++--- .../carpet/create_j_derivative_evaluator.js | 27 ++++++++---- src/traces/contourcarpet/plot.js | 6 +-- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/traces/carpet/axis_aligned_line.js b/src/traces/carpet/axis_aligned_line.js index d29dfa4dc48..c4bcd82489e 100644 --- a/src/traces/carpet/axis_aligned_line.js +++ b/src/traces/carpet/axis_aligned_line.js @@ -15,8 +15,10 @@ * Honestly this is the most complicated function I've implemente here so far because * of the way it handles knot insertion and direction/axis-agnostic slices. */ -module.exports = function (carpet, a, b, startpoint) { - var idx; +module.exports = function(carpet, a, b) { + var idx, tangent, tanIsoIdx, tanIsoPar, segment, refidx; + var p0, p1, v0, v1, start, end, range; + var axis = Array.isArray(a) ? 'a' : 'b'; var ax = axis === 'a' ? carpet.aaxis : carpet.baxis; var smoothing = ax.smoothing; @@ -27,18 +29,18 @@ module.exports = function (carpet, a, b, startpoint) { var m = axis === 'a' ? carpet.b.length : carpet.a.length; var isoIdx = axis === 'a' ? carpet.b2j(iso) : carpet.a2i(iso); - var xy = axis === 'a' ? function (value) { + var xy = axis === 'a' ? function(value) { return carpet._evalxy([], value, isoIdx); - } : function (value) { + } : function(value) { return carpet._evalxy([], isoIdx, value); }; - if (smoothing) { - var tanIsoIdx = Math.max(0, Math.min(m - 2, isoIdx)); - var tanIsoPar = isoIdx - tanIsoIdx; - var tangent = axis === 'a' ? function (i, ti) { + if(smoothing) { + tanIsoIdx = Math.max(0, Math.min(m - 2, isoIdx)); + tanIsoPar = isoIdx - tanIsoIdx; + tangent = axis === 'a' ? function(i, ti) { return carpet.dxydi([], i, tanIsoIdx, ti, tanIsoPar); - } : function (j, tj) { + } : function(j, tj) { return carpet.dxydj([], tanIsoIdx, j, tanIsoPar, tj); }; } @@ -60,15 +62,11 @@ module.exports = function (carpet, a, b, startpoint) { var idx0 = dirfloor(vstart + tol); var idx1 = dirceil(vend - tol); - var segments = []; - - var p0, p1, v0, v1, start, end, range; - p0 = xy(vstart); - segments.push([p0]); + var segments = [[p0]]; for(idx = idx0; idx * dir < idx1 * dir; idx += dir) { - var segment = []; + segment = []; start = dirmax(vstart, idx); end = dirmin(vend, idx + dir); range = end - start; @@ -77,15 +75,22 @@ module.exports = function (carpet, a, b, startpoint) { // the derivatives are *not* constant across grid lines), let's just average // the start and end points. This cuts out just a tiny bit of logic and // there's really no computational difference: - var refidx = Math.max(0, Math.min(n - 2, Math.floor(0.5 * (start + end)))); + refidx = Math.max(0, Math.min(n - 2, Math.floor(0.5 * (start + end)))); p1 = xy(end); - if (smoothing) { + if(smoothing) { v0 = tangent(refidx, start - refidx); v1 = tangent(refidx, end - refidx); - segment.push([p0[0] + v0[0] / 3 * range, p0[1] + v0[1] / 3 * range]); - segment.push([p1[0] - v1[0] / 3 * range, p1[1] - v1[1] / 3 * range]); + segment.push([ + p0[0] + v0[0] / 3 * range, + p0[1] + v0[1] / 3 * range + ]); + + segment.push([ + p1[0] - v1[0] / 3 * range, + p1[1] - v1[1] / 3 * range + ]); } segment.push(p1); diff --git a/src/traces/carpet/create_i_derivative_evaluator.js b/src/traces/carpet/create_i_derivative_evaluator.js index 5dc4b40ef88..5faea051eea 100644 --- a/src/traces/carpet/create_i_derivative_evaluator.js +++ b/src/traces/carpet/create_i_derivative_evaluator.js @@ -52,6 +52,11 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var u2 = u * u; var ou = 1 - u; var ou2 = ou * ou; + var ouu2 = ou * u * 2; + var a = -3 * ou2; + var b = 3 * (ou2 - ouu2); + var c = 3 * (ouu2 - u2); + var d = 3 * u2; var v2 = v * v; var v3 = v2 * v; @@ -62,10 +67,10 @@ module.exports = function(arrays, asmoothing, bsmoothing) { for(k = 0; k < arrays.length; k++) { ak = arrays[k]; // Compute the derivatives in the u-direction: - f0 = 3 * ((u2 - 1) * ak[j0 ][i0] + ou2 * ak[j0 ][i0 + 1] + u * (2 - 3 * u) * ak[j0 ][i0 + 2] + u2 * ak[j0 ][i0 + 3]); - f1 = 3 * ((u2 - 1) * ak[j0 + 1][i0] + ou2 * ak[j0 + 1][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 1][i0 + 2] + u2 * ak[j0 + 1][i0 + 3]); - f2 = 3 * ((u2 - 1) * ak[j0 + 2][i0] + ou2 * ak[j0 + 2][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 2][i0 + 2] + u2 * ak[j0 + 2][i0 + 3]); - f3 = 3 * ((u2 - 1) * ak[j0 + 3][i0] + ou2 * ak[j0 + 3][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 3][i0 + 2] + u2 * ak[j0 + 3][i0 + 3]); + f0 = a * ak[j0 ][i0] + b * ak[j0 ][i0 + 1] + c * ak[j0 ][i0 + 2] + d * ak[j0 ][i0 + 3]; + f1 = a * ak[j0 + 1][i0] + b * ak[j0 + 1][i0 + 1] + c * ak[j0 + 1][i0 + 2] + d * ak[j0 + 1][i0 + 3]; + f2 = a * ak[j0 + 2][i0] + b * ak[j0 + 2][i0 + 1] + c * ak[j0 + 2][i0 + 2] + d * ak[j0 + 2][i0 + 3]; + f3 = a * ak[j0 + 3][i0] + b * ak[j0 + 3][i0 + 1] + c * ak[j0 + 3][i0 + 2] + d * ak[j0 + 3][i0 + 3]; // Now just interpolate in the v-direction since it's all separable: out[k] = ov3 * f0 + 3 * (ov2 * v * f1 + ov * v2 * f2) + v3 * f3; @@ -83,11 +88,16 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var u2 = u * u; var ou = 1 - u; var ou2 = ou * ou; + var ouu2 = ou * u * 2; + var a = -3 * ou2; + var b = 3 * (ou2 - ouu2); + var c = 3 * (ouu2 - u2); + var d = 3 * u2; var ov = 1 - v; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((u2 - 1) * ak[j0 ][i0] + ou2 * ak[j0 ][i0 + 1] + u * (2 - 3 * u) * ak[j0 ][i0 + 2] + u2 * ak[j0 ][i0 + 3]); - f1 = 3 * ((u2 - 1) * ak[j0 + 1][i0] + ou2 * ak[j0 + 1][i0 + 1] + u * (2 - 3 * u) * ak[j0 + 1][i0 + 2] + u2 * ak[j0 + 1][i0 + 3]); + f0 = a * ak[j0 ][i0] + b * ak[j0 ][i0 + 1] + c * ak[j0 ][i0 + 2] + d * ak[j0 ][i0 + 3]; + f1 = a * ak[j0 + 1][i0] + b * ak[j0 + 1][i0 + 1] + c * ak[j0 + 1][i0 + 2] + d * ak[j0 + 1][i0 + 3]; out[k] = ov * f0 + v * f1; } diff --git a/src/traces/carpet/create_j_derivative_evaluator.js b/src/traces/carpet/create_j_derivative_evaluator.js index bf7e7a96635..f7c6b897740 100644 --- a/src/traces/carpet/create_j_derivative_evaluator.js +++ b/src/traces/carpet/create_j_derivative_evaluator.js @@ -28,14 +28,20 @@ module.exports = function(arrays, asmoothing, bsmoothing) { var v2 = v * v; var ov = 1 - v; var ov2 = ov * ov; + var ovv2 = ov * v * 2; + var a = -3 * ov2; + var b = 3 * (ov2 - ovv2); + var c = 3 * (ovv2 - v2); + var d = 3 * v2; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - // Compute the derivatives in the u-direction: - f0 = 3 * ((v2 - 1) * ak[j0][i0] + ov2 * ak[j0 + 1][i0] + v * (2 - 3 * v) * ak[j0 + 2][i0] + v2 * ak[j0 + 3][i0]); - f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); - f2 = 3 * ((v2 - 1) * ak[j0][i0 + 2] + ov2 * ak[j0 + 1][i0 + 2] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 2] + v2 * ak[j0 + 3][i0 + 2]); - f3 = 3 * ((v2 - 1) * ak[j0][i0 + 3] + ov2 * ak[j0 + 1][i0 + 3] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 3] + v2 * ak[j0 + 3][i0 + 3]); + + // Compute the derivatives in the v-direction: + f0 = a * ak[j0][i0] + b * ak[j0 + 1][i0] + c * ak[j0 + 2][i0] + d * ak[j0 + 3][i0]; + f1 = a * ak[j0][i0 + 1] + b * ak[j0 + 1][i0 + 1] + c * ak[j0 + 2][i0 + 1] + d * ak[j0 + 3][i0 + 1]; + f2 = a * ak[j0][i0 + 2] + b * ak[j0 + 1][i0 + 2] + c * ak[j0 + 2][i0 + 2] + d * ak[j0 + 3][i0 + 2]; + f3 = a * ak[j0][i0 + 3] + b * ak[j0 + 1][i0 + 3] + c * ak[j0 + 2][i0 + 3] + d * ak[j0 + 3][i0 + 3]; // Now just interpolate in the v-direction since it's all separable: out[k] = ou3 * f0 + 3 * (ou2 * u * f1 + ou * u2 * f2) + u3 * f3; @@ -80,14 +86,19 @@ module.exports = function(arrays, asmoothing, bsmoothing) { if(!out) out = []; var f0, f1, k, ak; j0 *= 3; + var ou = 1 - u; var v2 = v * v; var ov = 1 - v; var ov2 = ov * ov; - var ou = 1 - u; + var ovv2 = ov * v * 2; + var a = -3 * ov2; + var b = 3 * (ov2 - ovv2); + var c = 3 * (ovv2 - v2); + var d = 3 * v2; for(k = 0; k < arrays.length; k++) { ak = arrays[k]; - f0 = 3 * ((v2 - 1) * ak[j0][i0] + ov2 * ak[j0 + 1][i0] + v * (2 - 3 * v) * ak[j0 + 2][i0] + v2 * ak[j0 + 3][i0]); - f1 = 3 * ((v2 - 1) * ak[j0][i0 + 1] + ov2 * ak[j0 + 1][i0 + 1] + v * (2 - 3 * v) * ak[j0 + 2][i0 + 1] + v2 * ak[j0 + 3][i0 + 1]); + f0 = a * ak[j0][i0] + b * ak[j0 + 1][i0] + c * ak[j0 + 2][i0] + d * ak[j0 + 3][i0]; + f1 = a * ak[j0][i0 + 1] + b * ak[j0 + 1][i0 + 1] + c * ak[j0 + 2][i0 + 1] + d * ak[j0 + 3][i0 + 1]; out[k] = ou * f0 + u * f1; } diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 50115f964ed..2655044d44c 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -266,10 +266,10 @@ function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { segments = axisAlignedLine(carpet, 0.5 * (pt0[0] + pt1[0]), [pt0[1], pt1[1]]); } - for (i = 1; i < segments.length; i++) { + for(i = 1; i < segments.length; i++) { path += axis.smoothing ? 'C' : 'L'; - for (j = 0; j < segments[i].length; j++) { - var pt = segments[i][j] + for(j = 0; j < segments[i].length; j++) { + var pt = segments[i][j]; path += [xa.c2p(pt[0]), ya.c2p(pt[1])] + ' '; } } From 7304043ffa71cd8ca65b283c5840a9eb1ff4a974 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 1 Feb 2017 16:10:54 -0500 Subject: [PATCH 038/132] Restore badly merged cheater axis hiding logic --- src/plots/cartesian/layout_defaults.js | 3 ++- src/traces/carpet/axis_defaults.js | 2 +- src/traces/carpet/defaults.js | 5 +++++ src/traces/carpet/smooth_fill_2d_array.js | 1 - src/traces/carpet/xy_defaults.js | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 723b0834dae..8ae32ac8f1c 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -151,7 +151,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { name: axName, data: fullData, bgColor: bgColor, - calendar: layoutOut.calendar + calendar: layoutOut.calendar, + cheateronly: axLetter === 'x' && xaListNotCheater.indexOf(axName) === -1 }; handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index bead56102b0..bb4401440c7 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -61,7 +61,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) // now figure out type and do some more initialization var axType = coerce('type'); if(axType === '-') { - setAutoType(containerOut, options.data); + if(options.data) setAutoType(containerOut, options.data); if(containerOut.type === '-') { containerOut.type = 'linear'; diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index 3cc3ea02c79..8fba89280f7 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -28,6 +28,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayou handleABDefaults(traceIn, traceOut, fullLayout, coerce, defaultColor); + if(!traceOut.a || !traceOut.b) { + traceOut.visible = false; + return; + } + if(traceOut.a.length < 3) { traceOut.aaxis.smoothing = 0; } diff --git a/src/traces/carpet/smooth_fill_2d_array.js b/src/traces/carpet/smooth_fill_2d_array.js index 7550690f998..b1afe097951 100644 --- a/src/traces/carpet/smooth_fill_2d_array.js +++ b/src/traces/carpet/smooth_fill_2d_array.js @@ -64,7 +64,6 @@ module.exports = function smoothFill2dArray(data, a, b) { } } - if(!ip.length) return data; // The tolerance doesn't need to be excessive. It's just for display positioning diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 3fc69a72cd8..0cac2836ce1 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -29,7 +29,7 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { if(!x && !y) return; if(cols.length) { - convertColumnData(traceOut, traceOut.baxis, traceOut.aaxis, 'b', 'a', cols); + convertColumnData(traceOut, traceOut.aaxis, traceOut.baxis, 'a', 'b', cols); } return true; From e4dc03c36d1bbc2f9aee0b8e7bc4385ae977eed2 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 2 Feb 2017 13:20:59 -0500 Subject: [PATCH 039/132] Sorta account for missing a/b on contourcarpet --- src/traces/carpet/index.js | 1 + src/traces/contourcarpet/calc.js | 19 +++++++++++ src/traces/contourcarpet/defaults.js | 49 ++++++++++++++++------------ src/traces/contourcarpet/plot.js | 1 + 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index eb1987b12c1..44eb7104434 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -17,6 +17,7 @@ Carpet.plot = require('./plot'); Carpet.calc = require('./calc'); Carpet.animatable = true; Carpet.calcPriority = 1; +Carpet.defaultPriority = 1; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index 3742344a490..6d9ecd30e85 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -21,12 +21,14 @@ var maxRowLength = require('../heatmap/max_row_length'); var interp2d = require('../heatmap/interp2d'); var findEmpties = require('../heatmap/find_empties'); var makeBoundArray = require('../heatmap/make_bound_array'); +var supplyDefaults = require('./defaults'); // most is the same as heatmap calc, then adjust it // though a few things inside heatmap calc still look for // contour maps, because the makeBoundArray calls are too entangled module.exports = function calc(gd, trace) { + //console.trace('calc'); var i, carpet; for(i = 0; i < gd._fullData.length; i++) { @@ -39,6 +41,23 @@ module.exports = function calc(gd, trace) { if(!carpet) return; trace._carpet = carpet; + if (!trace.a || !trace.b) { + // Look up the original incoming carpet data: + var carpetdata = gd.data[carpet.index]; + + // Look up the incoming trace data, *except* perform a shallow + // copy so that we're not actually modifying it when we use it + // to supply defaults: + var tracedata = gd.data[trace.index]; + //var tracedata = extendFlat({}, gd.data[trace.index]); + + // If the data is not specified + if (!tracedata.a) tracedata.a = carpetdata.a; + if (!tracedata.b) tracedata.b = carpetdata.b; + + supplyDefaults(tracedata, trace, null, gd._fullLayout); + } + var cd = heatmappishCalc(gd, trace), contours = trace.contours; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index f812cbd27af..7256fc6de36 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -17,36 +17,45 @@ var hasColumns = require('../heatmap/has_columns'); var handleStyleDefaults = require('../contour/style_defaults'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + //console.trace('defaults'); function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } coerce('carpetid'); - var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); - if(!len) { - traceOut.visible = false; - return; - } + if (traceIn.a && traceIn.b) { + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); + + if(!len) { + traceOut.visible = false; + return; + } - coerce('text'); - coerce('connectgaps', hasColumns(traceOut)); + coerce('text'); + coerce('connectgaps', hasColumns(traceOut)); - var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), - contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), - missingEnd = (contourStart === false) || (contourEnd === false), + var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); + var contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); + var missingEnd = (contourStart === false) || (contourEnd === false); - // normally we only need size if autocontour is off. But contour.calc - // pushes its calculated contour size back to the input trace, so for - // things like restyle that can call supplyDefaults without calc - // after the initial draw, we can just reuse the previous calculation - contourSize = coerce('contours.size'), - autoContour; + // normally we only need size if autocontour is off. But contour.calc + // pushes its calculated contour size back to the input trace, so for + // things like restyle that can call supplyDefaults without calc + // after the initial draw, we can just reuse the previous calculation + var contourSize = coerce('contours.size'); + var autoContour; - if(missingEnd) autoContour = traceOut.autocontour = true; - else autoContour = coerce('autocontour', false); + if(missingEnd) { + autoContour = traceOut.autocontour = true; + } else { + autoContour = coerce('autocontour', false); + } - if(autoContour || !contourSize) coerce('ncontours'); + if(autoContour || !contourSize) { + coerce('ncontours'); + } - handleStyleDefaults(traceIn, traceOut, coerce, layout); + handleStyleDefaults(traceIn, traceOut, coerce, layout); + } }; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 2655044d44c..79e0f5719c0 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -31,6 +31,7 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { + //console.trace('plotone'); var i, j, k, path; var trace = cd[0].trace; var carpet = trace._carpet; From 490039c82a72d9059ddaf782da86ddf34ef4f6f3 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 2 Feb 2017 14:10:48 -0500 Subject: [PATCH 040/132] Add a detailed note about mutating user input --- src/traces/contourcarpet/defaults.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 7256fc6de36..550dbffbd40 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -24,6 +24,21 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('carpetid'); + // If either a or b is not present, then it's not a valid trace *unless* the carpet + // axis has the a or b valeus we're looking for. So if these are not found, just defer + // that decision until the calc step. + // + // NB: the calc step will modify the original data input by assigning whichever of + // a or b are missing. This is necessary because panning goes right from supplyDefaults + // to plot (skipping calc). That means on subsequent updates, this *will* need to be + // able to find a and b. + // + // The long-term proper fix is that this should perhaps use underscored attributes to + // at least modify the user input to a slightly lesser extent. Fully removing the + // input mutation is challenging. The underscore approach is not currently taken since + // it requires modification to all of the functions below that expect the coerced + // attribute name to match the property name -- except '_a' !== 'a' so that is not + // straightforward. if (traceIn.a && traceIn.b) { var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); From 8f8ea25387bd78724f1d2dde77c9903422462639 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 14:22:18 -0500 Subject: [PATCH 041/132] Contourcarpet inequalities --- src/traces/carpet/axis_defaults.js | 6 +- src/traces/carpet/map_2d_array.js | 2 +- src/traces/carpet/plot.js | 2 +- src/traces/contour/make_color_map.js | 5 ++ src/traces/contourcarpet/attributes.js | 59 +++++++++++- src/traces/contourcarpet/calc.js | 16 ++-- src/traces/contourcarpet/constants.js | 15 ++++ .../contourcarpet/constraint_mapping.js | 90 +++++++++++++++++++ .../contourcarpet/convert_to_constraints.js | 87 ++++++++++++++++++ src/traces/contourcarpet/defaults.js | 45 +++++++--- src/traces/contourcarpet/index.js | 2 +- src/traces/contourcarpet/plot.js | 11 +-- src/traces/contourcarpet/style.js | 16 ++-- src/traces/scattercarpet/attributes.js | 2 +- src/traces/scattercarpet/calc.js | 2 +- src/traces/scattercarpet/defaults.js | 2 +- src/traces/scattercarpet/hover.js | 2 +- src/traces/scattercarpet/index.js | 2 +- src/traces/scattercarpet/plot.js | 2 +- src/traces/scattercarpet/select.js | 2 +- src/traces/scattercarpet/style.js | 2 +- 21 files changed, 327 insertions(+), 45 deletions(-) create mode 100644 src/traces/contourcarpet/constants.js create mode 100644 src/traces/contourcarpet/constraint_mapping.js create mode 100644 src/traces/contourcarpet/convert_to_constraints.js diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index bb4401440c7..8a93acb29af 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -11,7 +11,7 @@ var carpetAttrs = require('./attributes'); var isNumeric = require('fast-isnumeric'); -var colorMix = require('tinycolor2').mix; +var addOpacity = require('../../components/color').addOpacity; var Registry = require('../../registry'); var Lib = require('../../lib'); var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); @@ -146,7 +146,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) handleTickMarkDefaults(containerIn, containerOut, coerce, options); handleCategoryOrderDefaults(containerIn, containerOut, coerce); - var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, 70).toRgbString()); + var gridColor = coerce2('gridcolor', addOpacity(dfltColor, 0.3)); var gridWidth = coerce2('gridwidth'); var showGrid = coerce('showgrid'); @@ -174,7 +174,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) coerce('minorgridcount'); coerce('minorgridwidth', gridWidth); - coerce('minorgridcolor', colorMix(gridColor, options.bgColor, 95).toRgbString()); + coerce('minorgridcolor', addOpacity(gridColor, 0.06)); } diff --git a/src/traces/carpet/map_2d_array.js b/src/traces/carpet/map_2d_array.js index cac5e8626f6..341f52b8e34 100644 --- a/src/traces/carpet/map_2d_array.js +++ b/src/traces/carpet/map_2d_array.js @@ -35,7 +35,7 @@ module.exports = function mapArray(out, data, func) { out[i] = out[i].slice(0, data.length); } - for(j = 0; j < data[1].length; j++) { + for(j = 0; j < data[0].length; j++) { out[i][j] = func(data[i][j]); } } diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 0c7b349d697..d4277e06c5c 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -43,7 +43,7 @@ function plotOne(gd, plotinfo, cd) { // var b = cd[0].b; // XXX: Layer choice?? - var gridLayer = plotinfo.plot.selectAll('.maplayer'); + var gridLayer = plotinfo.plot.selectAll('.barlayer'); var clipLayer = makeg(fullLayout._defs, 'g', 'clips'); var minorLayer = makeg(gridLayer, 'g', 'minorlayer'); diff --git a/src/traces/contour/make_color_map.js b/src/traces/contour/make_color_map.js index fc54a155705..8c2835455c7 100644 --- a/src/traces/contour/make_color_map.js +++ b/src/traces/contour/make_color_map.js @@ -21,6 +21,11 @@ module.exports = function makeColorMap(trace) { nc = Math.floor((end - start) / cs) + 1, extra = contours.coloring === 'lines' ? 0 : 1; + if(!isFinite(cs)) { + cs = 1; + nc = 1; + } + var scl = trace.colorscale, len = scl.length; diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 4aa88fd2ca5..6aa0cddb3c8 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -16,6 +16,7 @@ var colorbarAttrs = require('../../components/colorbar/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var scatterLineAttrs = scatterAttrs.line; +var constants = require('./constants'); module.exports = extendFlat({}, { carpetid: { @@ -115,7 +116,61 @@ module.exports = extendFlat({}, { 'Determines whether or not the contour lines are drawn.', 'Has only an effect if `contours.coloring` is set to *fill*.' ].join(' ') - } + }, + constraint: { + operation: { + valType: 'enumerated', + values: [].concat(constants.INEQUALITY_OPS).concat(constants.INTERVAL_OPS).concat(constants.SET_OPS), + description: [ + 'Sets the filter operation.', + + '*=* keeps items equal to `value`', + + '*<* keeps items less than `value`', + '*<=* keeps items less than or equal to `value`', + + '*>* keeps items greater than `value`', + '*>=* keeps items greater than or equal to `value`', + + '*[]* keeps items inside `value[0]` to value[1]` including both bounds`', + '*()* keeps items inside `value[0]` to value[1]` excluding both bounds`', + '*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]', + '*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]', + + '*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`', + '*)(* keeps items outside `value[0]` to value[1]`', + '*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`', + '*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`', + + '*{}* keeps items present in a set of values', + '*}{* keeps items not present in a set of values' + ].join(' ') + }, + value: { + valType: 'any', + dflt: 0, + description: [ + 'Sets the value or values by which to filter by.', + + 'Values are expected to be in the same type as the data linked', + 'to *target*.', + + 'When `operation` is set to one of the inequality values', + '(' + constants.INEQUALITY_OPS + ')', + '*value* is expected to be a number or a string.', + + 'When `operation` is set to one of the interval value', + '(' + constants.INTERVAL_OPS + ')', + '*value* is expected to be 2-item array where the first item', + 'is the lower bound and the second item is the upper bound.', + + 'When `operation`, is set to one of the set value', + '(' + constants.SET_OPS + ')', + '*value* is expected to be an array with as many items as', + 'the desired set elements.' + ].join(' ') + } + }, }, line: { diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index 6d9ecd30e85..97bc3cb65af 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -28,7 +28,6 @@ var supplyDefaults = require('./defaults'); // though a few things inside heatmap calc still look for // contour maps, because the makeBoundArray calls are too entangled module.exports = function calc(gd, trace) { - //console.trace('calc'); var i, carpet; for(i = 0; i < gd._fullData.length; i++) { @@ -41,7 +40,7 @@ module.exports = function calc(gd, trace) { if(!carpet) return; trace._carpet = carpet; - if (!trace.a || !trace.b) { + if(!trace.a || !trace.b) { // Look up the original incoming carpet data: var carpetdata = gd.data[carpet.index]; @@ -49,11 +48,11 @@ module.exports = function calc(gd, trace) { // copy so that we're not actually modifying it when we use it // to supply defaults: var tracedata = gd.data[trace.index]; - //var tracedata = extendFlat({}, gd.data[trace.index]); + // var tracedata = extendFlat({}, gd.data[trace.index]); // If the data is not specified - if (!tracedata.a) tracedata.a = carpetdata.a; - if (!tracedata.b) tracedata.b = carpetdata.b; + if(!tracedata.a) tracedata.a = carpetdata.a; + if(!tracedata.b) tracedata.b = carpetdata.b; supplyDefaults(tracedata, trace, null, gd._fullLayout); } @@ -209,7 +208,12 @@ function heatmappishCalc(gd, trace) { yIn = trace.ytype === 'scaled' ? '' : b, yArray = makeBoundArray(trace, yIn, b0, db, z.length, bax); - var cd0 = {a: xArray, b: yArray, z: z}; + var cd0 = { + a: xArray, + b: yArray, + z: z, + //mappedZ: mappedZ + }; // auto-z and autocolorscale if applicable colorscaleCalc(trace, z, '', 'z'); diff --git a/src/traces/contourcarpet/constants.js b/src/traces/contourcarpet/constants.js new file mode 100644 index 00000000000..123d6f45241 --- /dev/null +++ b/src/traces/contourcarpet/constants.js @@ -0,0 +1,15 @@ +/** +* 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'; + +module.exports = { + INEQUALITY_OPS: ['=', '<', '>=', '>', '<='], + INTERVAL_OPS: ['[]', '()', '[)', '(]', '][', ')(', '](', ')['], + SET_OPS: ['{}', '}{'] +}; diff --git a/src/traces/contourcarpet/constraint_mapping.js b/src/traces/contourcarpet/constraint_mapping.js new file mode 100644 index 00000000000..758b77b7498 --- /dev/null +++ b/src/traces/contourcarpet/constraint_mapping.js @@ -0,0 +1,90 @@ +/** +* 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'; + +var constants = require('./constants'); +var isNumeric = require('fast-isnumeric'); + +// This syntax conforms to the existing filter transform syntax, but we don't care +// about open vs. closed intervals for simply drawing contours constraints: +module.exports['[]'] = makeRangeSettings('[]'); +module.exports['()'] = makeRangeSettings('()'); +module.exports['[)'] = makeRangeSettings('[)'); +module.exports['(]'] = makeRangeSettings('(]'); + +// Inverted intervals simply flip the sign: +module.exports[']['] = makeRangeSettings(']['); +module.exports[')('] = makeRangeSettings(')('); +module.exports[')['] = makeRangeSettings(')['); +module.exports[']('] = makeRangeSettings(']('); + +module.exports['>'] = makeInequalitySettings('>'); +module.exports['>='] = makeInequalitySettings('>='); +module.exports['<'] = makeInequalitySettings('<'); +module.exports['<='] = makeInequalitySettings('<='); +module.exports['='] = makeInequalitySettings('='); + +// This does not in any way shape or form support calendars. It's adapted from +// transforms/filter.js. +function coerceValue(operation, value) { + var hasArrayValue = Array.isArray(value); + + var coercedValue; + + function coerce(value) { + return isNumeric(value) ? (+value) : null; + } + + if(constants.INEQUALITY_OPS.indexOf(operation) !== -1) { + coercedValue = hasArrayValue ? coerce(value[0]) : coerce(value); + } else if(constants.INTERVAL_OPS.indexOf(operation) !== -1) { + coercedValue = hasArrayValue ? + [coerce(value[0]), coerce(value[1])] : + [coerce(value), coerce(value)]; + } else if(constants.SET_OPS.indexOf(operation) !== -1) { + coercedValue = hasArrayValue ? value.map(coerce) : [coerce(value)]; + } + + return coercedValue; +} + +// Returns a parabola scaled so that the min/max is either +/- 1 and zero at the two values +// provided. The data is mapped by this function when constructing intervals so that it's +// very easy to construct contours as normal. +function makeRangeSettings(operation) { + return function(value) { + value = coerceValue(operation, value); + + // Ensure proper ordering: + var min = Math.min(value[0], value[1]); + var max = Math.max(value[0], value[1]); + + // Return a mapping function so that we don't have to repeat the math and sanitization + // for every point: + return { + start: min, + end: max, + size: max - min + }; + }; +} + +function makeInequalitySettings(operation) { + return function(value) { + value = coerceValue(operation, value); + + // Return a mapping function so that we don't have to repeat the math and sanitization + // for every point: + return { + start: value, + end: Infinity, + size: Infinity + }; + }; +} diff --git a/src/traces/contourcarpet/convert_to_constraints.js b/src/traces/contourcarpet/convert_to_constraints.js new file mode 100644 index 00000000000..ed449336572 --- /dev/null +++ b/src/traces/contourcarpet/convert_to_constraints.js @@ -0,0 +1,87 @@ +/** +* 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'; + +var Lib = require('../../lib'); + +// The contour extraction is great, except it totally fails for constraints because we +// need weird range loops and flipped contours instead of the usual format. This function +// does some weird manipulation of the extracted pathinfo data such that it magically +// draws contours correctly *as* constraints. +module.exports = function(pathinfo, constraints) { + var i, pi0, pi1; + + var op0 = function(arr) { return arr.reverse(); }; + var op1 = function(arr) { return arr; }; + + switch(constraints.operation) { + case '][': + case ')[': + case '](': + case ')(': + var tmp = op0; + op0 = op1; + op1 = tmp; + // It's a nice rule, except this definitely *is* what's intended here. + /* eslint-disable: no-fallthrough */ + case '[]': + case '[)': + case '(]': + case '()': + /* eslint-enable: no-fallthrough */ + if(pathinfo.length !== 2) { + Lib.warn('Contour data invalid for the specified inequality range operation.'); + return; + } + + // In this case there should be exactly two contour levels in pathinfo. We + // simply concatenate the info into one pathinfo and flip all of the data + // in one. This will draw the contour as closed. + pi0 = pathinfo[0]; + pi1 = pathinfo[1]; + + for(i = 0; i < pi0.edgepaths.length; i++) { + pi0.edgepaths[i] = op0(pi0.edgepaths[i]); + } + + for(i = 0; i < pi0.paths.length; i++) { + pi0.paths[i] = op0(pi0.paths[i]); + } + + while(pi1.edgepaths.length) { + pi0.edgepaths.push(op1(pi1.edgepaths.shift())); + } + while(pi1.paths.length) { + pi0.paths.push(op1(pi1.paths.shift())); + } + pathinfo.pop(); + + break; + case '>=': + case '>': + if(pathinfo.length !== 1) { + Lib.warn('Contour data invalid for the specified inequality operation.'); + return; + } + + // In this case there should be exactly two contour levels in pathinfo. We + // simply concatenate the info into one pathinfo and flip all of the data + // in one. This will draw the contour as closed. + pi0 = pathinfo[0]; + + for(i = 0; i < pi0.edgepaths.length; i++) { + pi0.edgepaths[i] = op0(pi0.edgepaths[i]); + } + + for(i = 0; i < pi0.paths.length; i++) { + pi0.paths[i] = op0(pi0.paths[i]); + } + break; + } +}; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 550dbffbd40..a604257f25f 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -15,9 +15,9 @@ var handleXYZDefaults = require('../heatmap/xyz_defaults'); var attributes = require('./attributes'); var hasColumns = require('../heatmap/has_columns'); var handleStyleDefaults = require('../contour/style_defaults'); +var constraintMapping = require('./constraint_mapping'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - //console.trace('defaults'); function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } @@ -39,7 +39,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // it requires modification to all of the functions below that expect the coerced // attribute name to match the property name -- except '_a' !== 'a' so that is not // straightforward. - if (traceIn.a && traceIn.b) { + if(traceIn.a && traceIn.b) { + var contourSize, contourStart, contourEnd, missingEnd, autoContour, constraint, map, op; + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); if(!len) { @@ -50,16 +52,31 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('connectgaps', hasColumns(traceOut)); - var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); - var contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); - var missingEnd = (contourStart === false) || (contourEnd === false); + op = coerce('contours.constraint.operation'); + coerce('contours.constraint.value'); + + if(op) { + constraint = traceOut.contours.constraint; + map = constraintMapping[constraint.operation](constraint.value); + + traceOut.contours.start = map.start; + traceOut.contours.end = map.end; + traceOut.contours.size = map.size; + traceOut.contours.constraint._map = map.fn; + traceOut._hasConstraint = true; + } else { + contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); + contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); + + // normally we only need size if autocontour is off. But contour.calc + // pushes its calculated contour size back to the input trace, so for + // things like restyle that can call supplyDefaults without calc + // after the initial draw, we can just reuse the previous calculation + contourSize = coerce('contours.size'); + } - // normally we only need size if autocontour is off. But contour.calc - // pushes its calculated contour size back to the input trace, so for - // things like restyle that can call supplyDefaults without calc - // after the initial draw, we can just reuse the previous calculation - var contourSize = coerce('contours.size'); - var autoContour; + missingEnd = (contourStart === false) || (contourEnd === false); + autoContour; if(missingEnd) { autoContour = traceOut.autocontour = true; @@ -72,5 +89,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } handleStyleDefaults(traceIn, traceOut, coerce, layout); + + if(constraint && constraint.operation === '=') { + traceOut.contours.coloring = 'none'; + } } }; diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index 2dae8e19c80..8f780a08c3a 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 79e0f5719c0..92226c2870b 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the @@ -17,6 +17,7 @@ var Drawing = require('../../components/drawing'); var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); var axisAlignedLine = require('../carpet/axis_aligned_line'); +var convertToConstraints = require('./convert_to_constraints'); function makeg(el, type, klass) { var join = el.selectAll(type + '.' + klass).data([0]); @@ -31,7 +32,6 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { - //console.trace('plotone'); var i, j, k, path; var trace = cd[0].trace; var carpet = trace._carpet; @@ -57,6 +57,7 @@ function plotOne(gd, plotinfo, cd) { makeCrossings(pathinfo); findAllPaths(pathinfo); + if(trace._hasConstraint) convertToConstraints(pathinfo, trace.contours.constraint); function ab2p(ab) { var pt = carpet.ab2xy(ab[0], ab[1], true); @@ -99,7 +100,7 @@ function plotOne(gd, plotinfo, cd) { // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, xa, ya, contours, carpet); + makeBackground(plotGroup, xa, ya, contours, carpet, trace._hasConstraint); makeFills(plotGroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya); makeLines(plotGroup, pathinfo, contours); clipBoundary(plotGroup, carpet); @@ -192,12 +193,12 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -function makeBackground(plotgroup, xaxis, yaxis, contours, carpet) { +function makeBackground(plotgroup, xaxis, yaxis, contours, carpet, hasConstraint) { var seg, xp, yp, i; var bggroup = makeg(plotgroup, 'g', 'contourbg'); var bgfill = bggroup.selectAll('path') - .data(contours.coloring === 'fill' ? [0] : []); + .data((contours.coloring === 'fill' && !hasConstraint) ? [0] : []); bgfill.enter().append('path'); bgfill.exit().remove(); diff --git a/src/traces/contourcarpet/style.js b/src/traces/contourcarpet/style.js index f8e6125f06f..7a2157c9ad8 100644 --- a/src/traces/contourcarpet/style.js +++ b/src/traces/contourcarpet/style.js @@ -24,12 +24,16 @@ module.exports = function style(gd) { }); contours.each(function(d) { - var c = d3.select(this), - trace = d.trace, - contours = trace.contours, - line = trace.line, - cs = contours.size || 1, - start = contours.start; + var c = d3.select(this); + var trace = d.trace; + var contours = trace.contours; + var line = trace.line; + var cs = contours.size || 1; + var start = contours.start; + + if(!isFinite(cs)) { + cs = 0; + } var colorMap = makeColorMap(trace); diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index daadb633f6c..1d3a6e6a2b6 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 1d516ba1749..f7c55c27853 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index f9c7a3fbfca..2bb68a90294 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/hover.js b/src/traces/scattercarpet/hover.js index c10aeb0f993..733437b0f14 100644 --- a/src/traces/scattercarpet/hover.js +++ b/src/traces/scattercarpet/hover.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index acc2939cb35..bf374aafd45 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/plot.js b/src/traces/scattercarpet/plot.js index 3b6b2ba0ad0..ebe356cf28e 100644 --- a/src/traces/scattercarpet/plot.js +++ b/src/traces/scattercarpet/plot.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/select.js b/src/traces/scattercarpet/select.js index bca550e040c..5682b0e1669 100644 --- a/src/traces/scattercarpet/select.js +++ b/src/traces/scattercarpet/select.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/traces/scattercarpet/style.js b/src/traces/scattercarpet/style.js index 25459e55e74..8ead87cc97e 100644 --- a/src/traces/scattercarpet/style.js +++ b/src/traces/scattercarpet/style.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the From acff10027168b72d6644a990d7dd6f1a0981dc89 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 14:29:54 -0500 Subject: [PATCH 042/132] Fix convertColumnData to default to original behavior --- src/traces/heatmap/convert_column_xyz.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 325d3941391..0a6b414d8f3 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -12,6 +12,10 @@ var Lib = require('../../lib'); module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, arrayVarNames) { + var1Name = var1Name || 'x'; + var2Name = var2Name || 'y'; + arrayVarNames = arrayVarNames || ['z']; + var col1 = trace[var1Name].slice(), col2 = trace[var2Name].slice(), textCol = trace.text, From 4088ee87ebf9a321e749b27c0e2a4d5fc656b8b1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 14:41:24 -0500 Subject: [PATCH 043/132] Fix bad plot schema attrs --- src/plots/cartesian/layout_attributes.js | 1 + src/plots/gl3d/layout/axis_attributes.js | 1 + src/traces/carpet/attributes.js | 5 +++-- src/traces/carpet/axis_attributes.js | 12 ++---------- src/traces/carpet/axis_defaults.js | 3 --- src/traces/contourcarpet/attributes.js | 2 ++ 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 134dd183b1e..22f15242c25 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -18,6 +18,7 @@ var constants = require('./constants'); module.exports = { visible: { valType: 'boolean', + role: 'info', description: [ 'A single toggle to hide the axis while preserving interaction like dragging.', 'Default is true when a cheater plot is present on the axis, otherwise', diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 18f9ca7d006..7d4d4bda6ca 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -16,6 +16,7 @@ var extendFlat = require('../../../lib/extend').extendFlat; module.exports = { visible: { valType: 'boolean', + role: 'info', description: [ 'A single toggle to hide the axis while preserving interaction like dragging.', 'Default is true when a cheater plot is present on the axis, otherwise', diff --git a/src/traces/carpet/attributes.js b/src/traces/carpet/attributes.js index 54dc153d603..dfc8ce34078 100644 --- a/src/traces/carpet/attributes.js +++ b/src/traces/carpet/attributes.js @@ -33,7 +33,7 @@ module.exports = { }, y: { valType: 'data_array', - descripotion: 'A two dimensional array of y coordinates at each carpet point.' + description: 'A two dimensional array of y coordinates at each carpet point.' }, a: { valType: 'data_array', @@ -63,7 +63,7 @@ module.exports = { }, b: { valType: 'data_array', - descripotion: 'A two dimensional array of y coordinates at each carpet point.' + description: 'A two dimensional array of y coordinates at each carpet point.' }, b0: { valType: 'number', @@ -87,6 +87,7 @@ module.exports = { }, cheaterslope: { valType: 'number', + role: 'info', dflt: 1, description: [ 'The shift applied to each successive row of data in creating a cheater plot.', diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index a3a515f8087..a097491b891 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -349,14 +349,6 @@ module.exports = { role: 'style', description: 'Sets a axis label suffix.' }, - showstartlabel: { - valType: 'boolean', - dflt: true, - }, - showendlabel: { - valType: 'boolean', - dflt: true, - }, labelfont: extendFlat({}, fontAttrs, { description: 'Sets the label font.' }), @@ -464,14 +456,14 @@ module.exports = { description: 'Sets the line color of the end line.' }, tick0: { - valType: 'any', + valType: 'integer', min: 0, dflt: 0, role: 'info', description: 'The starting index of grid lines along the axis' }, dtick: { - valType: 'any', + valType: 'integer', min: 1, dflt: 1, role: 'info', diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 8a93acb29af..1477d24284a 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -94,9 +94,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) // coerce('gridoffset'); // coerce('gridstep'); - coerce('showstartlabel'); - coerce('showendlabel'); - coerce('labelpadding'); containerOut._hovertitle = letter; diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 6aa0cddb3c8..4aca486e893 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -121,6 +121,7 @@ module.exports = extendFlat({}, { operation: { valType: 'enumerated', values: [].concat(constants.INEQUALITY_OPS).concat(constants.INTERVAL_OPS).concat(constants.SET_OPS), + role: 'info', description: [ 'Sets the filter operation.', @@ -149,6 +150,7 @@ module.exports = extendFlat({}, { value: { valType: 'any', dflt: 0, + role: 'info', description: [ 'Sets the value or values by which to filter by.', From 223d1207075fb732a00f5425c2dc3e570a936d8c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 14:43:55 -0500 Subject: [PATCH 044/132] Comment out bad tests --- test/jasmine/tests/carpet_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index 5d918ada97d..53ab4f0f625 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -39,7 +39,7 @@ describe('carpet supplyDefaults', function() { expect(traceOut.visible).toBe(false); }); - it('sets _cheater = true when x is provided', function() { + /*it('sets _cheater = true when x is provided', function() { traceIn = {y: [[1, 2], [3, 4]]}; supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut._cheater).toBe(true); @@ -49,7 +49,7 @@ describe('carpet supplyDefaults', function() { traceIn = {y: [[1, 2], [3, 4]], x: [[3, 4], [1, 2]]}; supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut._cheater).toBe(false); - }); + });*/ it('defaults to cheaterslope = 1', function() { traceIn = {y: [[1, 2], [3, 4]]}; From 5a11a85d668a2aae3e8b06c01f5b7940891ed68d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 15:17:18 -0500 Subject: [PATCH 045/132] Fix incosistent linter result? --- test/jasmine/tests/carpet_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index 53ab4f0f625..852dff38151 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -39,7 +39,7 @@ describe('carpet supplyDefaults', function() { expect(traceOut.visible).toBe(false); }); - /*it('sets _cheater = true when x is provided', function() { + /* it('sets _cheater = true when x is provided', function() { traceIn = {y: [[1, 2], [3, 4]]}; supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut._cheater).toBe(true); From 263d5dcfa566022046c1d82c36cd8fc941ab6b17 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 15:50:06 -0500 Subject: [PATCH 046/132] Fix header dates --- lib/carpet.js | 2 +- lib/scattercarpet.js | 2 +- src/lib/carpet.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/carpet.js b/lib/carpet.js index 0d63cd2e79d..83184629ebe 100644 --- a/lib/carpet.js +++ b/lib/carpet.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/lib/scattercarpet.js b/lib/scattercarpet.js index 238142c7b6f..7039812fd7c 100644 --- a/lib/scattercarpet.js +++ b/lib/scattercarpet.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the diff --git a/src/lib/carpet.js b/src/lib/carpet.js index 0d63cd2e79d..83184629ebe 100644 --- a/src/lib/carpet.js +++ b/src/lib/carpet.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the From 646255f2531df44bb318d82b5449818da7d841fb Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 18:35:03 -0500 Subject: [PATCH 047/132] Tweak cheater hiding logic --- src/plots/cartesian/layout_defaults.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 8ae32ac8f1c..c63911a7de5 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -27,7 +27,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { yaListCartesian = [], xaListGl2d = [], yaListGl2d = [], - xaListNotCheater = [], + xaListCheater = [], outerTicks = {}, noGrids = {}, i; @@ -58,11 +58,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // cheater (which is to say, visible by default): // 1. It's not in the carpet category at all // 2. Or if it is, then it's not a non-cheater carpet axis - // 3. And the axis isn't already marked non-cheater - if((!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) && - xaListNotCheater.indexOf(xaName) === -1 - ) { - xaListNotCheater.push(xaName); + if(Registry.traceIs(trace, 'carpet') && (!trace.type === 'carpet' || trace._cheater)) { + if(xaName) Lib.pushUnique(xaListCheater, xaName); } // add axes implied by traces @@ -152,7 +149,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { data: fullData, bgColor: bgColor, calendar: layoutOut.calendar, - cheateronly: axLetter === 'x' && xaListNotCheater.indexOf(axName) === -1 + cheateronly: axLetter === 'x' && xaListCheater.indexOf(axName) !== -1 }; handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); From ae0e3c867050a97a5edb83eb95d469dc9ce85777 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 6 Feb 2017 18:43:23 -0500 Subject: [PATCH 048/132] Add mising visibility attr to ternary axis duck-typing --- src/plots/ternary/ternary.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 547e427af99..96477b02f3e 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -202,6 +202,7 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // fictitious angles and domain, but then rotate and translate // it into place at the end var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, { + visible: true, range: [amin, sum - bmin - cmin], side: 'left', _counterangle: 30, @@ -222,6 +223,7 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // baxis goes across the bottom (backward). We can set it up as an x axis // without any enclosing transformation. var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, { + visible: true, range: [sum - amin - cmin, bmin], side: 'bottom', _counterangle: 30, @@ -241,6 +243,7 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // caxis goes down the right side. Set it up as a y axis, with // post-transformation similar to aaxis var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, { + visible: true, range: [sum - amin - bmin, cmin], side: 'right', _counterangle: 30, From 01b08033d397e4f82b341006d3a411ca4733e8bd Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 8 Feb 2017 14:04:37 -0500 Subject: [PATCH 049/132] Get rid of constraints subattr --- src/traces/contourcarpet/attributes.js | 120 ++++++++++-------- .../contourcarpet/convert_to_constraints.js | 4 +- src/traces/contourcarpet/defaults.js | 17 ++- src/traces/contourcarpet/plot.js | 11 +- 4 files changed, 82 insertions(+), 70 deletions(-) diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 4aca486e893..f49a6ccd133 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -66,6 +66,18 @@ module.exports = extendFlat({}, { }, contours: { + type: { + valType: 'enumerated', + values: ['levels', 'constraint'], + dflt: 'levels', + role: 'info', + description: [ + 'If `levels`, the data is represented as a contour plot with multiple', + 'levels displayed. If `constraint`, the data is represented as constraints', + 'with the invalid region shaded as specified by the `operation` and', + '`value` parameters.' + ].join(' ') + }, start: { valType: 'number', dflt: null, @@ -117,62 +129,60 @@ module.exports = extendFlat({}, { 'Has only an effect if `contours.coloring` is set to *fill*.' ].join(' ') }, - constraint: { - operation: { - valType: 'enumerated', - values: [].concat(constants.INEQUALITY_OPS).concat(constants.INTERVAL_OPS).concat(constants.SET_OPS), - role: 'info', - description: [ - 'Sets the filter operation.', - - '*=* keeps items equal to `value`', - - '*<* keeps items less than `value`', - '*<=* keeps items less than or equal to `value`', - - '*>* keeps items greater than `value`', - '*>=* keeps items greater than or equal to `value`', - - '*[]* keeps items inside `value[0]` to value[1]` including both bounds`', - '*()* keeps items inside `value[0]` to value[1]` excluding both bounds`', - '*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]', - '*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]', - - '*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`', - '*)(* keeps items outside `value[0]` to value[1]`', - '*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`', - '*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`', - - '*{}* keeps items present in a set of values', - '*}{* keeps items not present in a set of values' - ].join(' ') - }, - value: { - valType: 'any', - dflt: 0, - role: 'info', - description: [ - 'Sets the value or values by which to filter by.', - - 'Values are expected to be in the same type as the data linked', - 'to *target*.', - - 'When `operation` is set to one of the inequality values', - '(' + constants.INEQUALITY_OPS + ')', - '*value* is expected to be a number or a string.', - - 'When `operation` is set to one of the interval value', - '(' + constants.INTERVAL_OPS + ')', - '*value* is expected to be 2-item array where the first item', - 'is the lower bound and the second item is the upper bound.', - - 'When `operation`, is set to one of the set value', - '(' + constants.SET_OPS + ')', - '*value* is expected to be an array with as many items as', - 'the desired set elements.' - ].join(' ') - } + operation: { + valType: 'enumerated', + values: [].concat(constants.INEQUALITY_OPS).concat(constants.INTERVAL_OPS).concat(constants.SET_OPS), + role: 'info', + description: [ + 'Sets the filter operation.', + + '*=* keeps items equal to `value`', + + '*<* keeps items less than `value`', + '*<=* keeps items less than or equal to `value`', + + '*>* keeps items greater than `value`', + '*>=* keeps items greater than or equal to `value`', + + '*[]* keeps items inside `value[0]` to value[1]` including both bounds`', + '*()* keeps items inside `value[0]` to value[1]` excluding both bounds`', + '*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]', + '*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]', + + '*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`', + '*)(* keeps items outside `value[0]` to value[1]`', + '*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`', + '*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`', + + '*{}* keeps items present in a set of values', + '*}{* keeps items not present in a set of values' + ].join(' ') }, + value: { + valType: 'any', + dflt: 0, + role: 'info', + description: [ + 'Sets the value or values by which to filter by.', + + 'Values are expected to be in the same type as the data linked', + 'to *target*.', + + 'When `operation` is set to one of the inequality values', + '(' + constants.INEQUALITY_OPS + ')', + '*value* is expected to be a number or a string.', + + 'When `operation` is set to one of the interval value', + '(' + constants.INTERVAL_OPS + ')', + '*value* is expected to be 2-item array where the first item', + 'is the lower bound and the second item is the upper bound.', + + 'When `operation`, is set to one of the set value', + '(' + constants.SET_OPS + ')', + '*value* is expected to be an array with as many items as', + 'the desired set elements.' + ].join(' ') + } }, line: { diff --git a/src/traces/contourcarpet/convert_to_constraints.js b/src/traces/contourcarpet/convert_to_constraints.js index ed449336572..9105c73bd85 100644 --- a/src/traces/contourcarpet/convert_to_constraints.js +++ b/src/traces/contourcarpet/convert_to_constraints.js @@ -14,13 +14,13 @@ var Lib = require('../../lib'); // need weird range loops and flipped contours instead of the usual format. This function // does some weird manipulation of the extracted pathinfo data such that it magically // draws contours correctly *as* constraints. -module.exports = function(pathinfo, constraints) { +module.exports = function(pathinfo, operation) { var i, pi0, pi1; var op0 = function(arr) { return arr.reverse(); }; var op1 = function(arr) { return arr; }; - switch(constraints.operation) { + switch(operation) { case '][': case ')[': case '](': diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index a604257f25f..ee4262c5289 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -40,7 +40,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // attribute name to match the property name -- except '_a' !== 'a' so that is not // straightforward. if(traceIn.a && traceIn.b) { - var contourSize, contourStart, contourEnd, missingEnd, autoContour, constraint, map, op; + var contourSize, contourStart, contourEnd, missingEnd, autoContour, map; var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); @@ -52,18 +52,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); coerce('connectgaps', hasColumns(traceOut)); - op = coerce('contours.constraint.operation'); - coerce('contours.constraint.value'); + coerce('contours.type'); - if(op) { - constraint = traceOut.contours.constraint; - map = constraintMapping[constraint.operation](constraint.value); + if(traceOut.contours.type === 'constraint') { + coerce('contours.operation'); + coerce('contours.value'); + + map = constraintMapping[traceOut.contours.operation](traceOut.contours.value); traceOut.contours.start = map.start; traceOut.contours.end = map.end; traceOut.contours.size = map.size; - traceOut.contours.constraint._map = map.fn; - traceOut._hasConstraint = true; } else { contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); @@ -90,7 +89,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleStyleDefaults(traceIn, traceOut, coerce, layout); - if(constraint && constraint.operation === '=') { + if(traceOut.contours.type === 'constraint' && traceOut.contours.operation === '=') { traceOut.contours.coloring = 'none'; } } diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 92226c2870b..8f4e5c1b0f7 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -57,7 +57,10 @@ function plotOne(gd, plotinfo, cd) { makeCrossings(pathinfo); findAllPaths(pathinfo); - if(trace._hasConstraint) convertToConstraints(pathinfo, trace.contours.constraint); + + if(trace.contours.type === 'constraint') { + convertToConstraints(pathinfo, trace.contours.operation); + } function ab2p(ab) { var pt = carpet.ab2xy(ab[0], ab[1], true); @@ -100,7 +103,7 @@ function plotOne(gd, plotinfo, cd) { // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, xa, ya, contours, carpet, trace._hasConstraint); + makeBackground(plotGroup, xa, ya, contours, carpet, trace.contours.type === 'constraint'); makeFills(plotGroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya); makeLines(plotGroup, pathinfo, contours); clipBoundary(plotGroup, carpet); @@ -193,12 +196,12 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -function makeBackground(plotgroup, xaxis, yaxis, contours, carpet, hasConstraint) { +function makeBackground(plotgroup, xaxis, yaxis, contours, carpet, isConstraint) { var seg, xp, yp, i; var bggroup = makeg(plotgroup, 'g', 'contourbg'); var bgfill = bggroup.selectAll('path') - .data((contours.coloring === 'fill' && !hasConstraint) ? [0] : []); + .data((contours.coloring === 'fill' && !isConstraint) ? [0] : []); bgfill.enter().append('path'); bgfill.exit().remove(); From b708e327420115bcf3eb53307d3f723634564ce6 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 8 Feb 2017 14:49:45 -0500 Subject: [PATCH 050/132] Split everything into lots more files --- src/traces/contourcarpet/empty_pathinfo.js | 47 ++++ src/traces/contourcarpet/join_all_paths.js | 130 +++++++++++ src/traces/contourcarpet/map_pathinfo.js | 35 +++ src/traces/contourcarpet/plot.js | 252 ++++----------------- 4 files changed, 259 insertions(+), 205 deletions(-) create mode 100644 src/traces/contourcarpet/empty_pathinfo.js create mode 100644 src/traces/contourcarpet/join_all_paths.js create mode 100644 src/traces/contourcarpet/map_pathinfo.js diff --git a/src/traces/contourcarpet/empty_pathinfo.js b/src/traces/contourcarpet/empty_pathinfo.js new file mode 100644 index 00000000000..9dfd304f161 --- /dev/null +++ b/src/traces/contourcarpet/empty_pathinfo.js @@ -0,0 +1,47 @@ +/** +* 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'; + +var Lib = require('../../lib'); + +module.exports = function emptyPathinfo(contours, plotinfo, cd0) { + var cs = contours.size; + var pathinfo = []; + + var carpet = cd0.trace._carpet; + + for(var ci = contours.start; ci < contours.end + cs / 10; ci += cs) { + pathinfo.push({ + level: ci, + // all the cells with nontrivial marching index + crossings: {}, + // starting points on the edges of the lattice for each contour + starts: [], + // all unclosed paths (may have less items than starts, + // if a path is closed by rounding) + edgepaths: [], + // all closed paths + paths: [], + // store axes so we can convert to px + xaxis: carpet.aaxis, + yaxis: carpet.baxis, + // full data arrays to use for interpolation + x: cd0.a, + y: cd0.b, + z: cd0.z, + smoothing: cd0.trace.line.smoothing + }); + + if(pathinfo.length > 1000) { + Lib.warn('Too many contours, clipping at 1000', contours); + break; + } + } + return pathinfo; +}; diff --git a/src/traces/contourcarpet/join_all_paths.js b/src/traces/contourcarpet/join_all_paths.js new file mode 100644 index 00000000000..b665784f535 --- /dev/null +++ b/src/traces/contourcarpet/join_all_paths.js @@ -0,0 +1,130 @@ +/** +* 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'; + +var Drawing = require('../../components/drawing'); +var axisAlignedLine = require('../carpet/axis_aligned_line'); +var Lib = require('../../lib'); + +module.exports = function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { + var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? + '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'); + var i = 0; + var startsleft = pi.edgepaths.map(function(v, i) { return i; }); + var newloop = true; + var endpt, newendpt, cnt, nexti, possiblei, addpath; + + var atol = Math.abs(perimeter[0][0] - perimeter[2][0]) * 1e-8; + var btol = Math.abs(perimeter[0][1] - perimeter[2][1]) * 1e-8; + + function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < btol; } + function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < btol; } + function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < atol; } + function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < atol; } + + function pathto(pt0, pt1) { + var i, j, segments, axis; + var path = ''; + + if((istop(pt0) && !isright(pt0)) || (isbottom(pt0) && !isleft(pt0))) { + axis = carpet.aaxis; + segments = axisAlignedLine(carpet, [pt0[0], pt1[0]], 0.5 * (pt0[1] + pt1[1])); + } else { + axis = carpet.baxis; + segments = axisAlignedLine(carpet, 0.5 * (pt0[0] + pt1[0]), [pt0[1], pt1[1]]); + } + + for(i = 1; i < segments.length; i++) { + path += axis.smoothing ? 'C' : 'L'; + for(j = 0; j < segments[i].length; j++) { + var pt = segments[i][j]; + path += [xa.c2p(pt[0]), ya.c2p(pt[1])] + ' '; + } + } + return path; + } + + endpt = null; + while(startsleft.length) { + var startpt = pi.edgepaths[i][0]; + + if(endpt) { + fullpath += pathto(endpt, startpt); + } + + addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing); + fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); + startsleft.splice(startsleft.indexOf(i), 1); + endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; + nexti = -1; + + // now loop through sides, moving our endpoint until we find a new start + for(cnt = 0; cnt < 4; cnt++) { // just to prevent infinite loops + if(!endpt) { + Lib.log('Missing end?', i, pi); + break; + } + + if(istop(endpt) && !isright(endpt)) { + newendpt = perimeter[1]; // right top + } else if(isleft(endpt)) { + newendpt = perimeter[0]; // left top + } else if(isbottom(endpt)) { + newendpt = perimeter[3]; // right bottom + } else if(isright(endpt)) { + newendpt = perimeter[2]; // left bottom + } + + for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { + var ptNew = pi.edgepaths[possiblei][0]; + // is ptNew on the (horz. or vert.) segment from endpt to newendpt? + if(Math.abs(endpt[0] - newendpt[0]) < atol) { + if(Math.abs(endpt[0] - ptNew[0]) < atol && (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } else if(Math.abs(endpt[1] - newendpt[1]) < btol) { + if(Math.abs(endpt[1] - ptNew[1]) < btol && (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } else { + Lib.log('endpt to newendpt is not vert. or horz.', endpt, newendpt, ptNew); + } + } + + if(nexti >= 0) break; + fullpath += pathto(endpt, newendpt); + endpt = newendpt; + } + + if(nexti === pi.edgepaths.length) { + Lib.log('unclosed perimeter path'); + break; + } + + i = nexti; + + // if we closed back on a loop we already included, + // close it and start a new loop + newloop = (startsleft.indexOf(i) === -1); + if(newloop) { + i = startsleft[0]; + fullpath += pathto(endpt, newendpt) + 'Z'; + endpt = null; + } + } + + // finally add the interior paths + for(i = 0; i < pi.paths.length; i++) { + fullpath += Drawing.smoothclosed(pi.paths[i].map(ab2p), pi.smoothing); + } + + return fullpath; +}; diff --git a/src/traces/contourcarpet/map_pathinfo.js b/src/traces/contourcarpet/map_pathinfo.js new file mode 100644 index 00000000000..6dc5300835f --- /dev/null +++ b/src/traces/contourcarpet/map_pathinfo.js @@ -0,0 +1,35 @@ +/** +* 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'; + +module.exports = function mapPathinfo(pathinfo, map) { + var i, j, k, pi, pedgepaths, ppaths, pedgepath, ppath, path; + + for(i = 0; i < pathinfo.length; i++) { + pi = pathinfo[i]; + pedgepaths = pi.pedgepaths = []; + ppaths = pi.ppaths = []; + for(j = 0; j < pi.edgepaths.length; j++) { + path = pi.edgepaths[j]; + pedgepath = []; + for(k = 0; k < path.length; k++) { + pedgepath[k] = map(path[k]); + } + pedgepaths.push(pedgepath); + } + for(j = 0; j < pi.paths.length; j++) { + path = pi.paths[j]; + ppath = []; + for(k = 0; k < path.length; k++) { + ppath[k] = map(path[k]); + } + ppaths.push(ppath); + } + } +}; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 8f4e5c1b0f7..24cea224a07 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -11,13 +11,14 @@ var d3 = require('d3'); var map1dArray = require('../carpet/map_1d_array'); var makepath = require('../carpet/makepath'); -var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); -var axisAlignedLine = require('../carpet/axis_aligned_line'); var convertToConstraints = require('./convert_to_constraints'); +var joinAllPaths = require('./join_all_paths'); +var emptyPathinfo = require('./empty_pathinfo'); +var mapPathinfo = require('./map_pathinfo'); function makeg(el, type, klass) { var join = el.selectAll(type + '.' + klass).data([0]); @@ -32,13 +33,10 @@ module.exports = function plot(gd, plotinfo, cdcontours) { }; function plotOne(gd, plotinfo, cd) { - var i, j, k, path; var trace = cd[0].trace; var carpet = trace._carpet; var a = cd[0].a; var b = cd[0].b; - // var aa = carpet.aaxis; - // var ba = carpet.baxis; var contours = trace.contours; var uid = trace.uid; var xa = plotinfo.xaxis; @@ -46,67 +44,65 @@ function plotOne(gd, plotinfo, cd) { var fullLayout = gd._fullLayout; var id = 'contour' + uid; var pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + var isConstraint = trace.contours.type === 'constraint'; + + // Map [a, b] (data) --> [i, j] (pixels) + function ab2p(ab) { + var pt = carpet.ab2xy(ab[0], ab[1], true); + return [xa.c2p(pt[0]), ya.c2p(pt[1])]; + } if(trace.visible !== true) { - fullLayout._paper.selectAll('.' + id + ',.hm' + uid).remove(); fullLayout._infolayer.selectAll('.cb' + uid).remove(); return; } - fullLayout._paper.selectAll('.hm' + uid).remove(); - + // Extract the contour levels: makeCrossings(pathinfo); findAllPaths(pathinfo); + // Constraints might need to be draw inverted, which is not something contours + // handle by default since they're assumed fully opaque so that they can be + // drawn overlapping. This function flips the paths as necessary so that they're + // drawn correctly. + // + // TODO: Perhaps this should be generalized and *all* paths should be drawn as + // closed regions so that translucent contour levels would be valid. + // See: https://github.com/plotly/plotly.js/issues/1356 if(trace.contours.type === 'constraint') { convertToConstraints(pathinfo, trace.contours.operation); } - function ab2p(ab) { - var pt = carpet.ab2xy(ab[0], ab[1], true); - return [xa.c2p(pt[0]), ya.c2p(pt[1])]; - } - - for(i = 0; i < pathinfo.length; i++) { - var pi = pathinfo[i]; - var pedgepaths = pi.pedgepaths = []; - var ppaths = pi.ppaths = []; - for(j = 0; j < pi.edgepaths.length; j++) { - path = pi.edgepaths[j]; - var pedgepath = []; - for(k = 0; k < path.length; k++) { - pedgepath[k] = ab2p(path[k]); - } - pedgepaths.push(pedgepath); - } - for(j = 0; j < pi.paths.length; j++) { - path = pi.paths[j]; - var ppath = []; - for(k = 0; k < path.length; k++) { - ppath[k] = ab2p(path[k]); - } - ppaths.push(ppath); - } - } + // Map the paths in a/b coordinates to pixel coordinates: + mapPathinfo(pathinfo, ab2p); - // Mark the perimeter in a-b coordinates: - var leftedge = a[0]; - var rightedge = a[a.length - 1]; - var bottomedge = b[0]; - var topedge = b[b.length - 1]; + // Define the perimeter in a/b coordinates: var perimeter = [ - [leftedge, topedge], - [rightedge, topedge], - [rightedge, bottomedge], - [leftedge, bottomedge] + [a[0], b[b.length - 1]], + [a[a.length - 1], b[b.length - 1]], + [a[a.length - 1], b[0]], + [a[0], b[0]] ]; // draw everything var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, xa, ya, contours, carpet, trace.contours.type === 'constraint'); - makeFills(plotGroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya); + + // Draw the baseline background fill that fills in the space behind any other + // contour levels: + makeBackground(plotGroup, xa, ya, carpet._clipsegments, isConstraint, contours.coloring); + + // Draw the specific contour fills. As a simplification, they're assumed to be + // fully opaque so that it's easy to draw them simply overlapping. The alternative + // would be to flip adjacent paths and draw closed paths for each level instead. + makeFills(plotGroup, xa, ya, pathinfo, perimeter, ab2p, carpet, contours.coloring); + + // Draw contour lines: makeLines(plotGroup, pathinfo, contours); + + // Clip the boundary of the plot: clipBoundary(plotGroup, carpet); + + // Clip the space around null/undefined/missing data points: // clipGaps(plotGroup, plotinfo, cd[0], perimeter); } @@ -114,41 +110,6 @@ function clipBoundary(plotGroup, carpet) { plotGroup.attr('clip-path', 'url(#' + carpet.clipPathId + ')'); } -function emptyPathinfo(contours, plotinfo, cd0) { - var cs = contours.size; - var pathinfo = []; - - var carpet = cd0.trace._carpet; - - for(var ci = contours.start; ci < contours.end + cs / 10; ci += cs) { - pathinfo.push({ - level: ci, - // all the cells with nontrivial marching index - crossings: {}, - // starting points on the edges of the lattice for each contour - starts: [], - // all unclosed paths (may have less items than starts, - // if a path is closed by rounding) - edgepaths: [], - // all closed paths - paths: [], - // store axes so we can convert to px - xaxis: carpet.aaxis, - yaxis: carpet.baxis, - // full data arrays to use for interpolation - x: cd0.a, - y: cd0.b, - z: cd0.z, - smoothing: cd0.trace.line.smoothing - }); - - if(pathinfo.length > 1000) { - Lib.warn('Too many contours, clipping at 1000', contours); - break; - } - } - return pathinfo; -} function makeContourGroup(plotinfo, cd, id) { var plotgroup = plotinfo.plot.select('.maplayer') .selectAll('g.contour.' + id) @@ -196,20 +157,18 @@ function makeLines(plotgroup, pathinfo, contours) { .style('stroke-miterlimit', 1); } -function makeBackground(plotgroup, xaxis, yaxis, contours, carpet, isConstraint) { +function makeBackground(plotgroup, clipsegments, xaxis, yaxis, isConstraint, coloring) { var seg, xp, yp, i; var bggroup = makeg(plotgroup, 'g', 'contourbg'); var bgfill = bggroup.selectAll('path') - .data((contours.coloring === 'fill' && !isConstraint) ? [0] : []); + .data((coloring === 'fill' && !isConstraint) ? [0] : []); bgfill.enter().append('path'); bgfill.exit().remove(); - var segments = carpet._clipsegments; var segs = []; - - for(i = 0; i < segments.length; i++) { - seg = segments[i]; + for(i = 0; i < clipsegments.length; i++) { + seg = clipsegments[i]; xp = map1dArray([], seg.x, xaxis.c2p); yp = map1dArray([], seg.y, yaxis.c2p); segs.push(makepath(xp, yp, seg.bicubic)); @@ -220,14 +179,14 @@ function makeBackground(plotgroup, xaxis, yaxis, contours, carpet, isConstraint) .style('stroke', 'none'); } -function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p, carpet, xa, ya) { +function makeFills(plotgroup, xa, ya, pathinfo, perimeter, ab2p, carpet, coloring) { var fillgroup = plotgroup.selectAll('g.contourfill') .data([0]); fillgroup.enter().append('g') .classed('contourfill', true); var fillitems = fillgroup.selectAll('path') - .data(contours.coloring === 'fill' ? pathinfo : []); + .data(coloring === 'fill' ? pathinfo : []); fillitems.enter().append('path'); fillitems.exit().remove(); fillitems.each(function(pi) { @@ -243,123 +202,6 @@ function makeFills(plotgroup, pathinfo, perimeter, contours, ab2p, carpet, xa, y }); } -function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { - var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? - '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'); - var i = 0; - var startsleft = pi.edgepaths.map(function(v, i) { return i; }); - var newloop = true; - var endpt, newendpt, cnt, nexti, possiblei, addpath; - - var atol = Math.abs(perimeter[0][0] - perimeter[2][0]) * 1e-8; - var btol = Math.abs(perimeter[0][1] - perimeter[2][1]) * 1e-8; - - function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < btol; } - function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < btol; } - function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < atol; } - function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < atol; } - - function pathto(pt0, pt1) { - var i, j, segments, axis; - var path = ''; - - if((istop(pt0) && !isright(pt0)) || (isbottom(pt0) && !isleft(pt0))) { - axis = carpet.aaxis; - segments = axisAlignedLine(carpet, [pt0[0], pt1[0]], 0.5 * (pt0[1] + pt1[1])); - } else { - axis = carpet.baxis; - segments = axisAlignedLine(carpet, 0.5 * (pt0[0] + pt1[0]), [pt0[1], pt1[1]]); - } - - for(i = 1; i < segments.length; i++) { - path += axis.smoothing ? 'C' : 'L'; - for(j = 0; j < segments[i].length; j++) { - var pt = segments[i][j]; - path += [xa.c2p(pt[0]), ya.c2p(pt[1])] + ' '; - } - } - return path; - } - - endpt = null; - while(startsleft.length) { - var startpt = pi.edgepaths[i][0]; - - if(endpt) { - fullpath += pathto(endpt, startpt); - } - - addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing); - fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); - startsleft.splice(startsleft.indexOf(i), 1); - endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1]; - nexti = -1; - - // now loop through sides, moving our endpoint until we find a new start - for(cnt = 0; cnt < 4; cnt++) { // just to prevent infinite loops - if(!endpt) { - Lib.log('Missing end?', i, pi); - break; - } - - if(istop(endpt) && !isright(endpt)) { - newendpt = perimeter[1]; // right top - } else if(isleft(endpt)) { - newendpt = perimeter[0]; // left top - } else if(isbottom(endpt)) { - newendpt = perimeter[3]; // right bottom - } else if(isright(endpt)) { - newendpt = perimeter[2]; // left bottom - } - - for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) { - var ptNew = pi.edgepaths[possiblei][0]; - // is ptNew on the (horz. or vert.) segment from endpt to newendpt? - if(Math.abs(endpt[0] - newendpt[0]) < atol) { - if(Math.abs(endpt[0] - ptNew[0]) < atol && (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } else if(Math.abs(endpt[1] - newendpt[1]) < btol) { - if(Math.abs(endpt[1] - ptNew[1]) < btol && (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } else { - Lib.log('endpt to newendpt is not vert. or horz.', endpt, newendpt, ptNew); - } - } - - if(nexti >= 0) break; - fullpath += pathto(endpt, newendpt); - endpt = newendpt; - } - - if(nexti === pi.edgepaths.length) { - Lib.log('unclosed perimeter path'); - break; - } - - i = nexti; - - // if we closed back on a loop we already included, - // close it and start a new loop - newloop = (startsleft.indexOf(i) === -1); - if(newloop) { - i = startsleft[0]; - fullpath += pathto(endpt, newendpt) + 'Z'; - endpt = null; - } - } - - // finally add the interior paths - for(i = 0; i < pi.paths.length; i++) { - fullpath += Drawing.smoothclosed(pi.paths[i].map(ab2p), pi.smoothing); - } - - return fullpath; -} - /* function clipGaps(plotGroup, plotinfo, cd0, perimeter) { var clipId = 'clip' + cd0.trace.uid; From 9d99328f04efde0acd3623f3f04af5c215965cc6 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 8 Feb 2017 18:02:04 -0500 Subject: [PATCH 051/132] A/B axis titles --- src/traces/carpet/axis_attributes.js | 9 +++ src/traces/carpet/axis_defaults.js | 4 +- src/traces/carpet/measure_text.js | 20 ++++++ src/traces/carpet/orient_text.js | 31 ++++++++ src/traces/carpet/plot.js | 102 +++++++++++++++++++-------- src/traces/carpet/set_convert.js | 85 ++++++++++++++-------- 6 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 src/traces/carpet/measure_text.js create mode 100644 src/traces/carpet/orient_text.js diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index a097491b891..e821060db27 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -40,6 +40,15 @@ module.exports = { 'Sets this axis\' title font.' ].join(' ') }), + titleoffset: { + valType: 'number', + role: 'info', + dflt: 10, + description: [ + 'An additional amount by which to offset the title from the tick', + 'labels, given in pixels' + ].join(' '), + }, type: { valType: 'enumerated', // '-' means we haven't yet run autotype or couldn't find any data diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 1477d24284a..652a033d885 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -117,6 +117,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) size: Math.round(font.size * 1.2), color: dfltFontColor }); + coerce('titleoffset'); Lib.coerceFont(coerce, 'tickfont', { family: font.family, @@ -185,7 +186,8 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) // We'll never draw this. We just need a couple category management functions. Lib.coerceFont(coerce, 'labelfont', { - size: 12, + family: font.family, + size: font.size, color: containerOut.startlinecolor }); diff --git a/src/traces/carpet/measure_text.js b/src/traces/carpet/measure_text.js new file mode 100644 index 00000000000..2da83fbcd0e --- /dev/null +++ b/src/traces/carpet/measure_text.js @@ -0,0 +1,20 @@ +/** +* 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'; + +var Drawing = require('../../components/drawing'); + +module.exports = function measureText(tester, text, font) { + var dummyText = tester.append('text') + .text(text) + .call(Drawing.font, font); + + return Drawing.bBox(dummyText.node()); +}; diff --git a/src/traces/carpet/orient_text.js b/src/traces/carpet/orient_text.js new file mode 100644 index 00000000000..969a473455c --- /dev/null +++ b/src/traces/carpet/orient_text.js @@ -0,0 +1,31 @@ +/** +* 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'; + +module.exports = function orientText(carpet, xaxis, yaxis, xy, dxy) { + var dx = dxy[0] * carpet.dpdx(xaxis); + var dy = dxy[1] * carpet.dpdy(yaxis); + var flip = 1; + + var angle = Math.atan2(dy, dx) * 180 / Math.PI; + if(angle < -90) { + angle += 180; + flip = -flip; + } else if(angle > 90) { + angle -= 180; + flip = -flip; + } + + return { + angle: angle, + flip: flip, + p: carpet.c2p(xy, xaxis, yaxis) + }; +}; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index d4277e06c5c..dee53e267de 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -10,10 +10,11 @@ 'use strict'; var d3 = require('d3'); - var Drawing = require('../../components/drawing'); var map1dArray = require('./map_1d_array'); var makepath = require('./makepath'); +var orientText = require('./orient_text'); +var measureText = require('./measure_text'); module.exports = function plot(gd, plotinfo, cdcarpet) { for(var i = 0; i < cdcarpet.length; i++) { @@ -61,8 +62,10 @@ function plotOne(gd, plotinfo, cd) { drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); - drawAxisLabels(xa, ya, trace, labelLayer, aax._labels, 'a-label'); - drawAxisLabels(xa, ya, trace, labelLayer, bax._labels, 'b-label'); + var maxAExtent = drawAxisLabels(gd._tester, xa, ya, trace, labelLayer, aax._labels, 'a-label'); + var maxBExtent = drawAxisLabels(gd._tester, xa, ya, trace, labelLayer, bax._labels, 'b-label'); + + drawAxisTitles(labelLayer, trace, xa, ya, maxAExtent, maxBExtent); // Swap for debugging in order to draw directly: // drawClipPath(trace, gridLayer, xa, ya); @@ -126,42 +129,83 @@ function drawGridLines(xaxis, yaxis, layer, axis, axisLetter, gridlines) { gridJoin.exit().remove(); } -function drawAxisLabels(xaxis, yaxis, trace, layer, labels, labelClass) { +function drawAxisLabels(tester, xaxis, yaxis, trace, layer, labels, labelClass) { var labelJoin = layer.selectAll('text.' + labelClass).data(labels); labelJoin.enter().append('text') .classed(labelClass, true); + var maxExtent = 0; + labelJoin.each(function(label) { - // The rest of the calculation is in calc_labels. Only the parts that depend upon + // Most of the positioning is done in calc_labels. Only the parts that depend upon // the screen space representation of the x and y axes are here: - // - // Compute the direction of the labels in pixel coordinates: - var dx = label.dxy[0] * trace.dpdx(xaxis); - var dy = label.dxy[1] * trace.dpdy(yaxis); - - // Compute the angle and adjust so that the labels are always upright - // and the anchor is on the correct side: - var angle = Math.atan2(dy, dx) * 180 / Math.PI; - var endAnchor = label.endAnchor; - if(angle < -90) { - angle += 180; - endAnchor = !endAnchor; - } else if(angle > 90) { - angle -= 180; - endAnchor = !endAnchor; - } - - // Compute the position in pixel coordinates - var xy = trace.c2p(label.xy, xaxis, yaxis); - - d3.select(this).attr('x', xy[0] + label.axis.labelpadding * (endAnchor ? -1 : 1)) - .attr('y', xy[1] + label.font.size * 0.3) - .attr('text-anchor', endAnchor ? 'end' : 'start') + var orientation = orientText(trace, xaxis, yaxis, label.xy, label.dxy); + var direction = (label.endAnchor ? -1 : 1) * orientation.flip; + var bbox = measureText(tester, label.text, label.font); + + d3.select(this) + .attr('text-anchor', direction > 0 ? 'start' : 'end') .text(label.text) - .attr('transform', 'rotate(' + angle + ' ' + xy[0] + ',' + xy[1] + ')') + .attr('transform', + // Translate to the correct point: + 'translate(' + orientation.p[0] + ',' + orientation.p[1] + ') ' + + // Rotate to line up with grid line tangent: + 'rotate(' + orientation.angle + ')' + + // Adjust the baseline and indentation: + 'translate(' + label.axis.labelpadding * direction + ',' + bbox.height * 0.3 + ')' + ) .call(Drawing.font, label.font.family, label.font.size, label.font.color); + + maxExtent = Math.max(maxExtent, bbox.width + label.axis.labelpadding); }); labelJoin.exit().remove(); + + return maxExtent; +} + +function drawAxisTitles(layer, trace, xa, ya, maxAExtent, maxBExtent) { + var a, b, xy, dxy; + + a = 0.5 * (trace.a[0] + trace.a[trace.a.length - 1]); + b = trace.b[0]; + xy = trace.ab2xy(a, b, true); + dxy = trace.dxyda_rough(a, b); + drawAxisTitle(layer, trace, xy, dxy, trace.aaxis, xa, ya, maxAExtent, 'a-title'); + + a = trace.a[0]; + b = 0.5 * (trace.b[0] + trace.b[trace.b.length - 1]); + xy = trace.ab2xy(a, b, true); + dxy = trace.dxydb_rough(a, b); + drawAxisTitle(layer, trace, xy, dxy, trace.baxis, xa, ya, maxBExtent, 'b-title'); +} + +function drawAxisTitle(layer, trace, xy, dxy, axis, xa, ya, offset, labelClass) { + var titleJoin = layer.selectAll('text.' + labelClass).data([0]); + + titleJoin.enter().append('text') + .classed(labelClass, true); + + var orientation = orientText(trace, xa, ya, xy, dxy); + + // In addition to the size of the labels, add on some extra padding: + offset += axis.titlefont.size + axis.titleoffset; + + // There's only one, but we'll do it as a join so it's updated nicely: + titleJoin.each(function() { + var el = d3.select(this); + + el.text(axis.title) + .attr('transform', + 'translate(' + orientation.p[0] + ',' + orientation.p[1] + ') ' + + 'rotate(' + orientation.angle + ') ' + + 'translate(0,' + offset + ')' + ) + .classed('user-select-none', true) + .attr('text-anchor', 'middle') + .call(Drawing.font, axis.titlefont); + }); + + titleJoin.exit().remove(); } diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index ab54c24ec43..203886b177a 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -34,6 +34,34 @@ module.exports = function setConvert(trace) { var x = trace.x; var y = trace.y; + // Grab the limits once rather than recomputing the bounds for every point + // independently: + var amin = a[0]; + var amax = a[na - 1]; + var bmin = b[0]; + var bmax = b[nb - 1]; + var arange = a[a.length - 1] - a[0]; + var brange = b[b.length - 1] - b[0]; + + // Compute the tolerance so that points are visible slightly outside the + // defined carpet axis: + var atol = arange * constants.RELATIVE_CULL_TOLERANCE; + var btol = brange * constants.RELATIVE_CULL_TOLERANCE; + + // Expand the limits to include the relative tolerance: + amin -= atol; + amax += atol; + bmin -= btol; + bmax += btol; + + trace.isVisible = function(a, b) { + return a > amin && a < amax && b > bmin && b < bmax; + }; + + trace.isOccluded = function(a, b) { + return a < amin || a > amax || b < bmin || b > bmax; + }; + // XXX: ONLY PASSTHRU. ONLY. No, ONLY. aax.c2p = function(v) { return v; }; bax.c2p = function(v) { return v; }; @@ -116,7 +144,10 @@ module.exports = function setConvert(trace) { var pt = trace._evalxy([], i, j); if(extrapolate) { - // The amount by which to extrapolate: + // This section uses the boundary derivatives to extrapolate linearly outside + // the defined range. Consider a scatter line with one point inside the carpet + // axis and one point outside. If we don't extrapolate, we can't draw the line + // at all. var iex = 0; var jex = 0; var der = []; @@ -216,38 +247,36 @@ module.exports = function setConvert(trace) { return [dxydj[0] / dbdj, dxydj[1] / dbdj]; }; - trace.dpdx = function(xa) { - return xa._m; - }; - - trace.dpdy = function(ya) { - return ya._m; + // Sometimes we don't care about precision and all we really want is decent rough + // directions (as is the case with labels). In that case, we can do a very rough finite + // difference and spare having to worry about precise grid coordinates: + trace.dxyda_rough = function(a, b, reldiff) { + var h = arange * (reldiff || 0.1); + var plus = trace.ab2xy(a + h, b, true); + var minus = trace.ab2xy(a - h, b, true); + + return [ + (plus[0] - minus[0]) * 0.5 / h, + (plus[1] - minus[1]) * 0.5 / h + ]; }; + trace.dxydb_rough = function(a, b, reldiff) { + var h = brange * (reldiff || 0.1); + var plus = trace.ab2xy(a, b + h, true); + var minus = trace.ab2xy(a, b - h, true); - // Grab the limits once rather than recomputing the bounds for every point - // independently: - var amin = a[0]; - var amax = a[na - 1]; - var bmin = b[0]; - var bmax = b[nb - 1]; - - // Compute the tolerance so that points are visible slightly outside the - // defined carpet axis: - var atol = (amax - amin) * constants.RELATIVE_CULL_TOLERANCE; - var btol = (bmax - bmin) * constants.RELATIVE_CULL_TOLERANCE; - - // Expand the limits to include the relative tolerance: - amin -= atol; - amax += atol; - bmin -= btol; - bmax += btol; + return [ + (plus[0] - minus[0]) * 0.5 / h, + (plus[1] - minus[1]) * 0.5 / h + ]; + }; - trace.isVisible = function(a, b) { - return a > amin && a < amax && b > bmin && b < bmax; + trace.dpdx = function(xa) { + return xa._m; }; - trace.isOccluded = function(a, b) { - return a < amin || a > amax || b < bmin || b > bmax; + trace.dpdy = function(ya) { + return ya._m; }; }; From e09b97a0dd153114a079584112424d16cdab513c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 9 Feb 2017 12:18:31 -0500 Subject: [PATCH 052/132] Add cheater baseline --- test/image/baselines/cheater.png | Bin 0 -> 59118 bytes test/image/mocks/cheater.json | 758 +++++++------------------------ 2 files changed, 156 insertions(+), 602 deletions(-) create mode 100644 test/image/baselines/cheater.png diff --git a/test/image/baselines/cheater.png b/test/image/baselines/cheater.png new file mode 100644 index 0000000000000000000000000000000000000000..5e2b699387ebd3325e875ee979f122430eae338f GIT binary patch literal 59118 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/cheater.json b/test/image/mocks/cheater.json index a4343897a13..4bc55cd7b64 100644 --- a/test/image/mocks/cheater.json +++ b/test/image/mocks/cheater.json @@ -1,605 +1,159 @@ { - "data": [ - { - "na":7, - "nb":7, - "apower":2, - "bpower":2, - "carpetid":"mycarpetplot", - "cheaterslope":1, - "type":"carpet", - "aaxis":{ - "tickmode":"linear", - "tick0":0.2, - "dtick":1, - "arraytick0":0, - "arraydtick":1, - "smoothing":true, - "cheatertype":"value", - "showlabels":"both", - "showlabelprefix":"first", - "labelpadding":10, - "labelsuffix":"", - "labelfont":{ - "color":"#c53", - "size":12, - "family":"sans-serif" - }, - "startline":true, - "endline":true, - "startlinecolor":"#3333bb", - "endlinecolor":"#3333bb", - "gridoffset":0, - "gridstep":1, - "gridwidth":1, - "startlinewidth":1.5, - "endlinewidth":1.5, - "gridcolor":"#aaaaaa", - "minorgridcount":3, - "minorgridwidth":1, - "minorgridcolor":"#eeeeee" - }, - "baxis":{ - "tickmode":"linear", - "tick0":0.2, - "dtick":1, - "arraytick0":0, - "arraydtick":1, - "smoothing":true, - "cheatertype":"value", - "showlabels":"both", - "showlabelprefix":"all", - "labelpadding":10, - "labelsuffix":"m", - "labelfont":{ - "color":"#35c", - "size":12, - "family":"sans-serif" - }, - "startline":true, - "endline":true, - "startlinecolor":"#bb3333", - "endlinecolor":"#bb3333", - "gridoffset":0, - "gridstep":1, - "gridwidth":1, - "gridcolor":"#aaaaaa", - "startlinewidth":1.5, - "endlinewidth":1.5, - "minorgridcount":3, - "minorgridwidth":1, - "minorgridcolor":"#eeeeee", - "showstartlabel":true, - "showendlabel":false - }, - "a":[ - 0.2, - 0.44699699699699696, - 1.061861861861862, - 2.044594594594595, - 3.3951951951951957, - 5.113663663663665, - 7.2 - ], - "b":[ - 0.2, - 0.3323529411764705, - 0.6117647058823528, - 1.0382352941176467, - 1.6117647058823528, - 2.3323529411764703, - 3.2 - ], - "y":[ - [ - 1.071599562910793, - 1.1056643965530542, - 1.1605672651223444, - 1.217512515023184, - 1.2697068368119195, - 1.3163253288127295, - 1.3580160561518513 - ], - [ - 1.1306379427088766, - 1.1507886541512502, - 1.1909917237025123, - 1.2398504081629311, - 1.2883843506259547, - 1.3334075552793436, - 1.3744318398547064 - ], - [ - 1.220086516635068, - 1.231396000997655, - 1.258023789415197, - 1.2964823759581592, - 1.3393975096077138, - 1.3818805265561276, - 1.4219727522026016 - ], - [ - 1.2994464793201583, - 1.3082723950290278, - 1.3302386024452697, - 1.3643476366941656, - 1.404784011385966, - 1.446421874011667, - 1.4866434488491957 - ], - [ - 1.3659967488521796, - 1.3741784751599952, - 1.3949299510208388, - 1.4280304002806903, - 1.4682498701077298, - 1.510386823713694, - 1.5515307826639397 - ], - [ - 1.4226785829614574, - 1.4307498681967172, - 1.451377075219899, - 1.484647705872809, - 1.525504349621373, - 1.5686385550747566, - 1.6109623176754218 - ], - [ - 1.471999262687472, - 1.4801361241953204, - 1.5010041734554476, - 1.5348399159892836, - 1.5766012541906476, - 1.6208548037635522, - 1.6643807301424949 - ] - ], - "cheatersope":1 - }, - { - "carpetid":"mycarpetplot", - "type":"scattercarpet", - "a":[ - 0.2, - 0.27070707070707073, - 0.34141414141414145, - 0.4121212121212121, - 0.4828282828282829, - 0.5535353535353535, - 0.6242424242424243, - 0.694949494949495, - 0.7656565656565657, - 0.8363636363636364, - 0.9070707070707071, - 0.9777777777777776, - 1.0484848484848486, - 1.1191919191919193, - 1.1898989898989898, - 1.2606060606060605, - 1.3313131313131314, - 1.402020202020202, - 1.4727272727272727, - 1.5434343434343434, - 1.614141414141414, - 1.6848484848484848, - 1.7555555555555553, - 1.8262626262626263, - 1.896969696969697, - 1.9676767676767677, - 2.038383838383839, - 2.109090909090909, - 2.17979797979798, - 2.250505050505051, - 2.3212121212121213, - 2.3919191919191922, - 2.462626262626263, - 2.533333333333333, - 2.604040404040404, - 2.674747474747475, - 2.7454545454545456, - 2.8161616161616165, - 2.886868686868687, - 2.9575757575757575, - 3.0282828282828285, - 3.098989898989899, - 3.16969696969697, - 3.240404040404041, - 3.311111111111111, - 3.381818181818182, - 3.4525252525252528, - 3.5232323232323233, - 3.593939393939394, - 3.664646464646465, - 3.7353535353535356, - 3.806060606060606, - 3.8767676767676775, - 3.9474747474747476, - 4.018181818181818, - 4.0888888888888895, - 4.1595959595959595, - 4.2303030303030305, - 4.301010101010101, - 4.3717171717171714, - 4.442424242424242, - 4.513131313131313, - 4.583838383838384, - 4.654545454545454, - 4.725252525252526, - 4.795959595959596, - 4.866666666666666, - 4.937373737373738, - 5.008080808080808, - 5.078787878787879, - 5.14949494949495, - 5.22020202020202, - 5.290909090909091, - 5.361616161616162, - 5.432323232323233, - 5.503030303030303, - 5.573737373737374, - 5.644444444444445, - 5.715151515151515, - 5.785858585858587, - 5.856565656565657, - 5.927272727272728, - 5.997979797979798, - 6.068686868686869, - 6.13939393939394, - 6.210101010101011, - 6.2808080808080815, - 6.351515151515152, - 6.422222222222222, - 6.492929292929293, - 6.5636363636363635, - 6.634343434343434, - 6.705050505050505, - 6.775757575757576, - 6.846464646464646, - 6.917171717171717, - 6.987878787878788, - 7.058585858585858, - 7.12929292929293, - 7.2 - ], - "b":[ - 10, - 9.580757065605551, - 9.17151311090705, - 8.7722681359045, - 8.383022140597898, - 8.003775124987246, - 7.634527089072545, - 7.275278032853791, - 6.926027956330986, - 6.586776859504133, - 6.257524742373229, - 5.938271604938272, - 5.6290174471992644, - 5.329762269156209, - 5.040506070809102, - 4.761248852157944, - 4.491990613202734, - 4.2327313539434765, - 3.9834710743801662, - 3.7442097745128056, - 3.5149474543413946, - 3.2956841138659327, - 3.086419753086421, - 2.8871543720028576, - 2.697887970615244, - 2.51862054892358, - 2.3493521069278636, - 2.1900826446280997, - 2.0408121620242836, - 1.9015406591164163, - 1.7722681359044996, - 1.6529945923885316, - 1.5437200285685129, - 1.4444444444444449, - 1.355167840016325, - 1.2758902152841545, - 1.2066115702479339, - 1.1473319049076625, - 1.0980512192633405, - 1.058769513314968, - 1.0294867870625446, - 1.010203040506071, - 1.0009182736455464, - 1.0016324864809714, - 1.0123456790123455, - 1.0330578512396693, - 1.0637690031629425, - 1.104479134782165, - 1.155188246097337, - 1.2158963371084586, - 1.2866034078155293, - 1.367309458218549, - 1.4580144883175195, - 1.5587184981124373, - 1.6694214876033047, - 1.7901234567901243, - 1.92082440567289, - 2.061524334251607, - 2.2122232425262736, - 2.372921130496887, - 2.543617998163452, - 2.724313845525967, - 2.915008672584431, - 3.1157024793388417, - 3.3263952657892073, - 3.5470870319355168, - 3.777777777777776, - 4.01846750331599, - 4.2691562085501475, - 4.529843893480257, - 4.800530558106317, - 5.0812162024283225, - 5.37190082644628, - 5.672584430160188, - 5.983267013570045, - 6.3039485766758485, - 6.634629119477604, - 6.975308641975309, - 7.32598714416896, - 7.686664626058569, - 8.057341087644119, - 8.43801652892562, - 8.82869094990307, - 9.229364350576471, - 9.640036730945821, - 10.060708091011122, - 10.491378430772373, - 10.932047750229568, - 11.382716049382712, - 11.843383328231814, - 12.314049586776857, - 12.794714825017854, - 13.285379042954801, - 13.786042240587697, - 14.296704417916537, - 14.817365574941332, - 15.348025711662077, - 15.888684828078764, - 16.439342924191415, - 17 - ] - }, - { - "carpetid":"mycarpetplot", - "type":"scattercarpet", - "a":[ - 1.9771678356568936, - 1.7281677091353975, - 1.6451461648505092, - 1.7652820598110595, - 1.5179417916142373, - 2.099819189564812, - 1.8024322915995699, - 1.6683193117135444, - 1.1813891438521875, - 1.578247897765319, - 1.6314609420147512, - 1.8458871588188073, - 1.7181384141641556, - 2.2643673033453267, - 1.6376456285923953, - 1.6953881767473784, - 1.7745328938864666, - 1.5711398875234057, - 1.799620526880252, - 2.342986216987497, - 1.7633240331174955, - 1.4833952975931757, - 1.9747651253133203, - 1.4365390954305588, - 1.3911643074250069, - 1.7144456182372474, - 0.9611812192667324, - 2.125702115701399, - 1.7832077696756277, - 1.7678583784846384, - 1.1966026976818127, - 1.5266718096293928, - 1.6438662784411688, - 1.375351724472806, - 1.539528691650646, - 1.9343656055663472, - 1.6678985329789258, - 1.8838779397314183, - 1.8057610847488919, - 1.9867795658155845, - 1.7902207920753248, - 2.2428388139932585, - 1.769166870828669, - 1.7928805285667517, - 1.8961203808797211, - 1.655201962808252, - 1.748110448020544, - 2.1609390713338605, - 1.8582250531025166, - 2.260551220106619, - 2.174229914643124, - 1.899108040698505, - 1.6714200978666203, - 1.3422274118132156, - 1.5125353784890294, - 2.048671995637749, - 2.117472920743815, - 2.436665335120428, - 2.0428458099599434, - 1.888246012915576, - 2.3299074870741365, - 1.2183411433436957, - 1.5914325446683042, - 1.7982601061861565, - 1.5257107713313518, - 1.5944916088712748, - 1.9401436701458648, - 1.7439340903395262, - 1.5560646315993507, - 1.7629838531353392, - 1.5206893377929624, - 1.814475330486396, - 1.7380392383272825, - 1.8190668853737035, - 1.6863475372995764, - 2.1378300153137526, - 1.2178578603105077, - 2.110235458301153, - 2.0366896632400917, - 1.8698443512650469, - 1.442457218557926, - 1.7167433663934857, - 1.8963353433369978, - 1.343741972344295, - 2.0063463496455816, - 2.0295829326718207, - 2.3203724465670805, - 1.227492829769501, - 1.471026977200653, - 1.4617876360662163, - 1.5815843506558118, - 1.7580195616737262, - 1.5299129609761897, - 1.367460804568379, - 2.1174350194343776, - 1.8480754969735693, - 2.285146095070552, - 2.574443838453115, - 1.992071069597503, - 1.689951318926721 - ], - "b":[ - 1.3074519875714097, - 1.4531685621591914, - 2.1633966841963534, - 1.481073227909216, - 1.6799060485798523, - 2.274808308193735, - 1.7371033453009164, - 2.298620713452353, - 1.4214451222999855, - 1.5455663580766836, - 1.6164243290199265, - 1.9682442393920063, - 2.1427944629388787, - 1.4569045884037142, - 1.4281163749466375, - 1.7146767197928359, - 1.0755933816684946, - 0.8784094762145397, - 1.93282129111832, - 1.6448513383492502, - 1.5506155032993147, - 1.764263941854984, - 0.7032357405818286, - 1.616711421892543, - 1.5805996613997118, - 1.4725783403464852, - 1.2622550584210828, - 2.1434272945907717, - 1.8510120143131545, - 1.7208807703301083, - 2.292141617988955, - 1.6002564281676446, - 1.478085831795318, - 1.6796275144762962, - 1.5505730929373653, - 1.8149322552575557, - 1.5948050623614736, - 1.3932724557421987, - 1.780168826955217, - 1.664742252577551, - 1.850786769461731, - 1.653363910092044, - 0.7745920245470629, - 1.6329582257562196, - 1.2773964616345046, - 1.6237896986913494, - 1.7962206908187406, - 2.0890616549036434, - 1.914164477898335, - 1.150998812106018, - 1.5850923232263203, - 2.1528601952062716, - 1.7930066020059647, - 0.7711287657022957, - 1.8862596171137413, - 1.5020315442105905, - 1.5197809399661615, - 1.7113280011848166, - 1.9283144401619667, - 1.9789179945446944, - 1.7793246989822848, - 1.6167238328699642, - 1.9384044700452088, - 1.5660843637915092, - 1.7683028061308124, - 1.5244731497168458, - 1.102008322790224, - 1.6718157162563336, - 2.165694994302381, - 1.8190475051727986, - 1.5789080001223392, - 2.5057162064716687, - 1.2863412085249366, - 1.4843877421001979, - 2.333751888139704, - 1.3530084019232373, - 1.9143452926050923, - 1.507162154023091, - 1.8106757522755341, - 1.3652123999397956, - 1.6776974988628168, - 1.4932496818472836, - 1.590352820803672, - 1.4262241995107394, - 1.633121441166829, - 1.7717278114683015, - 1.6461264733739263, - 1.5974539598821782, - 1.1749746099921552, - 1.7222969846461536, - 1.5254755644233537, - 1.636011940215802, - 1.8344532843883337, - 1.4112779279811438, - 1.7188670382828906, - 1.3291969146673037, - 1.8487719803200113, - 1.9453457313487563, - 1.6248245939430164, - 1.5247741572596762 - ], - "mode":"markers" - } - ], - "layout": { - "xaxis":{ - "showgrid":false, - "showline":false, - "zeroline":false, - "showticklabels":false, - "range":[ - -1.470625, - 1.470625 - ], - "autorange":true - }, - "margin":{ - "t":20, - "r":20, - "b":20, - "l":40 - }, - "dragmode":"pan", - "yaxis":{ - "range":[ - 0.9398704146370815, - 1.7961098784162064 - ], - "autorange":true - } + "data":[ + { + "type":"carpet", + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ], + "y":[ + 4, 4.2, 4.4, 4.6, 4.8, 5, + 5.1, 5.3, 5.5, 5.7, 5.9, 6.1, + 6.2, 6.4, 6.6, 6.8, 7, 7.2, + 7.4, 7.6, 7.8, 8, 8.2, 8.4, + 8.8, 9, 9.2, 9.4, 9.6, 9.8 + ], + "cheaterslope":2, + "aaxis":{ + "title":"width, cm", + "tickformat":".1f", + "type":"linear" + }, + "baxis":{ + "title":"height, cm", + "tickformat":".2f", + "type":"linear" + } + }, + { + "type":"contourcarpet", + "name":"Power", + "z":[ + 100, 110, 120, 140, 180, 260, + 200, 210, 220, 240, 280, 360, + 300, 310, 320, 340, 380, 460, + 400, 410, 420, 440, 480, 560, + 500, 510, 520, 540, 580, 660 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":"][", + "value":[ + 400, + 540 + ] + }, + "colorscale":[ + [ 0, "red" ], + [ 1, "red" ] + ], + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] + }, + { + "type":"contourcarpet", + "name":"Strength", + "z":[ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, + 0.12, 0.22, 0.32, 0.42, 0.52, 0.62, + 0.14, 0.24, 0.34, 0.44, 0.54, 0.64, + 0.16, 0.26, 0.36, 0.46, 0.56, 0.66, + 0.18, 0.28, 0.38, 0.48, 0.58, 0.68 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":"<", + "value":[ + 0.5 + ] + }, + "colorscale":[ + [ 0, "blue" ], + [ 1, "blue" ] + ], + "uid":"977b5b", + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] + }, + { + "type":"contourcarpet", + "name":"Agility", + "z":[ + 800, 801, 802, 803, 804, 805, + 810, 811, 812, 813, 814, 815, + 820, 821, 822, 823, 824, 825, + 830, 831, 832, 833, 834, 835, + 840, 841, 842, 843, 844, 845 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":">", + "value":810 + }, + "colorscale":[ + [ 0, "green" ], + [ 1, "green" ] + ], + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] } + ], + "layout":{ + "dragmode":"pan", + "width":800, + "height":600, + "yaxis":{ + "tickprefix":"€", + "tickformat":".2f" + } + } } From b17e7e54f4fca5172e71daa0624fb75e298a8c1f Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 9 Feb 2017 12:34:45 -0500 Subject: [PATCH 053/132] Fix incorrect argument ordering in contourcarpet bg --- src/traces/contourcarpet/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 24cea224a07..cb2ef0d742c 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -89,7 +89,7 @@ function plotOne(gd, plotinfo, cd) { // Draw the baseline background fill that fills in the space behind any other // contour levels: - makeBackground(plotGroup, xa, ya, carpet._clipsegments, isConstraint, contours.coloring); + makeBackground(plotGroup, carpet._clipsegments, xa, ya, isConstraint, contours.coloring); // Draw the specific contour fills. As a simplification, they're assumed to be // fully opaque so that it's easy to draw them simply overlapping. The alternative From 05cd928066cbbd3fc1bfebbcb3fde0487005c7fb Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 9 Feb 2017 12:40:59 -0500 Subject: [PATCH 054/132] Add cheater mock without smoothing --- test/image/baselines/cheater.png | Bin 59118 -> 59249 bytes test/image/baselines/cheater_smooth.png | Bin 0 -> 59118 bytes test/image/mocks/cheater.json | 15 ++- test/image/mocks/cheater_smooth.json | 159 ++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 test/image/baselines/cheater_smooth.png create mode 100644 test/image/mocks/cheater_smooth.json diff --git a/test/image/baselines/cheater.png b/test/image/baselines/cheater.png index 5e2b699387ebd3325e875ee979f122430eae338f..e0c659d869e5f0a6fc95dcc7bce659c50bb3c051 100644 GIT binary patch literal 59249 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 literal 59118 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 diff --git a/test/image/baselines/cheater_smooth.png b/test/image/baselines/cheater_smooth.png new file mode 100644 index 0000000000000000000000000000000000000000..5e2b699387ebd3325e875ee979f122430eae338f GIT binary patch literal 59118 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/cheater.json b/test/image/mocks/cheater.json index 4bc55cd7b64..544a7f6b5f6 100644 --- a/test/image/mocks/cheater.json +++ b/test/image/mocks/cheater.json @@ -27,12 +27,14 @@ "aaxis":{ "title":"width, cm", "tickformat":".1f", - "type":"linear" + "type":"linear", + "smoothing": 0 }, "baxis":{ "title":"height, cm", "tickformat":".2f", - "type":"linear" + "type":"linear", + "smoothing": 0 } }, { @@ -54,6 +56,9 @@ 540 ] }, + "line": { + "smoothing": 0 + }, "colorscale":[ [ 0, "red" ], [ 1, "red" ] @@ -91,6 +96,9 @@ 0.5 ] }, + "line": { + "smoothing": 0 + }, "colorscale":[ [ 0, "blue" ], [ 1, "blue" ] @@ -131,6 +139,9 @@ [ 0, "green" ], [ 1, "green" ] ], + "line": { + "smoothing": 0 + }, "a":[ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, diff --git a/test/image/mocks/cheater_smooth.json b/test/image/mocks/cheater_smooth.json new file mode 100644 index 00000000000..4bc55cd7b64 --- /dev/null +++ b/test/image/mocks/cheater_smooth.json @@ -0,0 +1,159 @@ +{ + "data":[ + { + "type":"carpet", + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ], + "y":[ + 4, 4.2, 4.4, 4.6, 4.8, 5, + 5.1, 5.3, 5.5, 5.7, 5.9, 6.1, + 6.2, 6.4, 6.6, 6.8, 7, 7.2, + 7.4, 7.6, 7.8, 8, 8.2, 8.4, + 8.8, 9, 9.2, 9.4, 9.6, 9.8 + ], + "cheaterslope":2, + "aaxis":{ + "title":"width, cm", + "tickformat":".1f", + "type":"linear" + }, + "baxis":{ + "title":"height, cm", + "tickformat":".2f", + "type":"linear" + } + }, + { + "type":"contourcarpet", + "name":"Power", + "z":[ + 100, 110, 120, 140, 180, 260, + 200, 210, 220, 240, 280, 360, + 300, 310, 320, 340, 380, 460, + 400, 410, 420, 440, 480, 560, + 500, 510, 520, 540, 580, 660 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":"][", + "value":[ + 400, + 540 + ] + }, + "colorscale":[ + [ 0, "red" ], + [ 1, "red" ] + ], + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] + }, + { + "type":"contourcarpet", + "name":"Strength", + "z":[ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, + 0.12, 0.22, 0.32, 0.42, 0.52, 0.62, + 0.14, 0.24, 0.34, 0.44, 0.54, 0.64, + 0.16, 0.26, 0.36, 0.46, 0.56, 0.66, + 0.18, 0.28, 0.38, 0.48, 0.58, 0.68 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":"<", + "value":[ + 0.5 + ] + }, + "colorscale":[ + [ 0, "blue" ], + [ 1, "blue" ] + ], + "uid":"977b5b", + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] + }, + { + "type":"contourcarpet", + "name":"Agility", + "z":[ + 800, 801, 802, 803, 804, 805, + 810, 811, 812, 813, 814, 815, + 820, 821, 822, 823, 824, 825, + 830, 831, 832, 833, 834, 835, + 840, 841, 842, 843, 844, 845 + ], + "opacity":0.4, + "contours":{ + "type":"constraint", + "operation":">", + "value":810 + }, + "colorscale":[ + [ 0, "green" ], + [ 1, "green" ] + ], + "a":[ + 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, + 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, + 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, + 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ], + "b":[ + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82, + 1.01, 1.12, 1.24, 1.38, 1.56, 1.82 + ] + } + ], + "layout":{ + "dragmode":"pan", + "width":800, + "height":600, + "yaxis":{ + "tickprefix":"€", + "tickformat":".2f" + } + } +} From dfb9f3e8534307f257786dcd9c0499c02cae3226 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 10 Feb 2017 11:43:40 -0500 Subject: [PATCH 055/132] Dump editable carpet title + default --- src/traces/carpet/axis_defaults.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 652a033d885..340dab289ce 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -38,10 +38,7 @@ var autoType = require('../../plots/cartesian/axis_autotype'); module.exports = function handleAxisDefaults(containerIn, containerOut, options) { var letter = options.letter, font = options.font || {}, - attributes = carpetAttrs[letter + 'axis'], - defaultTitle = 'Click to enter ' + - (options.title || (letter.toUpperCase() + ' axis')) + - ' title'; + attributes = carpetAttrs[letter + 'axis']; function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); @@ -111,7 +108,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) // inherit from global font color in case that was provided. var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color; - coerce('title', defaultTitle); + coerce('title'); Lib.coerceFont(coerce, 'titlefont', { family: font.family, size: Math.round(font.size * 1.2), From ac0945a51c0e59ee6502ee4293a9a97205d8ba60 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 10 Feb 2017 16:09:54 -0500 Subject: [PATCH 056/132] Add carpet constraint mock --- src/traces/contourcarpet/join_all_paths.js | 37 +- src/traces/contourcarpet/plot.js | 17 +- test/image/baselines/carpet_inequalities.png | Bin 0 -> 78715 bytes test/image/mocks/carpet_inequalities.json | 386 +++++++++++++++++++ 4 files changed, 430 insertions(+), 10 deletions(-) create mode 100644 test/image/baselines/carpet_inequalities.png create mode 100644 test/image/mocks/carpet_inequalities.json diff --git a/src/traces/contourcarpet/join_all_paths.js b/src/traces/contourcarpet/join_all_paths.js index b665784f535..b57b5e6d80b 100644 --- a/src/traces/contourcarpet/join_all_paths.js +++ b/src/traces/contourcarpet/join_all_paths.js @@ -11,11 +11,39 @@ var Drawing = require('../../components/drawing'); var axisAlignedLine = require('../carpet/axis_aligned_line'); var Lib = require('../../lib'); +var map1dArray = require('../carpet/map_1d_array'); +var makepath = require('../carpet/makepath'); + +module.exports = function joinAllPaths(trace, pi, perimeter, ab2p, carpet, xa, ya) { + var i; + var fullpath = ''; + + if(!pi.edgepaths.length) { // || + var needsOutline = false; + + if(trace.contours.type === 'levels') { + // If the lower-left point is in bounds, then it's the case that + // the whole thing needs at least an outline: + needsOutline = pi.z[0][0] < pi.level; + } /* else { + // TODO: TODO! + switch(trace.contours.operation) { + } + }*/ + + if(needsOutline) { + var seg, xp, yp; + var segs = []; + for(i = 0; i < carpet._clipsegments.length; i++) { + seg = carpet._clipsegments[i]; + xp = map1dArray([], seg.x, xa.c2p); + yp = map1dArray([], seg.y, ya.c2p); + segs.push(makepath(xp, yp, seg.bicubic)); + } + fullpath = 'M' + segs.join('L') + 'Z'; + } + } -module.exports = function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { - var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? - '' : ('M' + perimeter.map(ab2p).join('L') + 'Z'); - var i = 0; var startsleft = pi.edgepaths.map(function(v, i) { return i; }); var newloop = true; var endpt, newendpt, cnt, nexti, possiblei, addpath; @@ -50,6 +78,7 @@ module.exports = function joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya) { return path; } + i = 0; endpt = null; while(startsleft.length) { var startpt = pi.edgepaths[i][0]; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index cb2ef0d742c..a2de7055fd9 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -94,7 +94,7 @@ function plotOne(gd, plotinfo, cd) { // Draw the specific contour fills. As a simplification, they're assumed to be // fully opaque so that it's easy to draw them simply overlapping. The alternative // would be to flip adjacent paths and draw closed paths for each level instead. - makeFills(plotGroup, xa, ya, pathinfo, perimeter, ab2p, carpet, contours.coloring); + makeFills(trace, plotGroup, xa, ya, pathinfo, perimeter, ab2p, carpet, contours.coloring); // Draw contour lines: makeLines(plotGroup, pathinfo, contours); @@ -179,7 +179,7 @@ function makeBackground(plotgroup, clipsegments, xaxis, yaxis, isConstraint, col .style('stroke', 'none'); } -function makeFills(plotgroup, xa, ya, pathinfo, perimeter, ab2p, carpet, coloring) { +function makeFills(trace, plotgroup, xa, ya, pathinfo, perimeter, ab2p, carpet, coloring) { var fillgroup = plotgroup.selectAll('g.contourfill') .data([0]); fillgroup.enter().append('g') @@ -195,10 +195,15 @@ function makeFills(plotgroup, xa, ya, pathinfo, perimeter, ab2p, carpet, colorin // if the whole perimeter is above this level, start with a path // enclosing the whole thing. With all that, the parity should mean // that we always fill everything above the contour, nothing below - var fullpath = joinAllPaths(pi, perimeter, ab2p, carpet, xa, ya); - - if(!fullpath) d3.select(this).remove(); - else d3.select(this).attr('d', fullpath).style('stroke', 'none'); + var fullpath = joinAllPaths(trace, pi, perimeter, ab2p, carpet, xa, ya); + + if(!fullpath) { + d3.select(this).remove(); + } else { + d3.select(this) + .attr('d', fullpath) + .style('stroke', 'none'); + } }); } diff --git a/test/image/baselines/carpet_inequalities.png b/test/image/baselines/carpet_inequalities.png new file mode 100644 index 0000000000000000000000000000000000000000..34f3f70da681ebbc77b68f0f7597ae8b87ad7c9d GIT binary patch literal 78715 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", + "value":-0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "blue"], [1, "blue"]] + }, + { + "carpetid":"c2", + "xaxis":"x2", + "yaxis":"y", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"[]", + "value":[2.5, 3.5] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "red"], [1, "red"]] + }, + { + "carpetid":"c2", + "xaxis":"x2", + "yaxis":"y", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"][", + "value":[ + 2.9, + 3.1 + ] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "gray"], [1, "gray"]] + }, + { + "carpetid":"c2", + "xaxis":"x2", + "yaxis":"y", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"<", + "value":0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "yellow"], [1, "yellow"]] + }, + { + "carpetid":"c2", + "xaxis":"x2", + "yaxis":"y", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":">", + "value":-0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "blue"], [1, "blue"]] + }, + { + "carpetid":"c3", + "xaxis":"x", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"[]", + "value":[2.5, 3.5] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "red"], [1, "red"]] + }, + { + "carpetid":"c3", + "xaxis":"x", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"][", + "value":[2.9, 3.1] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "gray"], [1, "gray"]] + }, + { + "carpetid":"c3", + "xaxis":"x", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"<", + "value":0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "yellow"], [1, "yellow"]] + }, + { + "carpetid":"c3", + "xaxis":"x", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":">", + "value":-0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "blue"], [1, "blue"]] + }, + { + "carpetid":"c4", + "xaxis":"x2", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"[]", + "value":[2.5, 3.5] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "red"], [1, "red"]] + }, + { + "carpetid":"c4", + "xaxis":"x2", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[1, 2.3, 3], [2.3, 3, 3.7], [3, 3.7, 5]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"][", + "value":[2.9, 3.1] + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "gray"], [1, "gray"]] + }, + { + "carpetid":"c4", + "xaxis":"x2", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":"<", + "value":0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "yellow"], [1, "yellow"]] + }, + { + "carpetid":"c4", + "xaxis":"x2", + "yaxis":"y2", + "type":"contourcarpet", + "z":[[0, 0.5, 1], [-0.5, 0, 0.5], [-1, -0.5, 0]], + "opacity":0.5, + "contours":{ + "type":"constraint", + "operation":">", + "value":-0.25 + }, + "line":{ + "smoothing":1 + }, + "colorscale":[[0, "blue"], [1, "blue"]] + } + ], + "layout":{ + "width":800, + "height":600, + "margin":{ + "t":0, + "r":0, + "b":0, + "l":30 + }, + "dragmode":"pan", + "xaxis":{ + "domain":[ + 0, + 0.49 + ], + "autorange":true + }, + "yaxis":{ + "domain":[ + 0, + 0.49 + ], + "autorange":true + }, + "xaxis2":{ + "domain":[ + 0.51, + 1 + ], + "autorange":true + }, + "yaxis2":{ + "domain":[ + 0.51, + 1 + ], + "autorange":true + } + } +} From d54ae59eaf85efcc0c8c238a03a8799ee78d794f Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 16 Mar 2017 15:00:31 -0400 Subject: [PATCH 057/132] Fix clip path --- src/traces/carpet/measure_text.js | 4 +++- src/traces/carpet/plot.js | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/traces/carpet/measure_text.js b/src/traces/carpet/measure_text.js index 2da83fbcd0e..8d5688e65e4 100644 --- a/src/traces/carpet/measure_text.js +++ b/src/traces/carpet/measure_text.js @@ -16,5 +16,7 @@ module.exports = function measureText(tester, text, font) { .text(text) .call(Drawing.font, font); - return Drawing.bBox(dummyText.node()); + var bbox = Drawing.bBox(dummyText.node()); + dummyText.remove(); + return bbox; }; diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index dee53e267de..86445cca689 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -75,7 +75,15 @@ function plotOne(gd, plotinfo, cd) { function drawClipPath(trace, layer, xaxis, yaxis) { var seg, xp, yp, i; // var clip = makeg(layer, 'g', 'carpetclip'); - var clip = makeg(layer, 'clipPath', 'carpetclip'); + trace.clipPathId = 'clip' + trace.uid + 'carpet'; + + var clip = layer.select('#' + trace.clipPathId); + + if (!clip.size()) { + clip = layer.append('clipPath') + .classed('carpetclip', true); + } + var path = makeg(clip, 'path', 'carpetboundary'); var segments = trace._clipsegments; var segs = []; @@ -90,7 +98,6 @@ function drawClipPath(trace, layer, xaxis, yaxis) { // This could be optimized ever so slightly to avoid no-op L segments // at the corners, but it's so negligible that I don't think it's worth // the extra complexity - trace.clipPathId = 'clip' + trace.uid + 'carpet'; trace.clipPathData = 'M' + segs.join('L') + 'Z'; clip.attr('id', trace.clipPathId); path.attr('d', trace.clipPathData); From a2b89b0f7554d1e648c632c4022bcf7d43cfaf11 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 16 Mar 2017 17:18:34 -0400 Subject: [PATCH 058/132] Add carpet axis baseline --- src/traces/carpet/axis_attributes.js | 6 +- src/traces/carpet/plot.js | 2 +- test/image/baselines/carpet_axis.png | Bin 0 -> 79721 bytes test/image/mocks/carpet_axis.json | 125 +++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 test/image/baselines/carpet_axis.png create mode 100644 test/image/mocks/carpet_axis.json diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index e821060db27..da1e4f7d404 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -465,15 +465,15 @@ module.exports = { description: 'Sets the line color of the end line.' }, tick0: { - valType: 'integer', + valType: 'number', min: 0, dflt: 0, role: 'info', description: 'The starting index of grid lines along the axis' }, dtick: { - valType: 'integer', - min: 1, + valType: 'number', + min: 0, dflt: 1, role: 'info', description: 'The stride between grid lines along the axis' diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index 86445cca689..9a414252127 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -79,7 +79,7 @@ function drawClipPath(trace, layer, xaxis, yaxis) { var clip = layer.select('#' + trace.clipPathId); - if (!clip.size()) { + if(!clip.size()) { clip = layer.append('clipPath') .classed('carpetclip', true); } diff --git a/test/image/baselines/carpet_axis.png b/test/image/baselines/carpet_axis.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7b2992c81a7bc7dfcf8b43d03c1023a5382994 GIT binary patch literal 79721 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 Tc0Xgm!hDqFU&)or82bMYcTRqA literal 0 HcmV?d00001 diff --git a/test/image/mocks/carpet_axis.json b/test/image/mocks/carpet_axis.json new file mode 100644 index 00000000000..f7ab216bbec --- /dev/null +++ b/test/image/mocks/carpet_axis.json @@ -0,0 +1,125 @@ +{ + "data": [ + { + "type": "carpet", + "carpetid": "c", + "a": [0, 5e-7, 0.000001], + "b": [0, 500000, 1000000], + "y": [ + [1.00000, 1.32287, 1.73205], + [1.32287, 1.58113, 1.93649], + [1.73205, 1.93649, 2.23606] + ], + "aaxis": { + "title": "length, l, m", + "ticksuffix": "m", + "tickformat": ".3s", + "smoothing": 1, + "tickmode": "linear", + "tick0": 1e-7, + "dtick": 2e-7, + "minorgridcount": 3, + "type": "linear", + "smoothing": 1 + }, + "baxis": { + "title": "pressure, p, Pa", + "ticksuffix": "Pa", + "tickformat": ".3s", + "smoothing": 1, + "tickmode": "linear", + "tick0": 100000, + "dtick": 200000, + "minorgridcount": 3, + "type": "linear", + "smoothing": 1 + }, + "xaxis": "x", + "yaxis": "y" + }, + { + "type": "carpet", + "carpetid": "c2", + "a": [0, 1e-7, 2e-7, 3e-7, 4e-7, 5e-7, 6e-7, 7e-7, 8e-7, 9e-7, 0.000001], + "b": [ + 0, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000, 450000, 500000, 550000, + 600000, 650000, 700000, 750000, 800000, 850000, 900000, 950000, 1000000 + ], + "y": [ + [1.0000, 1.0535, 1.1135, 1.1789, 1.2489, 1.3228, 1.4000, 1.4798, 1.5620, 1.6462, 1.7320], + [1.0259, 1.0781, 1.1368, 1.2010, 1.2698, 1.3425, 1.4186, 1.4974, 1.5787, 1.6620, 1.7471], + [1.0535, 1.1045, 1.1618, 1.2247, 1.2922, 1.3638, 1.4387, 1.5165, 1.5968, 1.6792, 1.7635], + [1.0828, 1.1324, 1.1884, 1.2500, 1.3162, 1.3865, 1.4603, 1.5370, 1.6163, 1.6977, 1.7811], + [1.1135, 1.1618, 1.2165, 1.2767, 1.3416, 1.4106, 1.4832, 1.5588, 1.6370, 1.7175, 1.8000], + [1.1456, 1.1926, 1.2459, 1.3047, 1.3683, 1.4361, 1.5074, 1.5819, 1.6590, 1.7385, 1.8200], + [1.1789, 1.2247, 1.2767, 1.3341, 1.3964, 1.4628, 1.5329, 1.6062, 1.6822, 1.7606, 1.8411], + [1.2134, 1.2579, 1.3086, 1.3647, 1.4256, 1.4908, 1.5596, 1.6317, 1.7066, 1.7839, 1.8634], + [1.2489, 1.2922, 1.3416, 1.3964, 1.4560, 1.5198, 1.5874, 1.6583, 1.7320, 1.8083, 1.8867], + [1.2854, 1.3275, 1.3756, 1.4291, 1.4874, 1.5500, 1.6163, 1.6859, 1.7585, 1.8337, 1.9111], + [1.3228, 1.3638, 1.4106, 1.4628, 1.5198, 1.5811, 1.6462, 1.7146, 1.7860, 1.8601, 1.9364], + [1.3610, 1.4008, 1.4465, 1.4974, 1.5532, 1.6132, 1.6770, 1.7442, 1.8145, 1.8874, 1.9627], + [1.4000, 1.4387, 1.4832, 1.5329, 1.5874, 1.6462, 1.7088, 1.7748, 1.8439, 1.9157, 1.9899], + [1.4396, 1.4773, 1.5206, 1.5692, 1.6224, 1.6800, 1.7414, 1.8062, 1.8741, 1.9448, 2.0180], + [1.4798, 1.5165, 1.5588, 1.6062, 1.6583, 1.7146, 1.7748, 1.8384, 1.9052, 1.9748, 2.0469], + [1.5206, 1.5564, 1.5976, 1.6439, 1.6948, 1.7500, 1.8090, 1.8714, 1.9371, 2.0056, 2.0766], + [1.5620, 1.5968, 1.6370, 1.6822, 1.7320, 1.7860, 1.8439, 1.9052, 1.9697, 2.0371, 2.1071], + [1.6039, 1.6378, 1.6770, 1.7211, 1.7698, 1.8227, 1.8794, 1.9397, 2.0031, 2.0694, 2.1383], + [1.6462, 1.6792, 1.7175, 1.7606, 1.8083, 1.8601, 1.9157, 1.9748, 2.0371, 2.1023, 2.1702], + [1.6889, 1.7211, 1.7585, 1.8006, 1.8472, 1.8980, 1.9525, 2.0105, 2.0718, 2.1360, 2.2028], + [1.7320, 1.7635, 1.8000, 1.8411, 1.8867, 1.9364, 1.9899, 2.0469, 2.1071, 2.1702, 2.2360] + ], + "aaxis": { + "title": "length, l, m", + "ticksuffix": "m", + "tickformat": ".3s", + "smoothing": 1, + "tickmode": "array", + "arraytick0": 1, + "arraydtick": 2, + "minorgridcount": 3, + "type": "linear", + "smoothing": 0 + }, + "baxis": { + "title": "pressure, p, Pa", + "ticksuffix": "Pa", + "tickformat": ".3s", + "smoothing": 1, + "tickmode": "array", + "arraytick0": 2, + "arraydtick": 4, + "minorgridcount": 3, + "type": "linear", + "smoothing": 0 + }, + "xaxis": "x2", + "yaxis": "y" + } + ], + "layout": { + "width": 800, + "height": 400, + "title": "Coarse/smoothed (left) and fine/unsmoothed (right) carpet axes", + "margin": { + "t": 80, + "r": 20, + "b": 40, + "l": 40 + }, + "dragmode": "pan", + "xaxis": { + "domain": [0, 0.48], + "range": [-1.4444, 1.4444], + "autorange": true + }, + "xaxis2": { + "domain": [0.52, 1], + "range": [-1.4444, 1.4444], + "autorange": true + }, + "yaxis": { + "range": [0.7253, 2.5107], + "autorange": true + } + } +} From 8a9cbea6a9ea9182de5e3f8297a7cc343213149d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 21 Mar 2017 16:10:43 -0400 Subject: [PATCH 059/132] Carpet legend --- src/components/legend/style.js | 12 +++++++++++- src/plots/cartesian/graph_interact.js | 5 +++++ src/traces/carpet/ab_defaults.js | 1 + src/traces/carpet/axis_defaults.js | 11 ++++++++++- src/traces/contourcarpet/attributes.js | 8 ++++++++ src/traces/contourcarpet/calc.js | 10 +++++++++- src/traces/contourcarpet/defaults.js | 2 ++ src/traces/contourcarpet/index.js | 2 +- 8 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/components/legend/style.js b/src/components/legend/style.js index c3c2101e62e..c2f39476a0f 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -62,9 +62,19 @@ module.exports = function style(s) { function styleLines(d) { var trace = d[0].trace, - showFill = trace.visible && trace.fill && trace.fill !== 'none', + showFill = trace.visible && ((trace.fill && trace.fill !== 'none') || trace.contours), showLine = subTypes.hasLines(trace); + if (trace && trace._module && trace._module.name === 'contourcarpet') { + showLine = trace.contours.type === 'constraint'; + showFill = trace.contours.type === 'constraint'; + console.log('ducktype fill data'); + d = [{trace: { + fillcolor: trace.colorscale[0][1], + line: {color: 'red', width: 2} + }}]; + } + var fill = d3.select(this).select('.legendfill').selectAll('path') .data(showFill ? [d] : []); fill.enter().append('path').classed('js-fill', true); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 85f0130a5e8..83221ed4f92 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -471,6 +471,11 @@ function hover(gd, evt, subplot) { if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; trace = cd[0].trace; + + // Explicitly bail out for these two. I don't know how to otherwise prevent + // the rest of this function from running and failing + if (['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; + subplotId = getSubplot(trace); subploti = subplots.indexOf(subplotId); diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index 5f8183d3ce4..6e6939e8a9e 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -58,6 +58,7 @@ function mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor) { calendar: traceOut.calendar, dfltColor: dfltColor, bgColor: fullLayout.paper_bgcolor, + separators: fullLayout.separators, }; handleAxisDefaults(axIn, axOut, defaultOptions); diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index 340dab289ce..f4b35177338 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -81,6 +81,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) coerce('showtickprefix'); coerce('showticksuffix'); + coerce('separatethousands'); + coerce('tickformat'); + coerce('exponentformat'); + coerce('showexponent'); + coerce('categoryorder'); + coerce('tickmode'); coerce('tickvals'); coerce('ticktext'); @@ -172,13 +178,16 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) coerce('minorgridcolor', addOpacity(gridColor, 0.06)); } + // Pass on the separator: + containerOut._separators = options.separators; // fill in categories containerOut._initialCategories = axType === 'category' ? orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : []; - // Something above overrides this deep in the axis code, but no, we actually want to coerce this. + // It needs to be coerced, then something above overrides this deep in the axis code, + // but no, we *actually* want to coerce this. coerce('tickmode'); // We'll never draw this. We just need a couple category management functions. diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index f49a6ccd133..983b8b6f603 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -38,6 +38,14 @@ module.exports = extendFlat({}, { atype: heatmapAttrs.xtype, btype: heatmapAttrs.ytype, + mode: { + valType: 'flaglist', + flags: ['lines', 'fill'], + extras: ['none'], + role: 'info', + description: ['The mode.'].join(' ') + }, + connectgaps: heatmapAttrs.connectgaps, autocontour: { diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index 97bc3cb65af..0de9dd090bb 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -22,7 +22,7 @@ var interp2d = require('../heatmap/interp2d'); var findEmpties = require('../heatmap/find_empties'); var makeBoundArray = require('../heatmap/make_bound_array'); var supplyDefaults = require('./defaults'); - +var tinycolor = require('tinycolor2'); // most is the same as heatmap calc, then adjust it // though a few things inside heatmap calc still look for @@ -218,5 +218,13 @@ function heatmappishCalc(gd, trace) { // auto-z and autocolorscale if applicable colorscaleCalc(trace, z, '', 'z'); + if (trace.contours.type === 'constraint') { + var c1 = tinycolor(trace.colorscale[0][1]); + var c2 = tinycolor(trace.colorscale[1][1]); + + var mix = tinycolor.mix(c1, c2, 50); + console.log('mix:', mix); + } + return [cd0]; } diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index ee4262c5289..af11666b2c8 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -16,6 +16,7 @@ var attributes = require('./attributes'); var hasColumns = require('../heatmap/has_columns'); var handleStyleDefaults = require('../contour/style_defaults'); var constraintMapping = require('./constraint_mapping'); +//var tinycolor = require('tinycolor2'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -63,6 +64,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut.contours.start = map.start; traceOut.contours.end = map.end; traceOut.contours.size = map.size; + } else { contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index 8f780a08c3a..7aa0d9aa9ee 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -22,7 +22,7 @@ ContourCarpet.style = require('./style'); ContourCarpet.moduleType = 'trace'; ContourCarpet.name = 'contourcarpet'; ContourCarpet.basePlotModule = require('../../plots/cartesian'); -ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'markerColorscale', 'showLegend']; +ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'markerColorscale', 'showLegend', 'hasLines']; ContourCarpet.meta = { hrName: 'contour_carpet', description: [].join(' ') From f65a10912aafeb9c8c2b255a740c3ed4294e2143 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 22 Mar 2017 16:22:02 -0400 Subject: [PATCH 060/132] Update mocks and baselines and carpet defaults --- src/components/legend/style.js | 7 +- src/plots/cartesian/graph_interact.js | 2 +- src/plots/plots.js | 11 +- src/traces/contour/style_defaults.js | 6 +- src/traces/contourcarpet/attributes.js | 10 ++ src/traces/contourcarpet/calc.js | 15 +-- src/traces/contourcarpet/defaults.js | 21 ++- src/traces/contourcarpet/style.js | 17 ++- test/image/baselines/carpet_inequalities.png | Bin 78715 -> 76736 bytes test/image/baselines/cheater.png | Bin 59249 -> 59332 bytes test/image/baselines/cheater_contour.png | Bin 0 -> 121578 bytes test/image/baselines/cheater_smooth.png | Bin 59118 -> 58898 bytes test/image/mocks/carpet_inequalities.json | 133 ++++++++++--------- test/image/mocks/cheater_contour.json | 127 ++++++++++++++++++ 14 files changed, 254 insertions(+), 95 deletions(-) create mode 100644 test/image/baselines/cheater_contour.png create mode 100644 test/image/mocks/cheater_contour.json diff --git a/src/components/legend/style.js b/src/components/legend/style.js index c2f39476a0f..cfd00b33497 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -65,14 +65,9 @@ function styleLines(d) { showFill = trace.visible && ((trace.fill && trace.fill !== 'none') || trace.contours), showLine = subTypes.hasLines(trace); - if (trace && trace._module && trace._module.name === 'contourcarpet') { + if(trace && trace._module && trace._module.name === 'contourcarpet') { showLine = trace.contours.type === 'constraint'; showFill = trace.contours.type === 'constraint'; - console.log('ducktype fill data'); - d = [{trace: { - fillcolor: trace.colorscale[0][1], - line: {color: 'red', width: 2} - }}]; } var fill = d3.select(this).select('.legendfill').selectAll('path') diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 83221ed4f92..0166676dcc2 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -474,7 +474,7 @@ function hover(gd, evt, subplot) { // Explicitly bail out for these two. I don't know how to otherwise prevent // the rest of this function from running and failing - if (['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; + if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; subplotId = getSubplot(trace); subploti = subplots.indexOf(subplotId); diff --git a/src/plots/plots.js b/src/plots/plots.js index 520dfbfd4a0..2cb812f422b 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -807,6 +807,12 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde // gets overwritten in pie, geo and ternary modules coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); + // gets overwritten by carpetcontour + if(plots.traceIs(traceOut, 'showLegend')) { + coerce('showlegend'); + coerce('legendgroup'); + } + // TODO add per-base-plot-module trace defaults step if(_module) _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); @@ -819,11 +825,6 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde coerceSubplotAttr('gl2d', 'xaxis'); coerceSubplotAttr('gl2d', 'yaxis'); - if(plots.traceIs(traceOut, 'showLegend')) { - coerce('showlegend'); - coerce('legendgroup'); - } - supplyTransformDefaults(traceIn, traceOut, layout); } diff --git a/src/traces/contour/style_defaults.js b/src/traces/contour/style_defaults.js index cd29ec6ccbe..cf87d2b4c2c 100644 --- a/src/traces/contour/style_defaults.js +++ b/src/traces/contour/style_defaults.js @@ -12,15 +12,15 @@ var colorscaleDefaults = require('../../components/colorscale/defaults'); -module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout) { +module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout, defaultColor, defaultWidth) { var coloring = coerce('contours.coloring'); var showLines; if(coloring === 'fill') showLines = coerce('contours.showlines'); if(showLines !== false) { - if(coloring !== 'lines') coerce('line.color', '#000'); - coerce('line.width', 0.5); + if(coloring !== 'lines') coerce('line.color', defaultColor || '#000'); + coerce('line.width', defaultWidth === undefined ? 0.5 : defaultWidth); coerce('line.dash'); } diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 983b8b6f603..d5efce83a28 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -48,6 +48,16 @@ module.exports = extendFlat({}, { connectgaps: heatmapAttrs.connectgaps, + fillcolor: { + valType: 'color', + role: 'style', + description: [ + 'Sets the fill color.', + 'Defaults to a half-transparent variant of the line color,', + 'marker color, or marker line color, whichever is available.' + ].join(' ') + }, + autocontour: { valType: 'boolean', dflt: true, diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index 0de9dd090bb..35e7707fb32 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -22,7 +22,6 @@ var interp2d = require('../heatmap/interp2d'); var findEmpties = require('../heatmap/find_empties'); var makeBoundArray = require('../heatmap/make_bound_array'); var supplyDefaults = require('./defaults'); -var tinycolor = require('tinycolor2'); // most is the same as heatmap calc, then adjust it // though a few things inside heatmap calc still look for @@ -54,7 +53,7 @@ module.exports = function calc(gd, trace) { if(!tracedata.a) tracedata.a = carpetdata.a; if(!tracedata.b) tracedata.b = carpetdata.b; - supplyDefaults(tracedata, trace, null, gd._fullLayout); + supplyDefaults(tracedata, trace, trace._defaultColor, gd._fullLayout); } var cd = heatmappishCalc(gd, trace), @@ -215,15 +214,9 @@ function heatmappishCalc(gd, trace) { //mappedZ: mappedZ }; - // auto-z and autocolorscale if applicable - colorscaleCalc(trace, z, '', 'z'); - - if (trace.contours.type === 'constraint') { - var c1 = tinycolor(trace.colorscale[0][1]); - var c2 = tinycolor(trace.colorscale[1][1]); - - var mix = tinycolor.mix(c1, c2, 50); - console.log('mix:', mix); + if(trace.contours.type === 'levels') { + // auto-z and autocolorscale if applicable + colorscaleCalc(trace, z, '', 'z'); } return [cd0]; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index af11666b2c8..ffcb6e05e27 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -16,7 +16,8 @@ var attributes = require('./attributes'); var hasColumns = require('../heatmap/has_columns'); var handleStyleDefaults = require('../contour/style_defaults'); var constraintMapping = require('./constraint_mapping'); -//var tinycolor = require('tinycolor2'); +var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); +var plotAttributes = require('../../plots/attributes'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -26,7 +27,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('carpetid'); // If either a or b is not present, then it's not a valid trace *unless* the carpet - // axis has the a or b valeus we're looking for. So if these are not found, just defer + // axis has the a or b values we're looking for. So if these are not found, just defer // that decision until the calc step. // // NB: the calc step will modify the original data input by assigning whichever of @@ -65,6 +66,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut.contours.end = map.end; traceOut.contours.size = map.size; + coerce('fillcolor', defaultColor); } else { contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); @@ -74,8 +76,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // things like restyle that can call supplyDefaults without calc // after the initial draw, we can just reuse the previous calculation contourSize = coerce('contours.size'); + + traceOut.showlegend = false; } + // Override the trace-level showlegend default with a default that takes + // into account whether this is a constraint or level contours: + Lib.coerce(traceIn, traceOut, plotAttributes, 'showlegend', traceOut.contours.type === 'constraint'); + missingEnd = (contourStart === false) || (contourEnd === false); autoContour; @@ -89,10 +97,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('ncontours'); } - handleStyleDefaults(traceIn, traceOut, coerce, layout); + if(traceOut.contours.type === 'constraint') { + handleStyleDefaults(traceIn, traceOut, coerce, layout, defaultColor, 2); + handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); + } else { + handleStyleDefaults(traceIn, traceOut, coerce, layout); + } if(traceOut.contours.type === 'constraint' && traceOut.contours.operation === '=') { traceOut.contours.coloring = 'none'; } + } else { + traceOut._defaultColor = defaultColor; } }; diff --git a/src/traces/contourcarpet/style.js b/src/traces/contourcarpet/style.js index 7a2157c9ad8..ab7ac1488b4 100644 --- a/src/traces/contourcarpet/style.js +++ b/src/traces/contourcarpet/style.js @@ -46,13 +46,18 @@ module.exports = function style(gd) { line.dash); }); - c.selectAll('g.contourbg path') - .style('fill', colorMap(start - cs / 2)); + if(trace.contours.type === 'levels') { + c.selectAll('g.contourbg path') + .style('fill', colorMap(start - cs / 2)); - c.selectAll('g.contourfill path') - .style('fill', function(d, i) { - return colorMap(start + (i + 0.5) * cs); - }); + c.selectAll('g.contourfill path') + .style('fill', function(d, i) { + return colorMap(start + (i + 0.5) * cs); + }); + } else { + c.selectAll('g.contourfill path') + .style('fill', trace.fillcolor); + } }); heatmapStyle(gd); diff --git a/test/image/baselines/carpet_inequalities.png b/test/image/baselines/carpet_inequalities.png index 34f3f70da681ebbc77b68f0f7597ae8b87ad7c9d..6d93a3d9658581ff4b93afab0244bbada8527d0a 100644 GIT binary patch literal 76736 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
`oX@m;=7nhj#R_`l*}Q^sM*Qk%Q&J);Rv(t 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