Skip to content

Commit 457f7fa

Browse files
authored
Merge branch 'main' into fil/dx-dy
2 parents 8806fee + 7262e9c commit 457f7fa

Some content is hidden

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

68 files changed

+4154
-1818
lines changed

CHANGELOG.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Observable Plot - Changelog
2+
3+
## 0.2.0
4+
5+
*Not yet released.* These notes are a work in progress.
6+
7+
[breaking] Plot is now published as an ES module.
8+
9+
[breaking] Plot now depends on D3 7 or higher.
10+
11+
[breaking] Plot now requires Node 12 or higher.
12+
13+
### Marks
14+
15+
The Plot.marks function provides *mark*.plot shorthand for array marks.
16+
17+
The plot *marks* option now accepts render functions and nullish values as shorthand mark definitions. Nullish marks produce no output and are useful for conditional display (equivalent to the empty array).
18+
19+
Marks now support more style options, including shapeRendering.
20+
21+
Marks now support fill and stroke defaults more consistently.
22+
23+
Marks now filter undefined values more consistently.
24+
25+
Marks now handle collapsed domains.
26+
27+
The link mark now supports shorthand for one-dimensional links.
28+
29+
The rect mark now supports one-dimensional (and zero-dimensional) rects: the *x1*, *x2*, *y1* and *y2* channels are now optional.
30+
31+
The text mark now uses attributes instead of styles for font rendering properties, improving compatibility with Firefox.
32+
33+
### Scales
34+
35+
New diverging scale types: *diverging-sqrt*, *diverging-pow*, *diverging-log*, *diverging-symlog*.
36+
37+
New *quantile* scale type.
38+
39+
New *threshold* scale type.
40+
41+
The axis *line* option can be used to show a line along the *x* or *y* axis.
42+
43+
### Facets
44+
45+
When the facet *data* is null, a better error message is thrown.
46+
47+
The mark *facet* option can be used to control whether a mark is faceted. The new *exclude* facet mode shows all data that are *not* present in the current facet.
48+
49+
### Transforms
50+
51+
The *outputs* argument to the bin transforms is now optional. It defaults to the *count* reducer for *y*, *x* and *fill* for Plot.binX, Plot.binY, and Plot.bin respectively. TODO Should the group transforms have similar default outputs?
52+
53+
The bin and group transforms now support *filter*, *sort* and *reverse* options on the *outputs* object.
54+
55+
The bin and group transforms now support the *distinct* and *mode* reducers.
56+
57+
The bin and group transforms now propagate *z*, *fill*, and *stroke* channel values correctly on empty bins when the this channel is used for grouping.
58+
59+
The default *thresholds* option for the bin transforms is now *auto* instead of *scott*, and applies a maximum limit of 200 bins to Scott’s rule.
60+
61+
The normalize, window, and stack transforms can now accept a transform *options* argument in addition to an *inputs* argument that specifies the input channels. This allows makes these transforms more consistent with the other transforms, reduces ambiguity, and allows for additional shorthand.
62+
63+
The select transforms now throw better error messages when required input channels are missing.
64+
65+
The stack *offset* options have been renamed: *normalize* and *center* replace *expand* and *silhouette*, respectively. The old names are supported for backwards compatibility.
66+
67+
The window *shift* option has been renamed to *anchor*. The *centered*, *leading*, and *trailing* shifts and replaced with *middle*, *start*, and *end* respectively. The old *shift* option is supported for backwards compatibility.
68+
69+
The basic transforms are now available as explicit option transforms: Plot.filter, Plot.sort, and Plot.reverse. These are useful when you wish to control the order of these transforms with respect to other transforms such as Plot.bin and Plot.stack.
70+
71+
When mark *transform* option is null, it is considered equivalent to undefined and no transform is applied instead of throwing an error.
72+
73+
## 0.1.0
74+
75+
Released May 3, 2021.

README.md

Lines changed: 102 additions & 68 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"eslint": "^7.12.1",
4242
"htl": "^0.3.0",
4343
"js-beautify": "^1.13.0",
44-
"jsdom": "^16.4.0",
44+
"jsdom": "^17.0.0",
4545
"jsesc": "^3.0.2",
4646
"mocha": "^9.0.3",
4747
"module-alias": "^2.2.2",

src/axis.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class AxisX {
1414
label,
1515
labelAnchor,
1616
labelOffset,
17+
line,
1718
tickRotate
1819
} = {}) {
1920
this.name = name;
@@ -26,6 +27,7 @@ export class AxisX {
2627
this.label = string(label);
2728
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "left", "right"]);
2829
this.labelOffset = number(labelOffset);
30+
this.line = boolean(line);
2931
this.tickRotate = number(tickRotate);
3032
}
3133
render(
@@ -51,6 +53,7 @@ export class AxisX {
5153
label,
5254
labelAnchor,
5355
labelOffset,
56+
line,
5457
tickRotate
5558
} = this;
5659
const offset = this.name === "x" ? 0 : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom;
@@ -62,7 +65,7 @@ export class AxisX {
6265
.call(maybeTickRotate, tickRotate)
6366
.attr("font-size", null)
6467
.attr("font-family", null)
65-
.call(g => g.select(".domain").remove())
68+
.call(!line ? g => g.select(".domain").remove() : () => {})
6669
.call(!grid ? () => {}
6770
: fy ? gridFacetX(fy, -ty)
6871
: gridX(offsetSign * (marginBottom + marginTop - height)))
@@ -94,6 +97,7 @@ export class AxisY {
9497
label,
9598
labelAnchor,
9699
labelOffset,
100+
line,
97101
tickRotate
98102
} = {}) {
99103
this.name = name;
@@ -106,6 +110,7 @@ export class AxisY {
106110
this.label = string(label);
107111
this.labelAnchor = maybeKeyword(labelAnchor, "labelAnchor", ["center", "top", "bottom"]);
108112
this.labelOffset = number(labelOffset);
113+
this.line = boolean(line);
109114
this.tickRotate = number(tickRotate);
110115
}
111116
render(
@@ -129,6 +134,7 @@ export class AxisY {
129134
label,
130135
labelAnchor,
131136
labelOffset,
137+
line,
132138
tickRotate
133139
} = this;
134140
const offset = this.name === "y" ? 0 : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight;
@@ -140,7 +146,7 @@ export class AxisY {
140146
.call(maybeTickRotate, tickRotate)
141147
.attr("font-size", null)
142148
.attr("font-family", null)
143-
.call(g => g.select(".domain").remove())
149+
.call(!line ? g => g.select(".domain").remove() : () => {})
144150
.call(!grid ? () => {}
145151
: fx ? gridFacetY(fx, -tx)
146152
: gridY(offsetSign * (marginLeft + marginRight - width)))

src/facet.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {cross, difference, groups, InternMap} from "d3";
22
import {create} from "d3";
3-
import {Mark, values, first, second} from "./mark.js";
3+
import {Mark, first, second, markify} from "./mark.js";
4+
import {applyScales} from "./scales.js";
5+
import {filterStyles} from "./style.js";
46

57
export function facets(data, {x, y, ...options}, marks) {
68
return x === undefined && y === undefined
@@ -19,7 +21,7 @@ class Facet extends Mark {
1921
],
2022
options
2123
);
22-
this.marks = marks.flat(Infinity);
24+
this.marks = marks.flat(Infinity).map(markify);
2325
// The following fields are set by initialize:
2426
this.marksChannels = undefined; // array of mark channels
2527
this.marksIndex = undefined; // array of mark indexes (for non-faceted marks)
@@ -70,13 +72,13 @@ class Facet extends Mark {
7072
}
7173
return {index, channels: [...channels, ...subchannels]};
7274
}
73-
render(index, scales, channels, dimensions, axes) {
75+
render(I, scales, channels, dimensions, axes) {
7476
const {marks, marksChannels, marksIndex, marksIndexByFacet} = this;
7577
const {fx, fy} = scales;
7678
const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()};
7779
const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()};
7880
const subdimensions = {...dimensions, ...fxMargins, ...fyMargins};
79-
const marksValues = marksChannels.map(channels => values(channels, scales));
81+
const marksValues = marksChannels.map(channels => applyScales(channels, scales));
8082
return create("svg:g")
8183
.call(g => {
8284
if (fy && axes.y) {
@@ -110,10 +112,12 @@ class Facet extends Mark {
110112
.each(function(key) {
111113
const marksFacetIndex = marksIndexByFacet.get(key) || marksIndex;
112114
for (let i = 0; i < marks.length; ++i) {
115+
const values = marksValues[i];
116+
const index = filterStyles(marksFacetIndex[i], values);
113117
const node = marks[i].render(
114-
marksFacetIndex[i],
118+
index,
115119
scales,
116-
marksValues[i],
120+
values,
117121
subdimensions
118122
);
119123
if (node != null) this.appendChild(node);

src/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export {plot} from "./plot.js";
2-
export {Mark, valueof} from "./mark.js";
2+
export {Mark, marks, valueof} from "./mark.js";
33
export {Area, area, areaX, areaY} from "./marks/area.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
55
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
@@ -11,6 +11,9 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js";
1111
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
1212
export {Text, text, textX, textY} from "./marks/text.js";
1313
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
14+
export {filter} from "./transforms/filter.js";
15+
export {reverse} from "./transforms/reverse.js";
16+
export {sort} from "./transforms/sort.js";
1417
export {bin, binX, binY} from "./transforms/bin.js";
1518
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
1619
export {normalizeX, normalizeY} from "./transforms/normalize.js";

src/mark.js

Lines changed: 26 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import {color} from "d3";
2-
import {ascendingDefined, nonempty} from "./defined.js";
2+
import {nonempty} from "./defined.js";
33
import {plot} from "./plot.js";
4+
import {styles} from "./style.js";
5+
import {basic} from "./transforms/basic.js";
46

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

911
export class Mark {
10-
constructor(data, channels = [], {facet = "auto", dx, dy, ...options} = {}) {
12+
constructor(data, channels = [], options = {}, defaults) {
13+
const {facet = "auto", dx, dy} = options;
1114
const names = new Set();
1215
this.data = data;
1316
this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null;
14-
const {transform} = maybeTransform(options);
17+
const {transform} = basic(options);
1518
this.transform = transform;
19+
if (defaults !== undefined) channels = styles(this, options, channels, defaults);
1620
this.channels = channels.filter(channel => {
1721
const {name, value, optional} = channel;
1822
if (value == null) {
@@ -224,23 +228,6 @@ export function maybeLazyChannel(source) {
224228
return source == null ? [source] : lazyChannel(source);
225229
}
226230

227-
// If both t1 and t2 are defined, returns a composite transform that first
228-
// applies t1 and then applies t2.
229-
export function maybeTransform({
230-
filter: f1,
231-
sort: s1,
232-
reverse: r1,
233-
transform: t1,
234-
...options
235-
} = {}, t2) {
236-
if (t1 === undefined) {
237-
if (f1 != null) t1 = filter(f1);
238-
if (s1 != null) t1 = compose(t1, sort(s1));
239-
if (r1) t1 = compose(t1, reverse);
240-
}
241-
return {...options, transform: compose(t1, t2)};
242-
}
243-
244231
// Assuming that both x1 and x2 and lazy channels (per above), this derives a
245232
// new a channel that’s the average of the two, and which inherits the channel
246233
// label (if any). Both input channels are assumed to be quantitative. If either
@@ -265,69 +252,13 @@ export function maybeValue(value) {
265252
typeof value.transform !== "function") ? value : {value};
266253
}
267254

268-
function compose(t1, t2) {
269-
if (t1 == null) return t2 === null ? undefined : t2;
270-
if (t2 == null) return t1 === null ? undefined : t1;
271-
return (data, facets) => {
272-
({data, facets} = t1(data, facets));
273-
return t2(arrayify(data), facets);
274-
};
275-
}
276-
277-
function sort(value) {
278-
return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value);
279-
}
280-
281-
function sortCompare(compare) {
282-
return (data, facets) => {
283-
const compareData = (i, j) => compare(data[i], data[j]);
284-
return {data, facets: facets.map(I => I.slice().sort(compareData))};
285-
};
286-
}
287-
288-
function sortValue(value) {
289-
return (data, facets) => {
290-
const V = valueof(data, value);
291-
const compareValue = (i, j) => ascendingDefined(V[i], V[j]);
292-
return {data, facets: facets.map(I => I.slice().sort(compareValue))};
293-
};
294-
}
295-
296-
function filter(value) {
297-
return (data, facets) => {
298-
const V = valueof(data, value);
299-
return {data, facets: facets.map(I => I.filter(i => V[i]))};
300-
};
301-
}
302-
303-
function reverse(data, facets) {
304-
return {data, facets: facets.map(I => I.slice().reverse())};
305-
}
306-
307255
export function numberChannel(source) {
308256
return {
309257
transform: data => valueof(data, source, Float64Array),
310258
label: labelof(source)
311259
};
312260
}
313261

314-
// TODO use Float64Array.from for position and radius scales?
315-
export function values(channels = [], scales) {
316-
const values = Object.create(null);
317-
for (let [name, {value, scale}] of channels) {
318-
if (name !== undefined) {
319-
if (scale !== undefined) {
320-
scale = scales[scale];
321-
if (scale !== undefined) {
322-
value = Array.from(value, scale);
323-
}
324-
}
325-
values[name] = value;
326-
}
327-
}
328-
return values;
329-
}
330-
331262
export function isOrdinal(values) {
332263
for (const value of values) {
333264
if (value == null) continue;
@@ -342,3 +273,22 @@ export function isTemporal(values) {
342273
return value instanceof Date;
343274
}
344275
}
276+
277+
export function markify(mark) {
278+
return mark instanceof Mark ? mark : new Render(mark);
279+
}
280+
281+
class Render extends Mark {
282+
constructor(render) {
283+
super();
284+
if (render == null) return;
285+
if (typeof render !== "function") throw new TypeError("invalid mark");
286+
this.render = render;
287+
}
288+
render() {}
289+
}
290+
291+
export function marks(...marks) {
292+
marks.plot = Mark.prototype.plot;
293+
return marks;
294+
}

0 commit comments

Comments
 (0)