Skip to content

Commit 3b03443

Browse files
authored
Merge branch 'main' into fil/strokewidth-channel
2 parents 92038f7 + ccb9c9a commit 3b03443

File tree

123 files changed

+17025
-15773
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+17025
-15773
lines changed

README.md

Lines changed: 654 additions & 176 deletions
Large diffs are not rendered by default.

img/bin.png

30.7 KB
Loading

img/frame.png

130 KB
Loading

img/group.png

19.5 KB
Loading

img/select.png

86.9 KB
Loading

img/stack.png

90.4 KB
Loading

img/window.png

90.3 KB
Loading

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@observablehq/plot",
33
"description": "A JavaScript library for exploratory data visualization.",
4-
"version": "0.0.1",
4+
"version": "0.1.0",
55
"author": {
66
"name": "Observable, Inc.",
77
"url": "https://observablehq.com"
@@ -50,8 +50,5 @@
5050
"dependencies": {
5151
"d3": "^6.7.0",
5252
"isoformat": "^0.1.0"
53-
},
54-
"publishConfig": {
55-
"registry": "https://npm.pkg.github.com"
5653
}
5754
}

src/mark.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {color} from "d3";
22
import {ascendingDefined, nonempty} from "./defined.js";
3+
import {plot} from "./plot.js";
34

45
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
56
const TypedArray = Object.getPrototypeOf(Uint8Array);
@@ -9,7 +10,8 @@ export class Mark {
910
constructor(data, channels = [], options = {}) {
1011
const names = new Set();
1112
this.data = data;
12-
this.transform = maybeTransform(options);
13+
const {transform} = maybeTransform(options);
14+
this.transform = transform;
1315
this.channels = channels.filter(channel => {
1416
const {name, value, optional} = channel;
1517
if (value == null) {
@@ -45,6 +47,9 @@ export class Mark {
4547
})
4648
};
4749
}
50+
plot({marks = [], ...options} = {}) {
51+
return plot({...options, marks: [...marks, this]});
52+
}
4853
}
4954

5055
// TODO Type coercion?
@@ -62,6 +67,7 @@ export function valueof(data, value, type) {
6267
const array = type === undefined ? Array : type;
6368
return typeof value === "string" ? array.from(data, field(value))
6469
: typeof value === "function" ? array.from(data, value)
70+
: typeof value === "number" || value instanceof Date ? array.from(data, constant(value))
6571
: value && typeof value.transform === "function" ? arrayify(value.transform(data), type)
6672
: arrayify(value, type); // preserve undefined type
6773
}
@@ -136,11 +142,11 @@ export function arrayify(data, type) {
136142
// For marks specified either as [0, x] or [x1, x2], such as areas and bars.
137143
export function maybeZero(x, x1, x2, x3 = identity) {
138144
if (x1 === undefined && x2 === undefined) { // {x} or {}
139-
x1 = zero, x2 = x === undefined ? x3 : x;
145+
x1 = 0, x2 = x === undefined ? x3 : x;
140146
} else if (x1 === undefined) { // {x, x2} or {x2}
141-
x1 = x === undefined ? zero : x;
147+
x1 = x === undefined ? 0 : x;
142148
} else if (x2 === undefined) { // {x, x1} or {x1}
143-
x2 = x === undefined ? zero : x;
149+
x2 = x === undefined ? 0 : x;
144150
}
145151
return [x1, x2];
146152
}
@@ -220,13 +226,19 @@ export function maybeLazyChannel(source) {
220226

221227
// If both t1 and t2 are defined, returns a composite transform that first
222228
// applies t1 and then applies t2.
223-
export function maybeTransform({filter: f1, sort: s1, reverse: r1, transform: t1} = {}, t2) {
229+
export function maybeTransform({
230+
filter: f1,
231+
sort: s1,
232+
reverse: r1,
233+
transform: t1,
234+
...options
235+
} = {}, t2) {
224236
if (t1 === undefined) {
225237
if (f1 != null) t1 = filter(f1);
226238
if (s1 != null) t1 = compose(t1, sort(s1));
227239
if (r1) t1 = compose(t1, reverse);
228240
}
229-
return compose(t1, t2);
241+
return {...options, transform: compose(t1, t2)};
230242
}
231243

232244
// Assuming that both x1 and x2 and lazy channels (per above), this derives a

src/marks/area.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {create} from "d3";
33
import {area as shapeArea} from "d3";
44
import {Curve} from "../curve.js";
55
import {defined} from "../defined.js";
6-
import {Mark, indexOf, maybeColor, maybeZero, titleGroup, maybeNumber} from "../mark.js";
6+
import {Mark, indexOf, maybeColor, titleGroup, maybeNumber} from "../mark.js";
77
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
8+
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
89

910
export class Area extends Mark {
1011
constructor(
@@ -85,12 +86,10 @@ export function area(data, options) {
8586
return new Area(data, options);
8687
}
8788

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

93-
export function areaY(data, {x = indexOf, y, y1, y2, ...options} = {}) {
94-
([y1, y2] = maybeZero(y, y1, y2));
95-
return new Area(data, {...options, x1: x, x2: undefined, y1, y2});
93+
export function areaY(data, {x = indexOf, ...options} = {}) {
94+
return new Area(data, maybeStackY({...options, x1: x, x2: undefined}));
9695
}

src/marks/bar.js

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import {ascending} from "d3";
21
import {create} from "d3";
32
import {filter} from "../defined.js";
4-
import {Mark, number, maybeColor, maybeZero, title, maybeNumber} from "../mark.js";
3+
import {Mark, number, maybeColor, title, maybeNumber} from "../mark.js";
54
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
5+
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

77
export class AbstractBar extends Mark {
88
constructor(
99
data,
1010
channels,
1111
{
12-
z,
1312
title,
1413
fill,
1514
fillOpacity,
@@ -33,7 +32,6 @@ export class AbstractBar extends Mark {
3332
data,
3433
[
3534
...channels,
36-
{name: "z", value: z, optional: true},
3735
{name: "title", value: title, optional: true},
3836
{name: "fill", value: vfill, scale: "color", optional: true},
3937
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
@@ -58,9 +56,8 @@ export class AbstractBar extends Mark {
5856
}
5957
render(I, scales, channels, dimensions) {
6058
const {rx, ry} = this;
61-
const {z: Z, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels;
59+
const {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels;
6260
const index = filter(I, ...this._positions(channels), F, FO, S, SO);
63-
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
6461
return create("svg:g")
6562
.call(applyIndirectStyles, this)
6663
.call(this._transform, scales)
@@ -157,12 +154,10 @@ export class BarY extends AbstractBar {
157154
}
158155
}
159156

160-
export function barX(data, {x, x1, x2, ...options} = {}) {
161-
([x1, x2] = maybeZero(x, x1, x2));
162-
return new BarX(data, {...options, x1, x2});
157+
export function barX(data, options) {
158+
return new BarX(data, maybeStackX(options));
163159
}
164160

165-
export function barY(data, {y, y1, y2, ...options} = {}) {
166-
([y1, y2] = maybeZero(y, y1, y2));
167-
return new BarY(data, {...options, y1, y2});
161+
export function barY(data, options) {
162+
return new BarY(data, maybeStackY(options));
168163
}

src/marks/cell.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {identity, maybeTuple} from "../mark.js";
1+
import {identity, indexOf, maybeColor, maybeTuple} from "../mark.js";
22
import {AbstractBar} from "./bar.js";
33

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

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

32-
export function cellY(data, {y = identity, ...options} = {}) {
33-
return new Cell(data, {...options, y});
33+
export function cellY(data, {y = indexOf, fill, stroke, ...options} = {}) {
34+
if (fill === undefined && maybeColor(stroke)[0] === undefined) fill = identity;
35+
return new Cell(data, {...options, y, fill, stroke});
3436
}

src/marks/dot.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {ascending} from "d3";
21
import {create} from "d3";
32
import {filter, positive} from "../defined.js";
43
import {Mark, identity, maybeColor, maybeNumber, maybeTuple, title} from "../mark.js";
@@ -10,7 +9,6 @@ export class Dot extends Mark {
109
{
1110
x,
1211
y,
13-
z,
1412
r,
1513
title,
1614
fill,
@@ -30,7 +28,6 @@ export class Dot extends Mark {
3028
[
3129
{name: "x", value: x, scale: "x", optional: true},
3230
{name: "y", value: y, scale: "y", optional: true},
33-
{name: "z", value: z, optional: true},
3431
{name: "r", value: vr, scale: "r", optional: true},
3532
{name: "title", value: title, optional: true},
3633
{name: "fill", value: vfill, scale: "color", optional: true},
@@ -53,12 +50,11 @@ export class Dot extends Mark {
5350
render(
5451
I,
5552
{x, y},
56-
{x: X, y: Y, z: Z, r: R, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
53+
{x: X, y: Y, r: R, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
5754
{width, height, marginTop, marginRight, marginBottom, marginLeft}
5855
) {
5956
let index = filter(I, X, Y, F, FO, S, SO);
6057
if (R) index = index.filter(i => positive(R[i]));
61-
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
6258
return create("svg:g")
6359
.call(applyIndirectStyles, this)
6460
.call(applyTransform, x, y, 0.5, 0.5)

src/marks/link.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {ascending} from "d3";
2-
import {create} from "d3";
1+
import {create, path} from "d3";
32
import {filter} from "../defined.js";
43
import {Mark, maybeColor, maybeNumber, title} from "../mark.js";
4+
import {Curve} from "../curve.js";
55
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
66

77
export class Link extends Mark {
@@ -12,14 +12,18 @@ export class Link extends Mark {
1212
y1,
1313
x2,
1414
y2,
15-
z,
1615
title,
16+
fill,
17+
fillOpacity,
1718
stroke,
1819
strokeOpacity,
1920
strokeWidth,
21+
curve,
2022
...options
2123
} = {}
2224
) {
25+
const [vfill, cfill] = maybeColor(fill, "none");
26+
const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity);
2327
const [vstroke, cstroke] = maybeColor(stroke, "currentColor");
2428
const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity);
2529
const [vstrokeWidth, cstrokeWidth] = maybeNumber(strokeWidth, 1);
@@ -30,16 +34,21 @@ export class Link extends Mark {
3034
{name: "y1", value: y1, scale: "y"},
3135
{name: "x2", value: x2, scale: "x"},
3236
{name: "y2", value: y2, scale: "y"},
33-
{name: "z", value: z, optional: true},
3437
{name: "title", value: title, optional: true},
38+
{name: "fill", value: vfill, scale: "color", optional: true},
39+
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
3540
{name: "stroke", value: vstroke, scale: "color", optional: true},
3641
{name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true},
3742
{name: "strokeWidth", value: vstrokeWidth, optional: true}
3843
],
3944
options
4045
);
46+
this.curve = Curve(curve);
4147
Style(this, {
48+
fill: cfill,
49+
fillOpacity: cfillOpacity,
4250
stroke: cstroke,
51+
strokeMiterlimit: cstroke === "none" ? undefined : 1,
4352
strokeOpacity: cstrokeOpacity,
4453
strokeWidth: cstrokeWidth,
4554
...options
@@ -49,20 +58,25 @@ export class Link extends Mark {
4958
I,
5059
{x, y},
5160
{x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, stroke: S, strokeOpacity: SO, strokeWidth: SW}
61+
{x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, stroke: S, strokeOpacity: SO}
5262
) {
5363
const index = filter(I, X1, Y1, X2, Y2, S, SO);
54-
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
5564
return create("svg:g")
5665
.call(applyIndirectStyles, this)
5766
.call(applyTransform, x, y, 0.5, 0.5)
5867
.call(g => g.selectAll()
5968
.data(index)
60-
.join("line")
69+
.join("path")
6170
.call(applyDirectStyles, this)
62-
.attr("x1", i => X1[i])
63-
.attr("y1", i => Y1[i])
64-
.attr("x2", i => X2[i])
65-
.attr("y2", i => Y2[i])
71+
.attr("d", i => {
72+
const p = path();
73+
const c = this.curve(p);
74+
c.lineStart();
75+
c.point(X1[i], Y1[i]);
76+
c.point(X2[i], Y2[i]);
77+
c.lineEnd();
78+
return p + "";
79+
})
6680
.call(applyAttr, "stroke", S && (i => S[i]))
6781
.call(applyAttr, "stroke-opacity", SO && (i => SO[i]))
6882
.call(applyAttr, "stroke-width", SW && (i => SW[i]))

src/marks/rect.js

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {ascending} from "d3";
21
import {create} from "d3";
32
import {filter} from "../defined.js";
4-
import {Mark, number, maybeColor, maybeZero, title, maybeNumber} from "../mark.js";
3+
import {Mark, number, maybeColor, title, maybeNumber} from "../mark.js";
54
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
5+
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
66

77
export class Rect extends Mark {
88
constructor(
@@ -12,7 +12,6 @@ export class Rect extends Mark {
1212
y1,
1313
x2,
1414
y2,
15-
z,
1615
title,
1716
fill,
1817
fillOpacity,
@@ -39,7 +38,6 @@ export class Rect extends Mark {
3938
{name: "y1", value: y1, scale: "y"},
4039
{name: "x2", value: x2, scale: "x"},
4140
{name: "y2", value: y2, scale: "y"},
42-
{name: "z", value: z, optional: true},
4341
{name: "title", value: title, optional: true},
4442
{name: "fill", value: vfill, scale: "color", optional: true},
4543
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
@@ -65,11 +63,10 @@ export class Rect extends Mark {
6563
render(
6664
I,
6765
{x, y},
68-
{x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}
66+
{x1: X1, y1: Y1, x2: X2, y2: Y2, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}
6967
) {
7068
const {rx, ry} = this;
7169
const index = filter(I, X1, Y2, X2, Y2, F, FO, S, SO);
72-
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
7370
return create("svg:g")
7471
.call(applyIndirectStyles, this)
7572
.call(applyTransform, x, y)
@@ -96,12 +93,10 @@ export function rect(data, options) {
9693
return new Rect(data, options);
9794
}
9895

99-
export function rectX(data, {x, x1, x2, y1, y2, ...options} = {}) {
100-
([x1, x2] = maybeZero(x, x1, x2));
101-
return new Rect(data, {...options, x1, x2, y1, y2});
96+
export function rectX(data, options) {
97+
return new Rect(data, maybeStackX(options));
10298
}
10399

104-
export function rectY(data, {x1, x2, y, y1, y2, ...options} = {}) {
105-
([y1, y2] = maybeZero(y, y1, y2));
106-
return new Rect(data, {...options, x1, x2, y1, y2});
100+
export function rectY(data, options) {
101+
return new Rect(data, maybeStackY(options));
107102
}

0 commit comments

Comments
 (0)