From 70c34c3244d44cf6d02d99ce92247b1264edda35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 19 Feb 2024 10:16:07 +0100 Subject: [PATCH] use Plot --- docs/components/apiHistogram.js | 125 ---------------------- docs/components/timeline.js | 16 --- docs/index.md | 179 ++++++++++++++------------------ yarn.lock | 5 - 4 files changed, 76 insertions(+), 249 deletions(-) delete mode 100644 docs/components/apiHistogram.js delete mode 100644 docs/components/timeline.js diff --git a/docs/components/apiHistogram.js b/docs/components/apiHistogram.js deleted file mode 100644 index d7ca07f..0000000 --- a/docs/components/apiHistogram.js +++ /dev/null @@ -1,125 +0,0 @@ -import * as Plot from "npm:@observablehq/plot"; -import * as d3 from "npm:d3"; -// import {dy} from "./apiHeatmap.js"; - -const dy = 400; // number of rows -const marginTop = 0; -const marginRight = 20; -const marginBottom = 30; -const marginLeft = 20; -const barWidth = 4; -const canvasCache = new WeakSet(); - -export function ApiHistogram( - value, - count, - category, - {canvas = document.createElement("canvas"), color, width, height = 360, label, y1, y2} -) { - const ky = 6; // number of requests per pixel - - const plot = Plot.plot({ - figure: true, - width, - height, - marginTop, - marginRight, - marginBottom, - marginLeft, - style: "overflow: visible;", - x: {type: "log", domain: [y1, y2 - 1], label}, - y: {axis: null, domain: [0, (height - marginTop - marginBottom) * ky], label: "requests"}, - color: {label: color.label, legend: false}, - marks: [ - Plot.ruleY([0]), - Plot.tip({length: 1}, {fill: [""], x: [y1], y: [0], format: {x: null, y: null}, render: renderTip}) - ] - }); - - const svg = plot.querySelector("svg"); - const div = document.createElement("div"); - div.style = "position: relative;"; - - if (!canvasCache.has(canvas)) { - canvasCache.add(canvas); - canvas.width = dy; - canvas.height = height - marginTop - marginBottom; - canvas.style = ` - image-rendering: pixelated; - position: absolute; - left: ${marginLeft}px; - top: ${marginTop}px; - width: calc(100% - ${marginLeft + marginRight}px); - height: calc(100% - ${marginTop + marginBottom}px); - `; - - const tick = (i, j1, j2) => { - for (let j = j1; j < j2; ++j) { - let sum = 0; - for (; i < value.length; ++i) { - const currentValue = value.get(i); - if (currentValue < j) continue; - if (currentValue > j) break; - const currentCount = count.get(i); - const currentCategory = category.get(i); - const y0 = plot.scale("y").apply(sum); - const y1 = plot.scale("y").apply((sum += currentCount)); - context.fillStyle = color.apply(currentCategory); - // console.log(context.fillStyle, currentCategory); - context.fillRect(j, y1, barWidth, y0 - y1); - } - } - if (j2 < dy) requestAnimationFrame(() => tick(i, j2, j2 + (j2 - j1))); - }; - - - const context = canvas.getContext("2d"); - requestAnimationFrame(() => tick(0, 0, 20)); - } - - svg.style.position = "relative"; - svg.replaceWith(div); - div.appendChild(canvas); - div.appendChild(svg); - - function renderTip(index, scales, values, dimensions, context, next) { - let g = next([], scales, values, dimensions, context); - const svg = context.ownerSVGElement; - svg.addEventListener("pointerenter", pointermove); - svg.addEventListener("pointermove", pointermove); - svg.addEventListener("pointerleave", pointerleave); - function pointermove(event) { - const [px, py] = d3.pointer(event); - const found = find(scales.x.invert(px), scales.y.invert(py)); - if (found == null) return pointerleave(); - const [k, y] = found; - values.x[0] = px; - values.y[0] = scales.y(y); - values.fill[0] = color.apply(category.get(k)); - values.channels.fill.value[0] = category.get(k); - const r = next([0], scales, values, dimensions, context); - g.replaceWith(r); - g = r; - } - function pointerleave() { - const r = next([], scales, values, dimensions, context); - g.replaceWith(r); - g = r; - } - return g; - } - - function find(y, currentCount) { - if (!(y1 <= y && y <= y2)) return; - const currentValue = Math.floor(((Math.log(y) - Math.log(y1)) / (Math.log(y2) - Math.log(y1))) * dy); - let i = 0, j, sum = 0; - for (; i < value.length; ++i) { - if (value.get(i) < currentValue) continue; - if (value.get(i) > currentValue) break; - if ((sum += count.get((j = i))) >= currentCount) break; - } - if (sum) return [j, sum - count.get(j) / 2]; - } - - return plot; -} \ No newline at end of file diff --git a/docs/components/timeline.js b/docs/components/timeline.js deleted file mode 100644 index 23d692c..0000000 --- a/docs/components/timeline.js +++ /dev/null @@ -1,16 +0,0 @@ -import * as Plot from "npm:@observablehq/plot"; - -export function timeline(events, {width, height} = {}) { - return Plot.plot({ - width, - height, - marginTop: 30, - x: {nice: true, label: null, tickFormat: ""}, - y: {axis: null}, - marks: [ - Plot.ruleX(events, {x: "year", y: "y", markerEnd: "dot", strokeWidth: 2.5}), - Plot.ruleY([0]), - Plot.text(events, {x: "year", y: "y", text: "name", lineAnchor: "bottom", dy: -10, lineWidth: 10, fontSize: 12}) - ] - }); -} diff --git a/docs/index.md b/docs/index.md index 3d67d67..d817977 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,125 +2,98 @@ toc: false --- - - -
-

American Community Survey

-

This is a testbed of visualizing the Census Bureau's American Community Survey data from 2022. Read about the data  here or see how these visualizations were generated on GitHub. Ping me on Twitter or email if you have ideas for what other variables in the census to look at or how else to display these millions of datapoints!

- 80% of visualization is data processing; learn how this data was processed here!↗︎ -
- - -```js -import "npm:apache-arrow"; -import "npm:parquet-wasm/esm/arrow1.js"; -import {ApiHistogram} from "./components/apiHistogram.js"; -``` +# American Community Survey +This is a testbed of visualizing the Census Bureau's American Community Survey data from 2022. Read about the data  here or see how these visualizations were generated on GitHub. Ping me on Twitter or email if you have ideas for what other variables in the census to look at or how else to display these millions of datapoints! +80% of visualization is data processing; learn how this data was processed here!↗︎ ```js -const incomeHistogram = FileAttachment("data/income-histogram.parquet").parquet(); -const incomeCanvas = document.createElement("canvas"); +const income = FileAttachment("data/income-histogram.parquet").parquet(); +const rent = FileAttachment("data/rent-histogram.parquet").parquet(); ``` ```js -// Assuming modification for categorization based on sector -const sectorColorMapping = d3.sort(d3.rollups(incomeHistogram.getChild("sector"), (D) => D.length, (d) => d).filter(([d]) => d), ([, d]) => -d).map(([sector, count]) => ({sector, count})); -const sectorColor = Object.assign(Plot.scale({color: {domain: sectorColorMapping.map((d) => d.sector)}}), {label: "sector"}); -const sectorSwatch = (sector) => html` ${sector}`; -``` - -```js -// Import the rent histogram data from the parquet file -const rentHistogram = FileAttachment("data/rent-histogram.parquet").parquet(); -const rentCanvas = document.createElement("canvas"); +function incomeChart(income, width) { + // Order the sectors by mean income + const orderSectors = d3.groupSort( + income, + (v) => -d3.sum(v, (d) => d.income * d.count) / d3.sum(v, (d) => d.count), + (d) => d.sector + ); + + // Create a histogram with a logarithmic base. + return Plot.plot({ + width, + marginLeft: 60, + x: { type: "log" }, + color: { legend: "swatches", columns: 6, domain: orderSectors }, + marks: [ + Plot.rectY( + income, + Plot.binX( + { y: "sum" }, + { + x: "income", + y: "count", + fill: "sector", + order: orderSectors, + thresholds: d3 + .ticks(Math.log10(2_000), Math.log10(1_000_000), 40) + .map((d) => +(10 ** d).toPrecision(3)), + tip: true, + } + ) + ), + ], + }); +} ``` ```js -// Create the color mapping for regions -const regionColorMapping = d3.sort(d3.rollups(rentHistogram.getChild("region"), (D) => D.length, (d) => d).filter(([d]) => d), ([, d]) => -d).map(([region, count]) => ({ region, count })); -const regionColor = Object.assign(Plot.scale({ color: { domain: regionColorMapping.map((d) => d.region) } }), {label: "region" }); -const regionSwatch = (region) => html`${region}`; +function rentChart(rent, width) { + // Order the regions by mean rent + const orderRegions = d3.groupSort( + rent, + (v) => d3.sum(v, (d) => d.rent * d.count) / d3.sum(v, (d) => +d.count), + (d) => d.region + ); + + // Create a histogram with a logarithmic base. + return Plot.plot({ + width, + marginLeft: 60, + x: { type: "log" }, + color: { legend: "swatches", columns: 6, domain: orderRegions }, + marks: [ + Plot.areaY( + rent, + Plot.binX( + { y: "sum" }, + { + x: "rent", + y: "count", + fill: "region", + order: orderRegions, + thresholds: d3 + .ticks(Math.log10(100), Math.log10(10000), 50) + .map((d) => +(10 ** d).toPrecision(3)), + tip: true, + curve: "monotone-x", + } + ) + ), + ], + }); +} ``` -

Income distribution by sector (code for data transform)

- ${resize((width) => ApiHistogram(incomeHistogram.getChild("income"), incomeHistogram.getChild("count"), incomeHistogram.getChild("sector"), {canvas: incomeCanvas, color: sectorColor, width, label: "Income ($)", y1: 1_000, y2: 200_000}))} + ${resize((width) => incomeChart(income, width))}

Rent Distribution by Region (code for data transform)

- ${resize((width) => ApiHistogram(rentHistogram.getChild("rent"), rentHistogram.getChild("count"), rentHistogram.getChild("region"), {canvas: rentCanvas, color: regionColor, width, label: "Rent ($)", y1: 100, y2: 20_000 }))} + ${resize((width) => rentChart(rent, width))}
- ---- - diff --git a/yarn.lock b/yarn.lock index b70b503..68548d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,11 +978,6 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-unicode-supported@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"