Skip to content

Commit 1409349

Browse files
authored
improved plot example (#782)
* improved plot example * signDisplay: always
1 parent 871b7bd commit 1409349

9 files changed

+231
-282
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as Plot from "npm:@observablehq/plot";
2+
import {resize} from "npm:@observablehq/stdlib";
3+
import * as d3 from "npm:d3";
4+
5+
export function BurndownPlot(issues, {x, round = true, ...options} = {}) {
6+
const [start, end] = x.domain;
7+
const days = d3.utcDay.range(start, end);
8+
const burndown = issues.flatMap((issue) =>
9+
Array.from(
10+
days.filter((d) => issue.created_at <= d && (!issue.closed_at || d < issue.closed_at)),
11+
(d) => ({date: d, number: issue.number, created_at: issue.created_at})
12+
)
13+
);
14+
return resize((width) =>
15+
Plot.plot({
16+
width,
17+
round,
18+
x,
19+
...options,
20+
marks: [
21+
Plot.axisY({anchor: "right", label: null}),
22+
Plot.areaY(
23+
burndown,
24+
Plot.groupX(
25+
{y: "count"},
26+
{
27+
x: "date",
28+
y: 1,
29+
curve: "step-before",
30+
fill: (d) => d3.utcMonth(d.created_at),
31+
tip: {format: {z: null}}
32+
}
33+
)
34+
),
35+
Plot.ruleY([0])
36+
]
37+
})
38+
);
39+
}
+24-95
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,38 @@
11
import * as Plot from "npm:@observablehq/plot";
2-
import * as d3 from "npm:d3";
32

4-
export const today = d3.utcDay(d3.utcHour.offset(d3.utcHour(), -10));
5-
export const start = d3.utcYear.offset(today, -2);
6-
7-
export function DailyPlot(data, {title, label = title, domain, width, height = 200, versions} = {}) {
3+
export function DailyPlot(data, {round = true, annotations, ...options} = {}) {
84
return Plot.plot({
9-
width,
10-
height,
11-
round: true,
12-
marginRight: 60,
13-
x: {domain: [start, today]},
14-
y: {domain, label, insetTop: versions ? 60 : 0},
5+
...options,
6+
round,
157
marks: [
16-
Plot.axisY({
17-
anchor: "right",
18-
label: `${title} (line = 28-day, blue = 7-day)`
19-
}),
20-
Plot.areaY(
21-
data,
22-
Plot.filter((d) => d.date >= start, {
23-
x: "date",
24-
y: "value",
25-
curve: "step",
26-
fillOpacity: 0.2
27-
})
28-
),
8+
Plot.axisY({anchor: "right", label: null}),
9+
Plot.areaY(data, {x: "date", y: "value", curve: "step", fillOpacity: 0.2}),
2910
Plot.ruleY([0]),
3011
Plot.lineY(
3112
data,
32-
Plot.filter(
33-
(d) => d.date >= start,
34-
Plot.windowY(
35-
{k: 7, anchor: "start", strict: true},
36-
{
37-
x: "date",
38-
y: "value",
39-
strokeWidth: 1,
40-
stroke: "var(--theme-foreground-focus)"
41-
}
42-
)
43-
)
44-
),
45-
Plot.lineY(
46-
data,
47-
Plot.filter(
48-
(d) => d.date >= start,
49-
Plot.windowY({k: 28, anchor: "start", strict: true}, {x: "date", y: "value"})
13+
Plot.windowY(
14+
{k: 7, anchor: "start", strict: true},
15+
{x: "date", y: "value", stroke: "var(--theme-foreground-focus)"}
5016
)
5117
),
52-
versionsMarks(versions),
53-
Plot.tip(
54-
data,
55-
Plot.pointerX({
18+
Plot.lineY(data, Plot.windowY({k: 28, anchor: "start", strict: true}, {x: "date", y: "value"})),
19+
annotations && [
20+
Plot.ruleX(annotations, {x: "date", strokeOpacity: 0.1}),
21+
Plot.text(annotations, {
5622
x: "date",
57-
y: "value",
58-
format: {y: ",.0f"}
23+
text: "text",
24+
href: "href",
25+
target: "_blank",
26+
rotate: -90,
27+
dx: -3,
28+
frameAnchor: "top-right",
29+
lineAnchor: "bottom",
30+
fontVariant: "tabular-nums",
31+
fill: "currentColor",
32+
stroke: "var(--theme-background)"
5933
})
60-
)
34+
],
35+
Plot.tip(data, Plot.pointerX({x: "date", y: "value"}))
6136
]
6237
});
6338
}
64-
65-
function versionsMarks(versions) {
66-
if (!versions) return [];
67-
const clip = true;
68-
return [
69-
Plot.ruleX(versions, {
70-
filter: (d) => !isPrerelease(d.version),
71-
x: "date",
72-
strokeOpacity: (d) => (isMajor(d.version) ? 1 : 0.1),
73-
clip
74-
}),
75-
Plot.text(versions, {
76-
filter: (d) => !isPrerelease(d.version) && !isMajor(d.version),
77-
x: "date",
78-
text: "version",
79-
rotate: -90,
80-
dx: -10,
81-
frameAnchor: "top-right",
82-
fontVariant: "tabular-nums",
83-
fill: "currentColor",
84-
stroke: "var(--theme-background)",
85-
clip
86-
}),
87-
Plot.text(versions, {
88-
filter: (d) => isMajor(d.version),
89-
x: "date",
90-
text: "version",
91-
rotate: -90,
92-
dx: -10,
93-
frameAnchor: "top-right",
94-
fontVariant: "tabular-nums",
95-
fill: "currentColor",
96-
stroke: "white",
97-
fontWeight: "bold",
98-
clip
99-
})
100-
];
101-
}
102-
103-
function isPrerelease(version) {
104-
return /-/.test(version);
105-
}
106-
107-
function isMajor(version) {
108-
return /^\d+\.0\.0$/.test(version);
109-
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function revive(object) {
2+
if (object && typeof object === "object") {
3+
for (const key in object) {
4+
const value = object[key];
5+
if (value && typeof value === "object") {
6+
revive(value);
7+
} else if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
8+
object[key] = new Date(value);
9+
}
10+
}
11+
}
12+
return object;
13+
}
+10-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
1-
import * as d3 from "npm:d3";
21
import {html} from "npm:htl";
32

4-
export function trend(
5-
value /*: number */,
3+
export function Trend(
4+
value,
65
{
7-
format = "+d",
6+
locale = "en-US",
7+
format,
88
positive = "green",
99
negative = "red",
1010
base = "muted",
1111
positiveSuffix = " ↗︎",
1212
negativeSuffix = " ↘︎",
1313
baseSuffix = ""
14-
} = {} /*
15-
as {
16-
format: string | ((x: number) => string);
17-
positive: string;
18-
negative: string;
19-
base: string;
20-
positiveSuffix: string;
21-
negativeSuffix: string;
22-
baseSuffix: string;
23-
}
24-
*/
25-
) /*: Node */ {
26-
if (typeof format === "string") format = d3.format(format);
27-
if (typeof format !== "function") throw new Error(`unsupported format ${format}`);
28-
return html`<span class="small ${value > 0 ? positive : value < 0 ? negative : base}">${format(value)}${
29-
value > 0 ? positiveSuffix : value < 0 ? negativeSuffix : baseSuffix
30-
}`;
14+
} = {}
15+
) {
16+
const variant = value > 0 ? positive : value < 0 ? negative : base;
17+
const text = value.toLocaleString(locale, {signDisplay: "always", ...format});
18+
const suffix = value > 0 ? positiveSuffix : value < 0 ? negativeSuffix : baseSuffix;
19+
return html`<span class="small ${variant}">${text}${suffix}`;
3120
}
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
import {githubList} from "./github.js";
22

3-
async function load(repo) {
4-
process.stdout.write("[");
5-
let first = true;
3+
async function load(repo: string) {
4+
const issues: any[] = [];
65
for await (const item of githubList(`/repos/${repo}/issues?state=all`)) {
7-
if (first) first = false;
8-
else process.stdout.write(",");
9-
process.stdout.write(
10-
`\n ${JSON.stringify({
11-
state: item.state,
12-
pull_request: !!item.pull_request,
13-
created_at: item.created_at,
14-
closed_at: item.closed_at,
15-
draft: item.draft,
16-
reactions: {...item.reactions, url: undefined},
17-
title: item.title,
18-
number: item.number
19-
})}`
20-
);
6+
issues.push({
7+
state: item.state,
8+
pull_request: !!item.pull_request,
9+
created_at: item.created_at,
10+
closed_at: item.closed_at,
11+
draft: item.draft,
12+
reactions: {...item.reactions, url: undefined},
13+
title: item.title,
14+
number: item.number
15+
});
2116
}
22-
process.stdout.write("\n]\n");
17+
return issues;
2318
}
2419

25-
await load("observablehq/plot");
20+
process.stdout.write(JSON.stringify(await load("observablehq/plot")));
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import {githubList} from "./github.js";
21
import {csvFormat} from "d3-dsv";
2+
import {githubList} from "./github.js";
33

4-
const repo = "observablehq/plot";
5-
const stars: any[] = [];
6-
for await (const item of githubList(`/repos/${repo}/stargazers`, {accept: "application/vnd.github.star+json"}))
7-
stars.push({starred_at: item.starred_at, login: item.user.login});
4+
async function load(repo: string) {
5+
const stars: any[] = [];
6+
for await (const item of githubList(`/repos/${repo}/stargazers`, {accept: "application/vnd.github.star+json"})) {
7+
stars.push({starred_at: item.starred_at, login: item.user.login});
8+
}
9+
return stars;
10+
}
811

9-
process.stdout.write(csvFormat(stars));
12+
process.stdout.write(csvFormat(await load("observablehq/plot")));
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
import {csvFormat} from "d3-dsv";
2+
import {json} from "d3-fetch";
23
import {timeDay, utcDay} from "d3-time";
3-
import {utcFormat} from "d3-time-format";
44

5-
async function load(project) {
6-
const end = utcDay(timeDay()); // exclusive
5+
function formatDate(date: Date): string {
6+
return date.toISOString().slice(0, 10);
7+
}
8+
9+
async function load(project: string, start: Date, end: Date) {
710
const data: any[] = [];
8-
const formatDate = utcFormat("%Y-%m-%d");
9-
const min = new Date("2021-01-01");
1011
let batchStart = end;
1112
let batchEnd;
12-
while (batchStart > min) {
13+
while (batchStart > start) {
1314
batchEnd = batchStart;
1415
batchStart = utcDay.offset(batchStart, -365);
15-
if (batchStart < min) batchStart = min;
16-
const response = await fetch(
16+
if (batchStart < start) batchStart = start;
17+
const batch = await json(
1718
`https://api.npmjs.org/downloads/range/${formatDate(batchStart)}:${formatDate(
1819
utcDay.offset(batchEnd, -1)
1920
)}/${project}`
2021
);
21-
if (!response.ok) throw new Error(`fetch failed: ${response.status}`);
22-
const batch = await response.json();
2322
for (const {downloads: value, day: date} of batch.downloads.reverse()) {
2423
data.push({date: new Date(date), value});
2524
}
@@ -28,10 +27,10 @@ async function load(project) {
2827
// trim zeroes at both ends
2928
do {
3029
if (data[0].value === 0) data.shift();
31-
else if (data.at(-1).value !== 0) data.pop();
30+
else if (data.at(-1)?.value !== 0) data.pop();
3231
else return data;
3332
} while (data.length);
3433
throw new Error("empty dataset");
3534
}
3635

37-
process.stdout.write(csvFormat(await load("@observablehq/plot")));
36+
process.stdout.write(csvFormat(await load("@observablehq/plot", new Date("2021-01-01"), utcDay(timeDay()))));

examples/plot/docs/data/plot-version-data.csv.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import {csvFormat} from "d3-dsv";
22
import {json} from "d3-fetch";
33

4-
async function load(project) {
5-
const downloads = new Map(
6-
Object.entries((await json(`https://api.npmjs.org/versions/${encodeURIComponent(project)}/last-week`)).downloads)
7-
);
4+
async function load(project: string) {
5+
const {downloads} = await json(`https://api.npmjs.org/versions/${encodeURIComponent(project)}/last-week`);
86
const info = await json(`https://registry.npmjs.org/${encodeURIComponent(project)}`);
97
return Object.values(info.versions as {version: string}[])
10-
.map(({version}) => ({
11-
version,
12-
date: new Date(info.time[version]),
13-
downloads: downloads.get(version)
14-
}))
8+
.map(({version}) => ({version, date: new Date(info.time[version]), downloads: downloads[version]}))
159
.sort((a, b) => (a.date > b.date ? 1 : -1));
1610
}
1711

0 commit comments

Comments
 (0)