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
28 changes: 22 additions & 6 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,33 @@ var helpers = require('./helpers');
module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
var containerIn = layoutIn.legend || {};

var visibleTraces = 0;
var legendTraceCount = 0;
var legendReallyHasATrace = false;
var defaultOrder = 'normal';

var defaultX, defaultY, defaultXAnchor, defaultYAnchor;

for(var i = 0; i < fullData.length; i++) {
var trace = fullData[i];

if(helpers.legendGetsTrace(trace)) {
visibleTraces++;
// always show the legend by default if there's a pie
if(Registry.traceIs(trace, 'pie')) visibleTraces++;
if(!trace.visible) continue;

// Note that we explicitly count any trace that is either shown or
// *would* be shown by default, toward the two traces you need to
// ensure the legend is shown by default, because this can still help
// disambiguate.
if(trace.showlegend || trace._dfltShowLegend) {
legendTraceCount++;
if(trace.showlegend) {
legendReallyHasATrace = true;
// Always show the legend by default if there's a pie,
// or if there's only one trace but it's explicitly shown
if(Registry.traceIs(trace, 'pie') ||
trace._input.showlegend === true
) {
legendTraceCount++;
}
}
}

if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
Expand All @@ -48,7 +63,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
}

var showLegend = Lib.coerce(layoutIn, layoutOut,
basePlotLayoutAttributes, 'showlegend', visibleTraces > 1);
basePlotLayoutAttributes, 'showlegend',
legendReallyHasATrace && legendTraceCount > 1);

if(showLegend === false) return;

Expand Down
10 changes: 5 additions & 5 deletions src/components/legend/get_legend_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ module.exports = function getLegendData(calcdata, opts) {

// build an { legendgroup: [cd0, cd0], ... } object
for(i = 0; i < calcdata.length; i++) {
var cd = calcdata[i],
cd0 = cd[0],
trace = cd0.trace,
lgroup = trace.legendgroup;
var cd = calcdata[i];
var cd0 = cd[0];
var trace = cd0.trace;
var lgroup = trace.legendgroup;

if(!helpers.legendGetsTrace(trace) || !trace.showlegend) continue;
if(!trace.visible || !trace.showlegend) continue;

if(Registry.traceIs(trace, 'pie')) {
if(!slicesShown[lgroup]) slicesShown[lgroup] = {};
Expand Down
10 changes: 0 additions & 10 deletions src/components/legend/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@

'use strict';

exports.legendGetsTrace = function legendGetsTrace(trace) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was used in getLegendData and in legendDefaults, but even before one of them overrode half the logic, and now that logic has diverged further, so it didn't make sense to pull it out.

Copy link
Contributor

Choose a reason for hiding this comment

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

nice 🔪 job here

// 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) {
return (legendLayout.traceorder || '').indexOf('grouped') !== -1;
};
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
8 changes: 7 additions & 1 deletion src/plots/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ module.exports = {
valType: 'boolean',
role: 'info',
editType: 'legend',
description: 'Determines whether or not a legend is drawn.'
description: [
'Determines whether or not a legend is drawn.',
'Default is `true` if there is a trace to show and any of these:',
'a) Two or more traces would by default be shown in the legend.',
'b) One pie trace is shown in the legend.',
'c) One trace is explicitly given with `showlegend: true`.'
].join(' ')
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for 📚

},
colorway: {
valType: 'colorlist',
Expand Down
4 changes: 4 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1144,9 +1144,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac
coerce('ids');

if(Registry.traceIs(traceOut, 'showLegend')) {
traceOut._dfltShowLegend = true;
coerce('showlegend');
coerce('legendgroup');
}
else {
traceOut._dfltShowLegend = false;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#749 🙄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the record: I added _dfltShowLegend because there's otherwise no robust way to tell from just traceIn and traceOut what the default is... so there would be some situations where we would NOT show a legend (eg scatter + colored contour) but then setting showlegend: false explicitly in the trace that already defaults to showlegend: false would make the legend appear 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, that's what I expected. I don't think there's a nicer way to do this than what you coded in this commit 👌

}

Registry.getComponentMethod(
'fx',
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
Loading