Skip to content

Commit

Permalink
first pass for legends
Browse files Browse the repository at this point in the history
- implements color legend (#23)
  • Loading branch information
Fil committed Jan 11, 2021
1 parent a362a0d commit 17e6126
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 1 deletion.
202 changes: 202 additions & 0 deletions src/legend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import {axisBottom, create, format, interpolate, interpolateRound, range, scaleBand, scaleLinear, quantile, quantize} from "d3";

export function Legend(
{color: colorScale},
{color} = {}
) {
return {
...(color && color.legend && {color: new ColorLegend(colorScale, color)})
};
}

export class ColorLegend {
constructor({name}, color) {
this.name = name || "color";
this.color = color;
}
render(
index,
{[this.name]: color}
,
channels,
{
width: canvasWidth,
height: canvasHeight
}
) {
let { color: {
legend: {
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
top = -20,
right = 0,
bottom,
left,
ticks = width / 64,
tickFormat,
tickValues
} = {}
} = {} } = this;
console.warn({
title,
tickSize,
width,
height,
top,
bottom,
left,
right
});
const tx = left !== undefined ? left : canvasWidth - width + right;
const ty = bottom !== undefined ? canvasHeight - bottom - height : top;
return create("svg:g")
.attr("transform", `translate(${tx},${ty})`)
.call(g => legend(g, {color, title, tickSize, width, height, ticks, tickFormat, tickValues}))
.node();
}
}

function legend(svg, {
color,
title,
tickSize,
width,
height,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks,
tickFormat,
tickValues
} = {}) {

let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height);
let x;

// Continuous
if (color.interpolate) {
const n = Math.min(color.domain().length, color.range().length);

x = color.copy().rangeRound(quantize(interpolate(marginLeft, width - marginRight), n));

svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.copy().domain(quantize(interpolate(0, 1), n))).toDataURL());
}

// Sequential
else if (color.interpolator) {
x = Object.assign(color.copy()
.interpolator(interpolateRound(marginLeft, width - marginRight)),
{range() { return [marginLeft, width - marginRight]; }});

svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());

// scaleSequentialQuantile doesn’t implement ticks or tickFormat.
if (!x.ticks) {
if (tickValues === undefined) {
const n = Math.round(ticks + 1);
tickValues = range(n).map(i => quantile(color.domain(), i / (n - 1)));
}
if (typeof tickFormat !== "function") {
tickFormat = format(tickFormat === undefined ? ",f" : tickFormat);
}
}
}

// Threshold
else if (color.invertExtent) {
const thresholds
= color.thresholds ? color.thresholds() // scaleQuantize
: color.quantiles ? color.quantiles() // scaleQuantile
: color.domain(); // scaleThreshold

const thresholdFormat
= tickFormat === undefined ? d => d
: typeof tickFormat === "string" ? format(tickFormat)
: tickFormat;

x = scaleLinear()
.domain([-1, color.range().length - 1])
.rangeRound([marginLeft, width - marginRight]);

svg.append("g")
.selectAll("rect")
.data(color.range())
.join("rect")
.attr("x", (d, i) => x(i - 1))
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", d => d);

tickValues = range(thresholds.length);
tickFormat = i => thresholdFormat(thresholds[i], i);
}

// Ordinal
else {
x = scaleBand()
.domain(color.domain())
.rangeRound([marginLeft, width - marginRight]);

svg.append("g")
.selectAll("rect")
.data(color.domain())
.join("rect")
.attr("x", x)
.attr("y", marginTop)
.attr("width", Math.max(0, x.bandwidth() - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", color);

tickAdjust = () => {};
}

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(axisBottom(x)
.ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined)
.tickFormat(typeof tickFormat === "function" ? tickFormat : undefined)
.tickSize(tickSize)
.tickValues(tickValues))
.call(tickAdjust)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", marginLeft)
.attr("y", marginTop + marginBottom - height - 6)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.attr("class", "title")
.text(title));

return svg.node();
}


function ramp(color, n = 256) {
const canvas = create("canvas")
.attr("width", n)
.attr("height", 1)
.node();
const context = canvas.getContext("2d");
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
10 changes: 9 additions & 1 deletion src/plot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {create} from "d3-selection";
import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js";
import {Legend} from "./legend.js";
import {facets} from "./facet.js";
import {Scales, autoScaleRange} from "./scales.js";

Expand Down Expand Up @@ -51,7 +52,8 @@ export function plot(options = {}) {
const scaleDescriptors = Scales(scaleChannels, options);
const scales = ScaleFunctions(scaleDescriptors);
const axes = Axes(scaleDescriptors, options);
const dimensions = Dimensions(scaleDescriptors, axes, options);
const legend = Legend(scaleDescriptors, options);
const dimensions = Dimensions(scaleDescriptors, axes, legend, options);

autoScaleRange(scaleDescriptors, dimensions);
autoAxisTicks(scaleDescriptors, axes);
Expand All @@ -69,6 +71,8 @@ export function plot(options = {}) {
const y = facet !== undefined && scales.fy ? "fy" : "y";
if (axes[x]) marks.unshift(axes[x]);
if (axes[y]) marks.unshift(axes[y]);

if (legend.color) marks.push(legend.color);

const {width, height} = dimensions;

Expand Down Expand Up @@ -102,6 +106,9 @@ function Dimensions(
fx: {axis: fxAxis} = {},
fy: {axis: fyAxis} = {}
},
{
color
},
{
width = 640,
height = y || fy ? 396 : 60,
Expand All @@ -117,6 +124,7 @@ function Dimensions(
marginLeft = Math.max((yAxis === "left" ? 40 : 0) + facetMarginLeft, xAxis || fxAxis ? 20 : 0)
} = {}
) {
color; // TDB: reserve space for the color legend?
return {
width,
height,
Expand Down

0 comments on commit 17e6126

Please sign in to comment.