composeui 0.1.0__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.
Files changed (97) hide show
  1. composeui/__init__.py +11 -0
  2. composeui/core/__init__.py +0 -0
  3. composeui/core/application.py +368 -0
  4. composeui/core/chartcolors.py +23 -0
  5. composeui/core/chartimage.py +336 -0
  6. composeui/core/dataframesource.py +307 -0
  7. composeui/core/jobs.py +426 -0
  8. composeui/core/model.py +66 -0
  9. composeui/core/registry.py +66 -0
  10. composeui/core/reload.py +476 -0
  11. composeui/core/renderer.py +193 -0
  12. composeui/core/signal.py +358 -0
  13. composeui/core/sqlitemodel.py +1183 -0
  14. composeui/core/study.py +121 -0
  15. composeui/core/translation.py +221 -0
  16. composeui/core/translationcheck.py +254 -0
  17. composeui/core/view.py +5508 -0
  18. composeui/images/composeui.svg +15 -0
  19. composeui/locales/fr.json +73 -0
  20. composeui/mypy.py +46 -0
  21. composeui/py.typed +0 -0
  22. composeui/qtcore/__init__.py +0 -0
  23. composeui/qtcore/maprenderer.py +115 -0
  24. composeui/qtcore/qtapplication.py +222 -0
  25. composeui/qtcore/qtrenderer.py +2862 -0
  26. composeui/qtcore/widgets/__init__.py +0 -0
  27. composeui/qtcore/widgets/map.qml +123 -0
  28. composeui/qtcore/widgets/qchart.py +393 -0
  29. composeui/qtcore/widgets/qdatefield.py +101 -0
  30. composeui/qtcore/widgets/qexpander.py +60 -0
  31. composeui/qtcore/widgets/qfilepathfield.py +34 -0
  32. composeui/qtcore/widgets/qfiltertable.py +394 -0
  33. composeui/qtcore/widgets/qformdialog.py +64 -0
  34. composeui/qtcore/widgets/qformlabel.py +53 -0
  35. composeui/qtcore/widgets/qmap.py +68 -0
  36. composeui/qtcore/widgets/qmarkicon.py +37 -0
  37. composeui/qtcore/widgets/qmetric.py +44 -0
  38. composeui/qtcore/widgets/qplaintext.py +29 -0
  39. composeui/qtcore/widgets/qradiogroup.py +57 -0
  40. composeui/qtcore/widgets/qsidebar.py +116 -0
  41. composeui/qtcore/widgets/qsourcetable.py +216 -0
  42. composeui/qtcore/widgets/qsplitter.py +69 -0
  43. composeui/qtcore/widgets/qtexttable.py +703 -0
  44. composeui/qtcore/widgets/qvaluecombobox.py +41 -0
  45. composeui/qtcore/widgets/qvalueslider.py +64 -0
  46. composeui/qtcore/widgets/qwindowrelease.py +32 -0
  47. composeui/textualcore/__init__.py +0 -0
  48. composeui/textualcore/textualapplication.py +121 -0
  49. composeui/textualcore/textualrenderer.py +2893 -0
  50. composeui/textualcore/widgets/__init__.py +0 -0
  51. composeui/textualcore/widgets/composebutton.py +17 -0
  52. composeui/textualcore/widgets/composechart.py +212 -0
  53. composeui/textualcore/widgets/composecheckbox.py +35 -0
  54. composeui/textualcore/widgets/composecollapsible.py +41 -0
  55. composeui/textualcore/widgets/composecombobox.py +54 -0
  56. composeui/textualcore/widgets/composedateinput.py +325 -0
  57. composeui/textualcore/widgets/composedialog.py +90 -0
  58. composeui/textualcore/widgets/composefilepathinput.py +85 -0
  59. composeui/textualcore/widgets/composefiltertable.py +367 -0
  60. composeui/textualcore/widgets/composeformdialog.py +86 -0
  61. composeui/textualcore/widgets/composeinput.py +50 -0
  62. composeui/textualcore/widgets/composelist.py +20 -0
  63. composeui/textualcore/widgets/composemarkdown.py +17 -0
  64. composeui/textualcore/widgets/composemetric.py +60 -0
  65. composeui/textualcore/widgets/composeradioset.py +54 -0
  66. composeui/textualcore/widgets/composesidebar.py +111 -0
  67. composeui/textualcore/widgets/composeslider.py +144 -0
  68. composeui/textualcore/widgets/composesourcetable.py +288 -0
  69. composeui/textualcore/widgets/composesplitter.py +211 -0
  70. composeui/textualcore/widgets/composetabbedcontent.py +35 -0
  71. composeui/textualcore/widgets/composetable.py +476 -0
  72. composeui/textualcore/widgets/composetoolbar.py +62 -0
  73. composeui/textualcore/widgets/composetree.py +139 -0
  74. composeui/webcore/__init__.py +0 -0
  75. composeui/webcore/headers.py +57 -0
  76. composeui/webcore/icons.py +58 -0
  77. composeui/webcore/limits.py +55 -0
  78. composeui/webcore/origin.py +63 -0
  79. composeui/webcore/page.py +146 -0
  80. composeui/webcore/routes.py +303 -0
  81. composeui/webcore/session.py +436 -0
  82. composeui/webcore/static/app.css +2 -0
  83. composeui/webcore/static/cally.js +1312 -0
  84. composeui/webcore/static/daisyui.LICENSE +21 -0
  85. composeui/webcore/static/deckgl.LICENSE +19 -0
  86. composeui/webcore/static/deckgl.min.js +6423 -0
  87. composeui/webcore/static/htmx.min.js +1 -0
  88. composeui/webcore/static/page.js +1175 -0
  89. composeui/webcore/static/uPlot.LICENSE +21 -0
  90. composeui/webcore/static/uPlot.iife.min.js +2 -0
  91. composeui/webcore/webapplication.py +495 -0
  92. composeui/webcore/webrenderer.py +5113 -0
  93. composeui-0.1.0.dist-info/METADATA +222 -0
  94. composeui-0.1.0.dist-info/RECORD +97 -0
  95. composeui-0.1.0.dist-info/WHEEL +5 -0
  96. composeui-0.1.0.dist-info/licenses/LICENSE +9 -0
  97. composeui-0.1.0.dist-info/top_level.txt +1 -0
composeui/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import version
4
+
5
+ from composeui.core.application import LOGGER
6
+ from composeui.core.signal import LOGGER as SIGNAL_LOGGER
7
+
8
+ __all__ = ["LOGGER", "SIGNAL_LOGGER", "__version__"]
9
+
10
+ #: the version of the installed package, from its tag (setuptools-scm)
11
+ __version__ = version("composeui")
File without changes
@@ -0,0 +1,368 @@
1
+ """The app: it builds the main view, wires its signals and runs it on a backend (`run`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import functools
7
+ import importlib
8
+ import importlib.util
9
+ import inspect
10
+ import logging
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast
15
+
16
+ from typing_extensions import TypedDict, TypeVarTuple, Unpack, overload, override
17
+
18
+ from composeui.core.registry import RendererType
19
+ from composeui.core.signal import LOGGER as SIGNAL_LOGGER
20
+ from composeui.core.signal import Signal
21
+ from composeui.core.study import open_study
22
+ from composeui.core.view import set_references
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable, Sequence
26
+
27
+ from composeui.core.model import Model
28
+ from composeui.core.signal import _BoundCallback, _MainViewCallback, _ModelCallback
29
+ from composeui.core.view import MainView, View
30
+
31
+ __all__ = ["Application", "ModelApplication", "run"]
32
+
33
+
34
+ LOGGER = logging.getLogger("COMPOSEUI")
35
+ _STREAM_HANDLER = logging.StreamHandler(sys.stdout)
36
+ _STREAM_HANDLER.setFormatter(logging.Formatter("%(asctime)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
37
+ LOGGER.addHandler(_STREAM_HANDLER)
38
+
39
+ # T = TypeVar("T", bound=MainView)
40
+ Ts = TypeVarTuple("Ts")
41
+
42
+ _ViewT_contra = TypeVar("_ViewT_contra", contravariant=True)
43
+ _MainViewT_contra = TypeVar("_MainViewT_contra", contravariant=True)
44
+ _ModelT_contra = TypeVar("_ModelT_contra", contravariant=True)
45
+ ViewT = TypeVar("ViewT", bound="View")
46
+ MainViewT = TypeVar("MainViewT", bound="MainView")
47
+ ModelT = TypeVar("ModelT", bound="Model")
48
+
49
+
50
+ _ViewTD = TypeVar("_ViewTD")
51
+ _MainViewTD = TypeVar("_MainViewTD")
52
+ _ModelTD = TypeVar("_ModelTD")
53
+ _TsD = TypeVarTuple("_TsD")
54
+
55
+
56
+ class AvailableBounds(TypedDict, Generic[_ViewT_contra, _MainViewT_contra]):
57
+ """The arguments `Application.connect` can give a callback by name."""
58
+
59
+ view: _ViewT_contra
60
+ main_view: _MainViewT_contra
61
+
62
+
63
+ class AvailableModelBounds(TypedDict, Generic[_ViewT_contra, _MainViewT_contra, _ModelT_contra]):
64
+ """The arguments `ModelApplication.connect` can give a callback by name."""
65
+
66
+ view: _ViewT_contra
67
+ main_view: _MainViewT_contra
68
+ model: _ModelT_contra
69
+
70
+
71
+ class _ViewOnlyCallback(Protocol[_ViewT_contra, Unpack[Ts]]):
72
+ def __call__(self, *args: Unpack[Ts], view: _ViewT_contra) -> None: ...
73
+
74
+
75
+ class _ViewMainCallback(Protocol[_ViewT_contra, _MainViewT_contra, Unpack[Ts]]):
76
+ def __call__(
77
+ self, *args: Unpack[Ts], view: _ViewT_contra, main_view: _MainViewT_contra
78
+ ) -> None: ...
79
+
80
+
81
+ class _ViewModelCallback(Protocol[_ViewT_contra, _ModelT_contra, Unpack[Ts]]):
82
+ def __call__(self, *args: Unpack[Ts], view: _ViewT_contra, model: _ModelT_contra) -> None: ...
83
+
84
+
85
+ class _ViewMainModelCallback(
86
+ Protocol[_ViewT_contra, _MainViewT_contra, _ModelT_contra, Unpack[Ts]]
87
+ ):
88
+ def __call__(
89
+ self,
90
+ *args: Unpack[Ts],
91
+ view: _ViewT_contra,
92
+ main_view: _MainViewT_contra,
93
+ model: _ModelT_contra,
94
+ ) -> None: ...
95
+
96
+
97
+ class _ConnectDecorator(Protocol[_ViewTD, _MainViewTD, Unpack[_TsD]]):
98
+ @overload
99
+ def __call__(
100
+ self, callback: Callable[[Unpack[_TsD]], None]
101
+ ) -> Callable[[Unpack[_TsD]], None]: ...
102
+ @overload
103
+ def __call__(
104
+ self, callback: _MainViewCallback[_MainViewTD, Unpack[_TsD]]
105
+ ) -> _MainViewCallback[_MainViewTD, Unpack[_TsD]]: ...
106
+ @overload
107
+ def __call__(
108
+ self, callback: _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]
109
+ ) -> _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]: ...
110
+ @overload
111
+ def __call__(
112
+ self, callback: _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]
113
+ ) -> _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]: ...
114
+
115
+
116
+ class _ConnectModelDecorator(Protocol[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]):
117
+ @overload
118
+ def __call__(
119
+ self, callback: Callable[[Unpack[_TsD]], None]
120
+ ) -> Callable[[Unpack[_TsD]], None]: ...
121
+ @overload
122
+ def __call__(
123
+ self, callback: _MainViewCallback[_MainViewTD, Unpack[_TsD]]
124
+ ) -> _MainViewCallback[_MainViewTD, Unpack[_TsD]]: ...
125
+ @overload
126
+ def __call__(
127
+ self, callback: _ModelCallback[_ModelTD, Unpack[_TsD]]
128
+ ) -> _ModelCallback[_ModelTD, Unpack[_TsD]]: ...
129
+ @overload
130
+ def __call__(
131
+ self, callback: _BoundCallback[_MainViewTD, _ModelTD, Unpack[_TsD]]
132
+ ) -> _BoundCallback[_MainViewTD, _ModelTD, Unpack[_TsD]]: ...
133
+ @overload
134
+ def __call__(
135
+ self, callback: _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]
136
+ ) -> _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]: ...
137
+ @overload
138
+ def __call__(
139
+ self, callback: _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]
140
+ ) -> _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]: ...
141
+ @overload
142
+ def __call__(
143
+ self, callback: _ViewModelCallback[_ViewTD, _ModelTD, Unpack[_TsD]]
144
+ ) -> _ViewModelCallback[_ViewTD, _ModelTD, Unpack[_TsD]]: ...
145
+ @overload
146
+ def __call__(
147
+ self, callback: _ViewMainModelCallback[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]
148
+ ) -> _ViewMainModelCallback[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]: ...
149
+
150
+
151
+ def _add_reference_to_signals(main_view: MainView, model: Model | None) -> None:
152
+ set_references(main_view, main_view, model)
153
+ if model is not None:
154
+ main_view.jobs.set_model(model)
155
+ for signal in model.signals:
156
+ signal.set_main_view(main_view)
157
+ signal.set_model(model)
158
+
159
+
160
+ class Application(Generic[MainViewT]):
161
+ """An app showing the main view `build_main_view` builds; each backend has its own.
162
+
163
+ The app builds its main view itself, so every backend takes the same arguments: a web
164
+ app builds one per browser session.
165
+ """
166
+
167
+ def __init__(self, *, build_main_view: Callable[[], MainViewT]) -> None:
168
+ self._main_view = build_main_view()
169
+ self._args: argparse.Namespace | None = None
170
+ self._parser = argparse.ArgumentParser()
171
+ self._parser.add_argument("--debug", help="Run the app in debug mode", action="store_true")
172
+ self._parser.add_argument(
173
+ "-f", metavar="FILE", help="Open the app with the FILE study", type=Path
174
+ )
175
+ self._parser.add_argument("--log", help="Display the debug Log.", action="store_true")
176
+ self._parser.add_argument(
177
+ "--log-signals", help="Log all the emitted signals.", action="store_true"
178
+ )
179
+ self._parser.add_argument(
180
+ "--language", metavar="LANGUAGE", help="Show the app in LANGUAGE, e.g. fr."
181
+ )
182
+ self._add_reference_to_signals()
183
+
184
+ def _add_reference_to_signals(self) -> None:
185
+ _add_reference_to_signals(self._main_view, None)
186
+
187
+ @property
188
+ def main_view(self) -> MainViewT:
189
+ return self._main_view
190
+
191
+ def connect(
192
+ self, *, view: ViewT, signals: Sequence[Signal[Unpack[Ts]]]
193
+ ) -> _ConnectDecorator[ViewT, MainViewT, Unpack[Ts]]:
194
+ def wrapper(callback: Callable[..., None]) -> Callable[..., None]:
195
+ available_bounds: AvailableBounds[ViewT, MainViewT] = {
196
+ "view": view,
197
+ "main_view": self._main_view,
198
+ }
199
+ requested_bounds = self._requested_kwargs(callback, tuple(available_bounds))
200
+ bound = functools.partial(
201
+ callback, **{k: v for k, v in available_bounds.items() if k in requested_bounds}
202
+ )
203
+ new_callback = cast("Callable[[Unpack[Ts]], None]", bound)
204
+ for s in signals:
205
+ s.connect(new_callback)
206
+ return callback
207
+
208
+ return cast("_ConnectDecorator[ViewT, MainViewT, Unpack[Ts]]", wrapper)
209
+
210
+ def render(self, renderer_type: RendererType) -> None:
211
+ self._main_view.is_visible = True
212
+ self._main_view.render(renderer_type)
213
+
214
+ def run(self) -> None:
215
+ self._args = self._parse_arguments()
216
+ if self._args.log or self._args.debug:
217
+ LOGGER.setLevel(level=logging.DEBUG)
218
+ else:
219
+ LOGGER.setLevel(level=logging.INFO)
220
+ if self._args.log_signals or self._args.debug:
221
+ SIGNAL_LOGGER.setLevel(level=logging.DEBUG)
222
+ else:
223
+ SIGNAL_LOGGER.setLevel(level=logging.INFO)
224
+ if self._args.language is not None:
225
+ self.main_view.set_language(self._args.language)
226
+ if self._args.f is not None:
227
+ open_study(self.main_view, self._study_model(), self._args.f)
228
+ self.main_view.apply_arguments(self._args)
229
+
230
+ def _study_model(self) -> Model | None:
231
+ """Get the model opened, saved and started over with the views: none here."""
232
+ return None
233
+
234
+ def _parse_arguments(self) -> argparse.Namespace:
235
+ # last: after the backend's own, which a subclass adds once built
236
+ type(self.main_view).add_arguments(self._parser)
237
+ args = self._parser.parse_args()
238
+ if (
239
+ args.f is not None
240
+ and isinstance(args.f, Path)
241
+ and args.f.is_file()
242
+ and args.f.suffix != self.main_view.extension
243
+ ):
244
+ msg = (
245
+ f"Invalid file extension: '{args.f.suffix}'. "
246
+ f"Expected extension: '{self.main_view.extension}'. "
247
+ "Please provide a file with the correct extension."
248
+ )
249
+ raise ValueError(msg)
250
+ return args
251
+
252
+ @staticmethod
253
+ def _requested_kwargs(callback: object, names: tuple[str, ...]) -> frozenset[str]:
254
+ try:
255
+ params = inspect.signature(callback).parameters # type: ignore[arg-type]
256
+ except (TypeError, ValueError):
257
+ return frozenset()
258
+ return frozenset(name for name in names if name in params)
259
+
260
+
261
+ class ModelApplication(Application[MainViewT], Generic[MainViewT, ModelT]):
262
+ """An app with a model beside its main view, given to the callbacks asking for `model`.
263
+
264
+ `setup_model` wires them once both are built, e.g. to refresh views when the model
265
+ changes.
266
+ """
267
+
268
+ def __init__(
269
+ self,
270
+ *,
271
+ build_main_view: Callable[[], MainViewT],
272
+ build_model: Callable[[], ModelT],
273
+ setup_model: Callable[[MainViewT, ModelT], None] | None = None,
274
+ ) -> None:
275
+ # first: the base class gives it to the signals
276
+ self._model = build_model()
277
+ super().__init__(build_main_view=build_main_view)
278
+ if setup_model is not None:
279
+ setup_model(self._main_view, self._model)
280
+
281
+ @override
282
+ def _add_reference_to_signals(self) -> None:
283
+ _add_reference_to_signals(self._main_view, self._model)
284
+
285
+ @property
286
+ def model(self) -> ModelT:
287
+ return self._model
288
+
289
+ @override
290
+ def _study_model(self) -> ModelT:
291
+ return self._model
292
+
293
+ def connect(
294
+ self, *, view: ViewT, signals: Sequence[Signal[Unpack[Ts]]]
295
+ ) -> _ConnectModelDecorator[ViewT, MainViewT, ModelT, Unpack[Ts]]:
296
+ def wrapper(callback: Callable[..., None]) -> Callable[..., None]:
297
+ available_bounds: AvailableModelBounds[ViewT, MainViewT, ModelT] = {
298
+ "view": view,
299
+ "main_view": self._main_view,
300
+ "model": self._model,
301
+ }
302
+ requested_bounds = self._requested_kwargs(callback, tuple(available_bounds))
303
+ bound = functools.partial(
304
+ callback, **{k: v for k, v in available_bounds.items() if k in requested_bounds}
305
+ )
306
+ new_callback = cast("Callable[[Unpack[Ts]], None]", bound)
307
+ for s in signals:
308
+ s.connect(new_callback)
309
+ return callback
310
+
311
+ return cast("_ConnectModelDecorator[ViewT, MainViewT, ModelT, Unpack[Ts]]", wrapper)
312
+
313
+
314
+ #: environment variable naming the backend `run` started: a process the app starts again,
315
+ #: e.g. the web's ``--reload`` server, gets the command line without ``--backend``
316
+ BACKEND_VARIABLE = "COMPOSEUI_BACKEND"
317
+
318
+
319
+ def run(
320
+ build_main_view: Callable[[], MainViewT],
321
+ *,
322
+ build_model: Callable[[], ModelT] | None = None,
323
+ setup_model: Callable[[MainViewT, ModelT], None] | None = None,
324
+ default_backend: RendererType = RendererType.WEB,
325
+ ) -> None:
326
+ """Run the app on the backend the command line names, e.g. ``--backend qt``.
327
+
328
+ Without ``--backend``, on the one `BACKEND_VARIABLE` names, else on `default_backend`.
329
+ The rest of the command line goes to the backend's app (``-f``, ``--language``,
330
+ ``--port``, ``--help``...). Only that backend is imported, so the others' extras
331
+ needn't be installed; without its own, it stops and names the extra. With `build_model`,
332
+ the app has a model, wired to its main view by `setup_model`: once, or per session on the
333
+ web.
334
+ """
335
+ if build_model is None and setup_model is not None:
336
+ msg = "setup_model needs a model: pass build_model too"
337
+ raise ValueError(msg)
338
+ backends = {backend.name.lower(): backend for backend in RendererType}
339
+ parser = argparse.ArgumentParser(add_help=False)
340
+ default = os.environ.get(BACKEND_VARIABLE, "")
341
+ if default not in backends:
342
+ default = default_backend.name.lower()
343
+ parser.add_argument("--backend", choices=backends, default=default)
344
+ args, rest = parser.parse_known_args()
345
+ # the app parses the command line itself when it runs; a process it starts again
346
+ # inherits its environment, and so the backend
347
+ sys.argv = [sys.argv[0], *rest]
348
+ os.environ[BACKEND_VARIABLE] = args.backend
349
+ backend = backends[args.backend].value
350
+ try:
351
+ module = importlib.import_module(backend.applications)
352
+ except ImportError:
353
+ # only the extra not installed: another import error is a bug, shown as it is
354
+ missing = [name for name in backend.packages if importlib.util.find_spec(name) is None]
355
+ if not missing:
356
+ raise
357
+ parser.error(
358
+ f"the {args.backend} backend needs {', '.join(missing)}, from the "
359
+ f'{backend.extra} extra: pip install "composeui[{backend.extra}]"'
360
+ )
361
+ app: Application[MainViewT]
362
+ if build_model is None:
363
+ app = module.APPLICATION(build_main_view=build_main_view)
364
+ else:
365
+ app = module.MODEL_APPLICATION(
366
+ build_main_view=build_main_view, build_model=build_model, setup_model=setup_model
367
+ )
368
+ app.run()
@@ -0,0 +1,23 @@
1
+ """The series colors of the backends drawing charts without a palette of their own."""
2
+
3
+ #: categorical hues, in order, told apart with color blindness too, for a light and for a
4
+ #: dark background (each stepped for its background); past them, around again dashed
5
+ from __future__ import annotations
6
+
7
+ __all__ = []
8
+
9
+ SERIES_COLORS = {
10
+ False: ("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"),
11
+ True: ("#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"),
12
+ }
13
+
14
+
15
+ def series_color(index: int, *, is_dark: bool) -> str:
16
+ """Get the color of the chart's series at `index`, as ``#rrggbb``."""
17
+ colors = SERIES_COLORS[is_dark]
18
+ return colors[index % len(colors)]
19
+
20
+
21
+ def is_dashed(index: int) -> bool:
22
+ """Tell whether the series at `index` is dashed: the colors went around once already."""
23
+ return index >= len(SERIES_COLORS[False])