Public
Edited
Mar 13, 2023
19 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = () => {
const chart = Plot.plot({
facet: { data: weather, y: "origin" },
//grid: true,
nice: true,
width,
height: 750,
fy: { padding: 0.15 },
x: {
axis: "top",
tickFormat: (d) =>
`${Plot.formatMonth()(d.getUTCMonth())} ${
d.getUTCMonth() === 0 ? d.getUTCFullYear() : ""
}`,
ticks: 12,
grid: true
},
y: { insetTop: 10, insetBottom: 25, grid: false },
marks: [
Plot.frame({ strokeWidth: 0.5 }),

Plot.lineY(
weather,
Plot.select(
{ y: toposimplify({ sample, include_ends: true, take: "high" }) },
{
x: "date",
y: "temp",
stroke: "orange",
curve: "monotone-x",
strokeWidth: 1
}
)
),
Plot.lineY(
weather,
Plot.select(
{ y: toposimplify({ sample, include_ends: true, take: "low" }) },
{
x: "date",
y: "temp",
stroke: "lightblue",
curve: "monotone-x",
strokeWidth: 1
}
)
),

// autolabel
Plot.text(
weather,

// This map allows to compute texts only on the few selected values
// It would also allow, if we wanted, to add "MAX" to the maximum value, etc
Plot.map(
{
text: (data) =>
data.map(
(d) =>
`${d3.utcFormat("%b %-e\n%H:00")(
d.date - 4 * 3600 * 1000
)}\n${(+d.temp).toFixed(1)}°F\n↓`
)
},
Plot.select(
{
y: toposimplify({
sample: labels >> 1,
include_ends: false,
take: "high"
})
},
{
x: "date",
y: "temp",
z: "origin",
text: weather,
dx: 0,
dy: -3,
stroke: "white",
fill: "black",
lineAnchor: "bottom",
lineHeight: 1.2
}
)
)
),

// lower temps autolabel
Plot.text(
weather,
Plot.select(
{
y: toposimplify({
sample: labels >> 1,
include_ends: false,
take: "low"
})
},
{
x: "date",
y: "temp",
z: "origin",
text: (d) =>
`↑\n${d3.utcFormat("%b %e\n%H:00")(
d.date - 4 * 3600 * 1000
)}\n${(+d.temp).toFixed(1)}°F`.replace(" ", " "),
dx: 0,
dy: 3,
stroke: "white",
fill: "black",
lineAnchor: "top",
lineHeight: 1.2
}
)
),

Plot.dot(weather, {
x: "date",
y: "temp",
z: "origin",
curve: "linear",
fill: "black",
r: 0.5
})
]
});

return chart;
}
Insert cell
toposimplify = ({ sample = 3, include_ends = false, take = "both" } = {}) => (
index,
values
) => {
// filter out invalid values
index = index.filter((i) => values[i] === +values[i]);
const sub = topoline(Array.from(index, (i) => values[i]));
const selection = d3.sort(
d3
.sort(sub, (d) => -d.persistence)
.slice(0, sample)
.flatMap(({ low, high }) =>
take === "both" ? [low, high] : take === "high" ? [high] : [low]
)
.map((j) => index[j])
);
if (include_ends) {
if (selection[0] !== index[0]) selection.unshift(index[0]);
if (selection[selection.length - 1] !== index[index.length - 1])
selection.push(index[index.length - 1]);
}
return selection;
}
Insert cell
topoline = function (series) {
const E_insert = 0;
const E_left = 1;
const E_right = 2;
const E_merge = 3;

const n = series.length;
const tree = new Int32Array(n).fill(-1);
const features = [];
let k = 0;
const order = d3.sort(
d3.range(0, tree.length),
(i) => series[i],
(i) => i
);

let event;
for (const i of order) {
const left = tree[i - 1] ?? tree[i];
const right = tree[i + 1] ?? tree[i];

if (left === -1 && right === -1) {
// insert leaf
tree[i] = i;
event = E_insert;
} else if (left === -1) {
tree[i] = right;
event = E_right;
} else if (right === -1) {
tree[i] = left;
event = E_left;
} else {
// merge node
let [a, b] = d3.sort(
[tree[left], tree[right]],
(i) => series[i],
(i) => i
);
if (i === order[order.length - 1]) b = a;
features.push({
low: b,
high: i,
persistence: series[i] - series[b],
error: error(b, i)
});
for (let j = i - 1; tree[j] === b; j--) tree[j] = a;
for (let j = i + 1; tree[j] === b; j++) tree[j] = a;
tree[i] = a;
event = E_merge;
}
k++;
}

// 🌶 ensure we have global min and max: if we didn't end on a merge event,
// it means that the maximum value was reached either on the left or on the right
if (event === E_left || event === E_right) {
const low = order[0];
const high = order.pop();
features.push({
low,
high,
persistence: series[high] - series[low],
error: error(low, high)
});
}

return d3.sort(features, (d) => -d.persistence);

function error(b, i) {
return d3.sum(d3.range(...d3.extent([i, b])), (k) =>
Math.abs(
(series[b] - series[i]) * ((k - i) / (b - i)) - (series[k] - series[i])
)
);
}
}
Insert cell
weather = FileAttachment("weather.csv").csv({ typed: true })
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more