1+ using Plots, StatsPlots
2+ using Plots. Measures
3+ import Plots: plot, plot!
4+ using DataFrames, Dates, Statistics, Distributions, LaTeXStrings
5+ import Bootstrap: bootstrap, BalancedSampling
6+
7+ import . Analysis: TemporalCrossSection
8+
9+
10+ function quickplot (node:: NetworkNode )
11+ fig = plot (node. outflow)
12+ return fig
13+ end
14+
15+
16+ function quickplot (node:: NetworkNode , climate:: Climate )
17+ date = timesteps (climate)
18+
19+ @assert length (date) == length (node. outflow) || " Date length and result lengths do not match!"
20+
21+ fig = plot (date, node. outflow)
22+
23+ return fig
24+ end
25+ function quickplot (obs, node:: NetworkNode , climate:: Climate , label= " " , log= false ; burn_in= 1 , limit= nothing , metric= Streamfall. mKGE)
26+ return quickplot (obs, node. outflow, climate, label, log; burn_in= burn_in, limit= limit, metric= metric)
27+ end
28+ function quickplot (obs:: DataFrame , sim:: Vector , climate:: Climate , label= " " , log= false ; burn_in= 1 , limit= nothing , metric= Streamfall. mKGE)
29+ return quickplot (Matrix (obs[:, Not (" Date" )])[:, 1 ], sim, climate, label, log; burn_in, limit, metric)
30+ end
31+ function quickplot (obs:: Vector , sim:: Vector , climate:: Climate , label= " " , log= false ; burn_in= 1 , limit= nothing , metric= Streamfall. mKGE)
32+ date = timesteps (climate)
33+ last_e = ! isnothing (limit) ? limit : lastindex (obs)
34+ show_range = burn_in: last_e
35+ return quickplot (obs[show_range], sim[show_range], date[show_range], label, log; metric= metric)
36+ end
37+ function quickplot (obs:: Vector , sim:: Vector , xticklabels:: Vector , label= " Modeled" , log= false ; metric= Streamfall. mKGE)
38+ @assert length (xticklabels) == length (obs) || " x-axis tick label length and observed lengths do not match!"
39+ @assert length (xticklabels) == length (sim) || " x-axis tick label length and simulated lengths do not match!"
40+
41+ score = round (metric (obs, sim), digits= 4 )
42+ metric_name = String (Symbol (metric))
43+
44+ if log
45+ # Add small constant in case of 0-flow
46+ obs = obs .+ 1e-2
47+ sim = sim .+ 1e-2
48+ end
49+
50+ label = " $(label) ($(metric_name) : $(score) )"
51+ fig = plot (
52+ xticklabels, obs,
53+ label= " Observed" ,
54+ legend= :best ,
55+ ylabel= " Streamflow" ,
56+ xlabel= " Date" ,
57+ fg_legend= :transparent ,
58+ bg_legend= :transparent
59+ )
60+ plot! (xticklabels, sim, label= label, alpha= 0.5 )
61+
62+ if log
63+ # modify yaxis
64+ yaxis! (fig, :log10 )
65+ end
66+
67+ qqfig = qqplot (
68+ obs, sim,
69+ legend= false , markerstrokewidth= 0.03 , markerstrokealpha= 0.1 , markeralpha= 0.2 ,
70+ xlabel= " Observed" , ylabel= " Modeled"
71+ )
72+
73+ if log
74+ xaxis! (qqfig, :log10 )
75+ yaxis! (qqfig, :log10 )
76+ end
77+
78+ combined = plot (fig, qqfig, size= (1000 , 500 ), left_margin= 10 mm, bottom_margin= 5 mm, layout= (1 , 2 ))
79+
80+ return combined
81+ end
82+
83+
84+ """
85+ plot_residuals(obs::Array, sim::Array; xlabel="", ylabel="", title="")
86+
87+ Plot residual between two sequences.
88+
89+ # Arguments
90+ - x : x-axis data
91+ - y : y-axis data
92+ - xlabel : x-axis label
93+ - ylabel : y-axis label
94+ - title : title text
95+ """
96+ function plot_residuals (x:: Array , y:: Array ; xlabel= " " , ylabel= " " , title= " " )
97+ # 1:1 Plot
98+ fig_1to1 = scatter (x, y, legend= false ,
99+ markerstrokewidth= 0 , markerstrokealpha= 0 , alpha= 0.2 )
100+ plot! (x, y, color= :red , markersize= 0.1 , markerstrokewidth= 0 ,
101+ xlabel= xlabel, ylabel= ylabel, title= title)
102+
103+ return fig_1to1
104+ end
105+
106+
107+ """ Symmetrical log values.
108+
109+ https://kar.kent.ac.uk/32810/2/2012_Bi-symmetric-log-transformation_v5.pdf
110+ https://discourse.julialang.org/t/symmetrical-log-plot/45709/3
111+ """
112+ function symlog (y)
113+ return sign .(y) .* log10 .(1.0 .+ abs .(y))
114+ end
115+
116+
117+ """
118+ temporal_cross_section(dates, obs; ylabel=nothing, period::Function=monthday)
119+
120+ Provides indication of temporal variation and uncertainty across time, grouped by `period`.
121+
122+ Notes:
123+ Assumes daily data.
124+ Filters out leap days.
125+
126+ # Arguments
127+ - dates : Date of each observation
128+ - obs : observed data
129+ - ylabel : Optional replacement ylabel. Uses name of `func` if not provided.
130+ - `period::Function` : Method from `Dates` package to group (defaults to `monthday`)
131+ """
132+ function temporal_cross_section (dates, obs;
133+ title= " " , ylabel= " ME" , label= nothing ,
134+ period:: Function = monthday,
135+ kwargs... ) # show_extremes::Bool=false,
136+ if isnothing (label)
137+ label = ylabel
138+ end
139+
140+ arg_keys = keys (kwargs)
141+ format_func = y -> y
142+ logscale = [:log , :log10 ]
143+ tmp = nothing
144+
145+ xsect_res = TemporalCrossSection (dates, obs, period)
146+
147+ if :yscale in arg_keys || :yaxis in arg_keys
148+ tmp = (:yscale in arg_keys) ? kwargs[:yscale ] : kwargs[:yaxis ]
149+
150+ if tmp in logscale
151+ log_obs = symlog (copy (obs))
152+
153+ # Format function for y-axis tick labels (e.g., 10^x)
154+ format_func = y -> (y != 0 ) ? L " %$(Int(round(sign(y)) * 10))^{%$(round(abs(y), digits=1))}" : L " 0"
155+
156+ log_xsect_res = TemporalCrossSection (dates, log_obs, period)
157+ target = log_xsect_res. cross_section
158+ else
159+ target = xsect_res. cross_section
160+ end
161+ else
162+ target = xsect_res. cross_section
163+ end
164+
165+ x_section = target. median
166+ lower_75 = target. lower_75
167+ upper_75 = target. upper_75
168+ lower_95 = target. lower_95
169+ upper_95 = target. upper_95
170+
171+ sp = target. subperiod
172+ xlabels = join .(sp, " -" )
173+
174+ if ! isnothing (tmp) & (tmp in logscale)
175+ # Remove keys so later plotting methods do not error
176+ kwargs = Dict (kwargs)
177+ delete! (kwargs, :yscale )
178+ delete! (kwargs, :yaxis )
179+ end
180+
181+ # Display indicator values using original data instead of log-transformed data
182+ med = xsect_res. cross_section. median
183+ m_ind = round (mean (med), digits= 2 )
184+ sd_ind = round (std (med), digits= 2 )
185+
186+ wr75_m_ind = round (xsect_res. mean_75, digits= 2 )
187+ wr75_sd_ind = round (xsect_res. std_75, digits= 2 )
188+ wr95_m_ind = round (xsect_res. mean_95, digits= 2 )
189+ wr95_sd_ind = round (xsect_res. std_95, digits= 2 )
190+
191+ fig = plot (xlabels, lower_95, fillrange= upper_95, color= " lightblue" , alpha= 0.3 , label= " CI₉₅ μ: $(wr95_m_ind) , σ: $(wr95_sd_ind) " , linealpha= 0 )
192+ plot! (fig, xlabels, lower_75, fillrange= upper_75, color= " lightblue" , alpha= 0.5 , label= " CI₇₅ μ: $(wr75_m_ind) , σ: $(wr75_sd_ind) " , linealpha= 0 )
193+ plot! (
194+ fig, xlabels, x_section,
195+ label= " Mean of $(label) μ: $(m_ind) , σ: $(sd_ind) " ,
196+ color= " black" ,
197+ xlabel= nameof (period),
198+ ylabel= ylabel,
199+ legend= :bottomleft ,
200+ legendfont= Plots. font (10 ),
201+ fg_legend= :transparent ,
202+ bg_legend= :transparent ,
203+ left_margin= 5 mm,
204+ bottom_margin= 5 mm,
205+ title= title,
206+ yformatter= format_func;
207+ kwargs...
208+ )
209+
210+ # if show_extremes
211+ # scatter!(fig, xlabels, min_section, label="", alpha=0.5, color="lightblue", markerstrokewidth=0; kwargs...)
212+ # scatter!(fig, xlabels, max_section, label="", alpha=0.5, color="lightblue", markerstrokewidth=0; kwargs...)
213+ # end
214+
215+ return fig
216+ end
217+
218+
219+ """
220+ temporal_cross_section(
221+ dates, obs, sim;
222+ title="", ylabel="Median Error", label=nothing, period::Function=monthday, kwargs...
223+ )
224+
225+ Provides indication of temporal variation and uncertainty across time, grouped by `period`.
226+
227+ Notes:
228+ Assumes daily data.
229+ Filters out leap days.
230+
231+ # Arguments
232+ - `dates` : Date of each observation
233+ - `obs` : Observed data
234+ - `sim` : Simulated data
235+ - `title` : Optional plot title. Blank if not provided.
236+ - `ylabel` : Optional replacement ylabel. Uses name of `period` if not provided.
237+ - `label` : Optional legend label. Uses `ylabel` if not provided.
238+ - `period` : Method from `Dates` package to group (defaults to `month`)
239+ """
240+ function temporal_cross_section (
241+ dates, obs, sim;
242+ title= " " , ylabel= " Median Error" , label= nothing , period:: Function = monthday, kwargs...
243+ ) # show_extremes::Bool=false,
244+ if isnothing (label)
245+ label = ylabel
246+ end
247+
248+ arg_keys = keys (kwargs)
249+ format_func = y -> y
250+ logscale = [:log , :log10 ]
251+ tmp = nothing
252+
253+ xsect_res = TemporalCrossSection (dates, obs, sim, period)
254+ target = xsect_res. cross_section
255+
256+ if :yscale in arg_keys || :yaxis in arg_keys
257+ tmp = (:yscale in arg_keys) ? kwargs[:yscale ] : kwargs[:yaxis ]
258+
259+ if tmp in logscale
260+ log_obs = symlog (copy (obs))
261+ log_sim = symlog (copy (sim))
262+
263+ # Format function for y-axis tick labels (e.g., 10^x)
264+ format_func = y -> (y != 0 ) ? L " %$(Int(round(sign(y)) * 10))^{%$(round(abs(y), digits=1))}" : L " 0"
265+
266+ log_xsect_res = TemporalCrossSection (dates, log_obs, log_sim, period)
267+ target = log_xsect_res. cross_section
268+ end
269+ end
270+
271+ x_section = target. median
272+ lower_75 = target. lower_75
273+ upper_75 = target. upper_75
274+ lower_95 = target. lower_95
275+ upper_95 = target. upper_95
276+
277+ sp = target. subperiod
278+ xlabels = join .(sp, " -" )
279+
280+ if ! isnothing (tmp) & (tmp in logscale)
281+ # Remove keys so later plotting methods do not error
282+ kwargs = Dict (kwargs)
283+ delete! (kwargs, :yscale )
284+ delete! (kwargs, :yaxis )
285+ end
286+
287+ xsect_df = xsect_res. cross_section
288+
289+ # Display indicator values using original data instead of log-transformed data
290+ med = xsect_df. median
291+ m_ind = round (mean (med), digits= 2 )
292+ sd_ind = round (std (med), digits= 2 )
293+
294+ wr75_m_ind = round (xsect_res. mean_75, digits= 2 )
295+ wr75_sd_ind = round (xsect_res. std_75, digits= 2 )
296+ wr95_m_ind = round (xsect_res. mean_95, digits= 2 )
297+ wr95_sd_ind = round (xsect_res. std_95, digits= 2 )
298+
299+ fig = plot (xlabels, lower_95, fillrange= upper_95, color= " lightblue" , alpha= 0.3 , label= " CI₉₅ μ: $(wr95_m_ind) , σ: $(wr95_sd_ind) " , linealpha= 0 )
300+ plot! (fig, xlabels, lower_75, fillrange= upper_75, color= " lightblue" , alpha= 0.5 , label= " CI₇₅ μ: $(wr75_m_ind) , σ: $(wr75_sd_ind) " , linealpha= 0 )
301+ plot! (
302+ fig, xlabels, x_section,
303+ label= " $(label) μ: $(m_ind) , σ: $(sd_ind) " ,
304+ color= " black" ,
305+ xlabel= nameof (period),
306+ ylabel= ylabel,
307+ legend= :bottomleft ,
308+ legendfont= Plots. font (10 ),
309+ fg_legend= :transparent ,
310+ bg_legend= :transparent ,
311+ left_margin= 5 mm,
312+ bottom_margin= 5 mm,
313+ title= title,
314+ yformatter= format_func;
315+ kwargs...
316+ )
317+
318+ # if show_extremes
319+ # scatter!(fig, xlabels, min_section, label="", alpha=0.5, color="lightblue", markerstrokewidth=0; kwargs...)
320+ # scatter!(fig, xlabels, max_section, label="", alpha=0.5, color="lightblue", markerstrokewidth=0; kwargs...)
321+ # end
322+
323+ return fig
324+ end
325+
326+ function plot (node:: NetworkNode , climate:: Climate )
327+ return plot (
328+ timesteps (climate),
329+ node. outflow,
330+ label= node. name,
331+ xlabel= " Date" ,
332+ ylabel= " Outflows" ,
333+ )
334+ end
335+
336+ function plot! (node:: NetworkNode , climate:: Climate )
337+ plot! (
338+ timesteps (climate),
339+ node. outflow,
340+ label= node. name
341+ )
342+ end
343+ function plot! (f, node:: NetworkNode , climate:: Climate )
344+ plot! (f, timesteps (climate), node. outflow, label= node. name)
345+ end
346+
347+ function plot (node:: DamNode , climate:: Climate )
348+ levels = plot (timesteps (climate), node. level, xlabel= " Date" , ylabel= " Dam Level" , label= node. name)
349+ outflows = plot (timesteps (climate), node. outflow, xlabel= " Date" , ylabel= " Discharge" , label= node. name)
350+
351+ combined = plot (levels, outflows, size= (1000 , 400 ), left_margin= 10 mm, bottom_margin= 5 mm, layout= (1 , 2 ))
352+
353+ return combined
354+ end
0 commit comments