Skip to content

Commit 6a71167

Browse files
Initial makie migration
1 parent a2776e0 commit 6a71167

File tree

4 files changed

+394
-0
lines changed

4 files changed

+394
-0
lines changed

Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4242
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
4343
ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea"
4444

45+
[weakdeps]
46+
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
47+
48+
[extensions]
49+
MakieExt = "Makie"
50+
4551
[compat]
4652
BlackBoxOptim = "0.5, 0.6"
4753
Bootstrap = "2"

ext/MakieExt/MakieExt.jl

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
module MakieExt
2+
3+
using Statistics
4+
using DataFrames
5+
using Dates
6+
7+
using Streamfall
8+
import Streamfall: TemporalCrossSection
9+
10+
using Makie
11+
12+
13+
function Streamfall.viz.quickplot(node::NetworkNode)
14+
fig = lines(node.outflow)
15+
return fig
16+
end
17+
18+
function Streamfall.viz.quickplot(node::NetworkNode, climate::Climate)
19+
date = timesteps(climate)
20+
21+
@assert length(date) == length(node.outflow) || "Date length and result lengths do not match!"
22+
23+
f, ax, sp = lines(date, node.outflow)
24+
25+
return f
26+
end
27+
28+
function Streamfall.viz.quickplot(
29+
obs::Vector, node::NetworkNode, climate::Climate;
30+
label="Modeled", log=false, burn_in=1, limit=nothing, metric=Streamfall.mKGE
31+
)
32+
return quickplot(obs, node.outflow, climate; label, log, burn_in=burn_in, limit=limit, metric=metric)
33+
end
34+
35+
function Streamfall.viz.quickplot(
36+
obs::DataFrame, node::NetworkNode, climate::Climate;
37+
label="", log=false, burn_in=1, limit=nothing, metric=Streamfall.mKGE
38+
)
39+
return Streamfall.viz.quickplot(obs[:, node.name], node.outflow, climate; label, log, burn_in, limit, metric)
40+
end
41+
42+
function Streamfall.viz.quickplot(
43+
obs::Vector, sim::Vector, climate::Climate;
44+
label="Modeled", log=false, burn_in=1, limit=nothing, metric=Streamfall.mKGE
45+
)
46+
date = timesteps(climate)
47+
last_e = !isnothing(limit) ? limit : lastindex(obs)
48+
show_range = burn_in:last_e
49+
return Streamfall.viz.quickplot(obs[show_range], sim[show_range], date[show_range], label, log; metric=metric)
50+
end
51+
function Streamfall.viz.quickplot(obs::Vector, sim::Vector, xticklabels::Vector, label="Modeled", log=false; metric=Streamfall.mKGE)
52+
@assert length(xticklabels) == length(obs) || "x-axis tick label length and observed lengths do not match!"
53+
@assert length(xticklabels) == length(sim) || "x-axis tick label length and simulated lengths do not match!"
54+
55+
score = round(metric(obs, sim), digits=4)
56+
metric_name = String(Symbol(metric))
57+
58+
if log
59+
# Add small constant in case of 0-flow
60+
obs = copy(obs)
61+
sim = copy(sim)
62+
obs[obs .== 0.0] .+= 1e-4
63+
sim[sim .== 0.0] .+= 1e-4
64+
end
65+
66+
scale = log == false ? identity : log10
67+
68+
f = Figure(size=(850,400))
69+
flow_ax = Axis(f[1,1]; xlabel="Date", ylabel="Streamflow", yscale=scale)
70+
qq_ax = Axis(f[1,2]; xlabel="Observed", ylabel="Modeled", xscale=scale, yscale=scale)
71+
72+
label = "$(label) ($(metric_name): $(score))"
73+
lines!(flow_ax, xticklabels, obs, label="Observed")
74+
lines!(flow_ax, xticklabels, sim; label=label, alpha=0.5)
75+
76+
qqplot!(
77+
qq_ax, obs, sim;
78+
qqline=:identity, strokewidth=0.01, strokecolor=(:blue, 0.01), # legend=false, strokewidth=0.03, strokealpha=0.1,
79+
markercolor=(:blue, 0.02)
80+
)
81+
leg = Legend(f[2, 1:2], flow_ax; orientation=:horizontal)
82+
83+
return f
84+
end
85+
86+
87+
using LaTeXStrings
88+
89+
"""
90+
temporal_cross_section(
91+
dates, obs;
92+
title="",
93+
ylabel="Mean",
94+
label=nothing,
95+
period=monthday,
96+
kwargs...
97+
)
98+
99+
Provides indication of temporal variation and uncertainty across time, grouped by `period`.
100+
101+
Notes:
102+
Assumes daily data.
103+
Filters out leap days.
104+
105+
# Arguments
106+
- `dates` : Date of each observation
107+
- `obs` : observed data
108+
- `title` : Plot title
109+
- `ylabel` : Optional replacement ylabel. Uses name of `func` if not provided.
110+
- `period` : Method from `Dates` package to group (defaults to `monthday`)
111+
- `kwargs` : Additional plotting keyword arguments
112+
"""
113+
function Streamfall.viz.temporal_cross_section(
114+
dates, obs;
115+
title="", ylabel="Mean", label=nothing,
116+
period::Function=monthday,
117+
kwargs...
118+
)
119+
if isnothing(label)
120+
label = ylabel
121+
end
122+
123+
arg_keys = keys(kwargs)
124+
format_func = y -> y
125+
logscale = [:log, :log10]
126+
tmp = nothing
127+
128+
xsect_res = TemporalCrossSection(dates, obs, period)
129+
130+
# TODO: This is currently broken
131+
if :yscale in arg_keys || :yaxis in arg_keys
132+
tmp = (:yscale in arg_keys) ? kwargs[:yscale] : kwargs[:yaxis]
133+
134+
if tmp in logscale
135+
log_obs = symlog(copy(obs))
136+
format_func = y -> (y != 0) ? L"%$(Int(round(sign(y)) * 10))^{%$(round(abs(y), digits=1))}" : L"0"
137+
log_xsect_res = TemporalCrossSection(dates, log_obs, period)
138+
target = log_xsect_res.cross_section
139+
else
140+
target = xsect_res.cross_section
141+
end
142+
else
143+
target = xsect_res.cross_section
144+
end
145+
146+
x_section = target.mean
147+
lower_75 = target.lower_75
148+
upper_75 = target.upper_75
149+
lower_95 = target.lower_95
150+
upper_95 = target.upper_95
151+
152+
sp = target.subperiod
153+
xlabels = join.(sp, "-")
154+
155+
# Calculate statistics for legend
156+
xs_mean = xsect_res.cross_section.mean
157+
m_ind = round(mean(xs_mean), digits=2)
158+
sd_ind = round(std(xs_mean), digits=2)
159+
wr75_m_ind = round(xsect_res.mean_75, digits=2)
160+
wr75_sd_ind = round(xsect_res.std_75, digits=2)
161+
wr95_m_ind = round(xsect_res.mean_95, digits=2)
162+
wr95_sd_ind = round(xsect_res.std_95, digits=2)
163+
164+
# Create figure
165+
fig = Figure(size=(800, 600))
166+
ax = Axis(
167+
fig[1, 1],
168+
xlabel=string(nameof(period)),
169+
ylabel=ylabel,
170+
title=title
171+
)
172+
173+
# Plot confidence intervals and median line
174+
x = 1:length(xlabels)
175+
176+
# 95% CI
177+
b95 = band!(
178+
ax, x, lower_95, upper_95,
179+
color=(:lightblue, 0.3)
180+
)
181+
182+
# 75% CI
183+
b75 = band!(
184+
ax, x, lower_75, upper_75,
185+
color=(:lightblue, 0.5)
186+
)
187+
188+
# Median line
189+
med_line = lines!(
190+
ax, x, x_section,
191+
color=:black
192+
)
193+
194+
# Set x-axis labels
195+
if period == monthday
196+
ax.xticks = (1:14:length(xlabels), xlabels[1:14:end])
197+
elseif period == yearmonth
198+
ax.xticks = (1:12:length(xlabels), xlabels[1:12:end])
199+
end
200+
201+
ax.xticklabelrotation = π/4
202+
203+
# Apply log scale if specified
204+
if !isnothing(tmp) && (tmp in logscale)
205+
ax.yscale = log10
206+
ax.formatter = format_func
207+
end
208+
209+
# Add legend at bottom
210+
Legend(
211+
fig[2, 1],
212+
[med_line, b75, b95],
213+
[
214+
"Mean of $(label) μ: $(m_ind), σ: $(sd_ind)",
215+
"CI₇₅ μ: $(wr75_m_ind), σ: $(wr75_sd_ind)",
216+
"CI₉₅ μ: $(wr95_m_ind), σ: $(wr95_sd_ind)"
217+
],
218+
orientation=:horizontal,
219+
tellwidth=false,
220+
tellheight=true
221+
)
222+
223+
# Adjust layout
224+
rowsize!(fig.layout, 1, Relative(0.9))
225+
rowsize!(fig.layout, 2, Relative(0.1))
226+
227+
return fig
228+
end
229+
230+
"""
231+
temporal_cross_section(
232+
dates, obs, sim;
233+
title="", ylabel="Median Error", label=nothing, period::Function=monthday, kwargs...
234+
)
235+
236+
Provides indication of temporal variation and uncertainty across time, grouped by `period`.
237+
238+
Notes:
239+
Assumes daily data.
240+
Filters out leap days.
241+
242+
# Arguments
243+
- `dates` : Date of each observation
244+
- `obs` : Observed data
245+
- `sim` : Simulated data
246+
- `title` : Optional plot title. Blank if not provided.
247+
- `ylabel` : Optional replacement ylabel. Uses name of `period` if not provided.
248+
- `label` : Optional legend label. Uses `ylabel` if not provided.
249+
- `period` : Method from `Dates` package to group (defaults to `month`)
250+
"""
251+
function Streamfall.viz.temporal_cross_section(
252+
dates, obs, sim;
253+
title="", ylabel="Median Error", label=nothing, period::Function=monthday, kwargs...
254+
)
255+
if isnothing(label)
256+
label = ylabel
257+
end
258+
259+
arg_keys = keys(kwargs)
260+
format_func = y -> y
261+
logscale = [:log, :log10]
262+
tmp = nothing
263+
264+
xsect_res = TemporalCrossSection(dates, obs, sim, period)
265+
target = xsect_res.cross_section
266+
267+
# TODO: This is currently broken
268+
if :yscale in arg_keys || :yaxis in arg_keys
269+
tmp = (:yscale in arg_keys) ? kwargs[:yscale] : kwargs[:yaxis]
270+
271+
if tmp in logscale
272+
log_obs = symlog(copy(obs))
273+
log_sim = symlog(copy(sim))
274+
275+
# Format function for y-axis tick labels (e.g., 10^x)
276+
format_func = y -> (y != 0) ? L"%$(Int(round(sign(y)) * 10))^{%$(round(abs(y), digits=1))}" : L"0"
277+
278+
log_xsect_res = TemporalCrossSection(dates, log_obs, log_sim, period)
279+
target = log_xsect_res.cross_section
280+
end
281+
end
282+
283+
x_section = target.median
284+
lower_75 = target.lower_75
285+
upper_75 = target.upper_75
286+
lower_95 = target.lower_95
287+
upper_95 = target.upper_95
288+
289+
sp = target.subperiod
290+
xlabels = join.(sp, "-")
291+
292+
xsect_df = xsect_res.cross_section
293+
294+
# Calculate statistics for legend
295+
med = xsect_df.median
296+
m_ind = round(mean(med), digits=2)
297+
sd_ind = round(std(med), digits=2)
298+
wr75_m_ind = round(xsect_res.mean_75, digits=2)
299+
wr75_sd_ind = round(xsect_res.std_75, digits=2)
300+
wr95_m_ind = round(xsect_res.mean_95, digits=2)
301+
wr95_sd_ind = round(xsect_res.std_95, digits=2)
302+
303+
x = 1:length(xlabels)
304+
305+
# Create figure
306+
fig = Figure(size=(800, 600))
307+
ax = Axis(
308+
fig[1, 1],
309+
xlabel=string(nameof(period)),
310+
ylabel=ylabel,
311+
title=title
312+
)
313+
314+
# Plot confidence intervals and median line
315+
# 95% CI
316+
b95 = band!(
317+
ax, x, lower_95, upper_95,
318+
color=(:lightblue, 0.3),
319+
label="CI₉₅ μ: $(wr95_m_ind), σ: $(wr95_sd_ind)"
320+
)
321+
322+
# 75% CI
323+
b75 = band!(
324+
ax, x, lower_75, upper_75,
325+
color=(:lightblue, 0.5),
326+
label="CI₇₅ μ: $(wr75_m_ind), σ: $(wr75_sd_ind)"
327+
)
328+
329+
# Median line
330+
med_line = lines!(
331+
ax, x, x_section,
332+
color=:black,
333+
label="$(label) μ: $(m_ind), σ: $(sd_ind)"
334+
)
335+
336+
# Set x-axis labels
337+
if period == monthday
338+
ax.xticks = (1:14:length(xlabels), xlabels[1:14:end])
339+
elseif period == yearmonth
340+
ax.xticks = (1:12:length(xlabels), xlabels[1:12:end])
341+
end
342+
343+
ax.xticklabelrotation = π/4
344+
345+
# Apply log scale if specified
346+
if !isnothing(tmp) && (tmp in logscale)
347+
ax.yscale = log10
348+
ax.formatter = format_func
349+
end
350+
351+
# Add legend at bottom
352+
Legend(
353+
fig[2, 1],
354+
[med_line, b75, b95],
355+
[
356+
"$(label) μ: $(m_ind), σ: $(sd_ind)",
357+
"CI₇₅ μ: $(wr75_m_ind), σ: $(wr75_sd_ind)",
358+
"CI₉₅ μ: $(wr95_m_ind), σ: $(wr95_sd_ind)"
359+
],
360+
orientation=:horizontal,
361+
tellwidth=false,
362+
tellheight=true,
363+
framevisible=false
364+
) # Equivalent to transparent background
365+
366+
# Adjust layout
367+
rowsize!(fig.layout, 1, Relative(0.9))
368+
rowsize!(fig.layout, 2, Relative(0.1))
369+
370+
# Process any additional keyword arguments
371+
for (key, value) in kwargs
372+
if key [:yscale, :yaxis] # Skip already processed arguments
373+
setproperty!(ax, key, value)
374+
end
375+
end
376+
377+
return fig
378+
end
379+
380+
end

0 commit comments

Comments
 (0)