• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blue-marble / gridpath / 18472848239

13 Oct 2025 04:55PM UTC coverage: 89.12% (+0.08%) from 89.043%
18472848239

push

github

web-flow
GridPath v2025.9.0

Merge pull request #1305 from blue-marble/develop

107 of 126 new or added lines in 19 files covered. (84.92%)

2 existing lines in 1 file now uncovered.

27711 of 31094 relevant lines covered (89.12%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.97
/viz/dispatch_plot.py
1
# Copyright 2016-2025 Blue Marble Analytics LLC.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
#     http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14

15
"""
1✔
16
Make results dispatch plot for a specified zone/stage/set of timepoints
17
"""
18

19
# TODO: adjust x-axis for timepoint duration? (assumes 1h now) - could
20
#  use timestamp from inputs_temporal instead and create x-axis automatically
21
#  using built-in datestime libraries?
22
# TODO: create a database table with technologies and colors for each tech
23
#   note: currently technology more narrowly defined than tech color (latter
24
#   includes curtailment etc.)
25
# TODO: okay to default stage to 1 for cases with only one stage? Need to
26
#   make sure this is aligned with SQL tables (default value for column)
27
#   and data validation
28

29

30
from argparse import ArgumentParser
1✔
31
from bokeh.models import ColumnDataSource, Legend, NumeralTickFormatter
1✔
32
from bokeh.plotting import figure
1✔
33
from bokeh.models.tools import HoverTool
1✔
34
from bokeh.embed import json_item
1✔
35
from bokeh.palettes import cividis
1✔
36

37
import pandas as pd
1✔
38
import sys
1✔
39

40
# GridPath modules
41
from db.common_functions import connect_to_database
1✔
42
from gridpath.auxiliary.db_interface import get_scenario_id_and_name
1✔
43
from viz.common_functions import (
1✔
44
    show_hide_legend,
45
    show_plot,
46
    get_parent_parser,
47
    get_tech_colors,
48
    get_tech_plotting_order,
49
    get_unit,
50
)
51

52

53
def create_parser():
1✔
54
    parser = ArgumentParser(add_help=True, parents=[get_parent_parser()])
1✔
55
    parser.add_argument(
1✔
56
        "--scenario_id",
57
        help="The scenario ID. Required if " "no --scenario is specified.",
58
    )
59
    parser.add_argument(
1✔
60
        "--scenario",
61
        help="The scenario name. Required if " "no --scenario_id is specified.",
62
    )
63
    parser.add_argument(
1✔
64
        "--load_zone",
65
        required=True,
66
        type=str,
67
        help="The name of the load zone. Required.",
68
    )
69
    parser.add_argument(
1✔
70
        "--starting_tmp",
71
        default=None,
72
        type=int,
73
        help="The starting timepoint. Defaults to None (" "first timepoint)",
74
    )
75
    parser.add_argument(
1✔
76
        "--ending_tmp",
77
        default=None,
78
        type=int,
79
        help="The ending timepoint. Defaults to None (" "last timepoint)",
80
    )
81
    parser.add_argument(
1✔
82
        "--stage", default=1, type=int, help="The stage ID. Defaults to 1."
83
    )
84
    parser.add_argument(
1✔
85
        "--weather_iteration", default=0, type=int, help="Defaults to 0."
86
    )
87
    parser.add_argument("--hydro_iteration", default=0, type=int, help="Defaults to 0.")
1✔
88
    parser.add_argument(
1✔
89
        "--availability_iteration", default=0, type=int, help="Defaults to 0."
90
    )
91

92
    return parser
1✔
93

94

95
def parse_arguments(arguments):
1✔
96
    """
97

98
    :return:
99
    """
100
    parser = create_parser()
1✔
101
    parsed_arguments = parser.parse_args(args=arguments)
1✔
102

103
    return parsed_arguments
1✔
104

105

106
def get_timepoints(conn, scenario_id, starting_tmp=None, ending_tmp=None, stage_id=1):
1✔
107
    """
108
    Note: assumes timepoints are ordered!
109
    :param conn:
110
    :param scenario_id:
111
    :param starting_tmp:
112
    :param ending_tmp:
113
    :param stage_id:
114
    :return:
115
    """
116

117
    if starting_tmp is None:
1✔
118
        start_query = ""
×
119
    else:
120
        start_query = "AND timepoint >= {}".format(starting_tmp)
1✔
121

122
    if ending_tmp is None:
1✔
123
        end_query = ""
×
124
    else:
125
        end_query = "AND timepoint <= {}".format(ending_tmp)
1✔
126

127
    query = """SELECT timepoint
1✔
128
        FROM inputs_temporal
129
        INNER JOIN
130
        (SELECT temporal_scenario_id FROM scenarios WHERE scenario_id = {})
131
        USING (temporal_scenario_id)
132
        WHERE stage_id = {}
133
        {}
134
        {}
135
        ;""".format(
136
        scenario_id, stage_id, start_query, end_query
137
    )
138

139
    tmps = [i[0] for i in conn.execute(query).fetchall()]
1✔
140

141
    return tmps
1✔
142

143

144
def get_power_by_tech_results(
1✔
145
    conn,
146
    scenario_id,
147
    load_zone,
148
    weather_iteration,
149
    hydro_iteration,
150
    availability_iteration,
151
    stage,
152
    timepoints,
153
):
154
    """
155
    Get results for power by technology for a given load_zone and set of
156
    points.
157
    :param conn:
158
    :param scenario_id:
159
    :param load_zone:
160
    :param weather_iteration:
161
    :param hydro_iteration:
162
    :param availability_iteration:
163
    :param stage:
164
    :param timepoints
165
    :return:
166
    """
167

168
    # Power by technology
169
    timepoints_str = ",".join(["?"] * len(timepoints))
1✔
170
    query = f"""SELECT timepoint, technology, power_mw
1✔
171
        FROM results_project_dispatch_by_technology
172
        WHERE scenario_id = {scenario_id}
173
        AND load_zone = '{load_zone}'
174
        AND weather_iteration = {weather_iteration}
175
        AND hydro_iteration = {hydro_iteration}
176
        AND availability_iteration = {availability_iteration}
177
        AND stage_id = {stage}
178
        AND timepoint IN ({timepoints_str})
179
        ;"""
180

181
    df = pd.read_sql(query, conn, params=timepoints)
1✔
182
    if not df.empty:
1✔
183
        df = df.pivot(index="timepoint", columns="technology")["power_mw"]
1✔
184
        return df
1✔
185
    # If the dataframe was empty, we still need to send the timepoint index
186
    # downstream
187
    else:
188
        index_only_df = pd.DataFrame(index=timepoints)
×
189
        return index_only_df
×
190

191

192
def get_variable_curtailment_results(
1✔
193
    c,
194
    scenario_id,
195
    load_zone,
196
    weather_iteration,
197
    hydro_iteration,
198
    availability_iteration,
199
    stage,
200
    timepoints,
201
):
202
    """
203
    Get variable generator curtailment for a given load_zone and set of
204
    timepoints.
205
    :param c:
206
    :param scenario_id:
207
    :param load_zone:
208
    :param weather_iteration:
209
    :param hydro_iteration:
210
    :param availability_iteration:
211
    :param stage:
212
    :param timepoints:
213
    :return:
214
    """
215
    query = f"""SELECT scheduled_curtailment_mw
1✔
216
            FROM results_project_curtailment_variable_periodagg
217
            WHERE scenario_id = {scenario_id}
218
            AND load_zone = '{load_zone}'
219
            AND weather_iteration = {weather_iteration}
220
            AND hydro_iteration = {hydro_iteration}
221
            AND availability_iteration = {availability_iteration}
222
            AND stage_id = {stage}
223
            AND timepoint IN ({",".join(["?"] * len(timepoints))})
224
            ;"""
225

226
    curtailment = [i[0] for i in c.execute(query, timepoints).fetchall()]
1✔
227

228
    return curtailment
1✔
229

230

231
def get_hydro_curtailment_results(
1✔
232
    c,
233
    scenario_id,
234
    load_zone,
235
    weather_iteration,
236
    hydro_iteration,
237
    availability_iteration,
238
    stage,
239
    timepoints,
240
):
241
    """
242
    Get conventional hydro curtailment for a given load_zone and set of
243
    timepoints.
244
    :param scenario_id:
245
    :param load_zone:
246
    :param weather_iteration:
247
    :param hydro_iteration:
248
    :param availability_iteration:
249
    :param stage:
250
    :param timepoints:
251
    :return:
252
    """
253
    query = f"""SELECT scheduled_curtailment_mw
1✔
254
            FROM results_project_curtailment_hydro_periodagg
255
            WHERE scenario_id = {scenario_id}
256
            AND load_zone = '{load_zone}'
257
            AND weather_iteration = {weather_iteration}
258
            AND hydro_iteration = {hydro_iteration}
259
            AND availability_iteration = {availability_iteration}
260
            AND stage_id = {stage}
261
            AND timepoint IN ({",".join(["?"] * len(timepoints))})
262
            ;"""
263

264
    curtailment = [i[0] for i in c.execute(query, timepoints).fetchall()]
1✔
265

266
    return curtailment
1✔
267

268

269
def get_imports_exports_results(
1✔
270
    c,
271
    scenario_id,
272
    load_zone,
273
    weather_iteration,
274
    hydro_iteration,
275
    availability_iteration,
276
    stage,
277
    timepoints,
278
):
279
    """
280
    Get imports/exports results for a given load_zone and set of timepoints.
281
    :param c:
282
    :param scenario_id:
283
    :param load_zone:
284
    :param weather_iteration:
285
    :param hydro_iteration:
286
    :param availability_iteration:
287
    :param stage:
288
    :param timepoints:
289
    :return:
290
    """
291
    query = f"""SELECT net_imports_mw
1✔
292
        FROM results_system_load_zone_timepoint
293
        WHERE scenario_id = {scenario_id}
294
        AND load_zone = '{load_zone}'
295
        AND weather_iteration = {weather_iteration}
296
        AND hydro_iteration = {hydro_iteration}
297
        AND availability_iteration = {availability_iteration}
298
        AND stage_id = {stage}
299
        AND timepoint IN ({",".join(["?"] * len(timepoints))})
300
        ;"""
301

302
    net_imports = c.execute(query, timepoints).fetchall()
1✔
303

304
    # None values should only happen if the transmission feature was not
305
    # included
306
    imports = [0 if i[0] is None else i[0] if i[0] > 0 else 0 for i in net_imports]
1✔
307
    exports = [0 if e[0] is None else -e[0] if e[0] < 0 else 0 for e in net_imports]
1✔
308

309
    return imports, exports
1✔
310

311

312
def get_market_participation_results(
1✔
313
    c,
314
    scenario_id,
315
    load_zone,
316
    weather_iteration,
317
    hydro_iteration,
318
    availability_iteration,
319
    stage,
320
    timepoints,
321
):
322
    """
323
    Get market participation results for a given load_zone and set of timepoints.
324
    :param c:
325
    :param scenario_id:
326
    :param load_zone:
327
    :param weather_iteration:
328
    :param hydro_iteration:
329
    :param availability_iteration:
330
    :param stage:
331
    :param timepoints:
332
    :return:
333
    """
334
    query = f"""SELECT net_market_purchases_mw
1✔
335
        FROM results_system_load_zone_timepoint
336
        WHERE scenario_id = {scenario_id}
337
            AND load_zone = '{load_zone}'
338
            AND weather_iteration = {weather_iteration}
339
            AND hydro_iteration = {hydro_iteration}
340
            AND availability_iteration = {availability_iteration}
341
            AND stage_id = {stage}
342
            AND timepoint IN ({",".join(["?"] * len(timepoints))})
343
            ;"""
344

345
    market_participation = c.execute(query, timepoints).fetchall()
1✔
346

347
    sales = []
1✔
348
    purchases = []
1✔
349
    for i in market_participation:
1✔
350
        if i[0] is None:  # markets feature not included
1✔
351
            sales.append(0)
1✔
352
            purchases.append(0)
1✔
353
        else:
354
            if i[0] < 0:
×
355
                sales.append(-i[0])
×
356
                purchases.append(0)
×
357
            elif i[0] > 0:
×
358
                sales.append(0)
×
359
                purchases.append(i[0])
×
360
            else:
361
                sales.append(0)
×
362
                purchases.append(0)
×
363

364
    return sales, purchases
1✔
365

366

367
def get_load(
1✔
368
    c,
369
    scenario_id,
370
    load_zone,
371
    weather_iteration,
372
    hydro_iteration,
373
    availability_iteration,
374
    stage,
375
    timepoints,
376
):
377
    """
378

379
    :param c:
380
    :param scenario_id:
381
    :param load_zone:
382
    :param weather_iteration:
383
    :param hydro_iteration:
384
    :param availability_iteration:
385
    :param stage:
386
    :param timepoints
387
    :return:
388
    """
389

390
    query = f"""SELECT static_load_mw, unserved_energy_mw
1✔
391
        FROM results_system_load_zone_timepoint
392
        WHERE scenario_id = {scenario_id}
393
        AND load_zone = '{load_zone}'
394
        AND weather_iteration = {weather_iteration}
395
        AND hydro_iteration = {hydro_iteration}
396
        AND availability_iteration = {availability_iteration}
397
        AND stage_id = {stage}
398
        AND timepoint IN ({",".join(["?"] * len(timepoints))})
399
        ;"""
400

401
    load_balance = c.execute(query, timepoints).fetchall()
1✔
402

403
    load = [i[0] for i in load_balance]
1✔
404
    unserved_energy = [i[1] for i in load_balance]
1✔
405

406
    return load, unserved_energy
1✔
407

408

409
def get_plotting_data(
1✔
410
    conn,
411
    scenario_id,
412
    load_zone,
413
    weather_iteration,
414
    hydro_iteration,
415
    availability_iteration,
416
    starting_tmp,
417
    ending_tmp,
418
    stage,
419
    **kwargs,
420
):
421
    """
422
    Get the dispatch data by timepoint and technology for a given
423
    scenario/load_zone/set of timepoints/stage.
424

425
    **kwargs needed, so that an error isn't thrown when calling this
426
    function with extra arguments from the UI.
427

428
    :param conn:
429
    :param scenario_id:
430
    :param load_zone:
431
    :param weather_iteration:
432
    :param hydro_iteration:
433
    :param availability_iteration:
434
    :param starting_tmp:
435
    :param ending_tmp:
436
    :param stage:
437
    :return:
438
    """
439

440
    c = conn.cursor()
1✔
441

442
    # Get the relevant timepoints
443
    timepoints = get_timepoints(conn, scenario_id, starting_tmp, ending_tmp, stage)
1✔
444

445
    # Get dispatch by technology
446
    # TODO: Let tech order depend on specified order in database table.
447
    #  Storage might be tricky because we manipulate it!
448
    df = get_power_by_tech_results(
1✔
449
        conn=conn,
450
        scenario_id=scenario_id,
451
        load_zone=load_zone,
452
        stage=stage,
453
        timepoints=timepoints,
454
        weather_iteration=weather_iteration,
455
        hydro_iteration=hydro_iteration,
456
        availability_iteration=availability_iteration,
457
    )
458

459
    # Add x axis
460
    # TODO: assumes hourly timepoints for now, make it flexible instead
461
    df["x"] = range(0, len(df))
1✔
462

463
    # Split storage into charging and discharging and aggregate storage charging
464
    # Assume any dispatch that is negative is storage charging
465
    df["Storage_Charging"] = 0
1✔
466
    stor_techs = df.columns[(df < 0).any()]
1✔
467
    for tech in stor_techs:
1✔
468
        df["Storage_Charging"] += -df[tech].clip(upper=0)
×
469
        df[tech] = df[tech].clip(lower=0)
×
470

471
    # Add variable curtailment (if any)
472
    curtailment_variable = get_variable_curtailment_results(
1✔
473
        c=c,
474
        scenario_id=scenario_id,
475
        load_zone=load_zone,
476
        weather_iteration=weather_iteration,
477
        hydro_iteration=hydro_iteration,
478
        availability_iteration=availability_iteration,
479
        stage=stage,
480
        timepoints=timepoints,
481
    )
482
    if curtailment_variable:
1✔
483
        df["Curtailment_Variable"] = curtailment_variable
×
484

485
    # Add hydro curtailment (if any)
486
    curtailment_hydro = get_hydro_curtailment_results(
1✔
487
        c=c,
488
        scenario_id=scenario_id,
489
        load_zone=load_zone,
490
        weather_iteration=weather_iteration,
491
        hydro_iteration=hydro_iteration,
492
        availability_iteration=availability_iteration,
493
        stage=stage,
494
        timepoints=timepoints,
495
    )
496
    if curtailment_hydro:
1✔
497
        df["Curtailment_Hydro"] = curtailment_hydro
×
498

499
    # Add imports and exports (if any)
500
    imports, exports = get_imports_exports_results(
1✔
501
        c=c,
502
        scenario_id=scenario_id,
503
        load_zone=load_zone,
504
        weather_iteration=weather_iteration,
505
        hydro_iteration=hydro_iteration,
506
        availability_iteration=availability_iteration,
507
        stage=stage,
508
        timepoints=timepoints,
509
    )
510
    if imports:
1✔
511
        df["Imports"] = imports
1✔
512
    if exports:
1✔
513
        df["Exports"] = exports
1✔
514

515
    # Add market participation (if any)
516
    market_sales, market_purchases = get_market_participation_results(
1✔
517
        c=c,
518
        scenario_id=scenario_id,
519
        load_zone=load_zone,
520
        weather_iteration=weather_iteration,
521
        hydro_iteration=hydro_iteration,
522
        availability_iteration=availability_iteration,
523
        stage=stage,
524
        timepoints=timepoints,
525
    )
526
    if market_sales:
1✔
527
        df["Market_Sales"] = market_sales
1✔
528
    if market_purchases:
1✔
529
        df["Market_Purchases"] = market_purchases
1✔
530

531
    # Add load
532
    load_balance = get_load(
1✔
533
        c=c,
534
        scenario_id=scenario_id,
535
        load_zone=load_zone,
536
        weather_iteration=weather_iteration,
537
        hydro_iteration=hydro_iteration,
538
        availability_iteration=availability_iteration,
539
        stage=stage,
540
        timepoints=timepoints,
541
    )
542

543
    df["Load"] = load_balance[0]
1✔
544
    df["Unserved_Energy"] = load_balance[1]
1✔
545

546
    # Dataframe for testing without database
547
    # df = pd.DataFrame(
548
    #     data={
549
    #         "unspecified": range(10),
550
    #         "Nuclear": range(10),
551
    #         "Coal": range(10),
552
    #         "CHP": range(10),
553
    #         "Geothermal": range(10),
554
    #         "Biomass": range(10),
555
    #         "Small_Hydro": range(10),
556
    #         "Steam": range(10),
557
    #         "CCGT": range(10),
558
    #         "Hydro": range(10),
559
    #         "Imports": range(10),
560
    #         "Peaker": range(10),
561
    #         "Wind": range(10),
562
    #         "Solar_BTM": range(10),
563
    #         "Solar": range(10),
564
    #         "Pumped_Storage": range(10),
565
    #         "Battery": range(10),
566
    #         "Curtailment_Variable": range(10),
567
    #         "Curtailment_Hydro": range(10),
568
    #         "x": range(10),
569
    #         "Load": range(10),
570
    #         "Exports": range(10),
571
    #         "Pumped_Storage_Charging": [-5] * 10,
572
    #         "Battery_Charging": [-10] * 10
573
    #     }
574
    # )
575

576
    return df
1✔
577

578

579
def create_plot(
1✔
580
    df, title, power_unit, tech_colors={}, tech_plotting_order={}, ylimit=None
581
):
582
    """
583

584
    :param df:
585
    :param title: string, plot title
586
    :param power_unit: string, the unit of power used in the database/model
587
    :param tech_colors: optional dict that maps technologies to colors.
588
        Technologies without a specified color map will use a default palette
589
    :param tech_plotting_order: optional dict that maps technologies to their
590
        plotting order in the stacked bar/area chart.
591
    :param ylimit: float/int, upper limit of y-axis; optional
592
    :return:
593
    """
594

595
    # Re-arrange df according to plotting order
596
    for col in df.columns:
1✔
597
        if col not in tech_plotting_order:
1✔
598
            tech_plotting_order[col] = max(tech_plotting_order.values()) + 1
1✔
599
    df = df.reindex(sorted(df.columns, key=lambda x: tech_plotting_order[x]), axis=1)
1✔
600

601
    # Set up data source
602
    source = ColumnDataSource(data=df)
1✔
603

604
    # Determine column types for plotting, legend and colors
605
    # Order of stacked_cols will define order of stacked areas in chart
606
    all_cols = list(df.columns)
1✔
607
    x_col = "x"
1✔
608
    # We'll need to remove the following from the stacked columns (will be
609
    # treated as lines instead)
610
    # Note: the order of this list determines the order of the 'Load + ...'
611
    # lines
612
    items_treated_as_load_plus_lines = ["Storage_Charging", "Exports", "Market_Sales"]
1✔
613
    line_cols_storage_sum_track = [
1✔
614
        "Load",
615
    ] + items_treated_as_load_plus_lines
616
    stacked_cols = [
1✔
617
        c for c in all_cols if c not in line_cols_storage_sum_track + [x_col]
618
    ]
619

620
    # Set up color scheme. Use cividis palette for unspecified colors
621
    unspecified_columns = [c for c in stacked_cols if c not in tech_colors.keys()]
1✔
622
    unspecified_tech_colors = dict(
1✔
623
        zip(unspecified_columns, cividis(len(unspecified_columns)))
624
    )
625
    colors = []
1✔
626
    for tech in stacked_cols:
1✔
627
        if tech in tech_colors:
1✔
628
            colors.append(tech_colors[tech])
1✔
629
        else:
630
            colors.append(unspecified_tech_colors[tech])
1✔
631

632
    # Set up the figure
633
    plot = figure(
1✔
634
        min_width=800,
635
        min_height=500,
636
        tools=["pan", "reset", "zoom_in", "zoom_out", "save", "help"],
637
        title=title,
638
        # sizing_mode="scale_both"
639
    )
640

641
    # Add stacked area chart to plot
642
    area_renderers = plot.vbar_stack(
1✔
643
        stackers=stacked_cols,
644
        x=x_col,
645
        source=source,
646
        color=colors,
647
        width=1,
648
    )
649
    # Note: can easily change vbar_stack to varea_stack by replacing the plot
650
    # function and removing the width argument. However, hovertools don't work
651
    # with varea_stack.
652

653
    # Add load line chart to plot
654
    load_renderer = plot.line(
1✔
655
        x=df[x_col], y=df["Load"], line_color="black", line_width=2, name="Load"
656
    )
657

658
    # Keep track of legend items and load renderers
659
    legend_items = [
1✔
660
        (x, [area_renderers[i]]) for i, x in enumerate(stacked_cols) if df[x].mean() > 0
661
    ] + [("Load", [load_renderer])]
662
    load_renderers = [load_renderer]
1✔
663

664
    # Add 'Load + ...' lines
665
    active_items = []
1✔
666
    for i in ["Storage_Charging", "Exports", "Market_Sales"]:
1✔
667
        if i not in df.columns:
1✔
NEW
668
            inactive = True
×
669
        else:
670
            inactive = (df[i] == 0).all()
1✔
671
        if not inactive:
1✔
NEW
672
            active_items.append(i)
×
673

674
    previously_processed_items = []
1✔
675
    for active_item in active_items:
1✔
NEW
676
        line_cols_storage_sum_track = (
×
677
            ["Load"] + previously_processed_items + [active_item]
678
        )
679
        # Add export line to plot
NEW
680
        label = " + ".join(str(x) for x in line_cols_storage_sum_track)
×
681
        exports_renderer = plot.line(
×
682
            x=df[x_col],
683
            y=df[line_cols_storage_sum_track].sum(axis=1),
684
            line_color="black",
685
            line_width=len(line_cols_storage_sum_track),
686
            line_dash=(
687
                "dashed" if (len(line_cols_storage_sum_track) % 2) == 0 else "dotted"
688
            ),
689
            name=label,
690
        )
691
        legend_items.append((label, [exports_renderer]))
×
692
        load_renderers.append(exports_renderer)
×
693

NEW
694
        previously_processed_items += [active_item]
×
695

696
    # Add Legend
697
    legend = Legend(items=legend_items)
1✔
698
    plot.add_layout(legend, "right")
1✔
699
    plot.legend[0].items.reverse()  # Reverse legend to match stacked order
1✔
700
    plot.legend.click_policy = "hide"  # Add interactivity to the legend
1✔
701
    # Note: Doesn't rescale the graph down, simply hides the area
702
    # Note2: There's currently no way to auto-size legend based on graph size(?)
703
    # except for maybe changing font size automatically?
704
    show_hide_legend(plot=plot)  # Hide legend on double click
1✔
705

706
    # Format Axes (labels, number formatting, range, etc.)
707
    plot.xaxis.axis_label = "Hour Ending"
1✔
708
    plot.yaxis.axis_label = "Dispatch ({})".format(power_unit)
1✔
709
    plot.yaxis.formatter = NumeralTickFormatter(format="0,0")
1✔
710
    if ylimit is not None:
1✔
711
        plot.y_range.end = ylimit  # will be ignored if ylimit is None
×
712

713
    # Add HoverTools for stacked bars/areas
714
    for r in area_renderers:
1✔
715
        power_source = r.name
1✔
716
        hover = HoverTool(
1✔
717
            tooltips=[
718
                ("Hour Ending", "@x"),
719
                ("Source", power_source),
720
                ("Dispatch", "@%s{0,0} %s" % (power_source, power_unit)),
721
            ],
722
            renderers=[r],
723
            visible=False,
724
        )
725
        plot.add_tools(hover)
1✔
726

727
    # Add HoverTools for load lines
728
    for r in load_renderers:
1✔
729
        load_type = r.name
1✔
730
        hover = HoverTool(
1✔
731
            tooltips=[
732
                ("Hour Ending", "@x"),
733
                (load_type, "@y{0,0} %s" % power_unit),
734
            ],
735
            renderers=[r],
736
            visible=False,
737
        )
738
        plot.add_tools(hover)
1✔
739

740
    return plot, source
1✔
741

742

743
def main(args=None):
1✔
744
    """
745
    Parse the arguments, get the data in a df, and create the plot
746

747
    :return: if requested, return the plot as JSON object
748
    """
749
    if args is None:
1✔
750
        args = sys.argv[1:]
×
751
    parsed_args = parse_arguments(arguments=args)
1✔
752

753
    conn = connect_to_database(db_path=parsed_args.database)
1✔
754
    c = conn.cursor()
1✔
755

756
    scenario_id, scenario = get_scenario_id_and_name(
1✔
757
        scenario_id_arg=parsed_args.scenario_id,
758
        scenario_name_arg=parsed_args.scenario,
759
        c=c,
760
        script="dispatch_plot",
761
    )
762

763
    tech_colors = get_tech_colors(c)
1✔
764
    tech_plotting_order = get_tech_plotting_order(c)
1✔
765
    power_unit = get_unit(c, "power")
1✔
766

767
    plot_title = "{}Dispatch Plot - {} - Stage {} - Timepoints {}-{}".format(
1✔
768
        "{} - ".format(scenario) if parsed_args.scenario_name_in_title else "",
769
        parsed_args.load_zone,
770
        parsed_args.stage,
771
        parsed_args.starting_tmp,
772
        parsed_args.ending_tmp,
773
    )
774
    plot_name = "dispatchPlot-{}-{}-{}-{}".format(
1✔
775
        parsed_args.load_zone,
776
        parsed_args.stage,
777
        parsed_args.starting_tmp,
778
        parsed_args.ending_tmp,
779
    )
780

781
    df = get_plotting_data(
1✔
782
        conn=conn,
783
        scenario_id=scenario_id,
784
        load_zone=parsed_args.load_zone,
785
        starting_tmp=parsed_args.starting_tmp,
786
        ending_tmp=parsed_args.ending_tmp,
787
        stage=parsed_args.stage,
788
        weather_iteration=parsed_args.weather_iteration,
789
        hydro_iteration=parsed_args.hydro_iteration,
790
        availability_iteration=parsed_args.availability_iteration,
791
    )
792

793
    conn.close()
1✔
794

795
    plot, source = create_plot(
1✔
796
        df=df,
797
        title=plot_title,
798
        power_unit=power_unit,
799
        tech_colors=tech_colors,
800
        tech_plotting_order=tech_plotting_order,
801
        ylimit=parsed_args.ylimit,
802
    )
803

804
    # Show plot in HTML browser file if requested
805
    if parsed_args.show:
1✔
806
        show_plot(
×
807
            plot=plot,
808
            plot_name=plot_name,
809
            plot_write_directory=parsed_args.plot_write_directory,
810
            scenario=scenario,
811
            source=source,
812
        )
813

814
    # Return plot in json format if requested
815
    if parsed_args.return_json:
1✔
816
        return json_item(plot, "plotHTMLTarget")
×
817

818

819
if __name__ == "__main__":
1✔
820
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc