Skip to content
Closed
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,31 @@ Plot.plot({
})
```

All the scale definitions are exposed as the *scales* property of the plot.

```js
color = Plot.plot({…}).scales.color;
color.range // ["red", "blue"]
```

And, to reuse the scale in another plot:

```js
const plot1 = Plot.plot(…);

Plot.plot({
color: plot1.scales.color
})
```

#### Plot.scale(*scaleOptions*)

```js
Plot.scale(plot1.scales.color)
```

Returns a [D3 scale](https://github.com/d3/d3-scale) that matches the given Plot scale *options* object.

### Position options

The position scales (*x*, *y*, *fx*, and *fy*) support additional options:
Expand Down
19 changes: 15 additions & 4 deletions src/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function autoAxisTicksK(scale, axis, k) {
}

// Mutates axis.{label,labelAnchor,labelOffset}!
// Mutates scale.label!
export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) {
if (fx) {
autoAxisLabelsX(fx, scales.fx, channels.get("fx"));
Expand Down Expand Up @@ -70,24 +71,34 @@ export function autoAxisLabels(channels, scales, {x, y, fx, fy}, dimensions) {

function autoAxisLabelsX(axis, scale, channels) {
if (axis.labelAnchor === undefined) {
axis.labelAnchor = scale.type === "ordinal" ? "center"
axis.labelAnchor = scale.family === "ordinal" ? "center"
: scale.reverse ? "left"
: "right";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "x");
}
scale.label = axis.label;
}

function autoAxisLabelsY(axis, opposite, scale, channels) {
if (axis.labelAnchor === undefined) {
axis.labelAnchor = scale.type === "ordinal" ? "center"
axis.labelAnchor = scale.family === "ordinal" ? "center"
: opposite && opposite.axis === "top" ? "bottom" // TODO scale.reverse?
: "top";
}
if (axis.label === undefined) {
axis.label = inferLabel(channels, scale, axis, "y");
}
scale.label = axis.label;
}

export function autoScaleLabel(scale, channels, options) {
if (scale === undefined) return;
if (options !== undefined) scale.label = options.label;
if (scale.label === undefined) {
scale.label = inferLabel(channels, scale, {});
}
}

// Channels can have labels; if all the channels for a given scale are
Expand All @@ -104,8 +115,8 @@ function inferLabel(channels = [], scale, axis, key) {
if (candidate !== undefined) {
const {percent, reverse} = scale;
// Ignore the implicit label for temporal scales if it’s simply “date”.
if (scale.type === "temporal" && /^(date|time|year)$/i.test(candidate)) return;
if (scale.type !== "ordinal" && (key === "x" || key === "y")) {
if (scale.family === "temporal" && /^(date|time|year)$/i.test(candidate)) return;
if (scale.family !== "ordinal" && (key === "x" || key === "y")) {
if (percent) candidate = `${candidate} (%)`;
if (axis.labelAnchor === "center") {
candidate = `${candidate} →`;
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {plot} from "./plot.js";
export {Mark, valueof} from "./mark.js";
export {scale} from "./scales.js";
export {Area, area, areaX, areaY} from "./marks/area.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
Expand Down
28 changes: 18 additions & 10 deletions src/plot.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {create} from "d3";
import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js";
import {Axes, autoAxisTicks, autoAxisLabels, autoScaleLabel} from "./axes.js";
import {facets} from "./facet.js";
import {values} from "./mark.js";
import {Scales, autoScaleRange} from "./scales.js";
import {Scales, autoScaleRange, exposeScales} from "./scales.js";
import {offset} from "./style.js";

export function plot(options = {}) {
Expand Down Expand Up @@ -52,6 +52,9 @@ export function plot(options = {}) {
autoScaleRange(scaleDescriptors, dimensions);
autoAxisTicks(scaleDescriptors, axes);
autoAxisLabels(scaleChannels, scaleDescriptors, axes, dimensions);
for (const key of ["color", "r", "opacity"]) {
autoScaleLabel(scaleDescriptors[key], scaleChannels.get(key), options[key]);
}

// Normalize the options.
options = {...scaleDescriptors, ...dimensions};
Expand Down Expand Up @@ -88,13 +91,7 @@ export function plot(options = {}) {
if (node != null) svg.appendChild(node);
}

// Wrap the plot in a figure with a caption, if desired.
if (caption == null) return svg;
const figure = document.createElement("figure");
figure.appendChild(svg);
const figcaption = figure.appendChild(document.createElement("figcaption"));
figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption));
return figure;
return exposeScales(wrap(svg, {caption}), scaleDescriptors);
}

function Dimensions(
Expand Down Expand Up @@ -140,6 +137,17 @@ function ScaleFunctions(scales) {

function autoHeight({y, fy, fx}) {
const nfy = fy ? fy.scale.domain().length : 1;
const ny = y ? (y.type === "ordinal" ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1;
const ny = y ? (y.family === "ordinal" ? y.scale.domain().length : Math.max(7, 17 / nfy)) : 1;
return !!(y || fy) * Math.max(1, Math.min(60, ny * nfy)) * 20 + !!fx * 30 + 60;
}

// Wrap the plot in a figure with a caption, if desired.
function wrap(svg, {caption} = {}) {
if (caption == null) return svg;
const figure = document.createElement("figure");
figure.appendChild(svg);
const figcaption = document.createElement("figcaption");
figcaption.appendChild(caption instanceof Node ? caption : document.createTextNode(caption));
figure.appendChild(figcaption);
return figure;
}
46 changes: 38 additions & 8 deletions src/scales.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,22 @@ function autoScaleRangeY(scale, dimensions) {
const {inset = 0} = scale;
const {height, marginTop = 0, marginBottom = 0} = dimensions;
const range = [height - marginBottom - inset, marginTop + inset];
if (scale.type === "ordinal") range.reverse();
if (scale.family === "ordinal") range.reverse();
scale.scale.range(range);
}
autoScaleRound(scale);
}

function autoScaleRound(scale) {
if (scale.round === undefined && scale.type === "ordinal" && scale.scale.step() >= 5) {
if (scale.round === undefined && scale.family === "ordinal" && scale.scale.step() >= 5) {
scale.scale.round(true);
}
}

function Scale(key, channels = [], options = {}) {
switch (inferScaleType(key, channels, options)) {
const type = options.type;
options.type = inferScaleType(key, channels, options);
switch (options.type) {
case "diverging": return ScaleDiverging(key, channels, options);
case "diverging-sqrt": return ScaleDivergingSqrt(key, channels, options);
case "diverging-pow": return ScaleDivergingPow(key, channels, options);
Expand All @@ -77,11 +79,15 @@ function Scale(key, channels = [], options = {}) {
case "point": return ScalePoint(key, channels, options);
case "band": return ScaleBand(key, channels, options);
case "identity": return registry.get(key) === position ? ScaleIdentity(key, channels, options) : undefined;
case undefined: return;
default: throw new Error(`unknown scale type: ${options.type}`);
case undefined: break;
default: throw new Error(`unknown scale type: ${type}`);
}
}

export function scale(options) {
return Scale(undefined, undefined, options).scale;
}

function inferScaleType(key, channels, {type, domain, range}) {
if (key === "fx" || key === "fy") return "band";
if (type !== undefined) {
Expand All @@ -97,7 +103,7 @@ function inferScaleType(key, channels, {type, domain, range}) {
for (const {type} of channels) if (type !== undefined) return type;
if ((domain || range || []).length > 2) return asOrdinalType(key);
if (domain !== undefined) {
if (isOrdinal(domain)) return asOrdinalType(key);
if (isOrdinal(domain)) return asOrdinalType(key, type);
if (isTemporal(domain)) return "utc";
return "linear";
}
Expand All @@ -109,6 +115,30 @@ function inferScaleType(key, channels, {type, domain, range}) {
}

// Positional scales default to a point scale instead of an ordinal scale.
function asOrdinalType(key) {
return registry.get(key) === position ? "point" : "ordinal";
function asOrdinalType(key, type = "categorical") {
return registry.get(key) === position ? "point" : type;
}

export function exposeScales(figure, scaleDescriptors) {
const scales = figure.scales = {};
for (const key in scaleDescriptors) {
let cache;
Object.defineProperty(scales, key, {
enumerable: true,
get: () => cache = cache || exposeScale(scaleDescriptors[key])
});
}
return figure;
}

function exposeScale({scale, ...options}) {
for (const remove of ["domain", "range", "interpolate", "clamp", "round", "nice", "padding", "inset", "reverse"]) delete options[remove];
return {
domain: scale.domain(),
range: scale.range(),
...scale.interpolate && {interpolate: scale.interpolate()},
...scale.interpolator && {interpolate: scale.interpolator(), range: undefined},
...scale.clamp && {clamp: scale.clamp()},
...options
};
}
13 changes: 6 additions & 7 deletions src/scales/ordinal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function ScaleO(scale, channels, {
domain = inferDomain(channels),
range,
reverse,
inset
...options
}) {
if (reverse = !!reverse) domain = reverseof(domain);
scale.domain(domain);
Expand All @@ -17,7 +17,7 @@ export function ScaleO(scale, channels, {
if (typeof range === "function") range = range(domain);
scale.range(range);
}
return {type: "ordinal", reverse, domain, range, scale, inset};
return {family: "ordinal", domain, range, scale, ...options};
}

export function ScaleOrdinal(key, channels, {
Expand All @@ -26,7 +26,7 @@ export function ScaleOrdinal(key, channels, {
range = registry.get(key) === color ? ordinalScheme(scheme) : undefined,
...options
}) {
return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, ...options});
return ScaleO(scaleOrdinal().unknown(undefined), channels, {range, type, ...options});
}

export function ScalePoint(key, channels, {
Expand All @@ -39,7 +39,7 @@ export function ScalePoint(key, channels, {
.align(align)
.padding(padding),
channels,
options
{align, padding, ...options}
);
}

Expand All @@ -56,12 +56,11 @@ export function ScaleBand(key, channels, {
.paddingInner(paddingInner)
.paddingOuter(paddingOuter),
channels,
options
{align, paddingInner, paddingOuter, ...options}
);
}

function maybeRound(scale, channels, options = {}) {
const {round} = options;
function maybeRound(scale, channels, {round, ...options} = {}) {
if (round !== undefined) scale.round(round);
scale = ScaleO(scale, channels, options);
scale.round = round;
Expand Down
32 changes: 17 additions & 15 deletions src/scales/quantitative.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,39 @@ export function ScaleQ(key, scale, channels, {
clamp,
zero,
domain = (registry.get(key) === radius || registry.get(key) === opacity ? inferZeroDomain : inferDomain)(channels),
percent,
round,
range = registry.get(key) === radius ? inferRadialRange(channels, domain) : registry.get(key) === opacity ? [0, 1] : undefined,
type,
scheme = type === "cyclical" ? "rainbow" : "turbo",
interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : round ? interpolateRound : undefined,
reverse,
inset
...rest
}) {
if (zero) domain = domain[1] < 0 ? [domain[0], 0] : domain[0] > 0 ? [0, domain[1]] : domain;
if (reverse = !!reverse) domain = reverseof(domain);
scale.domain(domain);
if (nice) scale.nice(nice === true ? undefined : nice);
reverse = !!reverse;

// Sometimes interpolator is named interpolator, such as "lab" for Lab color
// space. Other times interpolate is a function that takes two arguments and
// is used in conjunction with the range. And other times the interpolate
// function is a “fixed” interpolator independent of the range, as when a
// color scheme such as interpolateRdBu is used.
if (interpolate !== undefined) {
if (scale.interpolate && interpolate !== undefined) {
if (typeof interpolate !== "function") {
interpolate = Interpolator(interpolate);
} else if (interpolate.length === 1) {
if (reverse) interpolate = flip(interpolate);
if (reverse) interpolate = flip(interpolate), reverse = null;
interpolate = constant(interpolate);
}
scale.interpolate(interpolate);
}

if (reverse) domain = reverseof(domain);
scale.domain(domain);
if (nice) scale.nice(nice === true ? undefined : nice);

if (range !== undefined) scale.range(range);
if (clamp) scale.clamp(clamp);
return {type: "quantitative", reverse, domain, range, scale, inset, percent};
return {family: "quantitative", reverse, domain, range, scale, type, ...rest};
}

export function ScaleLinear(key, channels, options) {
Expand All @@ -98,11 +99,11 @@ export function ScaleSqrt(key, channels, options) {
}

export function ScalePow(key, channels, {exponent = 1, ...options}) {
return ScaleQ(key, scalePow().exponent(exponent), channels, options);
return ScaleQ(key, scalePow().exponent(exponent), channels, options.type === "sqrt" ? options : {exponent, ...options});
}

export function ScaleLog(key, channels, {base = 10, domain = inferLogDomain(channels), ...options}) {
return ScaleQ(key, scaleLog().base(base), channels, {domain, ...options});
return ScaleQ(key, scaleLog().base(base), channels, {base, domain, ...options});
}

export function ScaleQuantile(key, channels, {
Expand All @@ -118,7 +119,7 @@ export function ScaleQuantile(key, channels, {
}

export function ScaleSymlog(key, channels, {constant = 1, ...options}) {
return ScaleQ(key, scaleSymlog().constant(constant), channels, options);
return ScaleQ(key, scaleSymlog().constant(constant), channels, {constant, ...options});
}

export function ScaleThreshold(key, channels, {
Expand All @@ -134,7 +135,7 @@ export function ScaleThreshold(key, channels, {
}

export function ScaleIdentity() {
return {type: "identity", scale: scaleIdentity()};
return {family: "identity", scale: scaleIdentity(), type: "identity"};
}

function ScaleD(key, scale, channels, {
Expand All @@ -145,9 +146,10 @@ function ScaleD(key, scale, channels, {
range,
scheme = "rdbu",
interpolate = registry.get(key) === color ? (range !== undefined ? interpolateRgb : quantitativeScheme(scheme)) : undefined,
reverse
reverse,
...rest
}) {
domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)];
if (domain.length === 2) domain = [Math.min(domain[0], pivot), pivot, Math.max(domain[1], pivot)];
if (reverse = !!reverse) domain = reverseof(domain);

// Sometimes interpolator is named interpolator, such as "lab" for Lab color
Expand All @@ -162,7 +164,7 @@ function ScaleD(key, scale, channels, {
scale.domain(domain).interpolator(interpolate);
if (clamp) scale.clamp(clamp);
if (nice) scale.nice(nice);
return {type: "quantitative", reverse, domain, scale};
return {family: "quantitative", scale, ...rest};
}

export function ScaleDiverging(key, channels, options) {
Expand Down
2 changes: 1 addition & 1 deletion src/scales/temporal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ScaleQ} from "./quantitative.js";

function ScaleT(key, scale, channels, options) {
const s = ScaleQ(key, scale, channels, options);
s.type = "temporal";
s.family = "temporal";
return s;
}

Expand Down
Loading