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

symbol #652

Merged
merged 10 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"snowpack": "3"
},
"dependencies": {
"d3": "7",
"d3": "^7.3.0",
"isoformat": "0.2"
},
"engines": {
Expand Down
40 changes: 35 additions & 5 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {rgb} from "d3";
import {normalizeScale} from "./scales.js";
import {legendColor} from "./legends/color.js";
import {legendOpacity} from "./legends/opacity.js";
import {legendRamp} from "./legends/ramp.js";
import {legendSwatches, legendSymbols} from "./legends/swatches.js";
import {isObject} from "./mark.js";

const legendRegistry = new Map([
["color", legendColor],
["symbol", legendSymbols],
["opacity", legendOpacity]
]);

export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
if (isObject(scale)) { // e.g., ignore {color: "red"}
return value(normalizeScale(key, scale), legendOptions(scale, options));
return value(normalizeScale(key, scale), legendOptions(scale, options), key => isObject(options[key]) ? normalizeScale(key, options[key]) : null);
}
}
throw new Error("unknown legend type");
Expand All @@ -22,20 +24,48 @@ export function exposeLegends(scales, defaults = {}) {
return (key, options) => {
if (!legendRegistry.has(key)) throw new Error(`unknown legend type: ${key}`);
if (!(key in scales)) return;
return legendRegistry.get(key)(scales[key], legendOptions(defaults[key], options));
return legendRegistry.get(key)(scales[key], legendOptions(defaults[key], options), key => scales[key]);
};
}

function legendOptions({label, ticks, tickFormat} = {}, options = {}) {
return {label, ticks, tickFormat, ...options};
}

function legendColor(color, {
legend = true,
...options
}) {
if (legend === true) legend = color.type === "ordinal" ? "swatches" : "ramp";
switch (`${legend}`.toLowerCase()) {
case "swatches": return legendSwatches(color, options);
case "ramp": return legendRamp(color, options);
default: throw new Error(`unknown legend type: ${legend}`);
}
}

function legendOpacity({type, interpolate, ...scale}, {
legend = true,
color = rgb(0, 0, 0),
...options
}) {
if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
if (legend === true) legend = "ramp";
if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`);
return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options});
}

function interpolateOpacity(color) {
const {r, g, b} = rgb(color) || rgb(0, 0, 0); // treat invalid color as black
return t => `rgba(${r},${g},${b},${t})`;
}

export function Legends(scales, options) {
const legends = [];
for (const [key, value] of legendRegistry) {
const o = options[key];
if (o && o.legend) {
legends.push(value(scales[key], legendOptions(scales[key], o)));
legends.push(value(scales[key], legendOptions(scales[key], o), key => scales[key]));
}
}
return legends;
Expand Down
14 changes: 0 additions & 14 deletions src/legends/color.js

This file was deleted.

20 changes: 0 additions & 20 deletions src/legends/opacity.js

This file was deleted.

99 changes: 81 additions & 18 deletions src/legends/swatches.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,79 @@
import {create} from "d3";
import {create, path} from "d3";
import {inferFontVariant} from "../axes.js";
import {maybeTickFormat} from "../axis.js";
import {applyInlineStyles, impliedString, maybeClassName} from "../style.js";
import {maybeColorChannel, maybeNumberChannel} from "../mark.js";
import {applyInlineStyles, impliedString, maybeClassName, none} from "../style.js";

export function legendSwatches(color, {
function maybeScale(scale, key) {
if (key == null) return key;
const s = scale(key);
if (!s) throw new Error(`scale not found: ${key}`);
return s;
}

export function legendSwatches(color, options) {
return legendItems(
color,
options,
selection => selection.style("--color", color.scale),
className => `.${className}-swatch::before {
content: "";
width: var(--swatchWidth);
height: var(--swatchHeight);
margin-right: 0.5em;
background: var(--color);
}`
);
}

export function legendSymbols(symbol, {
fill = symbol.hint?.fill !== undefined ? symbol.hint.fill : "none",
fillOpacity = 1,
stroke = symbol.hint?.stroke !== undefined ? symbol.hint.stroke : none(fill) ? "currentColor" : "none",
strokeOpacity = 1,
strokeWidth = 1.5,
r = 4.5,
...options
} = {}, scale) {
const [vf, cf] = maybeColorChannel(fill);
const [vs, cs] = maybeColorChannel(stroke);
const sf = maybeScale(scale, vf);
const ss = maybeScale(scale, vs);
const size = r * r * Math.PI;
fillOpacity = maybeNumberChannel(fillOpacity)[1];
strokeOpacity = maybeNumberChannel(strokeOpacity)[1];
strokeWidth = maybeNumberChannel(strokeWidth)[1];
return legendItems(
symbol,
options,
selection => selection.append("svg")
.attr("viewBox", "-8 -8 16 16")
.attr("fill", vf === "color" ? d => sf.scale(d) : null)
.attr("stroke", vs === "color" ? d => ss.scale(d) : null)
.append("path")
.attr("d", d => {
const p = path();
symbol.scale(d).draw(p, size);
return p;
}),
className => `.${className}-swatch > svg {
width: var(--swatchWidth);
height: var(--swatchHeight);
margin-right: 0.5em;
overflow: visible;
fill: ${cf};
fill-opacity: ${fillOpacity};
stroke: ${cs};
stroke-width: ${strokeWidth}px;
stroke-opacity: ${strokeOpacity};
}`
);
}

function legendItems(scale, {
columns,
tickFormat,
fontVariant = inferFontVariant(color),
fontVariant = inferFontVariant(scale),
// TODO label,
swatchSize = 15,
swatchWidth = swatchSize,
Expand All @@ -15,9 +82,9 @@ export function legendSwatches(color, {
className,
style,
width
} = {}) {
} = {}, swatch, swatchStyle) {
className = maybeClassName(className);
tickFormat = maybeTickFormat(tickFormat, color.domain);
tickFormat = maybeTickFormat(tickFormat, scale.domain);

const swatches = create("div")
.attr("class", className)
Expand Down Expand Up @@ -49,10 +116,10 @@ export function legendSwatches(color, {
swatches
.style("columns", columns)
.selectAll()
.data(color.domain)
.data(scale.domain)
.join("div")
.attr("class", `${className}-swatch`)
.style("--color", color.scale)
.call(swatch, scale)
.call(item => item.append("div")
.attr("class", `${className}-label`)
.attr("title", tickFormat)
Expand All @@ -74,11 +141,13 @@ export function legendSwatches(color, {

swatches
.selectAll()
.data(color.domain)
.data(scale.domain)
.join("span")
.attr("class", `${className}-swatch`)
.style("--color", color.scale)
.text(tickFormat);
.call(swatch, scale)
.append(function() {
return document.createTextNode(tickFormat.apply(this, arguments));
});
}

return swatches
Expand All @@ -90,13 +159,7 @@ export function legendSwatches(color, {
margin-left: ${+marginLeft}px;`}${width === undefined ? "" : `
width: ${width}px;`}
}
.${className}-swatch::before {
content: "";
width: var(--swatchWidth);
height: var(--swatchHeight);
margin-right: 0.5em;
background: var(--color);
}
${swatchStyle(className)}
${extraStyle}
`))
.style("font-variant", impliedString(fontVariant, "normal"))
Expand Down
15 changes: 8 additions & 7 deletions src/mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ export class Mark {
}

// TODO Type coercion?
function Channel(data, {scale, type, value}) {
function Channel(data, {scale, type, value, hint}) {
return {
scale,
type,
value: valueof(data, value),
label: labelof(value)
label: labelof(value),
hint
};
}

Expand Down Expand Up @@ -134,16 +135,16 @@ const colors = new Set(["currentColor", "none"]);
// tuple [channel, constant] where one of the two is undefined, and the other is
// the given value. If you wish to reference a named field that is also a valid
// CSS color, use an accessor (d => d.red) instead.
export function maybeColor(value, defaultValue) {
export function maybeColorChannel(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null ? [undefined, "none"]
: typeof value === "string" && (colors.has(value) || color(value)) ? [undefined, value]
: [value, undefined];
}

// Similar to maybeColor, this tests whether the given value is a number
// Similar to maybeColorChannel, this tests whether the given value is a number
// indicating a constant, and otherwise assumes that it’s a channel value.
export function maybeNumber(value, defaultValue) {
export function maybeNumberChannel(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null || typeof value === "number" ? [undefined, value]
: [value, undefined];
Expand Down Expand Up @@ -202,8 +203,8 @@ export function maybeTuple(x, y) {
// A helper for extracting the z channel, if it is variable. Used by transforms
// that require series, such as moving average and normalize.
export function maybeZ({z, fill, stroke} = {}) {
if (z === undefined) ([z] = maybeColor(fill));
if (z === undefined) ([z] = maybeColor(stroke));
if (z === undefined) ([z] = maybeColorChannel(fill));
if (z === undefined) ([z] = maybeColorChannel(stroke));
return z;
}

Expand Down
6 changes: 3 additions & 3 deletions src/marks/cell.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {identity, indexOf, maybeColor, maybeTuple} from "../mark.js";
import {identity, indexOf, maybeColorChannel, maybeTuple} from "../mark.js";
import {AbstractBar} from "./bar.js";

export class Cell extends AbstractBar {
Expand Down Expand Up @@ -26,11 +26,11 @@ export function cell(data, {x, y, ...options} = {}) {
}

export function cellX(data, {x = indexOf, fill, stroke, ...options} = {}) {
if (fill === undefined && maybeColor(stroke)[0] === undefined) fill = identity;
if (fill === undefined && maybeColorChannel(stroke)[0] === undefined) fill = identity;
return new Cell(data, {...options, x, fill, stroke});
}

export function cellY(data, {y = indexOf, fill, stroke, ...options} = {}) {
if (fill === undefined && maybeColor(stroke)[0] === undefined) fill = identity;
if (fill === undefined && maybeColorChannel(stroke)[0] === undefined) fill = identity;
return new Cell(data, {...options, y, fill, stroke});
}
Loading