Public
Edited
Mar 21, 2024
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
black = "#262626"
Insert cell
margin = ({left: 40, right: 60, top: 180, bottom: 80})
Insert cell
Insert cell
Insert cell
Insert cell
dateFormat = d3.utcFormat("%Y");
Insert cell
arrow = (color) => htl.svg`<marker
id="arrow"
viewBox="0 0 10 10"
markerWidth="3"
markerHeight="3"
stroke-linejoin="miter"
refX="5" refY="5"
orient="auto">
<path fill="${color}" stroke="${color}" stroke-width=1 d="M 0 0 L 10 5 L 0 10 z" />
</marker>`
Insert cell
dot = (color) => htl.svg`<marker
id="arrow"
viewBox="0 0 10 10"
markerWidth="5"
markerHeight="5"
stroke-linejoin="miter"
refX="5" refY="5"
orient="auto">
<circle fill="${color}" stroke="${color}" stroke-width=1 cx=5 cy=5 r=2 />
</marker>`
Insert cell
getColor = (good_or_bad) => {
if (good_or_bad === "good") return {fill: fillGood, stroke: strokeGood};
if (good_or_bad === "bad") return {fill: fillBad, stroke: strokeBad};
return {fill: fillNeutral, stroke: strokeNeutral};
}
Insert cell

chart = ({
width = 1050,
height = 800,
data = aapl,
x = "Date",
y = "Close",
title = "Reusable little line chart",
source = "Source text goes here",
subtitle = "Can be embedded in other notebooks",
title_dy,
y_domain,
startvalue_dy = 0, endvalue_dy = 0, startvalue_dx = 0, endvalue_dx = 0,
good_or_bad,
multiplier,
x_labels,
numberFormat = d3.format("d"),
}={}) => {
data = data.toSorted((a,b) => d3.ascending(a[x],b[x]));
if(multiplier) data = data.map(m => {
const result = {...m};
if(multiplier === "thousands") result[y] = m[y]/1e3;
if(multiplier === "millions") result[y] = m[y]/1e6;
if(multiplier === "billions") result[y] = m[y]/1e9;
return result;
})
const extent = d3.extent(data, d=>d[x]);
const domainPadding = (extent[1] - extent[0]) / 20;
const domain = [
new Date(extent[0].valueOf() - domainPadding),
new Date(extent[1].valueOf() + domainPadding)
];
const ticks = x_labels ? (x_labels.split(", ")).map(m => d3.utcParse("%Y")(m)) : undefined;
const {fill, stroke} = getColor(good_or_bad);
subtitle = (subtitle || "") + (subtitle && multiplier ? ", " : "") + (multiplier || "");
const plot = Plot.plot({
width,
height,
marginTop: margin.top,
marginBottom: margin.bottom,
marginLeft: margin.left,
marginRight: margin.right,
x: {ticks, label: "", labelArrow: "none", tickFormat: dateFormat, tickSpacing: 100, fontVariant: "none"},
y: {ticks: [], domain: y_domain ? JSON.parse(y_domain) : undefined, label: "", labelArrow: "none"},
style: {fontFamily: fontFamily, fontWeight: weight, fontSize: fontSizeRegular, fill: black},
marks: [
Plot.areaY(data, {x, y, fill}),
//Plot.link(firstLast(data),{x1: 0, x2: 100, y1: 0, y2: 100}),
//Plot.lineY(firstLast(data), {x, y:0 ,stroke: "black", strokeWidth: 2}),
Plot.ruleY([0], {stroke: black, strokeWidth: 2}),
Plot.lineY(data, {
x, y,
markerStart: dot,
markerEnd: arrow,
stroke, strokeWidth, strokeLinejoin, strokeMiterlimit, strokeLinecap
}),

//Plot.dot(last(data), {x,y:d => 193,r: 10, fill: "black"}),
...markTitles(data, x, y, title_dy, title, subtitle, source),
...markFirstLastValues(data, x, y, numberFormat)
]
})

const xTickLabel = d3 //.select(plot)
.select('g[aria-label="x-axis tick label"]').select("text").node()

if (xTickLabel) {
const xTickLabelW = xTickLabel.getBBox().width;
d3.select(plot).select('g[aria-description="title"]')
.attr("transform", `translate(${0.5 - xTickLabelW/2},${0.5 -140 - (+title_dy || 0)})`)
d3.select(plot).select('g[aria-description="subtitle"]')
.attr("transform", `translate(${0.5 - xTickLabelW/2},${0.5 -100 - (+title_dy || 0)})`)
d3.select(plot).select('g[aria-description="source"]')
.attr("transform", `translate(${0.5 - xTickLabelW/2},${0.5 + 60})`)
d3.select(plot).select('g[aria-description="valueLeft"]')
.attr("transform", `translate(${0.5 - xTickLabelW/2 + +startvalue_dx},${0.5 - 35 - +startvalue_dy})`)
d3.select(plot).select('g[aria-description="dateLeft"]')
.attr("transform", `translate(${0.5 - xTickLabelW/2 + +startvalue_dx},${0.5 - 85 - +startvalue_dy})`)
d3.select(plot).select('g[aria-description="valueRight"]')
.attr("transform", `translate(${0.5 + xTickLabelW/2 + +endvalue_dx},${0.5 - 40 - +endvalue_dy})`)
d3.select(plot).select('g[aria-description="dateRight"]')
.attr("transform", `translate(${0.5 + xTickLabelW/2 + +endvalue_dx},${0.5 - 90 - +endvalue_dy})`)
d3.select(plot).select('g[aria-description="compareRight"]')
.attr("transform", `translate(${0.5 + xTickLabelW/2 + +endvalue_dx},${0.5 - 40 - +endvalue_dy})`)
}

//console.log(texts);
plot.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "style"))
.innerHTML = fontFace;
return plot;
}
Insert cell
chart()
Insert cell
markTitles = (data, x, y, title_dy, title, subtitle, source) => [
//TITLE
Plot.text(
first(data), {
x, dx: 0,
frameAnchor: "top-left",
ariaDescription: "title",
fill: black,
text: d => title,
lineAnchor: "bottom", fontSize: fontSizeLarge, fontWeight: weightTitle
}
),

//SUBTITLE
Plot.text(
first(data), {
x, dx: 0,
frameAnchor: "top-left",
ariaDescription: "subtitle",
fill: black,
text: d => subtitle,
lineAnchor: "bottom", fontSize: fontSizeRegular, fontWeight: weight
}
),

//SOURCE
Plot.text(
first(data), {
x, dx: 0,
text: (d) => source,
ariaDescription: "source",
frameAnchor: "bottom-left",
fill: "#BFBFBF",
lineAnchor: "bottom", fontSize: fontSizeRegular, fontWeight: weight
}
),
]
Insert cell
markFirstLastValues = (data, x, y, numberFormat) => [
//FIRST
Plot.text(
first(data), {
x, y, dx: 0,
ariaDescription: "valueLeft",
fill: black,
text: (d) => numberFormat(d[y]),
textAnchor: "start", fontSize: fontSizeLarge, fontWeight: weightTitle
}
),
Plot.text(
first(data),{
x, y, dx: 0,
ariaDescription: "dateLeft",
text: (d) => dateFormat(d[x]),
textAnchor: "start", fontSize: fontSizeLarge, fontWeight: weight
}
),

//LAST
Plot.text(
last(data),{
x, y, dx: 0,
ariaDescription: "valueRight",
fill: black,
text: (d) => numberFormat(d[y]),
textAnchor: "end", fontSize: fontSizeLarge, fontWeight: weightTitle
}
),
Plot.text(
last(data), {
x, y, dx: 0,
ariaDescription: "dateRight",
fill: black,
text: (d) => dateFormat(d[x]),
textAnchor: "end", fontSize: fontSizeLarge, fontWeight: weight
}
),

// //LAST
// Plot.text(
// last(data), {
// x, y: d => 193, dx: 0,
// ariaDescription: "compareRight",
// fill: black,
// text: (d) => numberFormat(193),
// textAnchor: "end", fontSize: fontSizeLarge, fontWeight: weightTitle
// }
// )
]
Insert cell
first = (data) => [data.at(0)]
Insert cell
last = (data) => [data.at(-1)]
Insert cell
firstLast = (data) => [data.at(0), data.at(-1)]
Insert cell
import {read} from "@open-numbers/read-google-spreadsheet"
Insert cell
graph_list = read("https://docs.google.com/spreadsheets/d/1rfTrNxyC16pS2Lv5RRa0RmFDm2_4UWzbhKXivb5CpkI/edit#gid=0", "graph_list")
Insert cell
makeChart = async (graphConfig) => {
const [sheet, url] = graphConfig.dataset.split(" from ");
const raw = await read(url, sheet);
const [x, y] = raw.columns;
const data = raw.map((m) => ({ [x]: d3.utcParse("%Y")(m[x]), [y]: +m[y] }));
return chart({
data,
x,
y,
numberFormat: (d) => d3.format("d")(d) + (graphConfig.format === "percent" ? "%" : ""),
...graphConfig
});
}
Insert cell
makeChart(graph_list[34])
Insert cell
import {toDataURL} from "@mootari/embedding-fonts-into-an-svg"
Insert cell
fontFace = {
const fontData = await toDataURL(fontFiles[fontFamily]);
return `@font-face {
font-family: '${fontFamily}';
font-style: normal;
font-weight: 300,400,500,600,700,800,900;
src: url(${fontData}) format('woff2');
}`
}
Insert cell
fontFiles = ({
"Rubik Light": "https://fonts.gstatic.com/s/rubik/v28/iJWKBXyIfDnIV7nBrXw.woff2",
})
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