Skip to content

Commit

Permalink
Merge branch 'main' into mbostock/type-module
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Jul 29, 2021
2 parents 78b9d0b + 5660871 commit 1612b68
Show file tree
Hide file tree
Showing 140 changed files with 17,103 additions and 16,504 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,3 +1,5 @@
.DS_Store
dist/
node_modules/
test/output/*-changed.svg
test/output/*-changed.html
8 changes: 3 additions & 5 deletions CONTRIBUTING.md
Expand Up @@ -15,7 +15,7 @@ development; this means that you can edit the Plot source code and examples, and
they’ll update live as you save changes. To start Snowpack:

```
yarn start
yarn dev
```

This should open http://localhost:8008/ where you can browse the tests.
Expand All @@ -25,10 +25,8 @@ This should open http://localhost:8008/ where you can browse the tests.
Plot has both unit tests and snapshot tests.

**Unit tests** live in `test` and have the `-test.js` extension. These tests are
written using [Tape](https://github.com/substack/tape) (more precisely,
[tape-await](https://github.com/mbostock/tape-await) for easier async testing).
Generally speaking, unit tests make specific assertions about the behavior of
Plot’s internals and helper methods.
written using [Mocha](https://mochajs.org). Generally speaking, unit tests make
specific assertions about the behavior of Plot’s internals and helper methods.

**Snapshot tests** live in `test/plots`; these also serve as examples of how to
use the Plot API. Each snapshot test defines a plot by exporting a default async
Expand Down
1,010 changes: 820 additions & 190 deletions README.md

Large diffs are not rendered by default.

Binary file added img/bin.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/frame.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/group.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/select.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/stack.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/window.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "@observablehq/plot",
"description": "A JavaScript library for exploratory data visualization.",
"version": "0.0.1",
"version": "0.1.0",
"author": {
"name": "Observable, Inc.",
"url": "https://observablehq.com"
Expand Down
8 changes: 7 additions & 1 deletion rollup.config.js
Expand Up @@ -14,6 +14,12 @@ const d3 = require("d3/package.json");
if (typeof d3.jsdelivr === "undefined") throw new Error("unable to resolve d3");
const d3Path = `d3@${d3.version}/${d3.jsdelivr}`;

// Extract copyrights from the LICENSE.
const copyrights = fs.readFileSync("./LICENSE", "utf-8")
.split(/\n/g)
.filter(line => /^copyright\s+/i.test(line))
.map(line => line.replace(/^copyright\s+/i, ""));

// A lil’ Rollup plugin to allow importing of style.css.
const cssPath = path.resolve("./src/style.css");
const css = {
Expand All @@ -39,7 +45,7 @@ const config = {
external: ["d3"],
output: {
indent: false,
banner: `// ${meta.name} v${meta.version} Copyright ${(new Date).getFullYear()} ${meta.author.name}`
banner: `// ${meta.name} v${meta.version} Copyright ${copyrights.join(", ")}`
},
plugins: [
css,
Expand Down
23 changes: 14 additions & 9 deletions src/facet.js
@@ -1,4 +1,4 @@
import {cross, groups, InternMap} from "d3";
import {cross, difference, groups, InternMap} from "d3";
import {create} from "d3";
import {Mark, values, first, second} from "./mark.js";

Expand All @@ -10,6 +10,7 @@ export function facets(data, {x, y, ...options}, marks) {

class Facet extends Mark {
constructor(data, {x, y, ...options} = {}, marks = []) {
if (data == null) throw new Error("missing facet data");
super(
data,
[
Expand All @@ -36,26 +37,30 @@ class Facet extends Mark {
for (const facetKey of facetsKeys) {
marksIndexByFacet.set(facetKey, new Array(this.marks.length));
}
let facetsExclude;
for (let i = 0; i < this.marks.length; ++i) {
const mark = this.marks[i];
const markFacets = mark.data === this.data ? facetsIndex : undefined;
const {index, channels} = mark.initialize(markFacets);
const {facet} = mark;
const markFacets = facet === "auto" ? mark.data === this.data ? facetsIndex : undefined
: facet === "include" ? facetsIndex
: facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(index, f))))
: undefined;
const {index: I, channels} = mark.initialize(markFacets);
// If an index is returned by mark.initialize, its structure depends on
// whether or not faceting has been applied: it is a flat index ([0, 1, 2,
// …]) when not faceted, and a nested index ([[0, 1, …], [2, 3, …], …])
// when faceted. Faceting is only applied if the mark data is the same as
// the facet’s data.
if (index !== undefined) {
// when faceted.
if (I !== undefined) {
if (markFacets) {
for (let j = 0; j < facetsKeys.length; ++j) {
marksIndexByFacet.get(facetsKeys[j])[i] = index[j];
marksIndexByFacet.get(facetsKeys[j])[i] = I[j];
}
marksIndex[i] = []; // implicit empty index for sparse facets
} else {
for (let j = 0; j < facetsKeys.length; ++j) {
marksIndexByFacet.get(facetsKeys[j])[i] = index;
marksIndexByFacet.get(facetsKeys[j])[i] = I;
}
marksIndex[i] = index;
marksIndex[i] = I;
}
}
for (const [, channel] of channels) {
Expand Down
34 changes: 22 additions & 12 deletions src/mark.js
@@ -1,24 +1,24 @@
import {color} from "d3";
import {ascendingDefined, nonempty} from "./defined.js";
import {plot} from "./plot.js";

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

export class Mark {
constructor(data, channels = [], options = {}) {
constructor(data, channels = [], {facet = "auto", ...options} = {}) {
const names = new Set();
this.data = data;
this.transform = maybeTransform(options);
this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null;
const {transform} = maybeTransform(options);
this.transform = transform;
this.channels = channels.filter(channel => {
const {name, value, optional} = channel;
if (value == null) {
if (optional) return false;
throw new Error(`missing channel value: ${name}`);
}
if (typeof value === "string") {
channel.value = field(value);
}
if (name !== undefined) {
const key = name + "";
if (key === "__proto__") throw new Error("illegal channel name");
Expand All @@ -45,6 +45,9 @@ export class Mark {
})
};
}
plot({marks = [], ...options} = {}) {
return plot({...options, marks: [...marks, this]});
}
}

// TODO Type coercion?
Expand All @@ -53,7 +56,7 @@ function Channel(data, {scale, type, value}) {
scale,
type,
value: valueof(data, value),
label: value ? value.label : undefined
label: labelof(value)
};
}

Expand All @@ -62,11 +65,12 @@ export function valueof(data, value, type) {
const array = type === undefined ? Array : type;
return typeof value === "string" ? array.from(data, field(value))
: typeof value === "function" ? array.from(data, value)
: typeof value === "number" || value instanceof Date ? array.from(data, constant(value))
: value && typeof value.transform === "function" ? arrayify(value.transform(data), type)
: arrayify(value, type); // preserve undefined type
}

export const field = label => Object.assign(d => d[label], {label});
export const field = name => d => d[name];
export const indexOf = (d, i) => i;
export const identity = {transform: d => d};
export const zero = () => 0;
Expand Down Expand Up @@ -136,11 +140,11 @@ export function arrayify(data, type) {
// For marks specified either as [0, x] or [x1, x2], such as areas and bars.
export function maybeZero(x, x1, x2, x3 = identity) {
if (x1 === undefined && x2 === undefined) { // {x} or {}
x1 = zero, x2 = x === undefined ? x3 : x;
x1 = 0, x2 = x === undefined ? x3 : x;
} else if (x1 === undefined) { // {x, x2} or {x2}
x1 = x === undefined ? zero : x;
x1 = x === undefined ? 0 : x;
} else if (x2 === undefined) { // {x, x1} or {x1}
x2 = x === undefined ? zero : x;
x2 = x === undefined ? 0 : x;
}
return [x1, x2];
}
Expand Down Expand Up @@ -220,13 +224,19 @@ export function maybeLazyChannel(source) {

// If both t1 and t2 are defined, returns a composite transform that first
// applies t1 and then applies t2.
export function maybeTransform({filter: f1, sort: s1, reverse: r1, transform: t1} = {}, t2) {
export function maybeTransform({
filter: f1,
sort: s1,
reverse: r1,
transform: t1,
...options
} = {}, t2) {
if (t1 === undefined) {
if (f1 != null) t1 = filter(f1);
if (s1 != null) t1 = compose(t1, sort(s1));
if (r1) t1 = compose(t1, reverse);
}
return compose(t1, t2);
return {...options, transform: compose(t1, t2)};
}

// Assuming that both x1 and x2 and lazy channels (per above), this derives a
Expand Down
13 changes: 6 additions & 7 deletions src/marks/area.js
Expand Up @@ -3,8 +3,9 @@ import {create} from "d3";
import {area as shapeArea} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, maybeColor, maybeZero, titleGroup, maybeNumber} from "../mark.js";
import {Mark, indexOf, maybeColor, titleGroup, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

export class Area extends Mark {
constructor(
Expand Down Expand Up @@ -85,12 +86,10 @@ export function area(data, options) {
return new Area(data, options);
}

export function areaX(data, {x, x1, x2, y = indexOf, ...options} = {}) {
([x1, x2] = maybeZero(x, x1, x2));
return new Area(data, {...options, x1, x2, y1: y, y2: undefined});
export function areaX(data, {y = indexOf, ...options} = {}) {
return new Area(data, maybeStackX({...options, y1: y, y2: undefined}));
}

export function areaY(data, {x = indexOf, y, y1, y2, ...options} = {}) {
([y1, y2] = maybeZero(y, y1, y2));
return new Area(data, {...options, x1: x, x2: undefined, y1, y2});
export function areaY(data, {x = indexOf, ...options} = {}) {
return new Area(data, maybeStackY({...options, x1: x, x2: undefined}));
}
19 changes: 7 additions & 12 deletions src/marks/bar.js
@@ -1,15 +1,14 @@
import {ascending} from "d3";
import {create} from "d3";
import {filter} from "../defined.js";
import {Mark, number, maybeColor, maybeZero, title, maybeNumber} from "../mark.js";
import {Mark, number, maybeColor, title, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

export class AbstractBar extends Mark {
constructor(
data,
channels,
{
z,
title,
fill,
fillOpacity,
Expand All @@ -33,7 +32,6 @@ export class AbstractBar extends Mark {
data,
[
...channels,
{name: "z", value: z, optional: true},
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
Expand All @@ -58,9 +56,8 @@ export class AbstractBar extends Mark {
}
render(I, scales, channels, dimensions) {
const {rx, ry} = this;
const {z: Z, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels;
const {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels;
const index = filter(I, ...this._positions(channels), F, FO, S, SO);
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, this)
.call(this._transform, scales)
Expand Down Expand Up @@ -157,12 +154,10 @@ export class BarY extends AbstractBar {
}
}

export function barX(data, {x, x1, x2, ...options} = {}) {
([x1, x2] = maybeZero(x, x1, x2));
return new BarX(data, {...options, x1, x2});
export function barX(data, options) {
return new BarX(data, maybeStackX(options));
}

export function barY(data, {y, y1, y2, ...options} = {}) {
([y1, y2] = maybeZero(y, y1, y2));
return new BarY(data, {...options, y1, y2});
export function barY(data, options) {
return new BarY(data, maybeStackY(options));
}
12 changes: 7 additions & 5 deletions src/marks/cell.js
@@ -1,4 +1,4 @@
import {identity, maybeTuple} from "../mark.js";
import {identity, indexOf, maybeColor, maybeTuple} from "../mark.js";
import {AbstractBar} from "./bar.js";

export class Cell extends AbstractBar {
Expand All @@ -25,10 +25,12 @@ export function cell(data, {x, y, ...options} = {}) {
return new Cell(data, {...options, x, y});
}

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

export function cellY(data, {y = identity, ...options} = {}) {
return new Cell(data, {...options, y});
export function cellY(data, {y = indexOf, fill, stroke, ...options} = {}) {
if (fill === undefined && maybeColor(stroke)[0] === undefined) fill = identity;
return new Cell(data, {...options, y, fill, stroke});
}
6 changes: 1 addition & 5 deletions src/marks/dot.js
@@ -1,4 +1,3 @@
import {ascending} from "d3";
import {create} from "d3";
import {filter, positive} from "../defined.js";
import {Mark, identity, maybeColor, maybeNumber, maybeTuple, title} from "../mark.js";
Expand All @@ -10,7 +9,6 @@ export class Dot extends Mark {
{
x,
y,
z,
r,
title,
fill,
Expand All @@ -30,7 +28,6 @@ export class Dot extends Mark {
[
{name: "x", value: x, scale: "x", optional: true},
{name: "y", value: y, scale: "y", optional: true},
{name: "z", value: z, optional: true},
{name: "r", value: vr, scale: "r", optional: true},
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
Expand All @@ -53,12 +50,11 @@ export class Dot extends Mark {
render(
I,
{x, y},
{x: X, y: Y, z: Z, r: R, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
{x: X, y: Y, r: R, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
{width, height, marginTop, marginRight, marginBottom, marginLeft}
) {
let index = filter(I, X, Y, F, FO, S, SO);
if (R) index = index.filter(i => positive(R[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, this)
.call(applyTransform, x, y, 0.5, 0.5)
Expand Down

0 comments on commit 1612b68

Please sign in to comment.