Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contour legend #2891

Merged
merged 9 commits into from
Aug 15, 2018
101 changes: 71 additions & 30 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,43 +266,76 @@ function makePointPath(symbolNumber, r) {

var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0};
var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0};
var stopFormatter = d3.format('~.1f');
var gradientInfo = {
radial: {node: 'radialGradient'},
radialreversed: {node: 'radialGradient', reversed: true},
horizontal: {node: 'linearGradient', attrs: HORZGRADIENT},
horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true},
vertical: {node: 'linearGradient', attrs: VERTGRADIENT},
verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true}
};

/**
* gradient: create and apply a gradient fill
*
* @param {object} sel: d3 selection to apply this gradient to
* You can use `selection.call(Drawing.gradient, ...)`
* @param {DOM element} gd: the graph div `sel` is part of
* @param {string} gradientID: a unique (within this plot) identifier
* for this gradient, so that we don't create unnecessary definitions
* @param {string} type: 'radial', 'horizontal', or 'vertical', optionally with
* 'reversed' at the end. Normally radial goes center to edge,
* horizontal goes right to left, and vertical goes bottom to top
* @param {array} colorscale: as in attribute values, [[fraction, color], ...]
* @param {string} prop: the property to apply to, 'fill' or 'stroke'
*/
drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) {
var len = colorscale.length;
var info = gradientInfo[type];
var colorStops = new Array(len);
for(var i = 0; i < len; i++) {
if(info.reversed) {
colorStops[len - 1 - i] = [stopFormatter((1 - colorscale[i][0]) * 100), colorscale[i][1]];
}
else {
colorStops[i] = [stopFormatter(colorscale[i][0] * 100), colorscale[i][1]];
}
}

var fullID = 'g' + gd._fullLayout._uid + '-' + gradientID;

drawing.gradient = function(sel, gd, gradientID, type, color1, color2) {
var gradient = gd._fullLayout._defs.select('.gradients')
.selectAll('#' + gradientID)
.data([type + color1 + color2], Lib.identity);
.selectAll('#' + fullID)
.data([type + colorStops.join(';')], Lib.identity);

gradient.exit().remove();

gradient.enter()
.append(type === 'radial' ? 'radialGradient' : 'linearGradient')
.append(info.node)
.each(function() {
var el = d3.select(this);
if(type === 'horizontal') el.attr(HORZGRADIENT);
else if(type === 'vertical') el.attr(VERTGRADIENT);

el.attr('id', gradientID);

var tc1 = tinycolor(color1);
var tc2 = tinycolor(color2);

el.append('stop').attr({
offset: '0%',
'stop-color': Color.tinyRGB(tc2),
'stop-opacity': tc2.getAlpha()
});

el.append('stop').attr({
offset: '100%',
'stop-color': Color.tinyRGB(tc1),
'stop-opacity': tc1.getAlpha()
if(info.attrs) el.attr(info.attrs);

el.attr('id', fullID);

var stops = el.selectAll('stop')
.data(colorStops);
stops.exit().remove();
stops.enter().append('stop');

stops.each(function(d) {
var tc = tinycolor(d[1]);
d3.select(this).attr({
offset: d[0] + '%',
'stop-color': Color.tinyRGB(tc),
'stop-opacity': tc.getAlpha()
});
});
});

sel.style({
fill: 'url(#' + gradientID + ')',
'fill-opacity': null
});
sel.style(prop, 'url(#' + fullID + ')')
.style(prop + '-opacity', null);
};

/*
Expand Down Expand Up @@ -420,21 +453,29 @@ drawing.singlePointStyle = function(d, sel, trace, fns, gd) {
if(gradientType) perPointGradient = true;
else gradientType = markerGradient && markerGradient.type;

// for legend - arrays will propagate through here, but we don't need
// to treat it as per-point.
if(Array.isArray(gradientType)) {
gradientType = gradientType[0];
if(!gradientInfo[gradientType]) gradientType = 0;
}

if(gradientType && gradientType !== 'none') {
var gradientColor = d.mgc;
if(gradientColor) perPointGradient = true;
else gradientColor = markerGradient.color;

var gradientID = 'g' + gd._fullLayout._uid + '-' + trace.uid;
var gradientID = trace.uid;
if(perPointGradient) gradientID += '-' + d.i;

sel.call(drawing.gradient, gd, gradientID, gradientType, fillColor, gradientColor);
drawing.gradient(sel, gd, gradientID, gradientType,
[[0, gradientColor], [1, fillColor]], 'fill');
} else {
sel.call(Color.fill, fillColor);
Color.fill(sel, fillColor);
}

if(lineWidth) {
sel.call(Color.stroke, lineColor);
Color.stroke(sel, lineColor);
}
}
};
Expand Down
77 changes: 65 additions & 12 deletions src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,78 @@ module.exports = function style(s, gd) {
var showFill = trace.visible && trace.fill && trace.fill !== 'none';
var showLine = subTypes.hasLines(trace);
var contours = trace.contours;
var showGradientLine = false;
var showGradientFill = false;

if(contours && contours.type === 'constraint') {
showLine = contours.showlines;
showFill = contours._operation !== '=';
if(contours) {
var coloring = contours.coloring;

if(coloring === 'lines') {
showGradientLine = true;
}
else {
showLine = coloring === 'none' || coloring === 'heatmap' ||
contours.showlines;
}

if(contours.type === 'constraint') {
showFill = contours._operation !== '=';
}
else if(coloring === 'fill' || coloring === 'heatmap') {
showGradientFill = true;
}
}

var fill = d3.select(this).select('.legendfill').selectAll('path')
.data(showFill ? [d] : []);
// with fill and no markers or text, move the line and fill up a bit
// so it's more centered
var markersOrText = subTypes.hasMarkers(trace) || subTypes.hasText(trace);
var anyFill = showFill || showGradientFill;
var anyLine = showLine || showGradientLine;
var pathStart = (markersOrText || !anyFill) ? 'M5,0' :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice touch!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peek 2018-08-10 14-46

// with a line leave it slightly below center, to leave room for the
// line thickness and because the line is usually more prominent
anyLine ? 'M5,-2' : 'M5,-3';

var this3 = d3.select(this);

var fill = this3.select('.legendfill').selectAll('path')
.data(showFill || showGradientFill ? [d] : []);
fill.enter().append('path').classed('js-fill', true);
fill.exit().remove();
fill.attr('d', 'M5,0h30v6h-30z')
.call(Drawing.fillGroupStyle);
fill.attr('d', pathStart + 'h30v6h-30z')
.call(showFill ? Drawing.fillGroupStyle : fillGradient);

var line = d3.select(this).select('.legendlines').selectAll('path')
.data(showLine ? [d] : []);
line.enter().append('path').classed('js-line', true)
.attr('d', 'M5,0h30');
var line = this3.select('.legendlines').selectAll('path')
.data(showLine || showGradientLine ? [d] : []);
line.enter().append('path').classed('js-line', true);
line.exit().remove();
line.call(Drawing.lineGroupStyle);

// this is ugly... but you can't apply a gradient to a perfectly
// horizontal or vertical line. Presumably because then
// the system doesn't know how to scale vertical variation, even
// though there *is* no vertical variation in this case.
// so add an invisibly small angle to the line
// This issue (and workaround) exist across (Mac) Chrome, FF, and Safari
line.attr('d', pathStart + (showGradientLine ? 'l30,0.0001' : 'h30'))
.call(showLine ? Drawing.lineGroupStyle : lineGradient);

function fillGradient(s) {
if(s.size()) {
var gradientID = 'legendfill-' + trace.uid;
Drawing.gradient(s, gd, gradientID, 'horizontalreversed',
trace.colorscale, 'fill');
}
}

function lineGradient(s) {
if(s.size()) {
var gradientID = 'legendline-' + trace.uid;
Drawing.lineGroupStyle(s);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should double-check that this works ok with line.dash and other line stylings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, added to a mock in 73f279b

Drawing.gradient(s, gd, gradientID, 'horizontalreversed',
trace.colorscale, 'stroke');
}
}

}

function stylePoints(d) {
Expand Down
3 changes: 0 additions & 3 deletions src/traces/contour/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
var isConstraint = (coerce('contours.type') === 'constraint');
coerce('connectgaps', Lib.isArray1D(traceOut.z));

// 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);
}
Expand Down
64 changes: 28 additions & 36 deletions src/traces/contour/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var Drawing = require('../../components/drawing');
var svgTextUtils = require('../../lib/svg_text_utils');
var Axes = require('../../plots/cartesian/axes');
var setConvert = require('../../plots/cartesian/set_convert');
var getUidsFromCalcData = require('../../plots/get_data').getUidsFromCalcData;

var heatmapPlot = require('../heatmap/plot');
var makeCrossings = require('./make_crossings');
Expand All @@ -28,32 +27,40 @@ var constants = require('./constants');
var costConstants = constants.LABELOPTIMIZER;

exports.plot = function plot(gd, plotinfo, cdcontours, contourLayer) {
var uidLookup = getUidsFromCalcData(cdcontours);
plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOne);
};

contourLayer.selectAll('g.contour').each(function(d) {
if(!uidLookup[d.trace.uid]) {
d3.select(this).remove();
}
});
function plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOneFn) {
var contours = contourLayer.selectAll('g.contour')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah. Way better.

By the looks of it, heatmap and carpet probably suffer from similar bugs:

image

From which we could 🔪 getUidsFromCalcdata entirely:

image

No need to do this in this PR of course, but opening a new issue about it would be nice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call -> #2907

.data(
cdcontours.map(function(d) { return d[0]; }),
function(cd) { return cd.trace.uid; }
);

for(var i = 0; i < cdcontours.length; i++) {
plotOne(gd, plotinfo, cdcontours[i], contourLayer);
}
};
contours.exit().remove();

contours.enter().append('g')
.classed('contour', true);

function plotOne(gd, plotinfo, cd, contourLayer) {
var trace = cd[0].trace;
var x = cd[0].x;
var y = cd[0].y;
contours.each(function(cd) {
plotOneFn(gd, plotinfo, cd, d3.select(this));
})
.order();
}
exports.plotWrapper = plotWrapper;

function plotOne(gd, plotinfo, cd, plotGroup) {
var trace = cd.trace;
var x = cd.x;
var y = cd.y;
var contours = trace.contours;
var id = 'contour' + trace.uid;
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var fullLayout = gd._fullLayout;
var pathinfo = emptyPathinfo(contours, plotinfo, cd[0]);
var pathinfo = emptyPathinfo(contours, plotinfo, cd);

// use a heatmap to fill - draw it behind the lines
var heatmapColoringLayer = Lib.ensureSingle(contourLayer, 'g', 'heatmapcoloring');
var heatmapColoringLayer = Lib.ensureSingle(plotGroup, 'g', 'heatmapcoloring');
var cdheatmaps = [];
if(contours.coloring === 'heatmap') {
if(trace.zauto && (trace.autocontour === false)) {
Expand All @@ -62,7 +69,7 @@ function plotOne(gd, plotinfo, cd, contourLayer) {
trace._input.zmax = trace.zmax =
trace.zmin + pathinfo.length * contours.size;
}
cdheatmaps = [cd];
cdheatmaps = [[cd]];
}
heatmapPlot(gd, plotinfo, cdheatmaps, heatmapColoringLayer);

Expand All @@ -87,27 +94,12 @@ function plotOne(gd, plotinfo, cd, contourLayer) {
}

// draw everything
var plotGroup = exports.makeContourGroup(contourLayer, cd, id);
makeBackground(plotGroup, perimeter, contours);
makeFills(plotGroup, fillPathinfo, perimeter, contours);
makeLinesAndLabels(plotGroup, pathinfo, gd, cd[0], contours, perimeter);
clipGaps(plotGroup, plotinfo, fullLayout._clips, cd[0], perimeter);
makeLinesAndLabels(plotGroup, pathinfo, gd, cd, contours, perimeter);
clipGaps(plotGroup, plotinfo, fullLayout._clips, cd, perimeter);
}

exports.makeContourGroup = function(layer, cd, id) {
var plotgroup = layer
.selectAll('g.contour.' + id)
.data(cd);

plotgroup.enter().append('g')
.classed('contour', true)
.classed(id, true);

plotgroup.exit().remove();

return plotgroup;
};

function makeBackground(plotgroup, perimeter, contours) {
var bggroup = Lib.ensureSingle(plotgroup, 'g', 'contourbg');

Expand Down
4 changes: 4 additions & 0 deletions src/traces/contour/style_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout,
}

if(coloring !== 'none') {
// plots/plots always coerces showlegend to true, but in this case
// we default to false and (by default) show a colorbar instead
if(traceIn.showlegend !== true) traceOut.showlegend = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nice ♻️ . This gets used in

image


colorscaleDefaults(
traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}
);
Expand Down
3 changes: 0 additions & 3 deletions src/traces/contourcarpet/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('text');
var isConstraint = (coerce('contours.type') === 'constraint');

// 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, {hasHover: false});
} else {
Expand Down
Loading