diff --git a/src/components/legend/helpers.js b/src/components/legend/helpers.js index 86a197c49fe..d249e681fb1 100644 --- a/src/components/legend/helpers.js +++ b/src/components/legend/helpers.js @@ -9,11 +9,14 @@ 'use strict'; -var Registry = require('../../registry'); - - exports.legendGetsTrace = function legendGetsTrace(trace) { - return trace.visible && Registry.traceIs(trace, 'showLegend'); + // traceIs(trace, 'showLegend') is not sufficient anymore, due to contour(carpet)? + // which are legend-eligible only if type: constraint. Otherwise, showlegend gets deleted. + + // Note that we explicitly include showlegend: false, so a trace that *could* be + // in the legend but is not shown still counts toward the two traces you need to + // ensure the legend is shown by default, because this can still help disambiguate. + return trace.visible && (trace.showlegend !== undefined); }; exports.isGrouped = function isGrouped(legendLayout) { diff --git a/src/components/legend/style.js b/src/components/legend/style.js index 1732f8c7fcf..31f8661a882 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -60,13 +60,14 @@ module.exports = function style(s, gd) { .each(stylePoints); function styleLines(d) { - var trace = d[0].trace, - showFill = trace.visible && trace.fill && trace.fill !== 'none', - showLine = subTypes.hasLines(trace); - - if(trace && trace._module && trace._module.name === 'contourcarpet') { - showLine = trace.contours.showlines; - showFill = trace.contours.coloring === 'fill'; + var trace = d[0].trace; + var showFill = trace.visible && trace.fill && trace.fill !== 'none'; + var showLine = subTypes.hasLines(trace); + var contours = trace.contours; + + if(contours && contours.type === 'constraint') { + showLine = contours.showlines; + showFill = contours._operation !== '='; } var fill = d3.select(this).select('.legendfill').selectAll('path') diff --git a/src/constants/filter_ops.js b/src/constants/filter_ops.js new file mode 100644 index 00000000000..3cd36b14d73 --- /dev/null +++ b/src/constants/filter_ops.js @@ -0,0 +1,36 @@ +/** +* Copyright 2012-2018, 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 = { + COMPARISON_OPS: ['=', '!=', '<', '>=', '>', '<='], + COMPARISON_OPS2: ['=', '<', '>=', '>', '<='], + INTERVAL_OPS: ['[]', '()', '[)', '(]', '][', ')(', '](', ')['], + SET_OPS: ['{}', '}{'], + CONSTRAINT_REDUCTION: { + // for contour constraints, open/closed endpoints are equivalent + '=': '=', + + '<': '<', + '<=': '<', + + '>': '>', + '>=': '>', + + '[]': '[]', + '()': '[]', + '[)': '[]', + '(]': '[]', + + '][': '][', + ')(': '][', + '](': '][', + ')[': '][' + } +}; diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js index 4b8611ead37..ab1e3ebc7e9 100644 --- a/src/traces/contour/attributes.js +++ b/src/traces/contour/attributes.js @@ -16,6 +16,10 @@ var dash = require('../../components/drawing/attributes').dash; var fontAttrs = require('../../plots/font_attributes'); var extendFlat = require('../../lib/extend').extendFlat; +var filterOps = require('../../constants/filter_ops'); +var COMPARISON_OPS2 = filterOps.COMPARISON_OPS2; +var INTERVAL_OPS = filterOps.INTERVAL_OPS; + var scatterLineAttrs = scatterAttrs.line; module.exports = extendFlat({ @@ -34,6 +38,17 @@ module.exports = extendFlat({ connectgaps: heatmapAttrs.connectgaps, + fillcolor: { + valType: 'color', + role: 'style', + editType: 'calc', + description: [ + 'Sets the fill color if `contours.type` is *constraint*.', + '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, @@ -67,6 +82,19 @@ module.exports = extendFlat({ }, contours: { + type: { + valType: 'enumerated', + values: ['levels', 'constraint'], + dflt: 'levels', + role: 'info', + editType: 'calc', + 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, @@ -155,6 +183,47 @@ module.exports = extendFlat({ 'https://github.com/d3/d3-format/blob/master/README.md#locale_format.' ].join(' ') }, + operation: { + valType: 'enumerated', + values: [].concat(COMPARISON_OPS2).concat(INTERVAL_OPS), + role: 'info', + dflt: '=', + editType: 'calc', + description: [ + 'Sets the constraint operation.', + + '*=* keeps regions equal to `value`', + + '*<* and *<=* keep regions less than `value`', + + '*>* and *>=* keep regions greater than `value`', + + '*[]*, *()*, *[)*, and *(]* keep regions inside `value[0]` to `value[1]`', + + '*][*, *)(*, *](*, *)[* keep regions outside `value[0]` to value[1]`', + + 'Open vs. closed intervals make no difference to constraint display, but', + 'all versions are allowed for consistency with filter transforms.' + ].join(' ') + }, + value: { + valType: 'any', + dflt: 0, + role: 'info', + editType: 'calc', + description: [ + 'Sets the value or values of the constraint boundary.', + + 'When `operation` is set to one of the comparison values', + '(' + COMPARISON_OPS2 + ')', + '*value* is expected to be a number.', + + 'When `operation` is set to one of the interval values', + '(' + INTERVAL_OPS + ')', + '*value* is expected to be an array of two numbers where the first', + 'is the lower bound and the second is the upper bound.', + ].join(' ') + }, editType: 'calc', impliedEdits: {'autocontour': false} }, diff --git a/src/traces/contour/calc.js b/src/traces/contour/calc.js index 768dee7ae5d..1a5a38bbf29 100644 --- a/src/traces/contour/calc.js +++ b/src/traces/contour/calc.js @@ -9,94 +9,14 @@ 'use strict'; -var Axes = require('../../plots/cartesian/axes'); -var extendFlat = require('../../lib').extendFlat; var heatmapCalc = require('../heatmap/calc'); - +var setContours = require('./set_contours'); // 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 cd = heatmapCalc(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. - // previously we copied the whole contours object back, but that had - // other info (coloring, showlines) that should be left to supplyDefaults - if(!trace._input.contours) trace._input.contours = {}; - extendFlat(trace._input.contours, { - start: contours.start, - end: contours.end, - size: contours.size - }); - trace._input.autocontour = true; - } - 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; - } - } - + var cd = heatmapCalc(gd, trace); + setContours(trace); 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; -} diff --git a/src/traces/contourcarpet/close_boundaries.js b/src/traces/contour/close_boundaries.js similarity index 59% rename from src/traces/contourcarpet/close_boundaries.js rename to src/traces/contour/close_boundaries.js index e65f29e3807..e5e1c51be6f 100644 --- a/src/traces/contourcarpet/close_boundaries.js +++ b/src/traces/contour/close_boundaries.js @@ -11,9 +11,11 @@ module.exports = function(pathinfo, operation, perimeter, trace) { // Abandon all hope, ye who enter here. var i, v1, v2; - var na = trace.a.length; - var nb = trace.b.length; - var z = trace.z; + var pi0 = pathinfo[0]; + var na = pi0.x.length; + var nb = pi0.y.length; + var z = pi0.z; + var contours = trace.contours; var boundaryMax = -Infinity; var boundaryMin = Infinity; @@ -32,36 +34,31 @@ module.exports = function(pathinfo, operation, perimeter, trace) { boundaryMax = Math.max(boundaryMax, z[nb - 1][i]); } + pi0.prefixBoundary = false; + switch(operation) { case '>': - case '>=': - if(trace.contours.value > boundaryMax) { - pathinfo[0].prefixBoundary = true; + if(contours.value > boundaryMax) { + pi0.prefixBoundary = true; } break; case '<': - case '<=': - if(trace.contours.value < boundaryMin) { - pathinfo[0].prefixBoundary = true; + if(contours.value < boundaryMin) { + pi0.prefixBoundary = true; } break; case '[]': - case '()': - v1 = Math.min.apply(null, trace.contours.value); - v2 = Math.max.apply(null, trace.contours.value); - if(v2 < boundaryMin) { - pathinfo[0].prefixBoundary = true; - } - if(v1 > boundaryMax) { - pathinfo[0].prefixBoundary = true; + v1 = Math.min.apply(null, contours.value); + v2 = Math.max.apply(null, contours.value); + if(v2 < boundaryMin || v1 > boundaryMax) { + pi0.prefixBoundary = true; } break; case '][': - case ')(': - v1 = Math.min.apply(null, trace.contours.value); - v2 = Math.max.apply(null, trace.contours.value); + v1 = Math.min.apply(null, contours.value); + v2 = Math.max.apply(null, contours.value); if(v1 < boundaryMin && v2 > boundaryMax) { - pathinfo[0].prefixBoundary = true; + pi0.prefixBoundary = true; } break; } diff --git a/src/traces/contour/constraint_defaults.js b/src/traces/contour/constraint_defaults.js new file mode 100644 index 00000000000..857542e8ed7 --- /dev/null +++ b/src/traces/contour/constraint_defaults.js @@ -0,0 +1,93 @@ +/** +* Copyright 2012-2018, 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 handleLabelDefaults = require('./label_defaults'); + +var Color = require('../../components/color'); +var addOpacity = Color.addOpacity; +var opacity = Color.opacity; + +var filterOps = require('../../constants/filter_ops'); +var CONSTRAINT_REDUCTION = filterOps.CONSTRAINT_REDUCTION; +var COMPARISON_OPS2 = filterOps.COMPARISON_OPS2; + +module.exports = function handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor, opts) { + var contours = traceOut.contours; + var showLines, lineColor, fillColor; + + var operation = coerce('contours.operation'); + contours._operation = CONSTRAINT_REDUCTION[operation]; + + handleConstraintValueDefaults(coerce, contours); + + if(operation === '=') { + showLines = contours.showlines = true; + } + else { + showLines = coerce('contours.showlines'); + fillColor = coerce('fillcolor', addOpacity( + (traceIn.line || {}).color || defaultColor, 0.5 + )); + } + + if(showLines) { + var lineDfltColor = fillColor && opacity(fillColor) ? + addOpacity(traceOut.fillcolor, 1) : + defaultColor; + lineColor = coerce('line.color', lineDfltColor); + coerce('line.width', 2); + coerce('line.dash'); + } + + coerce('line.smoothing'); + + handleLabelDefaults(coerce, layout, lineColor, opts); +}; + +function handleConstraintValueDefaults(coerce, contours) { + var zvalue; + + if(COMPARISON_OPS2.indexOf(contours.operation) === -1) { + // Requires an array of two numbers: + coerce('contours.value', [0, 1]); + + if(!Array.isArray(contours.value)) { + if(isNumeric(contours.value)) { + zvalue = parseFloat(contours.value); + contours.value = [zvalue, zvalue + 1]; + } + } else if(contours.value.length > 2) { + contours.value = contours.value.slice(2); + } else if(contours.length === 0) { + contours.value = [0, 1]; + } else if(contours.length < 2) { + zvalue = parseFloat(contours.value[0]); + contours.value = [zvalue, zvalue + 1]; + } else { + contours.value = [ + parseFloat(contours.value[0]), + parseFloat(contours.value[1]) + ]; + } + } else { + // Requires a single scalar: + coerce('contours.value', 0); + + if(!isNumeric(contours.value)) { + if(Array.isArray(contours.value)) { + contours.value = parseFloat(contours.value[0]); + } else { + contours.value = 0; + } + } + } +} diff --git a/src/traces/contourcarpet/constraint_mapping.js b/src/traces/contour/constraint_mapping.js similarity index 67% rename from src/traces/contourcarpet/constraint_mapping.js rename to src/traces/contour/constraint_mapping.js index d6b088dfa97..fe37c76c221 100644 --- a/src/traces/contourcarpet/constraint_mapping.js +++ b/src/traces/contour/constraint_mapping.js @@ -8,27 +8,18 @@ 'use strict'; -var constants = require('./constants'); +var filterOps = require('../../constants/filter_ops'); 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('='); +module.exports = { + '[]': makeRangeSettings('[]'), + '][': makeRangeSettings(']['), + '>': makeInequalitySettings('>'), + '<': makeInequalitySettings('<'), + '=': makeInequalitySettings('=') +}; // This does not in any way shape or form support calendars. It's adapted from // transforms/filter.js. @@ -41,13 +32,13 @@ function coerceValue(operation, value) { return isNumeric(value) ? (+value) : null; } - if(constants.INEQUALITY_OPS.indexOf(operation) !== -1) { + if(filterOps.COMPARISON_OPS2.indexOf(operation) !== -1) { coercedValue = hasArrayValue ? coerce(value[0]) : coerce(value); - } else if(constants.INTERVAL_OPS.indexOf(operation) !== -1) { + } else if(filterOps.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) { + } else if(filterOps.SET_OPS.indexOf(operation) !== -1) { coercedValue = hasArrayValue ? value.map(coerce) : [coerce(value)]; } diff --git a/src/traces/contour/contours_defaults.js b/src/traces/contour/contours_defaults.js index 1062cb9149f..918da1e14b3 100644 --- a/src/traces/contour/contours_defaults.js +++ b/src/traces/contour/contours_defaults.js @@ -8,12 +8,9 @@ 'use strict'; -var Lib = require('../../lib'); -var attributes = require('./attributes'); - -module.exports = function handleContourDefaults(traceIn, traceOut, coerce) { - var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'); - var contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'); +module.exports = function handleContourDefaults(traceIn, traceOut, coerce, coerce2) { + var contourStart = coerce2('contours.start'); + var contourEnd = coerce2('contours.end'); var missingEnd = (contourStart === false) || (contourEnd === false); // normally we only need size if autocontour is off. But contour.calc diff --git a/src/traces/contourcarpet/convert_to_constraints.js b/src/traces/contour/convert_to_constraints.js similarity index 87% rename from src/traces/contourcarpet/convert_to_constraints.js rename to src/traces/contour/convert_to_constraints.js index 992d643ab0e..76686d116f8 100644 --- a/src/traces/contourcarpet/convert_to_constraints.js +++ b/src/traces/contour/convert_to_constraints.js @@ -21,30 +21,44 @@ module.exports = function(pathinfo, operation) { var op1 = function(arr) { return arr; }; switch(operation) { + case '=': + case '<': + return pathinfo; + case '>': + if(pathinfo.length !== 1) { + Lib.warn('Contour data invalid for the specified inequality operation.'); + } + + // 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]); + } + return pathinfo; 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]; + pi0 = copyPathinfo(pathinfo[0]); + pi1 = copyPathinfo(pathinfo[1]); for(i = 0; i < pi0.edgepaths.length; i++) { pi0.edgepaths[i] = op0(pi0.edgepaths[i]); @@ -60,28 +74,13 @@ module.exports = function(pathinfo, operation) { 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; + return [pi0]; } }; + +function copyPathinfo(pi) { + return Lib.extendFlat({}, pi, { + edgepaths: Lib.extendDeep([], pi.edgepaths), + paths: Lib.extendDeep([], pi.paths) + }); +} diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index 2f3c52b910d..c8be70030a9 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -13,6 +13,7 @@ var Lib = require('../../lib'); var hasColumns = require('../heatmap/has_columns'); var handleXYZDefaults = require('../heatmap/xyz_defaults'); +var handleConstraintDefaults = require('./constraint_defaults'); var handleContoursDefaults = require('./contours_defaults'); var handleStyleDefaults = require('./style_defaults'); var attributes = require('./attributes'); @@ -23,6 +24,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + function coerce2(attr) { + return Lib.coerce2(traceIn, traceOut, attributes, attr); + } + var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); if(!len) { traceOut.visible = false; @@ -30,8 +35,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('text'); + var isConstraint = (coerce('contours.type') === 'constraint'); coerce('connectgaps', hasColumns(traceOut)); - handleContoursDefaults(traceIn, traceOut, coerce); - handleStyleDefaults(traceIn, traceOut, coerce, layout); + // trace-level showlegend has already been set, but is only allowed if this is a constraint + if(!isConstraint) delete traceOut.showlegend; + + if(isConstraint) { + handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor); + } + else { + handleContoursDefaults(traceIn, traceOut, coerce, coerce2); + handleStyleDefaults(traceIn, traceOut, coerce, layout); + } }; diff --git a/src/traces/contourcarpet/empty_pathinfo.js b/src/traces/contour/empty_pathinfo.js similarity index 56% rename from src/traces/contourcarpet/empty_pathinfo.js rename to src/traces/contour/empty_pathinfo.js index 19020ada266..2dffa586d5d 100644 --- a/src/traces/contourcarpet/empty_pathinfo.js +++ b/src/traces/contour/empty_pathinfo.js @@ -9,15 +9,36 @@ 'use strict'; var Lib = require('../../lib'); +var constraintMapping = require('./constraint_mapping'); +var endPlus = require('./end_plus'); module.exports = function emptyPathinfo(contours, plotinfo, cd0) { - var cs = contours.size; + var contoursFinal = (contours.type === 'constraint') ? + constraintMapping[contours._operation](contours.value) : + contours; + + var cs = contoursFinal.size; var pathinfo = []; + var end = endPlus(contoursFinal); var carpet = cd0.trace.carpetTrace; - for(var ci = contours.start; ci < contours.end + cs / 10; ci += cs) { - pathinfo.push({ + var basePathinfo = carpet ? { + // 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 + } : { + xaxis: plotinfo.xaxis, + yaxis: plotinfo.yaxis, + x: cd0.x, + y: cd0.y + }; + + for(var ci = contoursFinal.start; ci < end; ci += cs) { + pathinfo.push(Lib.extendFlat({ level: ci, // all the cells with nontrivial marching index crossings: {}, @@ -28,15 +49,9 @@ module.exports = function emptyPathinfo(contours, plotinfo, cd0) { 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 - }); + }, basePathinfo)); if(pathinfo.length > 1000) { Lib.warn('Too many contours, clipping at 1000', contours); diff --git a/src/traces/contour/hover.js b/src/traces/contour/hover.js index 79a5abee27c..41fbf836500 100644 --- a/src/traces/contour/hover.js +++ b/src/traces/contour/hover.js @@ -9,8 +9,26 @@ 'use strict'; +var Color = require('../../components/color'); + var heatmapHoverPoints = require('../heatmap/hover'); module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer) { - return heatmapHoverPoints(pointData, xval, yval, hovermode, hoverLayer, true); + var hoverData = heatmapHoverPoints(pointData, xval, yval, hovermode, hoverLayer, true); + + if(hoverData) { + hoverData.forEach(function(hoverPt) { + var trace = hoverPt.trace; + if(trace.contours.type === 'constraint') { + if(trace.fillcolor && Color.opacity(trace.fillcolor)) { + hoverPt.color = Color.addOpacity(trace.fillcolor, 1); + } + else if(trace.contours.showlines && Color.opacity(trace.line.color)) { + hoverPt.color = Color.addOpacity(trace.line.color, 1); + } + } + }); + } + + return hoverData; }; diff --git a/src/traces/contour/index.js b/src/traces/contour/index.js index ffcbf72d81a..f56f61cd7ec 100644 --- a/src/traces/contour/index.js +++ b/src/traces/contour/index.js @@ -22,7 +22,7 @@ Contour.hoverPoints = require('./hover'); Contour.moduleType = 'trace'; Contour.name = 'contour'; Contour.basePlotModule = require('../../plots/cartesian'); -Contour.categories = ['cartesian', '2dMap', 'contour']; +Contour.categories = ['cartesian', '2dMap', 'contour', 'showLegend']; Contour.meta = { description: [ 'The data from which contour lines are computed is set in `z`.', diff --git a/src/traces/contour/label_defaults.js b/src/traces/contour/label_defaults.js new file mode 100644 index 00000000000..8e1adfd13ac --- /dev/null +++ b/src/traces/contour/label_defaults.js @@ -0,0 +1,28 @@ +/** +* Copyright 2012-2018, 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 handleLabelDefaults(coerce, layout, lineColor, opts) { + if(!opts) opts = {}; + var showLabels = coerce('contours.showlabels'); + if(showLabels) { + var globalFont = layout.font; + Lib.coerceFont(coerce, 'contours.labelfont', { + family: globalFont.family, + size: globalFont.size, + color: lineColor + }); + coerce('contours.labelformat'); + } + + if(opts.hasHover !== false) coerce('zhoverformat'); +}; diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index c00dc06edaa..b116a276712 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -20,7 +20,9 @@ var setConvert = require('../../plots/cartesian/set_convert'); var heatmapPlot = require('../heatmap/plot'); var makeCrossings = require('./make_crossings'); var findAllPaths = require('./find_all_paths'); -var endPlus = require('./end_plus'); +var emptyPathinfo = require('./empty_pathinfo'); +var convertToConstraints = require('./convert_to_constraints'); +var closeBoundaries = require('./close_boundaries'); var constants = require('./constants'); var costConstants = constants.LABELOPTIMIZER; @@ -81,48 +83,20 @@ function plotOne(gd, plotinfo, cd) { [leftedge, bottomedge] ]; + var fillPathinfo = pathinfo; + if(contours.type === 'constraint') { + fillPathinfo = convertToConstraints(pathinfo, contours._operation); + closeBoundaries(fillPathinfo, contours._operation, perimeter, trace); + } + // draw everything var plotGroup = exports.makeContourGroup(plotinfo, cd, id); makeBackground(plotGroup, perimeter, contours); - makeFills(plotGroup, pathinfo, perimeter, contours); + makeFills(plotGroup, fillPathinfo, perimeter, contours); makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, perimeter); clipGaps(plotGroup, plotinfo, fullLayout._clips, cd[0], perimeter); } -function emptyPathinfo(contours, plotinfo, cd0) { - var cs = contours.size, - pathinfo = [], - end = endPlus(contours); - - for(var ci = contours.start; ci < end; 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: plotinfo.xaxis, - yaxis: plotinfo.yaxis, - // full data arrays to use for interpolation - x: cd0.x, - y: cd0.y, - z: cd0.z, - smoothing: cd0.trace.line.smoothing - }); - - if(pathinfo.length > 1000) { - Lib.warn('Too many contours, clipping at 1000', contours); - break; - } - } - return pathinfo; -} exports.makeContourGroup = function(plotinfo, cd, id) { var plotgroup = plotinfo.plot.select('.maplayer') .selectAll('g.contour.' + id) @@ -157,7 +131,7 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { .classed('contourfill', true); var fillitems = fillgroup.selectAll('path') - .data(contours.coloring === 'fill' ? pathinfo : []); + .data(contours.coloring === 'fill' || (contours.type === 'constraint' && contours._operation !== '=') ? pathinfo : []); fillitems.enter().append('path'); fillitems.exit().remove(); fillitems.each(function(pi) { @@ -173,10 +147,23 @@ function makeFills(plotgroup, pathinfo, perimeter, contours) { }); } +function initFullPath(pi, perimeter) { + var prefixBoundary = pi.prefixBoundary; + if(prefixBoundary === undefined) { + var edgeVal2 = Math.min(pi.z[0][0], pi.z[0][1]); + prefixBoundary = (!pi.edgepaths.length && edgeVal2 > pi.level); + } + + if(prefixBoundary) { + // TODO: why does ^^ not work for constraints? + // pi.prefixBoundary gets set by closeBoundaries + return 'M' + perimeter.join('L') + 'Z'; + } + return ''; +} + function joinAllPaths(pi, perimeter) { - var edgeVal2 = Math.min(pi.z[0][0], pi.z[0][1]), - fullpath = (pi.edgepaths.length || edgeVal2 <= pi.level) ? - '' : ('M' + perimeter.join('L') + 'Z'), + var fullpath = initFullPath(pi, perimeter), i = 0, startsleft = pi.edgepaths.map(function(v, i) { return i; }), newloop = true, @@ -432,10 +419,26 @@ exports.labelFormatter = function(contours, colorbar, fullLayout) { formatAxis = { type: 'linear', _id: 'ycontour', - nticks: (contours.end - contours.start) / contours.size, - showexponent: 'all', - range: [contours.start, contours.end] + showexponent: 'all' }; + + if(contours.type === 'constraint') { + var value = contours.value; + if(Array.isArray(value)) { + formatAxis.range = [value[0], value[value.length - 1]]; + } + else formatAxis.range = [value, value]; + + if(formatAxis.range[0] === formatAxis.range[1]) { + formatAxis.range[1] += formatAxis.range[0] || 1; + } + formatAxis.nticks = 1000; + } + else { + formatAxis.range = [contours.start, contours.end]; + formatAxis.nticks = (contours.end - contours.start) / contours.size; + } + setConvert(formatAxis, fullLayout); Axes.calcTicks(formatAxis); formatAxis._tmin = null; diff --git a/src/traces/contour/set_contours.js b/src/traces/contour/set_contours.js new file mode 100644 index 00000000000..46ec7436211 --- /dev/null +++ b/src/traces/contour/set_contours.js @@ -0,0 +1,96 @@ +/** +* Copyright 2012-2018, 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; + + +module.exports = function setContours(trace) { + var contours = trace.contours; + + // check if we need to auto-choose contour levels + if(trace.autocontour) { + 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. + // previously we copied the whole contours object back, but that had + // other info (coloring, showlines) that should be left to supplyDefaults + if(!trace._input.contours) trace._input.contours = {}; + extendFlat(trace._input.contours, { + start: contours.start, + end: contours.end, + size: contours.size + }); + trace._input.autocontour = true; + } + else if(contours.type !== 'constraint') { + // 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; + } + } +}; + + +/* + * 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; +} diff --git a/src/traces/contour/style_defaults.js b/src/traces/contour/style_defaults.js index d01a3dbc122..256b4a55586 100644 --- a/src/traces/contour/style_defaults.js +++ b/src/traces/contour/style_defaults.js @@ -10,11 +10,10 @@ 'use strict'; var colorscaleDefaults = require('../../components/colorscale/defaults'); -var Lib = require('../../lib'); +var handleLabelDefaults = require('./label_defaults'); module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout, opts) { - if(!opts) opts = {}; var coloring = coerce('contours.coloring'); var showLines; @@ -22,29 +21,18 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout, if(coloring === 'fill') showLines = coerce('contours.showlines'); if(showLines !== false) { - if(coloring !== 'lines') lineColor = coerce('line.color', opts.defaultColor || '#000'); - coerce('line.width', opts.defaultWidth === undefined ? 0.5 : opts.defaultWidth); + if(coloring !== 'lines') lineColor = coerce('line.color', '#000'); + coerce('line.width', 0.5); coerce('line.dash'); } - coerce('line.smoothing'); - if(coloring !== 'none') { colorscaleDefaults( traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} ); } - var showLabels = coerce('contours.showlabels'); - if(showLabels) { - var globalFont = layout.font; - Lib.coerceFont(coerce, 'contours.labelfont', { - family: globalFont.family, - size: globalFont.size, - color: lineColor - }); - coerce('contours.labelformat'); - } + coerce('line.smoothing'); - if(opts.hasHover !== false) coerce('zhoverformat'); + handleLabelDefaults(coerce, layout, lineColor, opts); }; diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index c349ded18ab..0ab2605299c 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -18,7 +18,6 @@ var colorbarAttrs = require('../../components/colorbar/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var scatterLineAttrs = scatterAttrs.line; -var constants = require('./constants'); module.exports = extendFlat({}, { carpet: { @@ -41,45 +40,16 @@ module.exports = extendFlat({}, { atype: heatmapAttrs.xtype, btype: heatmapAttrs.ytype, - mode: { - valType: 'flaglist', - flags: ['lines', 'fill'], - extras: ['none'], - role: 'info', - editType: 'calc', - description: ['The mode.'].join(' ') - }, - - connectgaps: heatmapAttrs.connectgaps, + // unimplemented - looks like connectgaps is implied true + // connectgaps: heatmapAttrs.connectgaps, - fillcolor: { - valType: 'color', - role: 'style', - editType: 'calc', - 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(' ') - }, + fillcolor: contourAttrs.fillcolor, autocontour: contourAttrs.autocontour, ncontours: contourAttrs.ncontours, contours: { - type: { - valType: 'enumerated', - values: ['levels', 'constraint'], - dflt: 'levels', - role: 'info', - editType: 'calc', - 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(' ') - }, + type: contourContourAttrs.type, start: contourContourAttrs.start, end: contourContourAttrs.end, size: contourContourAttrs.size, @@ -101,61 +71,10 @@ module.exports = extendFlat({}, { showlabels: contourContourAttrs.showlabels, labelfont: contourContourAttrs.labelfont, labelformat: contourContourAttrs.labelformat, - operation: { - valType: 'enumerated', - values: [].concat(constants.INEQUALITY_OPS).concat(constants.INTERVAL_OPS).concat(constants.SET_OPS), - role: 'info', - dflt: '=', - editType: 'calc', - 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]`' - ].join(' ') - }, - value: { - valType: 'any', - dflt: 0, - role: 'info', - editType: 'calc', - 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(' ') - }, - editType: 'calc' + operation: contourContourAttrs.operation, + value: contourContourAttrs.value, + editType: 'calc', + impliedEdits: {'autocontour': false} }, line: { diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index 1247e6f9c40..c8d05afffe6 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -8,8 +8,6 @@ 'use strict'; -var Axes = require('../../plots/cartesian/axes'); -var extendFlat = require('../../lib').extendFlat; var colorscaleCalc = require('../../components/colorscale/calc'); var hasColumns = require('../heatmap/has_columns'); var convertColumnData = require('../heatmap/convert_column_xyz'); @@ -20,6 +18,7 @@ var findEmpties = require('../heatmap/find_empties'); var makeBoundArray = require('../heatmap/make_bound_array'); var supplyDefaults = require('./defaults'); var lookupCarpet = require('../carpet/lookup_carpetid'); +var setContours = require('../contour/set_contours'); // most is the same as heatmap calc, then adjust it // though a few things inside heatmap calc still look for @@ -45,81 +44,13 @@ module.exports = function calc(gd, trace) { supplyDefaults(tracedata, trace, trace._defaultColor, gd._fullLayout); } - var cd = heatmappishCalc(gd, trace), - contours = trace.contours; + var cd = heatmappishCalc(gd, trace); - // Autocontour is unset for constraint plots so also autocontour if undefind: - if(trace.autocontour === true) { - 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; - } - } + setContours(trace); 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 @@ -163,7 +94,6 @@ function heatmappishCalc(gd, trace) { a: xArray, b: yArray, z: z, - //mappedZ: mappedZ }; if(trace.contours.type === 'levels') { diff --git a/src/traces/contourcarpet/constants.js b/src/traces/contourcarpet/constants.js deleted file mode 100644 index 2d0507fff6c..00000000000 --- a/src/traces/contourcarpet/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -* Copyright 2012-2018, 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_value_defaults.js b/src/traces/contourcarpet/constraint_value_defaults.js deleted file mode 100644 index 1c2331b4f64..00000000000 --- a/src/traces/contourcarpet/constraint_value_defaults.js +++ /dev/null @@ -1,59 +0,0 @@ -/** -* Copyright 2012-2018, 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 constraintMapping = require('./constraint_mapping'); -var isNumeric = require('fast-isnumeric'); - -module.exports = function(coerce, contours) { - var zvalue; - var scalarValuedOps = ['=', '<', '<=', '>', '>=']; - - if(scalarValuedOps.indexOf(contours.operation) === -1) { - // Requires an array of two numbers: - coerce('contours.value', [0, 1]); - - if(!Array.isArray(contours.value)) { - if(isNumeric(contours.value)) { - zvalue = parseFloat(contours.value); - contours.value = [zvalue, zvalue + 1]; - } - } else if(contours.value.length > 2) { - contours.value = contours.value.slice(2); - } else if(contours.length === 0) { - contours.value = [0, 1]; - } else if(contours.length < 2) { - zvalue = parseFloat(contours.value[0]); - contours.value = [zvalue, zvalue + 1]; - } else { - contours.value = [ - parseFloat(contours.value[0]), - parseFloat(contours.value[1]) - ]; - } - } else { - // Requires a single scalar: - coerce('contours.value', 0); - - if(!isNumeric(contours.value)) { - if(Array.isArray(contours.value)) { - contours.value = parseFloat(contours.value[0]); - } else { - contours.value = 0; - } - } - } - - var map = constraintMapping[contours.operation](contours.value); - - contours.start = map.start; - contours.end = map.end; - contours.size = map.size; -}; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 8ad0e4f741a..235d448d244 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -13,17 +13,19 @@ var Lib = require('../../lib'); var handleXYZDefaults = require('../heatmap/xyz_defaults'); var attributes = require('./attributes'); +var handleConstraintDefaults = require('../contour/constraint_defaults'); +var handleContoursDefaults = require('../contour/contours_defaults'); var handleStyleDefaults = require('../contour/style_defaults'); -var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); -var plotAttributes = require('../../plots/attributes'); -var supplyConstraintDefaults = require('./constraint_value_defaults'); -var addOpacity = require('../../components/color').addOpacity; module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + function coerce2(attr) { + return Lib.coerce2(traceIn, traceOut, attributes, attr); + } + coerce('carpet'); // If either a or b is not present, then it's not a valid trace *unless* the carpet @@ -42,7 +44,6 @@ 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; var len = handleXYZDefaults(traceIn, traceOut, coerce, layout, 'a', 'b'); @@ -52,103 +53,19 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('text'); - coerce('contours.type'); - - var contours = traceOut.contours; + var isConstraint = (coerce('contours.type') === 'constraint'); // Unimplemented: // coerce('connectgaps', hasColumns(traceOut)); - if(contours.type === 'constraint') { - coerce('contours.operation'); - - supplyConstraintDefaults(coerce, contours); - - // 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', true); - - // Override the above defaults with constraint-aware tweaks: - coerce('contours.coloring', contours.operation === '=' ? 'lines' : 'fill'); - coerce('contours.showlines', true); - - if(contours.operation === '=') { - contours.coloring = 'lines'; - } - handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - - // If there's a fill color, use it at full opacity for the line color - var lineDfltColor = traceOut.fillcolor ? addOpacity(traceOut.fillcolor, 1) : defaultColor; - - handleStyleDefaults(traceIn, traceOut, coerce, layout, { - hasHover: false, - defaultColor: lineDfltColor, - defaultWidth: 2 - }); - - if(contours.operation === '=') { - coerce('line.color', defaultColor); - - if(contours.coloring === 'fill') { - contours.coloring = 'lines'; - } + // trace-level showlegend has already been set, but is only allowed if this is a constraint + if(!isConstraint) delete traceOut.showlegend; - if(contours.coloring === 'lines') { - delete traceOut.fillcolor; - } - } - - delete traceOut.showscale; - delete traceOut.autocontour; - delete traceOut.autocolorscale; - delete traceOut.colorscale; - delete traceOut.ncontours; - delete traceOut.colorbar; - - if(traceOut.line) { - delete traceOut.line.autocolorscale; - delete traceOut.line.colorscale; - delete traceOut.line.mincolor; - delete traceOut.line.maxcolor; - } - - // TODO: These should be deleted in accordance with toolpanel convention, but - // we can't because we require them so that it magically makes the contour - // parts of the code happy: - // delete traceOut.contours.start; - // delete traceOut.contours.end; - // delete traceOut.contours.size; + if(isConstraint) { + handleConstraintDefaults(traceIn, traceOut, coerce, layout, defaultColor, {hasHover: false}); } else { - // 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', false); - - 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'); - coerce('contours.coloring'); - - missingEnd = (contourStart === false) || (contourEnd === false); - - if(missingEnd) { - autoContour = traceOut.autocontour = true; - } else { - autoContour = coerce('autocontour', false); - } - - if(autoContour || !contourSize) { - coerce('ncontours'); - } - + handleContoursDefaults(traceIn, traceOut, coerce, coerce2); handleStyleDefaults(traceIn, traceOut, coerce, layout, {hasHover: false}); - - delete traceOut.value; - delete traceOut.operation; } } else { traceOut._defaultColor = defaultColor; diff --git a/src/traces/contourcarpet/join_all_paths.js b/src/traces/contourcarpet/join_all_paths.js index 723dbdf9da7..e2e6feceaa5 100644 --- a/src/traces/contourcarpet/join_all_paths.js +++ b/src/traces/contourcarpet/join_all_paths.js @@ -11,8 +11,6 @@ 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, carpetcd, xa, ya) { var i; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 8724751eabc..8b5974e49dd 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -18,12 +18,12 @@ var makeCrossings = require('../contour/make_crossings'); var findAllPaths = require('../contour/find_all_paths'); var contourPlot = require('../contour/plot'); var constants = require('../contour/constants'); -var convertToConstraints = require('./convert_to_constraints'); +var convertToConstraints = require('../contour/convert_to_constraints'); var joinAllPaths = require('./join_all_paths'); -var emptyPathinfo = require('./empty_pathinfo'); +var emptyPathinfo = require('../contour/empty_pathinfo'); var mapPathinfo = require('./map_pathinfo'); var lookupCarpet = require('../carpet/lookup_carpetid'); -var closeBoundaries = require('./close_boundaries'); +var closeBoundaries = require('../contour/close_boundaries'); module.exports = function plot(gd, plotinfo, cdcontours) { @@ -49,7 +49,9 @@ 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'; + var isConstraint = contours.type === 'constraint'; + var operation = contours._operation; + var coloring = isConstraint ? (operation === '=' ? 'lines' : 'fill') : contours.coloring; // Map [a, b] (data) --> [i, j] (pixels) function ab2p(ab) { @@ -84,9 +86,10 @@ function plotOne(gd, plotinfo, cd) { // 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); - closeBoundaries(pathinfo, trace.contours.operation, perimeter, trace); + var fillPathinfo = pathinfo; + if(contours.type === 'constraint') { + fillPathinfo = convertToConstraints(pathinfo, operation); + closeBoundaries(fillPathinfo, operation, perimeter, trace); } // Map the paths in a/b coordinates to pixel coordinates: @@ -111,12 +114,12 @@ function plotOne(gd, plotinfo, cd) { // Draw the baseline background fill that fills in the space behind any other // contour levels: - makeBackground(plotGroup, carpetcd.clipsegments, xa, ya, isConstraint, contours.coloring); + makeBackground(plotGroup, carpetcd.clipsegments, xa, ya, isConstraint, 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(trace, plotGroup, xa, ya, pathinfo, perimeter, ab2p, carpet, carpetcd, contours.coloring, boundaryPath); + makeFills(trace, plotGroup, xa, ya, fillPathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath); // Draw contour lines: makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, plotinfo, carpet); diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index ca8bba0e20c..a0736fd5722 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -144,7 +144,9 @@ module.exports = function calc(gd, trace) { } // auto-z and autocolorscale if applicable - colorscaleCalc(trace, z, '', 'z'); + if(!isContour || trace.contours.type !== 'constraint') { + colorscaleCalc(trace, z, '', 'z'); + } if(isContour && trace.contours && trace.contours.coloring === 'heatmap') { var dummyTrace = { diff --git a/src/traces/histogram2dcontour/defaults.js b/src/traces/histogram2dcontour/defaults.js index 313bb4b69f9..5bfdd703310 100644 --- a/src/traces/histogram2dcontour/defaults.js +++ b/src/traces/histogram2dcontour/defaults.js @@ -22,9 +22,13 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + function coerce2(attr) { + return Lib.coerce2(traceIn, traceOut, attributes, attr); + } + handleSampleDefaults(traceIn, traceOut, coerce, layout); if(traceOut.visible === false) return; - handleContoursDefaults(traceIn, traceOut, coerce); + handleContoursDefaults(traceIn, traceOut, coerce, coerce2); handleStyleDefaults(traceIn, traceOut, coerce, layout); }; diff --git a/src/transforms/filter.js b/src/transforms/filter.js index f848ce99e15..5f9350590b5 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -13,9 +13,10 @@ var Registry = require('../registry'); var Axes = require('../plots/cartesian/axes'); var pointsAccessorFunction = require('./helpers').pointsAccessorFunction; -var COMPARISON_OPS = ['=', '!=', '<', '>=', '>', '<=']; -var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; -var SET_OPS = ['{}', '}{']; +var filterOps = require('../constants/filter_ops'); +var COMPARISON_OPS = filterOps.COMPARISON_OPS; +var INTERVAL_OPS = filterOps.INTERVAL_OPS; +var SET_OPS = filterOps.SET_OPS; exports.moduleType = 'transform'; @@ -72,15 +73,15 @@ exports.attributes = { '*>* 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 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 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' diff --git a/test/image/baselines/airfoil.png b/test/image/baselines/airfoil.png index 4edfcadbd74..562ffa99d90 100644 Binary files a/test/image/baselines/airfoil.png and b/test/image/baselines/airfoil.png differ diff --git a/test/image/baselines/cheater.png b/test/image/baselines/cheater.png index de015df4b0d..895cbe9509f 100644 Binary files a/test/image/baselines/cheater.png and b/test/image/baselines/cheater.png differ diff --git a/test/image/baselines/cheater_constraints.png b/test/image/baselines/cheater_constraints.png index 25da809facf..c2bdeae65cf 100644 Binary files a/test/image/baselines/cheater_constraints.png and b/test/image/baselines/cheater_constraints.png differ diff --git a/test/image/baselines/contour_constraints.png b/test/image/baselines/contour_constraints.png new file mode 100644 index 00000000000..e0924427939 Binary files /dev/null and b/test/image/baselines/contour_constraints.png differ diff --git a/test/image/mocks/cheater.json b/test/image/mocks/cheater.json index e9f0817735b..c83550ca3a6 100644 --- a/test/image/mocks/cheater.json +++ b/test/image/mocks/cheater.json @@ -49,7 +49,8 @@ "value":[ 400, 540 - ] + ], + "showlabels": true }, "line": { "smoothing": 0 @@ -83,7 +84,8 @@ "operation":"<", "value":[ 0.5 - ] + ], + "showlabels": true }, "line": { "smoothing": 0 @@ -115,7 +117,8 @@ "contours":{ "type":"constraint", "operation":">", - "value":810 + "value":810, + "showlabels": true }, "colorscale":[ [ 0, "green" ], diff --git a/test/image/mocks/contour_constraints.json b/test/image/mocks/contour_constraints.json new file mode 100644 index 00000000000..2beb7809997 --- /dev/null +++ b/test/image/mocks/contour_constraints.json @@ -0,0 +1,133 @@ +{ + "data":[{ + "contours":{ + "type": "constraint", + "operation": "[]", + "value": [2, 4], + "showlabels": true + }, + "hoverlabel": { + "font": {"size": 20, "color": "rgb(0,100,200)"}, + "bgcolor": "rgb(200,200,200)", + "bordercolor": "rgb(0,0,100)" + }, + "zhoverformat": ".2f", + "z":[[1, 2, 3], [2, 3, 4], [3, 4, 5]], + "type":"contour", + "name": "[2, 4]" + }, { + "contours":{ + "type": "constraint", + "operation": "=", + "value": 3.000101, + "showlabels": true + }, + "z":[[1, 2, 3], [2, 3, 4], [3, 4, 5]], + "type":"contour", + "name": "=3.0001" + }, { + "contours":{ + "type": "constraint", + "operation": ">", + "value": 1, + "showlabels": true + }, + "fillcolor": "rgba(0,200,0,0.3)", + "z":[[0, 0.5, 0], [0, 10, 0], [0, 0, 0]], + "type":"contour", + "name": ">1" + }, { + "contours":{ + "type": "constraint", + "operation": ">", + "value": 0.25, + "showlabels": true + }, + "line": {"color": "rgb(150,0,0)"}, + "z":[[0, 0.5, 0], [0, 10, 0], [0, 0, 0]], + "type":"contour", + "name": ">0.25" + }, { + "contours":{ + "type": "constraint", + "operation": "][", + "value": [6, 7], + "showlabels": true + }, + "fillcolor": "rgba(150,0,200,0.3)", + "line": {"color": "rgb(100,0,0)"}, + "z":[[0, 0.5, 0], [0, 10, 0], [0, 0, 0]], + "type":"contour", + "name": "]6, 7[" + }, { + "contours":{ + "type": "constraint", + "operation": "<", + "value": 8, + "showlabels": true, + "labelfont": {"size": 20, "color": "red"}, + "labelformat": ".2f" + }, + "line": {"width": 4, "dash": "dot"}, + "z":[[0, 0.5, 0], [0, 10, 0], [0, 0, 0]], + "type":"contour", + "name": "<8" + }, { + "contours":{ + "type": "constraint", + "operation": "][", + "value": [3, 4], + "showlabels": true + }, + "z":[[11, 10, 1], [11, 10, 1], [11, 10, 1]], + "type":"contour", + "name": "]3, 4[" + }, { + "contours":{ + "type": "constraint", + "operation": "<", + "value": -1 + }, + "z":[[0, -0.5, 0], [0, -10, 0], [0, 0, 0]], + "type":"contour", + "name": "< -1", + "xaxis": "x2" + }, { + "contours":{ + "type": "constraint", + "operation": "<", + "value": -0.25 + }, + "z":[[0, -0.5, 0], [0, -10, 0], [0, 0, 0]], + "type":"contour", + "name": "< -0.25", + "xaxis": "x2" + }, { + "contours":{ + "type": "constraint", + "operation": "][", + "value": [-7, -6] + }, + "z":[[0, -0.5, 0], [0, -10, 0], [0, 0, 0]], + "type":"contour", + "name": "]-7, -6[", + "xaxis": "x2" + }, { + "contours":{ + "type": "constraint", + "operation": ">", + "value": -8 + }, + "z":[[0, -0.5, 0], [0, -10, 0], [0, 0, 0]], + "type":"contour", + "name": "> -8", + "xaxis": "x2" + }], + "layout":{ + "xaxis": {"domain": [0, 0.4444]}, + "xaxis2": {"domain": [0.5556, 1]}, + "height": 500, + "width":1100, + "margin": {"l": 50, "r": 150, "t": 50, "b": 50, "autoexpand": false} + } +} diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index c906e6b43eb..d2d2bef0b7e 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -587,6 +587,76 @@ describe('hover info', function() { .then(done); }); + it('should get the right content and color for contour constraints', function(done) { + var contourConstraints = require('@mocks/contour_constraints.json'); + + Plotly.plot(gd, contourConstraints) + .then(function() { + _hover(gd, 250, 250); + assertHoverLabelContent({ + nums: [ + 'x: 1\ny: 1\nz: 3.00', // custom zhoverformat + 'x: 1\ny: 1\nz: 3', + 'x: 1\ny: 1\nz: 10', + 'x: 1\ny: 1\nz: 10', + 'x: 1\ny: 1\nz: 10', + 'x: 1\ny: 1\nz: 10', + 'x: 1\ny: 1\nz: 10' + ], + name: ['[2, 4]', '=3.0001', '>1', '>0.25', ']6, 7[', '<8', ']3, 4['] + }); + var styles = [{ + // This first one has custom styles. The others all inherit from trace styles. + bgcolor: 'rgb(200, 200, 200)', + bordercolor: 'rgb(0, 0, 100)', + fontSize: 20, + fontFamily: 'Arial', + fontColor: 'rgb(0, 100, 200)' + }, { + bgcolor: 'rgb(255, 127, 14)', + bordercolor: 'rgb(68, 68, 68)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(68, 68, 68)' + }, { + bgcolor: 'rgb(0, 200, 0)', + bordercolor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' + }, { + bgcolor: 'rgb(150, 0, 0)', + bordercolor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' + }, { + bgcolor: 'rgb(150, 0, 200)', + bordercolor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' + }, { + bgcolor: 'rgb(140, 86, 75)', + bordercolor: 'rgb(255, 255, 255)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' + }, { + bgcolor: 'rgb(227, 119, 194)', + bordercolor: 'rgb(68, 68, 68)', + fontSize: 13, + fontFamily: 'Arial', + fontColor: 'rgb(68, 68, 68)' + }]; + d3.selectAll('g.hovertext').each(function(_, i) { + assertHoverLabelStyle(d3.select(this), styles[i]); + }); + }) + .catch(fail) + .then(done); + }); + it('should display correct label content with specified format - histogram2d', function(done) { Plotly.plot(gd, [{ type: 'histogram2d', diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 6822de49f48..a216412c658 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -385,10 +385,17 @@ describe('legend helpers:', function() { var legendGetsTrace = helpers.legendGetsTrace; it('should return true when trace is visible and supports legend', function() { - expect(legendGetsTrace({ visible: true, type: 'bar' })).toBe(true); - expect(legendGetsTrace({ visible: false, type: 'bar' })).toBe(false); - expect(legendGetsTrace({ visible: true, type: 'contour' })).toBe(false); - expect(legendGetsTrace({ visible: false, type: 'contour' })).toBe(false); + expect(legendGetsTrace({ visible: true, showlegend: true })).toBe(true); + expect(legendGetsTrace({ visible: false, showlegend: true })).toBe(false); + expect(legendGetsTrace({ visible: 'legendonly', showlegend: true })).toBe(true); + + expect(legendGetsTrace({ visible: true, showlegend: false })).toBe(true); + expect(legendGetsTrace({ visible: false, showlegend: false })).toBe(false); + expect(legendGetsTrace({ visible: 'legendonly', showlegend: false })).toBe(true); + + expect(legendGetsTrace({ visible: true })).toBe(false); + expect(legendGetsTrace({ visible: false })).toBe(false); + expect(legendGetsTrace({ visible: 'legendonly' })).toBe(false); }); });