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

blue-marble / gridpath / 13015608658

24 Jan 2025 11:49PM UTC coverage: 88.844% (-0.002%) from 88.846%
13015608658

push

github

anamileva
Make dispatch plot work with iterations

16 of 16 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

25754 of 28988 relevant lines covered (88.84%)

2.66 hits per line

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

80.1
/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
"""
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
3✔
31
from bokeh.models import ColumnDataSource, Legend, NumeralTickFormatter
3✔
32
from bokeh.plotting import figure
3✔
33
from bokeh.models.tools import HoverTool
3✔
34
from bokeh.embed import json_item
3✔
35
from bokeh.palettes import cividis
3✔
36

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

40
# GridPath modules
41
from db.common_functions import connect_to_database
3✔
42
from gridpath.auxiliary.db_interface import get_scenario_id_and_name
3✔
43
from viz.common_functions import (
3✔
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():
3✔
54
    parser = ArgumentParser(add_help=True, parents=[get_parent_parser()])
3✔
55
    parser.add_argument(
3✔
56
        "--scenario_id",
57
        help="The scenario ID. Required if " "no --scenario is specified.",
58
    )
59
    parser.add_argument(
3✔
60
        "--scenario",
61
        help="The scenario name. Required if " "no --scenario_id is specified.",
62
    )
63
    parser.add_argument(
3✔
64
        "--load_zone",
65
        required=True,
66
        type=str,
67
        help="The name of the load zone. Required.",
68
    )
69
    parser.add_argument(
3✔
70
        "--starting_tmp",
71
        default=None,
72
        type=int,
73
        help="The starting timepoint. Defaults to None (" "first timepoint)",
74
    )
75
    parser.add_argument(
3✔
76
        "--ending_tmp",
77
        default=None,
78
        type=int,
79
        help="The ending timepoint. Defaults to None (" "last timepoint)",
80
    )
81
    parser.add_argument(
3✔
82
        "--stage", default=1, type=int, help="The stage ID. Defaults to 1."
83
    )
84
    parser.add_argument(
3✔
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.")
3✔
88
    parser.add_argument(
3✔
89
        "--availability_iteration", default=0, type=int, help="Defaults to 0."
90
    )
91

92
    return parser
3✔
93

94

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

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

103
    return parsed_arguments
3✔
104

105

106
def get_timepoints(conn, scenario_id, starting_tmp=None, ending_tmp=None, stage_id=1):
3✔
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:
3✔
118
        start_query = ""
×
119
    else:
120
        start_query = "AND timepoint >= {}".format(starting_tmp)
3✔
121

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

127
    query = """SELECT timepoint
3✔
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()]
3✔
140

141
    return tmps
3✔
142

143

144
def get_power_by_tech_results(
3✔
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))
3✔
170
    query = f"""SELECT timepoint, technology, power_mw
3✔
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)
3✔
182
    if not df.empty:
3✔
183
        df = df.pivot(index="timepoint", columns="technology")["power_mw"]
3✔
184
        return df
3✔
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(
3✔
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
3✔
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()]
3✔
227

228
    return curtailment
3✔
229

230

231
def get_hydro_curtailment_results(
3✔
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
3✔
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()]
3✔
265

266
    return curtailment
3✔
267

268

269
def get_imports_exports_results(
3✔
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
3✔
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()
3✔
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]
3✔
307
    exports = [0 if e[0] is None else -e[0] if e[0] < 0 else 0 for e in net_imports]
3✔
308

309
    return imports, exports
3✔
310

311

312
def get_market_participation_results(
3✔
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
3✔
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()
3✔
346

347
    sales = []
3✔
348
    purchases = []
3✔
349
    for i in market_participation:
3✔
350
        if i[0] is None:  # markets feature not included
3✔
351
            sales.append(0)
3✔
352
            purchases.append(0)
3✔
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
3✔
365

366

367
def get_load(
3✔
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
3✔
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()
3✔
402

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

406
    return load, unserved_energy
3✔
407

408

409
def get_plotting_data(
3✔
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()
3✔
441

442
    # Get the relevant timepoints
443
    timepoints = get_timepoints(conn, scenario_id, starting_tmp, ending_tmp, stage)
3✔
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(
3✔
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))
3✔
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
3✔
466
    stor_techs = df.columns[(df < 0).any()]
3✔
467
    for tech in stor_techs:
3✔
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(
3✔
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:
3✔
UNCOV
483
        df["Curtailment_Variable"] = curtailment_variable
×
484

485
    # Add hydro curtailment (if any)
486
    curtailment_hydro = get_hydro_curtailment_results(
3✔
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:
3✔
497
        df["Curtailment_Hydro"] = curtailment_hydro
×
498

499
    # Add imports and exports (if any)
500
    imports, exports = get_imports_exports_results(
3✔
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:
3✔
511
        df["Imports"] = imports
3✔
512
    if exports:
3✔
513
        df["Exports"] = exports
3✔
514

515
    # Add market participation (if any)
516
    market_sales, market_purchases = get_market_participation_results(
3✔
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:
3✔
527
        df["Market_Sales"] = market_sales
3✔
528
    if market_purchases:
3✔
529
        df["Market_Purchases"] = market_purchases
3✔
530

531
    # Add load
532
    load_balance = get_load(
3✔
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]
3✔
544
    df["Unserved_Energy"] = load_balance[1]
3✔
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
3✔
577

578

579
def create_plot(
3✔
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:
3✔
597
        if col not in tech_plotting_order:
3✔
598
            tech_plotting_order[col] = max(tech_plotting_order.values()) + 1
3✔
599
    df = df.reindex(sorted(df.columns, key=lambda x: tech_plotting_order[x]), axis=1)
3✔
600

601
    # Set up data source
602
    source = ColumnDataSource(data=df)
3✔
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)
3✔
607
    x_col = "x"
3✔
608
    # TODO: remove hard-coding?
609
    line_cols_storage_sum_track = [
3✔
610
        "Load",
611
        "Exports",
612
        "Storage_Charging",
613
        "Market_Sales",
614
    ]
615
    stacked_cols = [
3✔
616
        c for c in all_cols if c not in line_cols_storage_sum_track + [x_col]
617
    ]
618

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

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

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

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

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

663
    # Add 'Load + ...' lines
664
    if "Exports" not in df.columns:
3✔
665
        inactive_exports = True
×
666
    else:
667
        inactive_exports = (df["Exports"] == 0).all()
3✔
668

669
    if "Market_Sales" not in df.columns:
3✔
670
        inactive_markets = True
×
671
    else:
672
        inactive_markets = (df["Market_Sales"] == 0).all()
3✔
673

674
    inactive_storage = (df["Storage_Charging"] == 0).all()
3✔
675

676
    if inactive_exports and inactive_markets:
3✔
677
        line_cols_storage_sum_track = ["Load", "Storage_Charging"]
3✔
678
    if not inactive_exports and inactive_markets:
3✔
679
        line_cols_storage_sum_track = ["Load", "Exports", "Storage_Charging"]
×
680
        # Add export line to plot
681
        label = "Load + Exports"
×
682
        exports_renderer = plot.line(
×
683
            x=df[x_col],
684
            y=df[["Load", "Exports"]].sum(axis=1),
685
            line_color="black",
686
            line_width=2,
687
            line_dash="dashed",
688
            name=label,
689
        )
690
        legend_items.append((label, [exports_renderer]))
×
691
        load_renderers.append(exports_renderer)
×
692
    if not inactive_exports and not inactive_markets:
3✔
693
        line_cols_storage_sum_track = [
×
694
            "Load",
695
            "Exports",
696
            "Market_Sales",
697
            "Storage_Charging",
698
        ]
699
        # Add export and market lines to plot
700
        label = "Load + Exports + Market Sales"
×
701
        exports_renderer = plot.line(
×
702
            x=df[x_col],
703
            y=df[["Load", "Exports", "Market_Sales"]].sum(axis=1),
704
            line_color="black",
705
            line_width=3,
706
            line_dash="dashed",
707
            name=label,
708
        )
709
        legend_items.append((label, [exports_renderer]))
×
710
        load_renderers.append(exports_renderer)
×
711
    if inactive_exports and not inactive_markets:
3✔
712
        line_cols_storage_sum_track = ["Load", "Storage_Charging", "Market_Sales"]
×
713
        # Add export line to plot
714
        label = "Load + Market Sales"
×
715
        exports_renderer = plot.line(
×
716
            x=df[x_col],
717
            y=df[["Load", "Market_Sales"]].sum(axis=1),
718
            line_color="black",
719
            line_width=2,
720
            line_dash="dashed",
721
            name=label,
722
        )
723
        legend_items.append((label, [exports_renderer]))
×
724
        load_renderers.append(exports_renderer)
×
725

726
    if not inactive_storage:
3✔
727
        # Add storage line to plot
728
        label = legend_items[-1][0] + " + Storage Charging"
×
729
        stor_renderer = plot.line(
×
730
            x=df[x_col],
731
            y=df[line_cols_storage_sum_track].sum(axis=1),
732
            line_color="black",
733
            line_width=2,
734
            line_dash="dotted",
735
            name=label,
736
        )
737
        legend_items.append((label, [stor_renderer]))
×
738
        load_renderers.append(stor_renderer)
×
739

740
    # Add Legend
741
    legend = Legend(items=legend_items)
3✔
742
    plot.add_layout(legend, "right")
3✔
743
    plot.legend[0].items.reverse()  # Reverse legend to match stacked order
3✔
744
    plot.legend.click_policy = "hide"  # Add interactivity to the legend
3✔
745
    # Note: Doesn't rescale the graph down, simply hides the area
746
    # Note2: There's currently no way to auto-size legend based on graph size(?)
747
    # except for maybe changing font size automatically?
748
    show_hide_legend(plot=plot)  # Hide legend on double click
3✔
749

750
    # Format Axes (labels, number formatting, range, etc.)
751
    plot.xaxis.axis_label = "Hour Ending"
3✔
752
    plot.yaxis.axis_label = "Dispatch ({})".format(power_unit)
3✔
753
    plot.yaxis.formatter = NumeralTickFormatter(format="0,0")
3✔
754
    plot.y_range.end = ylimit  # will be ignored if ylimit is None
3✔
755

756
    # Add HoverTools for stacked bars/areas
757
    for r in area_renderers:
3✔
758
        power_source = r.name
3✔
759
        hover = HoverTool(
3✔
760
            tooltips=[
761
                ("Hour Ending", "@x"),
762
                ("Source", power_source),
763
                ("Dispatch", "@%s{0,0} %s" % (power_source, power_unit)),
764
            ],
765
            renderers=[r],
766
            toggleable=False,
767
        )
768
        plot.add_tools(hover)
3✔
769

770
    # Add HoverTools for load lines
771
    for r in load_renderers:
3✔
772
        load_type = r.name
3✔
773
        hover = HoverTool(
3✔
774
            tooltips=[
775
                ("Hour Ending", "@x"),
776
                (load_type, "@y{0,0} %s" % power_unit),
777
            ],
778
            renderers=[r],
779
            toggleable=False,
780
        )
781
        plot.add_tools(hover)
3✔
782

783
    return plot
3✔
784

785

786
def main(args=None):
3✔
787
    """
788
    Parse the arguments, get the data in a df, and create the plot
789

790
    :return: if requested, return the plot as JSON object
791
    """
792
    if args is None:
3✔
793
        args = sys.argv[1:]
×
794
    parsed_args = parse_arguments(arguments=args)
3✔
795

796
    conn = connect_to_database(db_path=parsed_args.database)
3✔
797
    c = conn.cursor()
3✔
798

799
    scenario_id, scenario = get_scenario_id_and_name(
3✔
800
        scenario_id_arg=parsed_args.scenario_id,
801
        scenario_name_arg=parsed_args.scenario,
802
        c=c,
803
        script="dispatch_plot",
804
    )
805

806
    tech_colors = get_tech_colors(c)
3✔
807
    tech_plotting_order = get_tech_plotting_order(c)
3✔
808
    power_unit = get_unit(c, "power")
3✔
809

810
    plot_title = "{}Dispatch Plot - {} - Stage {} - Timepoints {}-{}".format(
3✔
811
        "{} - ".format(scenario) if parsed_args.scenario_name_in_title else "",
812
        parsed_args.load_zone,
813
        parsed_args.stage,
814
        parsed_args.starting_tmp,
815
        parsed_args.ending_tmp,
816
    )
817
    plot_name = "dispatchPlot-{}-{}-{}-{}".format(
3✔
818
        parsed_args.load_zone,
819
        parsed_args.stage,
820
        parsed_args.starting_tmp,
821
        parsed_args.ending_tmp,
822
    )
823

824
    df = get_plotting_data(
3✔
825
        conn=conn,
826
        scenario_id=scenario_id,
827
        load_zone=parsed_args.load_zone,
828
        starting_tmp=parsed_args.starting_tmp,
829
        ending_tmp=parsed_args.ending_tmp,
830
        stage=parsed_args.stage,
831
        weather_iteration=parsed_args.weather_iteration,
832
        hydro_iteration=parsed_args.hydro_iteration,
833
        availability_iteration=parsed_args.availability_iteration,
834
    )
835

836
    plot = create_plot(
3✔
837
        df=df,
838
        title=plot_title,
839
        power_unit=power_unit,
840
        tech_colors=tech_colors,
841
        tech_plotting_order=tech_plotting_order,
842
        ylimit=parsed_args.ylimit,
843
    )
844

845
    # Show plot in HTML browser file if requested
846
    if parsed_args.show:
3✔
847
        show_plot(
×
848
            plot=plot,
849
            plot_name=plot_name,
850
            plot_write_directory=parsed_args.plot_write_directory,
851
            scenario=scenario,
852
        )
853

854
    # Return plot in json format if requested
855
    if parsed_args.return_json:
3✔
856
        return json_item(plot, "plotHTMLTarget")
×
857

858

859
if __name__ == "__main__":
3✔
860
    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