Skip to content

Commit a0d1330

Browse files
author
James Goldie
committed
Add time series example
1 parent 5d667cb commit a0d1330

File tree

9 files changed

+137336
-1
lines changed

9 files changed

+137336
-1
lines changed

docs/_quarto.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ website:
1313
file: index.qmd
1414
- text: "Examples"
1515
menu:
16-
- text: "Bar chart"
16+
- text: "Simple bar chart"
1717
file: examples/barchart/index.qmd
18+
- text: "Time series"
19+
file: examples/time-series/index.qmd
1820
- text: Changelog
1921
file: news.qmd
2022
# - about.qmd
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<script>
2+
// import { fly } from "svelte/transition";
3+
import { extent } from "d3-array"
4+
import { scaleLinear } from "d3-scale"
5+
import { scaleSequential } from "d3-scale"
6+
import { interpolateYlGnBu, interpolateYlOrRd, select, axisLeft, axisBottom, format, tickFormat, formatLocale } from "d3"
7+
import { regressionLinear } from "d3-regression"
8+
// ,
9+
// should ba array of objects with:
10+
// x
11+
// y
12+
// colour
13+
// tooltip text maybe?
14+
export let data = []
15+
export let colourScheme = "cool" // or warm
16+
$: console.log(colourScheme)
17+
$: colourRamp = (colourScheme == "cool") ?
18+
interpolateYlGnBu :
19+
interpolateYlOrRd
20+
21+
// calculate trend line from data
22+
const regress = regressionLinear()
23+
.x(d => d.date)
24+
.y(d => d.value)
25+
.domain(extent(data.map(d => d.date)))
26+
27+
$: trendLine = regress(data)
28+
29+
30+
// dimensions bound to size of container
31+
let height = 500
32+
let width = 300
33+
34+
// add padding to chart
35+
$: padX = [60, width - 10]
36+
$: padY = [height - 30, 10]
37+
38+
$: xDomain = extent(data.map(d => d.year))
39+
$: yDomain = extent(data.map(d => d.value))
40+
41+
// scales (flip the colours if they're cool)
42+
$: xScale = scaleLinear()
43+
.domain(xDomain)
44+
.range(padX)
45+
$: yScale = scaleLinear()
46+
.domain(yDomain)
47+
.range(padY)
48+
$: colourScale = scaleSequential()
49+
.domain(colourScheme == "cool" ? yDomain.reverse() : yDomain)
50+
.interpolator(colourRamp)
51+
52+
// temperature formatter (for x-axis)
53+
const tempFormat = formatLocale({
54+
currency: ["", "°C"]
55+
});
56+
57+
// axes
58+
let xAxisGroup
59+
let yAxisGroup
60+
$: select(xAxisGroup)
61+
.transition()
62+
.duration(500)
63+
.call(axisBottom(xScale).tickFormat(format(".0f")))
64+
$: select(yAxisGroup)
65+
.transition()
66+
.duration(500)
67+
.call(axisLeft(yScale).tickFormat(tempFormat.format("$.1f")))
68+
69+
</script>
70+
71+
<style>
72+
73+
svg circle {
74+
transition:
75+
cx 0.5s ease-in-out,
76+
cy 0.5s ease-in-out,
77+
fill 0.5s ease-in-out;
78+
}
79+
80+
#x-axis, #y-axis {
81+
font-family: system-ui, -apple-system;
82+
font-size: 14px;
83+
}
84+
85+
86+
</style>
87+
88+
<main bind:clientHeight={height} bind:clientWidth={width}>
89+
<svg width={width} height={height}>
90+
91+
<g>
92+
{#each data as { year, value }}
93+
<!-- points go here-->
94+
<circle
95+
cx="{xScale(year)}px"
96+
cy="{yScale(value)}px"
97+
r="5"
98+
fill="{colourScale(value)}">
99+
</circle>
100+
{/each}
101+
</g>
102+
<!-- trend line goes here -->
103+
104+
<!-- axes goes here (is rendered imperatively above)-->
105+
<g bind:this={xAxisGroup} id="x-axis"
106+
style:transform="translateY({padY[0]}px)"
107+
/>
108+
<g bind:this={yAxisGroup} id="y-axis"
109+
style:transform="translateX({padX[0]}px)"
110+
/>
111+
</svg>
112+
</main>
113+
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
title: "Examples: time series"
3+
author: James Goldie
4+
date: last-modified
5+
format:
6+
html:
7+
code-fold: true
8+
resources:
9+
- "*.csv"
10+
filters:
11+
- sverto
12+
sverto:
13+
use:
14+
- TimeSeriesChart.svelte
15+
---
16+
17+
Let's do something more useful: a time series of temperature extremes.
18+
19+
In Quarto, we'll download the data for two cities (Melbourne and Brisbane), letting the user choose which to display. We'll also let them choose a month and the extreme to display.
20+
21+
:::{.callout-tip appearance="simple"}
22+
In climate parlance, the highest temperature of the day is called the "daily maximum temperature", or `tmax` for short. The coldest temperature of the day is called "daily minimum temperature", or `tmin` for short.
23+
24+
I'm just calling them "daytime temperature" and "nighttime temperature" here — although the lowest temperature can technically happen during the day, it's usually at night!
25+
:::
26+
27+
Once the data has been appropriately filtered and calculated, we'll pass it to our Svelte chart.
28+
29+
The chart is fairly agnostic in the sense that it could be used with other time series datasets too, rather than being tailored specifically to this one. It expects a prop called `data`, which is an array of objects that each have a numerical `year` and a `value`.
30+
31+
:::{.callout-note collapse="true" appearance="simple"}
32+
We could generalise this chart further if we wanted: we could have it expect columns called `x` and `y`, and perhaps allow full date objects as the x values.
33+
:::
34+
35+
36+
:::{.panel-input}
37+
38+
##### Controls
39+
40+
```{ojs}
41+
//| label: controls-city-variable
42+
viewof selectedCity = Inputs.select(
43+
new Map([
44+
["Melbourne", "086338"],
45+
["Brisbane", "040842"]
46+
]),
47+
{
48+
value: "086338"
49+
}
50+
)
51+
52+
viewof selectedVariable = Inputs.select(
53+
new Map([
54+
["Daytime", "tmax"],
55+
["Nighttime", "tmin"]
56+
]),
57+
{
58+
value: "tmax"
59+
}
60+
)
61+
```
62+
63+
Let's also let users select a month and whether to look at the hottest, coldest or average temperature:
64+
65+
```{ojs}
66+
//| label: controls-season-metric
67+
viewof selectedSeason = Inputs.select(
68+
new Map([
69+
["Whole year", 0],
70+
["January", 1],
71+
["February", 2],
72+
["March", 3],
73+
["April", 4],
74+
["May", 5],
75+
["June", 6],
76+
["July", 7],
77+
["August", 8],
78+
["September", 9],
79+
["October", 10],
80+
["November", 11],
81+
["December", 12]
82+
]),
83+
{
84+
value: 0
85+
}
86+
)
87+
88+
viewof selectedMetric = Inputs.select(
89+
new Map([
90+
["Hottest", "max"],
91+
["Average", "mean"],
92+
["Coldest", "min"],
93+
]),
94+
{
95+
value: "Hottest"
96+
}
97+
)
98+
```
99+
100+
:::
101+
102+
Now let's use [Arquero](https://github.com/uwdata/arquero) to download and filter the selected data.
103+
104+
105+
```{ojs}
106+
//| label: download-filter-data
107+
import { aq, op } from "@uwdata/arquero"
108+
109+
fullCity = aq.loadCSV(selectedVariable + "." + selectedCity + ".daily.csv")
110+
111+
tidiedCity = fullCity
112+
.rename(aq.names("date", "value"))
113+
.filter(d => d.date !== null)
114+
.params({ selectedSeason: selectedSeason })
115+
.derive({
116+
year: d => op.year(d.date),
117+
month: d => op.month(d.date) + 1
118+
})
119+
120+
// filter unless "Whole year" is selected
121+
filteredCity = selectedSeason == 0 ?
122+
tidiedCity :
123+
tidiedCity.filter(d => d.month == selectedSeason)
124+
125+
// now group by year and calculate the metrics
126+
allMetrics = filteredCity
127+
.groupby("year")
128+
.rollup({
129+
min: d => op.min(d.value),
130+
mean: d => op.mean(d.value),
131+
max: d => op.max(d.value),
132+
})
133+
134+
// finally, select the year and whichever metric column is chosen by the user
135+
finalData = allMetrics
136+
.select("year", selectedMetric)
137+
.rename(aq.names("year", "value"))
138+
```
139+
140+
And so we have our data! Here it is as a table, so we can see what we're sending to Svelte:
141+
142+
:::{.callout-note title="Table of values" collapse="true" appearance="simple"}
143+
```{ojs}
144+
//| label: data-table
145+
Inputs.table(finalData)
146+
```
147+
:::
148+
149+
But, more importantly, here it is as an animated chart:
150+
151+
```{ojs}
152+
//| label: import-chart
153+
timeSeriesChart = new TimeSeriesChart.default({
154+
target: document.querySelector("#chart")
155+
})
156+
```
157+
158+
:::{#chart}
159+
:::
160+
161+
And there we go! And now to send our data to it:
162+
163+
```{ojs}
164+
//| label: update-chart-data
165+
timeSeriesChart.data = finalData.objects()
166+
```
167+
168+
This chart also takes an additional prop: `colourScheme` can be either `cool` or `warm` (`cool` is the default). Let's set that depending on whether we're looking at daytime or nighttime temperatures:
169+
170+
171+
```{ojs}
172+
//| label: update-chart-colours
173+
timeSeriesChart.colourScheme = selectedVariable == "tmax" ? "warm" : "cool"
174+
```

0 commit comments

Comments
 (0)