Here is an example using the @hpcc-js/observable-md:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@hpcc-js/common/font-awesome/css/font-awesome.min.css">
<style>
body {
padding: 0px;
margin: 8px;
background: white;
color: black;
}
#placeholder {
position: absolute;
left: 8px;
top: 8px;
right: 8px;
bottom: 8px;
max-width: 480px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/@hpcc-js/observable-md/dist/index.full.js" type="text/javascript" charset="utf-8"></script>
<script>
var omdMod = window["@hpcc-js/observable-md"]
</script>
</head>
<body onresize="doResize()">
<div id="placeholder">
</div>
<script>
var app = new omdMod.Observable()
.target("placeholder")
.showValues(true)
.mode("omd")
.text(`
# Stacked Bar Chart
This chart shows the estimated population by age and U.S. state. Compare to [horizontal stacked bars](/@d3/stacked-horizontal-bar-chart), [normalized stacked bars](/@d3/stacked-normalized-horizontal-bar), [grouped bars](https://observablehq.com/@d3/grouped-bar-chart) and a [dot plot](/@d3/dot-plot). Data: [American Community Survey](/@mbostock/working-with-the-census-api)
\`\`\`
key = Legend(chart.scales.color, {title: "Age (years)"}) // try also Swatches
chart = StackedBarChart(stateages, {
x: d => d.state,
y: d => d.population / 1e6,
z: d => d.age,
xDomain: d3.groupSort(stateages, D => d3.sum(D, d => -d.population), d => d.state),
yLabel: "↑ Population (millions)",
zDomain: ages,
colors: d3.schemeSpectral[ages.length],
width,
height: 500
})
states = FileAttachment(/* "us-population-state-age.csv" */"https://static.observableusercontent.com/files/cacf3b872e296fd3cf25b9b8762dc0c3aa1863857ecba3f23e8da269c584a4cea9db2b5d390b103c7b386586a1104ce33e17eee81b5cc04ee86929f1ee599bfe").csv({typed: true})
ages = states.columns.slice(1)
stateages = ages.flatMap(age => states.map(d => ({state: d.name, age, population: d[age]}))) // pivot longer
howto("StackedBarChart")
altplot(\`Plot.plot({
width,
y: {tickFormat: "s"},
color: {scheme: "spectral", domain: ages},
marks: [
Plot.barY(stateages, {
x: "state",
y: "population",
fill: "age",
sort: {x: "y", reverse: true}
}),
Plot.ruleY([0])
]
})\`)
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/stacked-bar-chart
function StackedBarChart(data, {
x = (d, i) => i, // given d in data, returns the (ordinal) x-value
y = d => d, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
title, // given d in data, returns the title text
marginTop = 30, // top margin, in pixels
marginRight = 0, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xDomain, // array of x-values
xRange = [marginLeft, width - marginRight], // [left, right]
xPadding = 0.1, // amount of x-range to reserve to separate bars
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetDiverging, // stack offset method
order = d3.stackOrderNone, // stack order method
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
colors = d3.schemeTableau10, // array of colors
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default x- and z-domains, and unique them.
if (xDomain === undefined) xDomain = X;
if (zDomain === undefined) zDomain = Z;
xDomain = new d3.InternSet(xDomain);
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the x- and z-domains.
const I = d3.range(X.length).filter(i => xDomain.has(X[i]) && zDomain.has(Z[i]));
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).paddingInner(xPadding);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat);
// Compute titles.
if (title === undefined) {
const formatValue = yScale.tickFormat(100, yFormat);
title = i => \`\${X[i]}\\n\${Z[i]}\\n\${formatValue(Y[i])}\`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", \`translate(\${marginLeft},0)\`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
const bar = svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", ([{i}]) => color(Z[i]))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", ({i}) => xScale(X[i]))
.attr("y", ([y1, y2]) => Math.min(yScale(y1), yScale(y2)))
.attr("height", ([y1, y2]) => Math.abs(yScale(y1) - yScale(y2)))
.attr("width", xScale.bandwidth());
if (title) bar.append("title")
.text(({i}) => title(i));
svg.append("g")
.attr("transform", \`translate(0,\${yScale(0)})\`)
.call(xAxis);
return Object.assign(svg.node(), {scales: {color}});
}
import {Legend, Swatches} from "@d3/color-legend"
import {howto, altplot} from "@d3/example-components"
\`\`\``)
;
doResize();
function doResize() {
if (app) {
app
.resize()
.lazyRender()
;
}
}
</script>
</body>
</html>
- Installed the following VS Code extension: Observable JS - Visual Studio Marketplace
- Created an empty
test.omd
file.
- Imported the notebook by URL
- Exported to html