Public
Edited
Jan 21
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
width = 954
Insert cell
height = width
Insert cell
margin = 10
Insert cell
innerRadius = width / 10
Insert cell
outerRadius = width / 2 - margin
Insert cell
x = d3
.scaleTime()
.domain([d3.timeYear.floor(extent[0]), d3.timeYear.ceil(extent[1])])
.range([Math.PI, 3 * Math.PI])
Insert cell
extent = d3.extent(data.flatMap((d) => d.season).flat(), (d) => d.date)
Insert cell
y = d3
.scaleBand()
.domain(data.map((d) => d.item))
.range([innerRadius + 20, outerRadius])
Insert cell
color = d3.scaleOrdinal(data.map(d => d.item), data.map(d => d.color))
Insert cell
xAxis = g => g
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.call(g => g.selectAll("g")
.data(x.ticks())
.join("g")
.each((d, i) => d.id = DOM.uid("month"))
.call(g => g.append("path")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.attr("d", d => `
M${d3.pointRadial(x(d), innerRadius)}
L${d3.pointRadial(x(d), outerRadius)}
`))
.call(g => g.append("path")
.attr("id", d => d.id.id)
.datum(d => [d, d3.utcMonth.offset(d, 1)])
.attr("fill", "none")
.attr("d", ([a, b]) => `
M${d3.pointRadial(x(a), innerRadius)}
A${innerRadius},${innerRadius} 0,0,1 ${d3.pointRadial(x(b), innerRadius)}
`))
.call(g => g.append("text")
.attr("text-anchor", "middle")
.style("text-transform", "uppercase").attr("font-weight", "bold")
.append("textPath")
.attr("startOffset", 25)
.attr("xlink:href", d => d.id.href)
.text(d3.timeFormat("%b"))))
Insert cell
line = d3
.lineRadial()
.angle((d) => x(d.date))
.radius((d) => y(d.item))
Insert cell
area = d3
.areaRadial()
.curve(d3.curveLinearClosed)
.angle((d) => x(d.date))
Insert cell
workbook = FileAttachment("Phenological Clock.xlsx").xlsx()

// When this notebook is private (for development), it can pull from Google Sheets:
// https://docs.google.com/spreadsheets/d/1hxH6SfAezuZ3T2vMytwE9ek3w97e91iydfEUXdcb_9U/edit#gid=0
// workbook = FileAttachment("google://Phenological Clock").xlsx()
Insert cell
_data = workbook.sheet(1, {headers: true})
.flatMap(({ item, color, ...rest }) => Object.keys(rest).map(place => ({
item,
color,
place,
season: JSON.parse(`[${rest[place].replace(/\w+/g, `"$&"`)}]`).map((d) => {
const [start, end] = d.map(parseMonth);
return d3.timeDay
.range(start, d3.timeDay.offset(d3.timeMonth.offset(end || start, 1), 1))
.map((date) => ({ item, date }));
})
})))
Insert cell
data = _data
.filter(d => d.place === place)
.sort(
(a, b) =>
a.season[0][0]?.date - b.season[0][0]?.date ||
a.season[0].length - b.season[0].length
)
Insert cell
places = [...new Set(_data.map(d => d.place))]
Insert cell
parseMonth = d3.timeParse("%b")
Insert cell
formatMonth = d3.timeFormat("%b")
Insert cell
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