Skip to content

Commit

Permalink
improved plot example (#782)
Browse files Browse the repository at this point in the history
* improved plot example

* signDisplay: always
  • Loading branch information
mbostock authored Feb 14, 2024
1 parent 871b7bd commit 1409349
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 282 deletions.
39 changes: 39 additions & 0 deletions examples/plot/docs/components/burndownPlot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Plot from "npm:@observablehq/plot";
import {resize} from "npm:@observablehq/stdlib";
import * as d3 from "npm:d3";

export function BurndownPlot(issues, {x, round = true, ...options} = {}) {
const [start, end] = x.domain;
const days = d3.utcDay.range(start, end);
const burndown = issues.flatMap((issue) =>
Array.from(
days.filter((d) => issue.created_at <= d && (!issue.closed_at || d < issue.closed_at)),
(d) => ({date: d, number: issue.number, created_at: issue.created_at})
)
);
return resize((width) =>
Plot.plot({
width,
round,
x,
...options,
marks: [
Plot.axisY({anchor: "right", label: null}),
Plot.areaY(
burndown,
Plot.groupX(
{y: "count"},
{
x: "date",
y: 1,
curve: "step-before",
fill: (d) => d3.utcMonth(d.created_at),
tip: {format: {z: null}}
}
)
),
Plot.ruleY([0])
]
})
);
}
119 changes: 24 additions & 95 deletions examples/plot/docs/components/dailyPlot.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,38 @@
import * as Plot from "npm:@observablehq/plot";
import * as d3 from "npm:d3";

export const today = d3.utcDay(d3.utcHour.offset(d3.utcHour(), -10));
export const start = d3.utcYear.offset(today, -2);

export function DailyPlot(data, {title, label = title, domain, width, height = 200, versions} = {}) {
export function DailyPlot(data, {round = true, annotations, ...options} = {}) {
return Plot.plot({
width,
height,
round: true,
marginRight: 60,
x: {domain: [start, today]},
y: {domain, label, insetTop: versions ? 60 : 0},
...options,
round,
marks: [
Plot.axisY({
anchor: "right",
label: `${title} (line = 28-day, blue = 7-day)`
}),
Plot.areaY(
data,
Plot.filter((d) => d.date >= start, {
x: "date",
y: "value",
curve: "step",
fillOpacity: 0.2
})
),
Plot.axisY({anchor: "right", label: null}),
Plot.areaY(data, {x: "date", y: "value", curve: "step", fillOpacity: 0.2}),
Plot.ruleY([0]),
Plot.lineY(
data,
Plot.filter(
(d) => d.date >= start,
Plot.windowY(
{k: 7, anchor: "start", strict: true},
{
x: "date",
y: "value",
strokeWidth: 1,
stroke: "var(--theme-foreground-focus)"
}
)
)
),
Plot.lineY(
data,
Plot.filter(
(d) => d.date >= start,
Plot.windowY({k: 28, anchor: "start", strict: true}, {x: "date", y: "value"})
Plot.windowY(
{k: 7, anchor: "start", strict: true},
{x: "date", y: "value", stroke: "var(--theme-foreground-focus)"}
)
),
versionsMarks(versions),
Plot.tip(
data,
Plot.pointerX({
Plot.lineY(data, Plot.windowY({k: 28, anchor: "start", strict: true}, {x: "date", y: "value"})),
annotations && [
Plot.ruleX(annotations, {x: "date", strokeOpacity: 0.1}),
Plot.text(annotations, {
x: "date",
y: "value",
format: {y: ",.0f"}
text: "text",
href: "href",
target: "_blank",
rotate: -90,
dx: -3,
frameAnchor: "top-right",
lineAnchor: "bottom",
fontVariant: "tabular-nums",
fill: "currentColor",
stroke: "var(--theme-background)"
})
)
],
Plot.tip(data, Plot.pointerX({x: "date", y: "value"}))
]
});
}

function versionsMarks(versions) {
if (!versions) return [];
const clip = true;
return [
Plot.ruleX(versions, {
filter: (d) => !isPrerelease(d.version),
x: "date",
strokeOpacity: (d) => (isMajor(d.version) ? 1 : 0.1),
clip
}),
Plot.text(versions, {
filter: (d) => !isPrerelease(d.version) && !isMajor(d.version),
x: "date",
text: "version",
rotate: -90,
dx: -10,
frameAnchor: "top-right",
fontVariant: "tabular-nums",
fill: "currentColor",
stroke: "var(--theme-background)",
clip
}),
Plot.text(versions, {
filter: (d) => isMajor(d.version),
x: "date",
text: "version",
rotate: -90,
dx: -10,
frameAnchor: "top-right",
fontVariant: "tabular-nums",
fill: "currentColor",
stroke: "white",
fontWeight: "bold",
clip
})
];
}

function isPrerelease(version) {
return /-/.test(version);
}

function isMajor(version) {
return /^\d+\.0\.0$/.test(version);
}
13 changes: 13 additions & 0 deletions examples/plot/docs/components/revive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function revive(object) {
if (object && typeof object === "object") {
for (const key in object) {
const value = object[key];
if (value && typeof value === "object") {
revive(value);
} else if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
object[key] = new Date(value);
}
}
}
return object;
}
31 changes: 10 additions & 21 deletions examples/plot/docs/components/trend.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
import * as d3 from "npm:d3";
import {html} from "npm:htl";

export function trend(
value /*: number */,
export function Trend(
value,
{
format = "+d",
locale = "en-US",
format,
positive = "green",
negative = "red",
base = "muted",
positiveSuffix = " ↗︎",
negativeSuffix = " ↘︎",
baseSuffix = ""
} = {} /*
as {
format: string | ((x: number) => string);
positive: string;
negative: string;
base: string;
positiveSuffix: string;
negativeSuffix: string;
baseSuffix: string;
}
*/
) /*: Node */ {
if (typeof format === "string") format = d3.format(format);
if (typeof format !== "function") throw new Error(`unsupported format ${format}`);
return html`<span class="small ${value > 0 ? positive : value < 0 ? negative : base}">${format(value)}${
value > 0 ? positiveSuffix : value < 0 ? negativeSuffix : baseSuffix
}`;
} = {}
) {
const variant = value > 0 ? positive : value < 0 ? negative : base;
const text = value.toLocaleString(locale, {signDisplay: "always", ...format});
const suffix = value > 0 ? positiveSuffix : value < 0 ? negativeSuffix : baseSuffix;
return html`<span class="small ${variant}">${text}${suffix}`;
}
33 changes: 14 additions & 19 deletions examples/plot/docs/data/plot-github-issues.json.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import {githubList} from "./github.js";

async function load(repo) {
process.stdout.write("[");
let first = true;
async function load(repo: string) {
const issues: any[] = [];
for await (const item of githubList(`/repos/${repo}/issues?state=all`)) {
if (first) first = false;
else process.stdout.write(",");
process.stdout.write(
`\n ${JSON.stringify({
state: item.state,
pull_request: !!item.pull_request,
created_at: item.created_at,
closed_at: item.closed_at,
draft: item.draft,
reactions: {...item.reactions, url: undefined},
title: item.title,
number: item.number
})}`
);
issues.push({
state: item.state,
pull_request: !!item.pull_request,
created_at: item.created_at,
closed_at: item.closed_at,
draft: item.draft,
reactions: {...item.reactions, url: undefined},
title: item.title,
number: item.number
});
}
process.stdout.write("\n]\n");
return issues;
}

await load("observablehq/plot");
process.stdout.write(JSON.stringify(await load("observablehq/plot")));
15 changes: 9 additions & 6 deletions examples/plot/docs/data/plot-github-stars.csv.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {githubList} from "./github.js";
import {csvFormat} from "d3-dsv";
import {githubList} from "./github.js";

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

process.stdout.write(csvFormat(stars));
process.stdout.write(csvFormat(await load("observablehq/plot")));
23 changes: 11 additions & 12 deletions examples/plot/docs/data/plot-npm-downloads.csv.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import {csvFormat} from "d3-dsv";
import {json} from "d3-fetch";
import {timeDay, utcDay} from "d3-time";
import {utcFormat} from "d3-time-format";

async function load(project) {
const end = utcDay(timeDay()); // exclusive
function formatDate(date: Date): string {
return date.toISOString().slice(0, 10);
}

async function load(project: string, start: Date, end: Date) {
const data: any[] = [];
const formatDate = utcFormat("%Y-%m-%d");
const min = new Date("2021-01-01");
let batchStart = end;
let batchEnd;
while (batchStart > min) {
while (batchStart > start) {
batchEnd = batchStart;
batchStart = utcDay.offset(batchStart, -365);
if (batchStart < min) batchStart = min;
const response = await fetch(
if (batchStart < start) batchStart = start;
const batch = await json(
`https://api.npmjs.org/downloads/range/${formatDate(batchStart)}:${formatDate(
utcDay.offset(batchEnd, -1)
)}/${project}`
);
if (!response.ok) throw new Error(`fetch failed: ${response.status}`);
const batch = await response.json();
for (const {downloads: value, day: date} of batch.downloads.reverse()) {
data.push({date: new Date(date), value});
}
Expand All @@ -28,10 +27,10 @@ async function load(project) {
// trim zeroes at both ends
do {
if (data[0].value === 0) data.shift();
else if (data.at(-1).value !== 0) data.pop();
else if (data.at(-1)?.value !== 0) data.pop();
else return data;
} while (data.length);
throw new Error("empty dataset");
}

process.stdout.write(csvFormat(await load("@observablehq/plot")));
process.stdout.write(csvFormat(await load("@observablehq/plot", new Date("2021-01-01"), utcDay(timeDay()))));
12 changes: 3 additions & 9 deletions examples/plot/docs/data/plot-version-data.csv.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import {csvFormat} from "d3-dsv";
import {json} from "d3-fetch";

async function load(project) {
const downloads = new Map(
Object.entries((await json(`https://api.npmjs.org/versions/${encodeURIComponent(project)}/last-week`)).downloads)
);
async function load(project: string) {
const {downloads} = await json(`https://api.npmjs.org/versions/${encodeURIComponent(project)}/last-week`);
const info = await json(`https://registry.npmjs.org/${encodeURIComponent(project)}`);
return Object.values(info.versions as {version: string}[])
.map(({version}) => ({
version,
date: new Date(info.time[version]),
downloads: downloads.get(version)
}))
.map(({version}) => ({version, date: new Date(info.time[version]), downloads: downloads[version]}))
.sort((a, b) => (a.date > b.date ? 1 : -1));
}

Expand Down
Loading

0 comments on commit 1409349

Please sign in to comment.