gsplot 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,540 @@
1
+ from typing import Any
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ from matplotlib.axes import Axes
6
+ from matplotlib.collections import LineCollection
7
+ from matplotlib.colors import Normalize
8
+ from numpy.typing import ArrayLike, NDArray
9
+
10
+ from ..base.base import CreateClassParams, ParamsGetter, bind_passed_params
11
+ from ..base.base_alias_validator import AliasValidator
12
+ from ..figure.axes_base import AxesRangeSingleton, AxesResolver, AxisLayout
13
+ from ..style.legend_colormap import LegendColormap
14
+ from .line_colormap_base import LineColormapBase
15
+
16
+ __all__: list[str] = ["line_colormap_dashed"]
17
+
18
+
19
+ class LineColormapDashed:
20
+ """
21
+ A class for creating and plotting dashed lines with colormap interpolation.
22
+
23
+ This class plots a dashed line with varying colors based on a provided colormap
24
+ and associated data. It supports interpolation, axis scaling, and flexible dash patterns.
25
+
26
+ Parameters
27
+ --------------------
28
+ axis_target : int or matplotlib.axes.Axes
29
+ The target axis where the line should be plotted.
30
+ x : ArrayLike
31
+ The x-coordinates of the line data.
32
+ y : ArrayLike
33
+ The y-coordinates of the line data.
34
+ cmapdata : ArrayLike
35
+ Data used for mapping the colormap to the line.
36
+ cmap : str, optional
37
+ The name of the Matplotlib colormap to use (default is "viridis").
38
+ linewidth : int or float, optional
39
+ The width of the line (default is 1).
40
+ line_pattern : tuple[float, float], optional
41
+ The pattern of solid and gap lengths for the dashed line (default is (10, 10)).
42
+ label : str or None, optional
43
+ The label for the line, used in legends (default is None).
44
+ xspan : int or float or None, optional
45
+ The span of the x-axis data for scaling (default is None).
46
+ yspan : int or float or None, optional
47
+ The span of the y-axis data for scaling (default is None).
48
+ **kwargs : Any
49
+ Additional keyword arguments passed to `LegendColormap`.
50
+
51
+ Methods
52
+ --------------------
53
+ add_legend_colormap()
54
+ Adds a legend for the colormap to the target axis.
55
+ verify_line_pattern()
56
+ Validates and adjusts the line pattern for solid and gap lengths.
57
+ get_data_span()
58
+ Calculates the span of the x and y data.
59
+ get_scales()
60
+ Retrieves scaling factors for the x and y axes based on figure and axis sizes.
61
+ get_interpolated_data(interpolation_points)
62
+ Interpolates the line and colormap data to create smooth segments.
63
+ plot()
64
+ Plots the dashed line with the interpolated colormap.
65
+
66
+ Examples
67
+ --------------------
68
+ >>> x = np.linspace(0, 10, 100)
69
+ >>> y = np.sin(x)
70
+ >>> cmapdata = np.linspace(0, 1, 100)
71
+ >>> dashed_line = LineColormapDashed(0, x, y, cmapdata, line_pattern=(5, 5))
72
+ >>> lc_list = dashed_line.plot()
73
+ >>> print(len(lc_list))
74
+ 10 # Number of dashed line segments
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ axis_target: int | Axes,
80
+ x: ArrayLike,
81
+ y: ArrayLike,
82
+ cmapdata: ArrayLike,
83
+ cmap: str = "viridis",
84
+ linewidth: int | float = 1,
85
+ line_pattern: tuple[int | float, int | float] = (10, 10),
86
+ label: str | None = None,
87
+ xspan: int | float | None = None,
88
+ yspan: int | float | None = None,
89
+ **kwargs: Any,
90
+ ) -> None:
91
+
92
+ self.axis_target: int | Axes = axis_target
93
+
94
+ self.axis_index: int = AxesResolver(axis_target).axis_index
95
+ self.axis: Axes = AxesResolver(axis_target).axis
96
+
97
+ self._x: ArrayLike = x
98
+ self._y: ArrayLike = y
99
+ self._cmapdata: ArrayLike = cmapdata
100
+ self.cmap: str = cmap
101
+ self.linewidth = linewidth
102
+ self.line_pattern: tuple[int | float, int | float] = line_pattern
103
+ self.label: str | None = label
104
+ self._xspan: int | float | None = xspan
105
+ self._yspan: int | float | None = yspan
106
+ self.kwargs: Any = kwargs
107
+
108
+ self.x: NDArray[Any] = np.array(self._x)
109
+ self.y: NDArray[Any] = np.array(self._y)
110
+ self.cmapdata: NDArray[Any] = np.array(self._cmapdata)
111
+
112
+ if self.label is not None:
113
+ self.add_legend_colormap()
114
+
115
+ self.xspan: float = (
116
+ self.get_data_span()[0] if self._xspan is None else self._xspan
117
+ )
118
+ self.yspan: float = (
119
+ self.get_data_span()[1] if self._yspan is None else self._yspan
120
+ )
121
+
122
+ self.fig = plt.gcf()
123
+
124
+ self.verify_line_pattern()
125
+
126
+ self._calculate_uniform_coordinates()
127
+
128
+ def add_legend_colormap(self) -> None:
129
+ """
130
+ Adds a legend entry for the colormap associated with the dashed line.
131
+
132
+ This method creates a legend entry that represents the colormap used in the dashed
133
+ line plot. The legend is customized with a specified number of stripes.
134
+
135
+ Parameters
136
+ --------------------
137
+ None
138
+
139
+ Returns
140
+ --------------------
141
+ None
142
+
143
+ Examples
144
+ --------------------
145
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1, 2], y=[1, 2, 3], cmapdata=[0.1, 0.5, 1.0])
146
+ >>> line.add_legend_colormap()
147
+ """
148
+ LegendColormap(
149
+ self.axis_index,
150
+ self.cmap,
151
+ self.label,
152
+ num_stripes=len(self.cmapdata),
153
+ **self.kwargs,
154
+ ).axis_patch()
155
+
156
+ def verify_line_pattern(self) -> None:
157
+ """
158
+ Verifies and adjusts the line pattern for the dashed line.
159
+
160
+ This method ensures that the provided `line_pattern` parameter is a tuple of exactly
161
+ two elements (solid and gap lengths). It also adjusts the solid line length to account
162
+ for the projected `capstyle`.
163
+
164
+ Parameters
165
+ --------------------
166
+ None
167
+
168
+ Returns
169
+ --------------------
170
+ None
171
+
172
+ Raises
173
+ --------------------
174
+ ValueError
175
+ If the `line_pattern` is not a tuple with exactly two elements.
176
+
177
+ Examples
178
+ --------------------
179
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1], y=[1, 2], cmapdata=[0.1, 0.2])
180
+ >>> line.verify_line_pattern()
181
+ """
182
+ if len(self.line_pattern) != 2:
183
+ raise ValueError(
184
+ f"Line pattern must be a tuple with two elements, not {len(self.line_pattern)}."
185
+ )
186
+
187
+ self.length_solid: int | float = self.line_pattern[0]
188
+ self.length_gap: int | float = self.line_pattern[1]
189
+
190
+ # Due to projecting capstyle, the solid line must be shrinked by half of the linewidth
191
+ self.length_solid = np.abs(self.length_solid - self.linewidth / 2)
192
+
193
+ def get_data_span(self) -> NDArray[np.float64]:
194
+ """
195
+ Calculates the span of the x and y data.
196
+
197
+ This method determines the range of the x and y coordinates and calculates
198
+ their respective spans.
199
+
200
+ Parameters
201
+ --------------------
202
+ None
203
+
204
+ Returns
205
+ --------------------
206
+ numpy.ndarray
207
+ A 1D array containing the spans of the x and y data as `[xspan, yspan]`.
208
+
209
+ Examples
210
+ --------------------
211
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1, 2], y=[1, 2, 3], cmapdata=[0.1, 0.5, 1.0])
212
+ >>> spans = line.get_data_span()
213
+ >>> print(spans)
214
+ array([2.0, 2.0])
215
+ """
216
+
217
+ xmax, xmin = np.max(self.x), np.min(self.x)
218
+ ymax, ymin = np.max(self.y), np.min(self.y)
219
+ xspan = xmax - xmin
220
+ yspan = ymax - ymin
221
+ return np.array([xspan, yspan])
222
+
223
+ def get_scales(self) -> tuple[float, float]:
224
+ """
225
+ Computes scaling factors for the x and y axes based on the figure and axis sizes.
226
+
227
+ This method calculates how the x and y data should be scaled to match the
228
+ current figure and axis dimensions.
229
+
230
+ Parameters
231
+ --------------------
232
+ None
233
+
234
+ Returns
235
+ --------------------
236
+ tuple[float, float]
237
+ A tuple containing the scaling factors for the x and y axes.
238
+
239
+ Examples
240
+ --------------------
241
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1, 2], y=[1, 2, 3], cmapdata=[0.1, 0.5, 1.0])
242
+ >>> xscale, yscale = line.get_scales()
243
+ >>> print(xscale, yscale)
244
+ 100.0 150.0 # Example output
245
+ """
246
+ canvas_width, canvas_height = self.fig.canvas.get_width_height()
247
+ # get axis size
248
+ axis_width, axis_height = AxisLayout(self.axis_index).get_axis_size()
249
+
250
+ xscale: float
251
+ yscale: float
252
+ if self.xspan == 0:
253
+ xscale = 1.0
254
+ else:
255
+ xscale = (canvas_width / self.xspan) * (axis_width)
256
+ if self.yspan == 0:
257
+ yscale = 1.0
258
+ else:
259
+ yscale = canvas_height / self.yspan * (axis_height)
260
+ return xscale, yscale
261
+
262
+ def get_interpolated_data(self, interpolation_points: int) -> tuple:
263
+ """
264
+ Interpolates the x, y, and colormap data to ensure uniform dash spacing.
265
+
266
+ This method calculates evenly spaced points along the input x and y data
267
+ and interpolates the colormap data accordingly.
268
+
269
+ Parameters
270
+ --------------------
271
+ interpolation_points : int
272
+ The number of points to interpolate.
273
+
274
+ Returns
275
+ --------------------
276
+ tuple
277
+ A tuple containing the interpolated x, y, and colormap data arrays.
278
+
279
+ Examples
280
+ --------------------
281
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1, 2], y=[1, 2, 3], cmapdata=[0.1, 0.5, 1.0])
282
+ >>> x_interp, y_interp, cmap_interp = line.get_interpolated_data(100)
283
+ """
284
+
285
+ xdiff = np.diff(self.x)
286
+ ydiff = np.diff(self.y)
287
+ distances = np.sqrt(xdiff**2 + ydiff**2)
288
+ cumulative_distances = np.insert(np.cumsum(distances), 0, 0)
289
+ interpolated_distances = np.linspace(
290
+ 0, cumulative_distances[-1], interpolation_points
291
+ )
292
+
293
+ x_interpolated = np.interp(interpolated_distances, cumulative_distances, self.x)
294
+ y_interpolated = np.interp(interpolated_distances, cumulative_distances, self.y)
295
+
296
+ # Interpolate cmapdata
297
+ cmap_interpolated = np.interp(
298
+ interpolated_distances, cumulative_distances, self.cmapdata
299
+ )
300
+
301
+ return x_interpolated, y_interpolated, cmap_interpolated
302
+
303
+ def _calculate_uniform_coordinates(self) -> None:
304
+ """
305
+ Interpolates the x, y, and colormap data to ensure uniform dash spacing.
306
+
307
+ This method calculates evenly spaced points along the input x and y data
308
+ and interpolates the colormap data accordingly.
309
+
310
+ Parameters
311
+ --------------------
312
+ interpolation_points : int
313
+ The number of points to interpolate.
314
+
315
+ Returns
316
+ --------------------
317
+ tuple
318
+ A tuple containing the interpolated x, y, and colormap data arrays.
319
+
320
+ Examples
321
+ --------------------
322
+ >>> line = LineColormapDashed(axis_target=0, x=[0, 1, 2], y=[1, 2, 3], cmapdata=[0.1, 0.5, 1.0])
323
+ >>> x_interp, y_interp, cmap_interp = line.get_interpolated_data(100)
324
+ """
325
+
326
+ xscale, yscale = self.get_scales()
327
+ self.scaled_x = self.x * xscale
328
+ self.scaled_y = self.y * yscale
329
+
330
+ self.scaled_xdiff = np.diff(self.scaled_x)
331
+ self.scaled_ydiff = np.diff(self.scaled_y)
332
+
333
+ self.scaled_xdiff = np.nan_to_num(np.diff(self.scaled_x), nan=0.0)
334
+ self.scaled_ydiff = np.nan_to_num(np.diff(self.scaled_y), nan=0.0)
335
+
336
+ self.scaled_distances = np.sqrt(self.scaled_xdiff**2 + self.scaled_ydiff**2)
337
+ self.scaled_total_distances = np.sum(self.scaled_distances)
338
+
339
+ FACTOR = 5
340
+ INTERPOLATION_POINTS = int(
341
+ self.scaled_total_distances * FACTOR // self.length_solid
342
+ )
343
+
344
+ self.x_interpolated, self.y_interpolated, self.cmap_interpolated = (
345
+ self.get_interpolated_data(INTERPOLATION_POINTS)
346
+ )
347
+
348
+ self.scaled_inter_xdiff = np.gradient(self.x_interpolated * xscale)
349
+ self.scaled_inter_ydiff = np.gradient(self.y_interpolated * yscale)
350
+ self.scaled_inter_distances = np.sqrt(
351
+ self.scaled_inter_xdiff**2 + self.scaled_inter_ydiff**2
352
+ )
353
+
354
+ @AxesRangeSingleton.update
355
+ def plot(self) -> list[LineCollection]:
356
+ """
357
+ Plots the dashed line with a colormap applied to individual segments.
358
+
359
+ This method creates dashed line segments by iterating over the interpolated
360
+ coordinates and adding `LineCollection` objects to the target axis. Each
361
+ dash segment is colored based on the provided colormap data.
362
+
363
+ Parameters
364
+ --------------------
365
+ None
366
+
367
+ Returns
368
+ --------------------
369
+ list[matplotlib.collections.LineCollection]
370
+ A list of `LineCollection` objects representing the plotted dashed line segments.
371
+
372
+ Notes
373
+ --------------------
374
+ - This method is decorated with `@AxesRangeSingleton.update` to update the axis range
375
+ singleton with the plotted data.
376
+ - The method uses `LineColormapBase` for creating line segments and normalizing the
377
+ colormap data.
378
+
379
+ Raises
380
+ --------------------
381
+ ValueError
382
+ If the input data or configuration parameters are invalid.
383
+
384
+ Examples
385
+ --------------------
386
+ >>> x = [0, 1, 2, 3, 4]
387
+ >>> y = [1, 3, 2, 5, 4]
388
+ >>> cmapdata = [0.1, 0.3, 0.6, 0.9, 1.0]
389
+ >>> line = LineColormapDashed(axis_target=0, x=x, y=y, cmapdata=cmapdata)
390
+ >>> lc_list = line.plot()
391
+ """
392
+ current_length = 0
393
+ draw_dash = True
394
+ idx_start = 0
395
+
396
+ norm = LineColormapBase()._create_cmap(self.cmapdata)
397
+
398
+ lc_list: list[LineCollection] = []
399
+ for i in range(len(self.x_interpolated) - 1):
400
+ current_length += self.scaled_inter_distances[i]
401
+
402
+ if draw_dash:
403
+ if current_length >= self.length_solid:
404
+ segments = LineColormapBase()._create_segment(
405
+ self.x_interpolated[idx_start : i + 1],
406
+ self.y_interpolated[idx_start : i + 1],
407
+ )
408
+
409
+ lc = LineCollection(
410
+ segments.tolist(),
411
+ cmap=self.cmap,
412
+ norm=norm,
413
+ capstyle="projecting",
414
+ )
415
+ lc.set_array(self.cmap_interpolated[idx_start : i + 1])
416
+ lc.set_linewidth(self.linewidth)
417
+ lc.set_linestyle("solid")
418
+ self.axis.add_collection(lc)
419
+
420
+ lc_list.append(lc)
421
+
422
+ draw_dash = False
423
+ current_length = 0
424
+ idx_start = i
425
+ else:
426
+ if current_length >= self.length_gap:
427
+ draw_dash = True
428
+ current_length = 0
429
+ idx_start = i
430
+
431
+ # at last with the last point if draw_dash is True
432
+ if i == len(self.x_interpolated) - 2 and draw_dash:
433
+ segments = LineColormapBase()._create_segment(
434
+ self.x_interpolated[idx_start:],
435
+ self.y_interpolated[idx_start:],
436
+ )
437
+
438
+ lc = LineCollection(
439
+ segments.tolist(),
440
+ cmap=self.cmap,
441
+ norm=norm,
442
+ )
443
+ lc.set_array(self.cmap_interpolated[idx_start:])
444
+ lc.set_linewidth(self.linewidth)
445
+ lc.set_linestyle("solid")
446
+
447
+ self.axis.add_collection(lc)
448
+
449
+ lc_list.append(lc)
450
+
451
+ return lc_list
452
+
453
+
454
+ @bind_passed_params()
455
+ def line_colormap_dashed(
456
+ axis_target: int | Axes,
457
+ x: ArrayLike,
458
+ y: ArrayLike,
459
+ cmapdata: ArrayLike,
460
+ cmap: str = "viridis",
461
+ linewidth: int | float = 1,
462
+ line_pattern: tuple[float, float] = (10, 10),
463
+ label: str | None = None,
464
+ xspan: int | float | None = None,
465
+ yspan: int | float | None = None,
466
+ **kwargs: Any,
467
+ ) -> list[LineCollection]:
468
+ """
469
+ A convenience function to plot dashed lines with a colormap applied to individual segments.
470
+
471
+ This function wraps the `LineColormapDashed` class for ease of use. It handles axis
472
+ resolution, alias validation, and parameter merging automatically.
473
+
474
+ Parameters
475
+ --------------------
476
+ axis_target : int or matplotlib.axes.Axes
477
+ The target axis where the line will be plotted. Can be an axis index or an `Axes` object.
478
+ x : ArrayLike
479
+ The x-coordinates of the line.
480
+ y : ArrayLike
481
+ The y-coordinates of the line.
482
+ cmapdata : ArrayLike
483
+ The data used for coloring the line segments. Values will be normalized to map to colors.
484
+ cmap : str, optional
485
+ The name of the colormap to use (default is "viridis").
486
+ linewidth : int or float, optional
487
+ The width of the line (default is 1).
488
+ line_pattern : tuple[float, float], optional
489
+ A tuple specifying the lengths of solid and gap segments in the dash pattern (default is (10, 10)).
490
+ label : str or None, optional
491
+ The label for the line, used in legends (default is `None`).
492
+ xspan : float or None, optional
493
+ The span of x-coordinates for scaling, calculated automatically if `None` (default is `None`).
494
+ yspan : float or None, optional
495
+ The span of y-coordinates for scaling, calculated automatically if `None` (default is `None`).
496
+ **kwargs : Any
497
+ Additional keyword arguments passed to Matplotlib functions.
498
+
499
+ Notes
500
+ --------------------
501
+ - This function utilizes the `ParamsGetter` to retrieve bound parameters and the `CreateClassParams` class to handle the merging of default, configuration, and passed parameters.
502
+ - Alias validation is performed using the `AliasValidator` class.
503
+
504
+ - `lw` for `linewidth`
505
+
506
+ Returns
507
+ --------------------
508
+ list[matplotlib.collections.LineCollection]
509
+ A list of `LineCollection` objects representing the plotted dashed line.
510
+
511
+ Examples
512
+ --------------------
513
+ >>> import gsplot as gs
514
+ >>> x = [0, 1, 2, 3]
515
+ >>> y = [1, 2, 3, 4]
516
+ >>> cmapdata = [0.1, 0.4, 0.6, 0.9]
517
+ >>> line_collections = gs.line_colormap_dashed(0, x, y, cmapdata, line_pattern=(5, 5))
518
+ """
519
+ alias_map = {
520
+ "lw": "linewidth",
521
+ }
522
+
523
+ passed_params: dict[str, Any] = ParamsGetter("passed_params").get_bound_params()
524
+ AliasValidator(alias_map, passed_params).validate()
525
+ class_params = CreateClassParams(passed_params).get_class_params()
526
+
527
+ _line_colormap_dashed: LineColormapDashed = LineColormapDashed(
528
+ class_params["axis_target"],
529
+ class_params["x"],
530
+ class_params["y"],
531
+ class_params["cmapdata"],
532
+ class_params["cmap"],
533
+ class_params["linewidth"],
534
+ class_params["line_pattern"],
535
+ class_params["label"],
536
+ class_params["xspan"],
537
+ class_params["yspan"],
538
+ **class_params["kwargs"],
539
+ )
540
+ return _line_colormap_dashed.plot()