Density chart¶
Load data
# https://observablehq.com/@d3/density-contours
import detroit as d3
import polars as pl
URL = (
"https://static.observableusercontent.com/files/98d78d7f290f9776833e989617d49b59203"
"9ea65fee3b451764067cccd582eac122b3a07619cf223e8526910284fc105dfcb24b9af785535ee1dc"
"6914687f9ac?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27faith"
"ful.tsv"
)
faithful = pl.read_csv(URL, separator="\t")
shape: (272, 2)
┌───────────┬─────────┐
│ eruptions ┆ waiting │
│ --- ┆ --- │
│ f64 ┆ i64 │
╞═══════════╪═════════╡
│ 3.6 ┆ 79 │
│ 1.8 ┆ 54 │
│ 3.333 ┆ 74 │
│ 2.283 ┆ 62 │
│ 4.533 ┆ 85 │
│ … ┆ … │
│ 4.117 ┆ 81 │
│ 2.15 ┆ 46 │
│ 4.417 ┆ 90 │
│ 1.817 ┆ 46 │
│ 4.467 ┆ 74 │
└───────────┴─────────┘
Make the line chart
# Make data as list[dict]
faithful = faithful.to_dicts()
# Declare the chart's dimensions.
width = 928
height = 600
margin_top = 20
margin_right = 30
margin_bottom = 30
margin_left = 40
# Create the horizontal and vertical scales.
x = (
d3.scale_linear()
.set_domain(d3.extent(faithful, lambda d: d["waiting"]))
.nice()
.set_range_round([margin_left, width - margin_right])
)
y = (
d3.scale_linear()
.set_domain(d3.extent(faithful, lambda d: d["eruptions"]))
.nice()
.set_range_round([height - margin_bottom, margin_top])
)
# Compute the density contours.
contours = (
d3.contour_density()
.x(lambda d: x(d["waiting"]))
.y(lambda d: y(d["eruptions"]))
.set_size([width, height])
.set_bandwidth(30)
.set_thresholds(30)(faithful)
)
# Create the SVG container.
svg = (
d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("view_box", f"0 0 {width} {height}")
.attr("style", "max-width: 100%; height: auto;")
)
# Append the axes.
(
svg.append("g")
.attr("transform", f"translate(0,{height - margin_bottom})")
.call(d3.axis_bottom(x).set_tick_size_outer(0))
.call(lambda g: g.select(".domain").remove())
.call(
lambda g: g.select(".tick:last-of-type text")
.clone()
.attr("y", -3)
.attr("dy", "null")
.attr("font-weight", "bold")
.text("Idle (min.)")
)
)
(
svg.append("g")
.attr("transform", f"translate({margin_left},0)")
.call(d3.axis_left(y).set_tick_size_outer(0))
.call(lambda g: g.select(".domain").remove())
.call(
lambda g: g.select(".tick:last-of-type text")
.clone()
.attr("x", 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Erupting (min.)")
)
)
# Append the contours.
(
svg.append("g")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.select_all()
.data(contours)
.join("path")
.attr("stroke-width", lambda d, i: 1 if i % 5 else 0.25)
.attr("d", d3.geo_path())
)
# Append dots.
(
svg.append("g")
.attr("stroke", "white")
.select_all()
.data(faithful)
.join("circle")
.attr("cx", lambda d: x(d["waiting"]))
.attr("cy", lambda d: y(d["eruptions"]))
.attr("r", 2)
)
Save your chart
with open("light-density.svg", "w") as file:
file.write(str(svg))