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