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

sabvdf / datasim / 15344616636

30 May 2025 10:21AM UTC coverage: 59.629% (-2.6%) from 62.179%
15344616636

push

github

sabvdf
Fixes all CI tests and adds support files for using uv instead of pip.

16 of 25 new or added lines in 8 files covered. (64.0%)

337 existing lines in 8 files now uncovered.

836 of 1402 relevant lines covered (59.63%)

0.6 hits per line

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

70.18
/datasim/plot.py
1
from abc import ABC
1✔
2
from typing import Any, Final, List, Optional
1✔
3
from pandas import DataFrame
1✔
4
from plotly.graph_objs._figure import Figure
1✔
5
from plotly.subplots import make_subplots
1✔
6
from plotly.colors import convert_colors_to_same_type, unlabel_rgb
1✔
7

8
import numpy as np
1✔
9
import plotly.express as px
1✔
10
from webcolors import name_to_rgb
1✔
11

12
from .dashboard import Dashboard
1✔
13
from .entity import Entity
1✔
14
from .queue import Queue
1✔
15
from .resource import Resource
1✔
16
from .types import PlotOptions, PlotType
1✔
17

18

19
class PlotData(ABC):
1✔
20
    """Abstract superclass of different types of data to plot."""
21

22
    world: Final
1✔
23
    trace: Optional[Figure] = None
1✔
24
    plot: Optional[Any] = None
1✔
25
    plot_index: Optional[int] = 0
1✔
26
    options: PlotOptions
1✔
27

28
    def __init__(
1✔
29
        self,
30
        world,
31
        options: PlotOptions = PlotOptions(),
32
    ):
33
        """Create a data source to plot from.
34

35
        Args:
36
            plot_type ("scatter", "line", "bar", "pie"): Type of plot to render.
37
            title (str, optional): Title to use over the plot. Defaults to None.
38
        """
39
        self.world = world
1✔
40

41
        if options.legend_x == "":
1✔
42
            options.legend_x = "x"
1✔
43
        if options.legend_y == "":
1✔
44
            options.legend_y = "y"
1✔
45
        if options.plot_type is None:
1✔
46
            options.plot_type = PlotType.line
1✔
47
        self.options = options
1✔
48

49
        self._buffer_size = max(10000, self.world.end_tick)
1✔
50
        self._buffer_index = 0
1✔
51
        self._x_buffer = np.zeros(self._buffer_size)
1✔
52
        self._y_buffer = np.zeros(self._buffer_size)
1✔
53

54
    @property
1✔
55
    def _data_frame(self):
1✔
56
        return DataFrame(
1✔
57
            {
58
                self.options.legend_x: self._x_buffer[: self._buffer_index],
59
                self.options.legend_y: self._y_buffer[: self._buffer_index],
60
            }
61
        )
62

63
    def _update_trace(self):
1✔
64
        match self.options.plot_type:
1✔
65
            case PlotType.bar:
1✔
66
                self.trace = px.bar(
×
67
                    self._data_frame,
68
                    title=self.options.title,
69
                    x=self.options.legend_x,
70
                    y=self.options.legend_y,
71
                    color=self.options.color,
72
                    color_continuous_scale=self.options.color_continuous_scale,
73
                    color_continuous_midpoint=self.options.color_continuous_midpoint,
74
                    color_discrete_map=self.options.color_discrete_map,
75
                    color_discrete_sequence=self.options.color_discrete_sequence,
76
                    range_color=self.options.range_color,
77
                    hover_name=self.options.hover_name,
78
                    hover_data=self.options.hover_data,
79
                    custom_data=self.options.custom_data,
80
                    text=self.options.text,
81
                    facet_row=self.options.facet_row,
82
                    facet_col=self.options.facet_col,
83
                    facet_row_spacing=self.options.facet_row_spacing,
84
                    facet_col_spacing=self.options.facet_col_spacing,
85
                    facet_col_wrap=self.options.facet_col_wrap,
86
                    error_x=self.options.error_x,
87
                    error_y=self.options.error_y,
88
                    error_x_minus=self.options.error_x_minus,
89
                    error_y_minus=self.options.error_y_minus,
90
                    category_orders=self.options.category_orders,
91
                    labels=(
92
                        self.options.labels
93
                        or {self.options.legend_y: self.options.name}
94
                        if self.options.name
95
                        else None
96
                    ),
97
                    orientation=self.options.orientation,
98
                    opacity=self.options.opacity,
99
                    log_x=self.options.log_x,
100
                    log_y=self.options.log_y,
101
                    range_x=self.options.range_x,
102
                    range_y=self.options.range_y,
103
                    pattern_shape=self.options.pattern_shape,
104
                    pattern_shape_map=self.options.pattern_shape_map,
105
                    pattern_shape_sequence=self.options.pattern_shape_sequence,
106
                    base=self.options.base,
107
                    barmode=self.options.barmode,
108
                    text_auto=self.options.text_auto,
109
                    template=self.options.template,
110
                    width=self.options.width,
111
                    height=self.options.height,
112
                    animation_frame=self.options.animation_frame,
113
                    animation_group=self.options.animation_group,
114
                )
UNCOV
115
                if self.options.secondary_y:
×
UNCOV
116
                    self.trace.update_yaxes(secondary_y=True)
×
UNCOV
117
                    self.trace.update_traces(yaxis="y2")
×
118
            case PlotType.line:
1✔
119
                self.trace = px.line(
1✔
120
                    self._data_frame,
121
                    title=self.options.title,
122
                    x=self.options.legend_x,
123
                    y=self.options.legend_y,
124
                    color=self.options.color,
125
                    color_discrete_map=self.options.color_discrete_map,
126
                    color_discrete_sequence=self.options.color_discrete_sequence,
127
                    symbol=self.options.symbol,
128
                    symbol_map=self.options.symbol_map,
129
                    symbol_sequence=self.options.symbol_sequence,
130
                    hover_name=self.options.hover_name,
131
                    hover_data=self.options.hover_data,
132
                    custom_data=self.options.custom_data,
133
                    text=self.options.text,
134
                    facet_row=self.options.facet_row,
135
                    facet_col=self.options.facet_col,
136
                    facet_row_spacing=self.options.facet_row_spacing,
137
                    facet_col_spacing=self.options.facet_col_spacing,
138
                    facet_col_wrap=self.options.facet_col_wrap,
139
                    error_x=self.options.error_x,
140
                    error_y=self.options.error_y,
141
                    error_x_minus=self.options.error_x_minus,
142
                    error_y_minus=self.options.error_y_minus,
143
                    category_orders=self.options.category_orders,
144
                    labels=(
145
                        self.options.labels
146
                        or {self.options.legend_y: self.options.name}
147
                        if self.options.name
148
                        else None
149
                    ),
150
                    orientation=self.options.orientation,
151
                    log_x=self.options.log_x,
152
                    log_y=self.options.log_y,
153
                    range_x=self.options.range_x,
154
                    range_y=self.options.range_y,
155
                    render_mode=self.options.render_mode,
156
                    template=self.options.template,
157
                    width=self.options.width,
158
                    height=self.options.height,
159
                    line_dash=self.options.line_dash,
160
                    line_dash_map=self.options.line_dash_map,
161
                    line_dash_sequence=self.options.line_dash_sequence,
162
                    line_group=self.options.line_group,
163
                    line_shape=self.options.line_shape,
164
                    markers=self.options.markers,
165
                    animation_frame=self.options.animation_frame,
166
                    animation_group=self.options.animation_group,
167
                )
168
                if self.options.secondary_y:
1✔
UNCOV
169
                    self.trace.update_yaxes(secondary_y=True)
×
UNCOV
170
                    self.trace.update_traces(yaxis="y2")
×
UNCOV
171
            case PlotType.pie:
×
UNCOV
172
                self.trace = px.pie(
×
173
                    self._data_frame,
174
                    title=self.options.title,
175
                    names=self.options.legend_x,
176
                    values=self.options.legend_y,
177
                    color=self.options.color,
178
                    color_discrete_map=self.options.color_discrete_map,
179
                    color_discrete_sequence=self.options.color_discrete_sequence,
180
                    hover_name=self.options.hover_name,
181
                    hover_data=self.options.hover_data,
182
                    custom_data=self.options.custom_data,
183
                    facet_row=self.options.facet_row,
184
                    facet_col=self.options.facet_col,
185
                    facet_row_spacing=self.options.facet_row_spacing,
186
                    facet_col_spacing=self.options.facet_col_spacing,
187
                    facet_col_wrap=self.options.facet_col_wrap,
188
                    category_orders=self.options.category_orders,
189
                    hole=self.options.hole,
190
                    labels=(
191
                        self.options.labels
192
                        or {self.options.legend_y: self.options.name}
193
                        if self.options.name
194
                        else None
195
                    ),
196
                    opacity=self.options.opacity,
197
                    template=self.options.template,
198
                    width=self.options.width,
199
                    height=self.options.height,
200
                )
UNCOV
201
                if self.options.secondary_y:
×
UNCOV
202
                    self.trace.update_yaxes(secondary_y=True)
×
UNCOV
203
                    self.trace.update_traces(yaxis="y2")
×
UNCOV
204
            case PlotType.scatter:
×
UNCOV
205
                self.trace = px.scatter(
×
206
                    self._data_frame,
207
                    x=self.options.legend_x,
208
                    y=self.options.legend_y,
209
                    title=self.options.title,
210
                    color=self.options.color,
211
                    color_continuous_scale=self.options.color_continuous_scale,
212
                    color_continuous_midpoint=self.options.color_continuous_midpoint,
213
                    color_discrete_map=self.options.color_discrete_map,
214
                    color_discrete_sequence=self.options.color_discrete_sequence,
215
                    range_color=self.options.range_color,
216
                    size=self.options.size,
217
                    size_max=self.options.size_max,
218
                    symbol=self.options.symbol,
219
                    symbol_map=self.options.symbol_map,
220
                    symbol_sequence=self.options.symbol_sequence,
221
                    hover_name=self.options.hover_name,
222
                    hover_data=self.options.hover_data,
223
                    custom_data=self.options.custom_data,
224
                    text=self.options.text,
225
                    facet_row=self.options.facet_row,
226
                    facet_col=self.options.facet_col,
227
                    facet_row_spacing=self.options.facet_row_spacing,
228
                    facet_col_spacing=self.options.facet_col_spacing,
229
                    facet_col_wrap=self.options.facet_col_wrap,
230
                    error_x=self.options.error_x,
231
                    error_y=self.options.error_y,
232
                    error_x_minus=self.options.error_x_minus,
233
                    error_y_minus=self.options.error_y_minus,
234
                    category_orders=self.options.category_orders,
235
                    labels=(
236
                        self.options.labels
237
                        or {self.options.legend_y: self.options.name}
238
                        if self.options.name
239
                        else None
240
                    ),
241
                    orientation=self.options.orientation,
242
                    opacity=self.options.opacity,
243
                    marginal_x=self.options.marginal_x,
244
                    marginal_y=self.options.marginal_y,
245
                    trendline=self.options.trendline,
246
                    trendline_options=self.options.trendline_options,
247
                    trendline_scope=self.options.trendline_scope,
248
                    trendline_color_override=self.options.trendline_color_override,
249
                    log_x=self.options.log_x,
250
                    log_y=self.options.log_y,
251
                    range_x=self.options.range_x,
252
                    range_y=self.options.range_y,
253
                    render_mode=self.options.render_mode,
254
                    template=self.options.template,
255
                    width=self.options.width,
256
                    height=self.options.height,
257
                    animation_frame=self.options.animation_frame,
258
                    animation_group=self.options.animation_group,
259
                )
UNCOV
260
                if self.options.secondary_y:
×
UNCOV
261
                    self.trace.update_yaxes(secondary_y=True)
×
UNCOV
262
                    self.trace.update_traces(yaxis="y2")
×
263

264
    def _tick(self):
1✔
265
        pass
1✔
266

267

268
class XYPlotData(PlotData):
1✔
269
    """Data with x and y values as float."""
270

271
    def __init__(
1✔
272
        self,
273
        world,
274
        data_x: List[float] = [],
275
        data_y: List[float] = [],
276
        options: PlotOptions = PlotOptions(),
277
    ):
278
        """Create a data source from x and y lists of floats.
279

280
        Args:
281
            data_x (List[float], optional): x values. Defaults to [] to start with an empty data set.
282
            data_y (List[float], optional): y values. Defaults to [] to start with an empty data set.
283
            plot_type ("scatter", "line", "bar", "pie", optional): Type of plot to show. Defaults to "line".
284
            title (Optional[str], optional): Title to use over the plot. Defaults to None.
285
        """
286
        super().__init__(world, options)
1✔
287
        self._x_buffer[: len(data_x)] = data_x
1✔
288
        self._y_buffer[: len(data_y)] = data_y
1✔
289

290
    def append(self, x: float, y: float):
1✔
291
        """Add a data point to this data set.
292

293
        Args:
294
            x (float): x value of the data point.
295
            y (float): y value of the data point.
296
        """
297
        if len(self._x_buffer) <= self._buffer_index:
1✔
UNCOV
298
            self._x_buffer = np.append(self._x_buffer, np.zeros(self._buffer_size))
×
UNCOV
299
            self._y_buffer = np.append(self._y_buffer, np.zeros(self._buffer_size))
×
300
        self._x_buffer[self._buffer_index] = x
1✔
301
        self._y_buffer[self._buffer_index] = y
1✔
302
        self._buffer_index += 1
1✔
303

304

305
class CategoryPlotData(PlotData):
1✔
306
    """Data with named categories with float values."""
307

308
    labels: List[str]
1✔
309
    values: List[float]
1✔
310

311
    def __init__(
1✔
312
        self,
313
        world,
314
        data_x: List[str] = [],
315
        data_y: List[float] = [],
316
        options: PlotOptions = PlotOptions(),
317
    ):
318
        """Create a data source from x as categories and y as float values.
319

320
        Args:
321
            data_x (List[str], optional): labels. Defaults to [] to start with an empty data set.
322
            data_y (List[float], optional): values for each label. Defaults to [] to start with an empty data set.
323
            plot_type ("scatter", "line", "bar", "pie", optional): Type of plot to show. Defaults to "line".
324
            title (Optional[str], optional): Title to use over the plot. Defaults to None.
325
        """
UNCOV
326
        if options.legend_x == "":
×
UNCOV
327
            options.legend_x = "category"
×
328
        if options.legend_y == "":
×
329
            options.legend_y = "value"
×
UNCOV
330
        super().__init__(world, options)
×
UNCOV
331
        self.data_x = data_x
×
UNCOV
332
        self.data_y = data_y
×
333

334
    def append(self, label: str, value: float):
1✔
335
        """Add a data point to this data set.
336

337
        Args:
338
            label (str): label of the data point.
339
            value (float): value of the data point.
340
        """
341
        # self.labels.append(label)
342
        # self.values.append(value)
343
        # TODO
344

345

346
class NPPlotData(PlotData):
1✔
347
    """Data with Numpy array as source."""
348

349
    data: np.ndarray
1✔
350

351
    def __init__(
1✔
352
        self,
353
        world,
354
        data: np.ndarray,
355
        options: PlotOptions = PlotOptions(),
356
    ):
357
        """Create a data source from a Numpy array.
358

359
        Args:
360
            data (:class:`np.ndarray`): Array of data points. Shape should correspond to the dimensions of the plot
361
                (2 columns for 2D plots, 3 columns for 3D plots).
362
            plot_type (:class:`PlotType`, optional): Type of plot to show. Defaults to `"line"`.
363
            title (`str`, optional): Title to use over the plot. Defaults to `None`.
364

365
        Raises:
366
            `TypeError`: When trying to take from a capacity resource without specifying an amount.
367
        """
UNCOV
368
        super().__init__(world, options)
×
UNCOV
369
        if data.shape[1] != 2:  # TODO add 3 when adding 3D plots
×
UNCOV
370
            raise ValueError("")
×
UNCOV
371
        self.data = data
×
372

373
    @property
1✔
374
    def _data_frame(self):
1✔
UNCOV
375
        return DataFrame(
×
376
            {
377
                self.options.legend_x: self.data[0],
378
                self.options.legend_y: self.data[1],
379
            }
380
        )
381

382

383
class ResourcePlotData(PlotData):
1✔
384
    """Data source from watching the amount of a :class:`Resource`."""
385

386
    source: Resource
1✔
387
    plot_users: bool
1✔
388
    frequency: int
1✔
389

390
    def __init__(
1✔
391
        self,
392
        world,
393
        source_id: str,
394
        plot_users: bool = False,
395
        frequency: int = 1,
396
        options: PlotOptions = PlotOptions(),
397
    ):
398
        """Create a data source from watching the amount of a :class:`Resource`.
399

400
        Args:
401
            data (:class:`Resource`): Source :class:`Resource`.
402
            frequency (int, optional): Frequency in ticks to add data points.
403
                Defaults to 1, meaning a point gets added every tick.
404
            plot_type ("scatter", "line", "bar", "pie", optional): Type of plot to show. Defaults to "line".
405
            title (Optional[str], optional): Title to use over the plot. Defaults to None.
406
        """
407
        if options.legend_x == "":
1✔
408
            options.legend_x = world.time_unit
1✔
409
        if options.legend_y == "":
1✔
UNCOV
410
            options.legend_y = "amount"
×
411
        super().__init__(world, options)
1✔
412
        self.source = self.world.resource(source_id)
1✔
413
        self.plot_users = plot_users
1✔
414
        self.frequency = frequency
1✔
415

416
    def _tick(self):
1✔
417
        if (self.frequency == 0 and self.source.changed_tick == self.world.ticks) or (
1✔
418
            self.world.ticks % self.frequency == 0
419
        ):
420
            if len(self._x_buffer) <= self._buffer_index:
1✔
UNCOV
421
                self._x_buffer = np.append(self._x_buffer, np.zeros(self._buffer_size))
×
UNCOV
422
                self._y_buffer = np.append(self._y_buffer, np.zeros(self._buffer_size))
×
423
            self._x_buffer[self._buffer_index] = self.world.time
1✔
424
            self._y_buffer[self._buffer_index] = (
1✔
425
                len(self.source.users) if self.plot_users else self.source.amount
426
            )
427
            self._buffer_index += 1
1✔
428

429

430
class QueuePlotData(PlotData):
1✔
431
    """Data source from watching the size of a :class:`Queue`."""
432

433
    source: Queue
1✔
434
    frequency: int
1✔
435

436
    def __init__(
1✔
437
        self,
438
        world,
439
        source_id: str,
440
        frequency: int = 1,
441
        options: PlotOptions = PlotOptions(),
442
    ):
443
        """Create a data source from watching the size of a :class:`Queue`.
444

445
        Args:
446
            data (:class:`Queue`): Source :class:`Queue`.
447
            frequency (int, optional): Frequency in ticks to add data points.
448
                Defaults to 1, meaning a point gets added every tick.
449
            plot_type ("scatter", "line", "bar", "pie", optional): Type of plot to show. Defaults to "line".
450
            title (Optional[str], optional): Title to use over the plot. Defaults to None.
451
        """
452
        if options.legend_x == "":
1✔
453
            options.legend_x = world.time_unit
1✔
454
        if options.legend_y == "":
1✔
455
            options.legend_y = "length"
1✔
456
        super().__init__(world, options)
1✔
457
        self.source = self.world.queue(source_id)
1✔
458
        self.frequency = frequency
1✔
459

460
    def _tick(self):
1✔
461
        if (self.frequency == 0 and self.source.changed_tick == self.world.ticks) or (
1✔
462
            self.world.ticks % self.frequency == 0
463
        ):
464
            if len(self._x_buffer) <= self._buffer_index:
1✔
UNCOV
465
                self._x_buffer = np.append(self._x_buffer, np.zeros(self._buffer_size))
×
UNCOV
466
                self._y_buffer = np.append(self._y_buffer, np.zeros(self._buffer_size))
×
467
            self._x_buffer[self._buffer_index] = self.world.time
1✔
468
            self._y_buffer[self._buffer_index] = len(self.source)
1✔
469
            self._buffer_index += 1
1✔
470

471

472
class StatePlotData(PlotData):
1✔
473
    """Data source from watching the state of an :class:`Entity`."""
474

475
    data: Entity
1✔
476
    frequency: int
1✔
477

478
    def __init__(
1✔
479
        self,
480
        world,
481
        data: Entity,
482
        frequency: int = 1,
483
        options: PlotOptions = PlotOptions(),
484
    ):
485
        """Create a data source from watching the state of an :class:`Entity`.
486

487
        Args:
488
            data (:class:`Entity`): Source :class:`Entity`.
489
            frequency (int, optional): Frequency in ticks to add data points.
490
                Defaults to 1, meaning a point gets added every tick.
491
            plot_type ("scatter", "line", "bar", "pie", optional): Type of plot to show. Defaults to "line".
492
            title (Optional[str], optional): Title to use over the plot. Defaults to None.
493
        """
UNCOV
494
        if options.legend_x == "":
×
UNCOV
495
            options.legend_x = world.time_unit
×
UNCOV
496
        if options.legend_y == "":
×
UNCOV
497
            options.legend_y = "state"
×
UNCOV
498
        if options.plot_type is None:
×
UNCOV
499
            options.plot_type = PlotType.pie
×
UNCOV
500
        super().__init__(world, options)
×
UNCOV
501
        self.data = data
×
UNCOV
502
        self.frequency = frequency
×
UNCOV
503
        self._y_buffer = np.full(self._buffer_size, "")
×
504

505
    def _tick(self):
1✔
UNCOV
506
        if (self.frequency == 0 and self.data.ticks_in_current_state == 1) or (
×
507
            self.world.ticks % self.frequency == 0
508
        ):
UNCOV
509
            if len(self._x_buffer) <= self._buffer_index:
×
UNCOV
510
                self._x_buffer = np.append(self._x_buffer, np.zeros(self._buffer_size))
×
UNCOV
511
                self._y_buffer = np.append(
×
512
                    self._y_buffer, np.full(self._buffer_size, "")
513
                )
UNCOV
514
            self._x_buffer[self._buffer_index] = self.world.time
×
UNCOV
515
            self._y_buffer[self._buffer_index] = self.data.state.name
×
UNCOV
516
            self._buffer_index += 1
×
517

518

519
class Plot:
1✔
520
    """Base class for easily updating data for plots to be made on the dashboard."""
521

522
    world: Final
1✔
523
    id: Final[str]
1✔
524
    title: Optional[str]
1✔
525
    figure: Figure
1✔
526
    data: List[PlotData]
1✔
527
    dashboard: Dashboard
1✔
528

529
    def __init__(self, world, id: str, *args: PlotData):
1✔
530
        """Create a plot to add to the dashboard using `World.add_plot()`.
531

532
        Args:
533
            id (str): identifier, needs to be unique.
534
            *args (:class:`PlotData`): Data to start the plot with.
535
        """
536
        self.world = world
1✔
537
        self.id = id
1✔
538
        self.data = []
1✔
539

540
        dash = self.world.runner.dashboard
1✔
541
        if dash is None:
1✔
UNCOV
542
            return
×
543
        self.dashboard: Dashboard = dash
1✔
544

545
        for arg in args:
1✔
546
            self.add_trace(arg)
1✔
547

548
    def __getitem__(self, key: int) -> PlotData:
1✔
549
        """Get a reference to a data set from this plot.
550

551
        Args:
552
            key (int): Index of the data set.
553

554
        Returns:
555
            tuple(list[float] | list[str], list[float]): Data set at index.
556
        """
UNCOV
557
        return self.data[key]
×
558

559
    def _tick(self):
1✔
560
        for data in self.data:
1✔
561
            data._tick()
1✔
562

563
    def add_trace(self, data: PlotData) -> int:
1✔
564
        """Add a data trace to the plot.
565

566
        Args:
567
            data (PlotData): Data set.
568

569
        Returns:
570
            int: Index of the added data set.
571
        """
572
        if data in self.data:
1✔
UNCOV
573
            return self.data.index(data)
×
574
        data.plot = self
1✔
575
        data.plot_index = len(self.data)
1✔
576
        self.data.append(data)
1✔
577
        return data.plot_index
1✔
578

579
    def _update(self):
1✔
580
        secondary_y = any([data.options.secondary_y for data in self.data])
1✔
581

582
        for plotdata in self.data:
1✔
583
            plotdata._update_trace()
1✔
584
            if plotdata.plot and plotdata.trace:
1✔
585
                if self.id not in self.dashboard.plots:
1✔
586
                    self.dashboard.plots[self.id] = make_subplots(
1✔
587
                        specs=[[{"secondary_y": secondary_y}]]
588
                    )
589
                    self.dashboard.plots[self.id].layout.xaxis.title = (  # type: ignore
1✔
590
                        plotdata.options.legend_x
591
                    )
592

593
        self.dashboard.dataframes[self.id] = DataFrame()
1✔
594
        self.dashboard.dataframe_names[self.id] = (
1✔
595
            self.world.title + f" - {self.world.variation}"
596
            if self.world.variation
597
            else ""
598
        )
599

600
        for plotdata in self.data:
1✔
601
            plotdata._data_frame
1✔
602
            if plotdata.plot and plotdata.trace:
1✔
603
                for data in plotdata.trace["data"]:
1✔
604
                    data["showlegend"] = True  # type: ignore
1✔
605
                    data["name"] = plotdata.options.name  # type: ignore
1✔
606
                    self.dashboard.plots[self.id].add_traces([data])
1✔
607

608
                    dataframe = plotdata._data_frame.copy()
1✔
609
                    dataframe.columns = [self.world.time_unit, plotdata.options.name]
1✔
610
                    if self.dashboard.dataframes[self.id].empty:
1✔
611
                        self.dashboard.dataframes[self.id] = dataframe
1✔
612
                    else:
UNCOV
613
                        self.dashboard.dataframes[self.id] = self.dashboard.dataframes[
×
614
                            self.id
615
                        ].merge(dataframe, on=self.world.time_unit, how="outer")
616

617
                    if plotdata.options.secondary_y:
1✔
UNCOV
618
                        self.dashboard.plots[self.id].layout.yaxis2.title = (  # type: ignore
×
619
                            plotdata.options.legend_y
620
                        )
UNCOV
621
                        if isinstance(plotdata.options.color_discrete_sequence, list):
×
UNCOV
622
                            color = plotdata.options.color_discrete_sequence[0]
×
UNCOV
623
                            plcolors = convert_colors_to_same_type(color, "rgb")
×
UNCOV
624
                            if len(plcolors[0]) == 0:
×
UNCOV
625
                                rgb = name_to_rgb(color)
×
UNCOV
626
                                r, g, b = rgb.red, rgb.green, rgb.blue
×
627
                            else:
UNCOV
628
                                (r, g, b) = unlabel_rgb(plcolors[0][0])
×
UNCOV
629
                            self.dashboard.plots[self.id].update_yaxes(
×
630
                                gridcolor=f"rgba({r},{g},{b},0.5)", secondary_y=True
631
                            )
632
                    else:
633
                        self.dashboard.plots[self.id].layout.yaxis.title = (  # type: ignore
1✔
634
                            plotdata.options.legend_y
635
                        )
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