dash-dracula-theme 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.
@@ -0,0 +1,72 @@
1
+ """
2
+ dash_dracula
3
+ ~~~~~~~~~~~~
4
+ Dracula dark/light theme for Plotly Dash applications.
5
+
6
+ Quick start::
7
+
8
+ import dash
9
+ import dash_bootstrap_components as dbc
10
+ import dash_dracula as dr
11
+
12
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
13
+ theme_store = dr.apply_theme(app) # wire CSS + toggle callback
14
+
15
+ # In your layout:
16
+ # dr.theme_toggle_button() — the ☀️/🌙 button component
17
+ # theme_store — dcc.Store that must be in the layout
18
+
19
+ # In your figures:
20
+ # template="dracula" — auto-registered on import
21
+ # line_color=dr.CYAN — use palette constants directly
22
+
23
+ # For PDF export (always light):
24
+ # from dash_dracula.pdf import fig_to_print_image
25
+ # img = fig_to_print_image(fig, w_mm=170, h_mm=100)
26
+ """
27
+
28
+ # ── Auto-register the Plotly template on import ──────────────────────────────
29
+ from .plotly_template import register as _register, TEMPLATE_NAME
30
+
31
+ _register()
32
+
33
+ # ── Colour palette ───────────────────────────────────────────────────────────
34
+ from .colors import (
35
+ BACKGROUND,
36
+ CURRENT_LINE,
37
+ SELECTION,
38
+ FOREGROUND,
39
+ COMMENT,
40
+ RED,
41
+ ORANGE,
42
+ YELLOW,
43
+ GREEN,
44
+ CYAN,
45
+ PURPLE,
46
+ PINK,
47
+ COLORWAY,
48
+ NEUTRAL_COLORWAY,
49
+ PRINT_COLORS,
50
+ LIGHT_COLORS,
51
+ AS_DICT as PALETTE,
52
+ )
53
+
54
+ # ── Bootstrap / theme wiring ─────────────────────────────────────────────────
55
+ from .bootstrap import apply_theme, theme_toggle_button
56
+
57
+ # ── PDF utility (lazy — reportlab not required for basic theme usage) ─────────
58
+ from .pdf import fig_to_print_image
59
+
60
+ __all__ = [
61
+ # Template
62
+ "TEMPLATE_NAME",
63
+ # Official palette
64
+ "BACKGROUND", "CURRENT_LINE", "SELECTION", "FOREGROUND", "COMMENT",
65
+ "RED", "ORANGE", "YELLOW", "GREEN", "CYAN", "PURPLE", "PINK",
66
+ # Convenience collections
67
+ "COLORWAY", "NEUTRAL_COLORWAY", "PRINT_COLORS", "LIGHT_COLORS", "PALETTE",
68
+ # Bootstrap helpers
69
+ "apply_theme", "theme_toggle_button",
70
+ # PDF
71
+ "fig_to_print_image",
72
+ ]
@@ -0,0 +1,493 @@
1
+ /* =============================================================
2
+ Dracula Theme
3
+ Palette: https://draculatheme.com/contribute
4
+ ============================================================= */
5
+
6
+ /* ── CSS variables ─────────────────────────────────────────── */
7
+ :root {
8
+ --dr-bg: #282a36;
9
+ --dr-surface: #44475a;
10
+ --dr-fg: #f8f8f2;
11
+ --dr-comment: #6272a4;
12
+ --dr-cyan: #8be9fd;
13
+ --dr-green: #50fa7b;
14
+ --dr-orange: #ffb86c;
15
+ --dr-pink: #ff79c6;
16
+ --dr-purple: #bd93f9;
17
+ --dr-red: #ff5555;
18
+ --dr-yellow: #f1fa8c;
19
+
20
+ /* Override Bootstrap 5 vars */
21
+ --bs-body-bg: var(--dr-bg);
22
+ --bs-body-color: var(--dr-fg);
23
+ --bs-card-bg: var(--dr-surface);
24
+ --bs-card-border-color:var(--dr-comment);
25
+ --bs-border-color: var(--dr-comment);
26
+ --bs-link-color: var(--dr-cyan);
27
+ --bs-link-hover-color: var(--dr-purple);
28
+ --bs-secondary-color: var(--dr-comment);
29
+ --bs-light-rgb: 68, 71, 90;
30
+ --bs-dark-rgb: 40, 42, 54;
31
+ --bs-nav-tabs-border-color: var(--dr-comment);
32
+ --bs-nav-tabs-link-active-bg: var(--dr-surface);
33
+ --bs-nav-tabs-link-active-color: var(--dr-cyan);
34
+ --bs-nav-tabs-link-hover-border-color: var(--dr-comment);
35
+ /* Override Bootstrap primary so text-primary / btn-primary use Dracula purple */
36
+ --bs-primary: #bd93f9;
37
+ --bs-primary-rgb: 189, 147, 249;
38
+ --bs-link-color-rgb: 139, 233, 253;
39
+ }
40
+
41
+ /* ── Base ───────────────────────────────────────────────────── */
42
+ body {
43
+ background-color: var(--dr-bg) !important;
44
+ color: var(--dr-fg) !important;
45
+ }
46
+
47
+ /* ── Typography ─────────────────────────────────────────────── */
48
+ h1, h2, h3, h4, h5, h6 { color: var(--dr-fg); }
49
+ hr { border-color: var(--dr-comment); opacity: 0.5; }
50
+ .text-muted { color: var(--dr-comment) !important; }
51
+ .text-primary { color: #8be9fd !important; } /* cyan, readable on any Dracula bg */
52
+ .text-success { color: #50fa7b !important; }
53
+ .text-warning { color: #f1fa8c !important; }
54
+ .text-danger { color: #ff5555 !important; }
55
+ .text-info { color: #8be9fd !important; }
56
+ .fw-semibold { font-weight: 600; }
57
+
58
+ /* ── Cards ──────────────────────────────────────────────────── */
59
+ .card {
60
+ background-color: #44475a !important;
61
+ border-color: #6272a4 !important;
62
+ color: #f8f8f2 !important;
63
+ }
64
+ .card-title { color: #8be9fd !important; }
65
+
66
+ /* ── Buttons ────────────────────────────────────────────────── */
67
+ .btn-primary { background-color: #bd93f9; border-color: #bd93f9; color: #282a36; }
68
+ .btn-primary:hover { background-color: #ff79c6; border-color: #ff79c6; }
69
+ .btn-outline-primary { border-color: #bd93f9; color: #bd93f9; }
70
+ .btn-outline-primary:hover { background-color: #bd93f9; color: #282a36; }
71
+ .btn-success { background-color: #50fa7b; border-color: #50fa7b; color: #282a36; }
72
+ .btn-outline-secondary { border-color: #6272a4; color: #6272a4; }
73
+ .btn-outline-secondary:hover { background-color: #6272a4; color: #282a36; }
74
+ .btn-outline-info { border-color: #8be9fd; color: #8be9fd; }
75
+ .btn-outline-info:hover { background-color: #8be9fd; color: #282a36; }
76
+
77
+ /* ── Forms ──────────────────────────────────────────────────── */
78
+ .form-control,
79
+ .form-select,
80
+ input[type="text"],
81
+ input[type="number"] {
82
+ background-color: var(--dr-surface) !important;
83
+ color: var(--dr-fg) !important;
84
+ border-color: var(--dr-comment) !important;
85
+ }
86
+ .form-control:focus,
87
+ .form-select:focus,
88
+ input:focus {
89
+ background-color: var(--dr-surface) !important;
90
+ color: var(--dr-fg) !important;
91
+ border-color: var(--dr-purple) !important;
92
+ box-shadow: 0 0 0 0.2rem rgba(189, 147, 249, 0.25) !important;
93
+ }
94
+ .form-text { color: var(--dr-comment) !important; }
95
+ label { color: var(--dr-fg) !important; }
96
+
97
+ /* ── Tabs ───────────────────────────────────────────────────── */
98
+ .nav-tabs { border-bottom-color: var(--dr-comment) !important; }
99
+ .nav-tabs .nav-link {
100
+ color: var(--dr-comment) !important;
101
+ border-color: transparent !important;
102
+ }
103
+ .nav-tabs .nav-link:hover {
104
+ color: var(--dr-fg) !important;
105
+ border-color: var(--dr-comment) !important;
106
+ }
107
+ .nav-tabs .nav-link.active {
108
+ background-color: var(--dr-surface) !important;
109
+ color: var(--dr-cyan) !important;
110
+ border-color: var(--dr-comment) var(--dr-comment) var(--dr-surface) !important;
111
+ }
112
+ .tab-content { background-color: var(--dr-bg) !important; }
113
+
114
+ /* ── Accordion ──────────────────────────────────────────────── */
115
+ .accordion-button {
116
+ background-color: var(--dr-surface) !important;
117
+ color: var(--dr-fg) !important;
118
+ }
119
+ .accordion-button:not(.collapsed) {
120
+ background-color: var(--dr-surface) !important;
121
+ color: var(--dr-cyan) !important;
122
+ box-shadow: none !important;
123
+ }
124
+ .accordion-button::after { filter: invert(1) brightness(2); }
125
+ .accordion-body {
126
+ background-color: var(--dr-bg) !important;
127
+ color: var(--dr-fg) !important;
128
+ }
129
+ .accordion-item {
130
+ background-color: var(--dr-bg) !important;
131
+ border-color: var(--dr-comment) !important;
132
+ }
133
+
134
+ /* ── dcc.Dropdown (Dash 4.x — Radix UI, dash-dropdown-* classes) ─────────── */
135
+ /* The Popover portal renders inside .dash-dropdown-wrapper, so CSS vars */
136
+ /* set here cascade into .dash-dropdown-content (the visible popup menu). */
137
+
138
+ /* Override Dash design-system CSS variables at the wrapper level */
139
+ .dash-dropdown-wrapper {
140
+ --Dash-Fill-Inverse-Strong: #44475a;
141
+ --Dash-Text-Strong: #f8f8f2;
142
+ --Dash-Text-Weak: #f8f8f2;
143
+ --Dash-Text-Disabled: #6272a4;
144
+ --Dash-Stroke-Strong: #6272a4;
145
+ --Dash-Fill-Interactive-Strong: #bd93f9;
146
+ --Dash-Fill-Interactive-Weak: rgba(189, 147, 249, 0.2);
147
+ --Dash-Fill-Disabled: #6272a4;
148
+ --Dash-Shading-Strong: rgba(0, 0, 0, 0.50);
149
+ --Dash-Shading-Weak: rgba(0, 0, 0, 0.25);
150
+ position: relative;
151
+ }
152
+
153
+ /* Trigger button */
154
+ .dash-dropdown {
155
+ background-color: #44475a !important;
156
+ border: 1px solid #6272a4 !important;
157
+ color: #f8f8f2 !important;
158
+ }
159
+ .dash-dropdown:focus {
160
+ border-color: #bd93f9 !important;
161
+ outline-color: #bd93f9 !important;
162
+ }
163
+
164
+ /* Value / placeholder text */
165
+ .dash-dropdown-value { color: #f8f8f2 !important; }
166
+ .dash-dropdown-placeholder { color: #6272a4 !important; }
167
+ .dash-dropdown-value-count { color: #f8f8f2 !important; background: rgba(189,147,249,0.2) !important; }
168
+
169
+ /* Caret icon & clear button */
170
+ .dash-dropdown-trigger-icon { color: #f8f8f2 !important; fill: #f8f8f2 !important; }
171
+ .dash-dropdown-clear { color: #6272a4 !important; }
172
+ .dash-dropdown-clear:hover { color: #ff5555 !important; }
173
+
174
+ /* ── Dropdown popup (the Radix Popover.Content) ────────────── */
175
+ .dash-dropdown-content {
176
+ background-color: #44475a !important;
177
+ border-color: #6272a4 !important;
178
+ color: #f8f8f2 !important;
179
+ z-index: 9999 !important;
180
+ }
181
+
182
+ /* Search area */
183
+ .dash-dropdown-search-container {
184
+ background-color: #282a36 !important;
185
+ border-color: #6272a4 !important;
186
+ }
187
+ .dash-dropdown-search {
188
+ background-color: transparent !important;
189
+ color: #f8f8f2 !important;
190
+ }
191
+ .dash-dropdown-search::placeholder { color: #6272a4 !important; }
192
+
193
+ /* Options list */
194
+ .dash-dropdown-options { background-color: #44475a !important; }
195
+ .dash-dropdown-option { background-color: #44475a !important; color: #f8f8f2 !important; }
196
+ .dash-dropdown-option:hover,
197
+ .dash-dropdown-option:focus-within { background-color: #6272a4 !important; color: #f8f8f2 !important; }
198
+ /* Selected option highlight (radio/checkbox checked inside option) */
199
+ .dash-dropdown-option:has(input:checked) { background-color: #44475a !important; color: #bd93f9 !important; }
200
+ .dash-dropdown-option:has(input:checked):hover { background-color: #6272a4 !important; }
201
+
202
+ /* Multi-select action buttons (Select All / Deselect All) */
203
+ .dash-dropdown-actions { background-color: #282a36 !important; border-color: #6272a4 !important; }
204
+ .dash-dropdown-action-button { color: #8be9fd !important; }
205
+ .dash-dropdown-action-button:hover { color: #bd93f9 !important; }
206
+
207
+ /* ── Bootstrap native <select> (dbc.Select) ────────────────── */
208
+ .form-select {
209
+ background-color: #44475a !important;
210
+ color: #f8f8f2 !important;
211
+ border-color: #6272a4 !important;
212
+ }
213
+ .form-select option { background-color: #44475a; color: #f8f8f2; }
214
+
215
+ /* ── Bootstrap dropdown menu ────────────────────────────────── */
216
+ .dropdown-menu { background-color: #44475a !important; border-color: #6272a4 !important; }
217
+ .dropdown-item { color: #f8f8f2 !important; }
218
+ .dropdown-item:hover,
219
+ .dropdown-item:focus { background-color: #6272a4 !important; color: #f8f8f2 !important; }
220
+
221
+ /* ── dash-ag-grid (ag-theme-alpine-dark Dracula overrides) ──────────────── */
222
+ /* ag-theme-alpine-dark already provides a dark background; we just nudge */
223
+ /* the colours to match Dracula. */
224
+ .ag-theme-alpine-dark {
225
+ --ag-background-color: #282a36;
226
+ --ag-header-background-color: #44475a;
227
+ --ag-odd-row-background-color: #2f3145;
228
+ --ag-row-hover-color: #44475a;
229
+ --ag-selected-row-background-color: rgba(189,147,249,0.25);
230
+ --ag-border-color: #6272a4;
231
+ --ag-header-foreground-color: #f8f8f2;
232
+ --ag-foreground-color: #f8f8f2;
233
+ --ag-secondary-foreground-color: #6272a4;
234
+ --ag-input-focus-border-color: #bd93f9;
235
+ --ag-range-selection-border-color: #bd93f9;
236
+ --ag-alpine-active-color: #bd93f9;
237
+ font-family: monospace;
238
+ font-size: 13px;
239
+ }
240
+ /* Cell editor (text / select input inside the grid) */
241
+ .ag-theme-alpine-dark .ag-popup-editor,
242
+ .ag-theme-alpine-dark .ag-select-list,
243
+ .ag-theme-alpine-dark .ag-select-list-item,
244
+ .ag-theme-alpine-dark .ag-popup,
245
+ .ag-theme-alpine-dark .ag-rich-select-list,
246
+ .ag-theme-alpine-dark .ag-select {
247
+ background-color: #44475a !important;
248
+ color: #f8f8f2 !important;
249
+ border-color: #6272a4 !important;
250
+ }
251
+ .ag-theme-alpine-dark .ag-select-list-item:hover,
252
+ .ag-theme-alpine-dark .ag-select-list-item.ag-active-item {
253
+ background-color: #6272a4 !important;
254
+ color: #f8f8f2 !important;
255
+ }
256
+ /* Input cells */
257
+ .ag-theme-alpine-dark .ag-cell-edit-wrapper input,
258
+ .ag-theme-alpine-dark .ag-text-field-input {
259
+ background-color: #282a36 !important;
260
+ color: #f8f8f2 !important;
261
+ border-color: #bd93f9 !important;
262
+ }
263
+
264
+ /* ── RadioItems / Checkboxes ────────────────────────────────── */
265
+ .form-check-label { color: #f8f8f2 !important; }
266
+ /* Radio/checkbox circle on both dark body AND lighter card (#44475a) backgrounds */
267
+ .form-check-input {
268
+ background-color: #282a36 !important;
269
+ border: 2px solid #8be9fd !important; /* cyan border — visible on both bg colours */
270
+ }
271
+ .form-check-input:checked {
272
+ background-color: #bd93f9 !important;
273
+ border-color: #bd93f9 !important;
274
+ }
275
+ .form-check-input:focus {
276
+ border-color: #bd93f9 !important;
277
+ box-shadow: 0 0 0 0.2rem rgba(189,147,249,0.25) !important;
278
+ }
279
+
280
+ /* ── Badges ─────────────────────────────────────────────────── */
281
+ .badge.bg-primary { background-color: var(--dr-purple) !important; }
282
+ .badge.bg-success { background-color: var(--dr-green) !important; color: var(--dr-bg) !important; }
283
+ .badge.bg-info { background-color: var(--dr-cyan) !important; color: var(--dr-bg) !important; }
284
+
285
+ /* ── Markdown (Handleiding tab) ─────────────────────────────── */
286
+ .dash-markdown h1 { color: var(--dr-purple); border-bottom: 1px solid var(--dr-comment); padding-bottom: 6px; }
287
+ .dash-markdown h2 { color: var(--dr-cyan); }
288
+ .dash-markdown h3 { color: var(--dr-green); }
289
+ .dash-markdown h4 { color: var(--dr-orange); }
290
+ .dash-markdown code {
291
+ background-color: var(--dr-surface);
292
+ color: var(--dr-yellow);
293
+ padding: 1px 5px;
294
+ border-radius: 4px;
295
+ }
296
+ .dash-markdown pre {
297
+ background-color: var(--dr-surface);
298
+ border: 1px solid var(--dr-comment);
299
+ border-radius: 6px;
300
+ padding: 12px;
301
+ color: var(--dr-fg);
302
+ }
303
+ .dash-markdown table { width: 100%; border-collapse: collapse; }
304
+ .dash-markdown th {
305
+ background-color: var(--dr-surface);
306
+ color: var(--dr-cyan);
307
+ border: 1px solid var(--dr-comment);
308
+ padding: 6px 10px;
309
+ }
310
+ .dash-markdown td {
311
+ border: 1px solid var(--dr-comment);
312
+ padding: 5px 10px;
313
+ color: var(--dr-fg);
314
+ }
315
+ .dash-markdown tr:nth-child(odd) td { background-color: #2f3145; }
316
+
317
+ /* ── Scrollbars ─────────────────────────────────────────────── */
318
+ * { scrollbar-color: var(--dr-comment) var(--dr-surface); scrollbar-width: thin; }
319
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
320
+ ::-webkit-scrollbar-track { background: var(--dr-surface); }
321
+ ::-webkit-scrollbar-thumb { background: var(--dr-comment); border-radius: 4px; }
322
+ ::-webkit-scrollbar-thumb:hover { background: var(--dr-purple); }
323
+
324
+ /* ── Alert / toast ──────────────────────────────────────────── */
325
+ .alert { background-color: var(--dr-surface) !important; border-color: var(--dr-comment) !important; color: var(--dr-fg) !important; }
326
+
327
+ /* =============================================================
328
+ LIGHT THEME OVERRIDES (active when <body class="light-theme">)
329
+ Override the CSS variables → almost everything re-colours
330
+ automatically.
331
+ ============================================================= */
332
+ body.light-theme {
333
+ /* Overwrite Dracula palette with clean light-mode colours */
334
+ --dr-bg: #f5f5f5;
335
+ --dr-surface: #ffffff;
336
+ --dr-fg: #212529;
337
+ --dr-comment: #6c757d;
338
+ --dr-cyan: #0066cc;
339
+ --dr-green: #198754;
340
+ --dr-orange: #e06c00;
341
+ --dr-pink: #c0357a;
342
+ --dr-purple: #6610f2;
343
+ --dr-red: #dc3545;
344
+ --dr-yellow: #b58900;
345
+
346
+ /* Bootstrap body variables */
347
+ --bs-body-bg: #f5f5f5;
348
+ --bs-body-color: #212529;
349
+ --bs-card-bg: #ffffff;
350
+ --bs-card-border-color:#dee2e6;
351
+ --bs-border-color: #dee2e6;
352
+ --bs-link-color: #0066cc;
353
+ --bs-link-hover-color: #6610f2;
354
+ --bs-secondary-color: #6c757d;
355
+ --bs-primary: #6610f2;
356
+ --bs-primary-rgb: 102, 16, 242;
357
+ --bs-link-color-rgb: 0, 102, 204;
358
+ --bs-nav-tabs-border-color: #dee2e6;
359
+ --bs-nav-tabs-link-active-bg: #ffffff;
360
+ --bs-nav-tabs-link-active-color: #0066cc;
361
+ --bs-nav-tabs-link-hover-border-color: #dee2e6;
362
+ }
363
+
364
+ /* Body & base text */
365
+ body.light-theme { background-color: #f5f5f5 !important; color: #212529 !important; }
366
+ body.light-theme h1,
367
+ body.light-theme h2,
368
+ body.light-theme h3,
369
+ body.light-theme h4,
370
+ body.light-theme h5,
371
+ body.light-theme h6 { color: #212529; }
372
+ body.light-theme hr { border-color: #dee2e6; opacity: 0.8; }
373
+ body.light-theme .text-muted { color: #6c757d !important; }
374
+ body.light-theme label { color: #212529 !important; }
375
+
376
+ /* Cards */
377
+ body.light-theme .card { background-color: #ffffff !important; border-color: #dee2e6 !important; color: #212529 !important; }
378
+ body.light-theme .card-title { color: #0066cc !important; }
379
+ body.light-theme .tab-content { background-color: #f5f5f5 !important; }
380
+
381
+ /* Nav tabs */
382
+ body.light-theme .nav-tabs { border-bottom-color: #dee2e6 !important; }
383
+ body.light-theme .nav-tabs .nav-link { color: #6c757d !important; border-color: transparent !important; }
384
+ body.light-theme .nav-tabs .nav-link:hover { color: #212529 !important; border-color: #dee2e6 !important; }
385
+ body.light-theme .nav-tabs .nav-link.active { background-color: #ffffff !important; color: #0066cc !important; border-color: #dee2e6 #dee2e6 #ffffff !important; }
386
+
387
+ /* Buttons */
388
+ body.light-theme .btn-primary { background-color: #6610f2; border-color: #6610f2; color: #fff; }
389
+ body.light-theme .btn-primary:hover { background-color: #520dc2; border-color: #520dc2; }
390
+ body.light-theme .btn-outline-primary { border-color: #6610f2; color: #6610f2; }
391
+ body.light-theme .btn-outline-primary:hover { background-color: #6610f2; color: #fff; }
392
+ body.light-theme .btn-success { background-color: #198754; border-color: #198754; color: #fff; }
393
+ body.light-theme .btn-outline-secondary { border-color: #adb5bd; color: #495057; }
394
+ body.light-theme .btn-outline-secondary:hover { background-color: #adb5bd; color: #212529; }
395
+ body.light-theme .btn-outline-info { border-color: #0dcaf0; color: #0dcaf0; }
396
+
397
+ /* Forms */
398
+ body.light-theme .form-control,
399
+ body.light-theme .form-select,
400
+ body.light-theme input[type="text"],
401
+ body.light-theme input[type="number"] {
402
+ background-color: #ffffff !important;
403
+ color: #212529 !important;
404
+ border-color: #ced4da !important;
405
+ }
406
+ body.light-theme .form-control:focus,
407
+ body.light-theme input:focus {
408
+ border-color: #6610f2 !important;
409
+ box-shadow: 0 0 0 0.2rem rgba(102,16,242,0.2) !important;
410
+ }
411
+ body.light-theme .form-check-label { color: #212529 !important; }
412
+ body.light-theme .form-check-input {
413
+ background-color: #ffffff !important;
414
+ border: 2px solid #adb5bd !important;
415
+ }
416
+ body.light-theme .form-check-input:checked {
417
+ background-color: #6610f2 !important;
418
+ border-color: #6610f2 !important;
419
+ }
420
+
421
+ /* Alerts */
422
+ body.light-theme .alert {
423
+ background-color: #e8f4fd !important;
424
+ border-color: #90c6f0 !important;
425
+ color: #1a4a70 !important;
426
+ }
427
+
428
+ /* Accordion */
429
+ body.light-theme .accordion-button { background-color: #f0f0f0 !important; color: #212529 !important; }
430
+ body.light-theme .accordion-button:not(.collapsed) { background-color: #e9ecef !important; color: #0066cc !important; box-shadow: none !important; }
431
+ body.light-theme .accordion-button::after { filter: none; }
432
+ body.light-theme .accordion-body { background-color: #f5f5f5 !important; color: #212529 !important; }
433
+ body.light-theme .accordion-item { background-color: #f5f5f5 !important; border-color: #dee2e6 !important; }
434
+
435
+ /* dcc.Dropdown (Dash 4 Radix) */
436
+ body.light-theme .dash-dropdown-wrapper {
437
+ --Dash-Fill-Inverse-Strong: #ffffff;
438
+ --Dash-Text-Strong: #212529;
439
+ --Dash-Text-Weak: #495057;
440
+ --Dash-Text-Disabled: #adb5bd;
441
+ --Dash-Stroke-Strong: #ced4da;
442
+ --Dash-Fill-Interactive-Strong: #6610f2;
443
+ --Dash-Fill-Interactive-Weak: rgba(102,16,242,0.12);
444
+ --Dash-Fill-Disabled: #e9ecef;
445
+ --Dash-Shading-Strong: rgba(0,0,0,0.12);
446
+ --Dash-Shading-Weak: rgba(0,0,0,0.06);
447
+ }
448
+ body.light-theme .dash-dropdown { background-color: #ffffff !important; border-color: #ced4da !important; color: #212529 !important; }
449
+ body.light-theme .dash-dropdown-value { color: #212529 !important; }
450
+ body.light-theme .dash-dropdown-placeholder { color: #adb5bd !important; }
451
+ body.light-theme .dash-dropdown-trigger-icon { color: #495057 !important; fill: #495057 !important; }
452
+ body.light-theme .dash-dropdown-content { background-color: #ffffff !important; border-color: #ced4da !important; color: #212529 !important; }
453
+ body.light-theme .dash-dropdown-options { background-color: #ffffff !important; }
454
+ body.light-theme .dash-dropdown-option { background-color: #ffffff !important; color: #212529 !important; }
455
+ body.light-theme .dash-dropdown-option:hover { background-color: #f2f2f2 !important; }
456
+ body.light-theme .dash-dropdown-search-container { background-color: #f5f5f5 !important; border-color: #dee2e6 !important; }
457
+ body.light-theme .dash-dropdown-search { color: #212529 !important; }
458
+ body.light-theme .dash-dropdown-search::placeholder { color: #adb5bd !important; }
459
+
460
+ /* ag-grid */
461
+ body.light-theme .ag-theme-alpine-dark {
462
+ --ag-background-color: #ffffff;
463
+ --ag-header-background-color: #f0f0f0;
464
+ --ag-odd-row-background-color: #fafafa;
465
+ --ag-row-hover-color: #e9ecef;
466
+ --ag-selected-row-background-color: rgba(102,16,242,0.10);
467
+ --ag-border-color: #dee2e6;
468
+ --ag-header-foreground-color: #212529;
469
+ --ag-foreground-color: #212529;
470
+ --ag-secondary-foreground-color: #6c757d;
471
+ --ag-input-focus-border-color: #6610f2;
472
+ --ag-range-selection-border-color: #6610f2;
473
+ --ag-alpine-active-color: #6610f2;
474
+ }
475
+ body.light-theme .ag-theme-alpine-dark .ag-popup-editor,
476
+ body.light-theme .ag-theme-alpine-dark .ag-select-list,
477
+ body.light-theme .ag-theme-alpine-dark .ag-popup {
478
+ background-color: #ffffff !important;
479
+ color: #212529 !important;
480
+ border-color: #dee2e6 !important;
481
+ }
482
+ body.light-theme .ag-theme-alpine-dark .ag-cell-edit-wrapper input,
483
+ body.light-theme .ag-theme-alpine-dark .ag-text-field-input {
484
+ background-color: #f5f5f5 !important;
485
+ color: #212529 !important;
486
+ border-color: #6610f2 !important;
487
+ }
488
+
489
+ /* Scrollbars */
490
+ body.light-theme * { scrollbar-color: #adb5bd #f5f5f5; }
491
+ body.light-theme ::-webkit-scrollbar-track { background: #f5f5f5; }
492
+ body.light-theme ::-webkit-scrollbar-thumb { background: #adb5bd; }
493
+ body.light-theme ::-webkit-scrollbar-thumb:hover { background: #6610f2; }
@@ -0,0 +1,226 @@
1
+ """
2
+ dash_dracula.bootstrap
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+ Helpers to wire the Dracula theme into any Dash application:
5
+
6
+ * ``apply_theme(app, default_theme="dark")``
7
+ Copies ``dracula.css`` into the app's ``assets/`` folder, registers the
8
+ Plotly-graph theme-toggle ``clientside_callback`` on the app, and returns
9
+ a ``dcc.Store`` component that must be added to the layout.
10
+
11
+ * ``theme_toggle_button()``
12
+ Returns a pre-configured ``dbc.Button`` that drives the toggle.
13
+
14
+ Usage::
15
+
16
+ import dash
17
+ import dash_bootstrap_components as dbc
18
+ import dash_dracula as dr
19
+
20
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
21
+ theme_store = dr.apply_theme(app) # add theme_store to your layout
22
+
23
+ # Somewhere in your layout:
24
+ layout = dbc.Container([
25
+ dr.theme_toggle_button(),
26
+ theme_store,
27
+ ...
28
+ ])
29
+ """
30
+ import os
31
+ import shutil
32
+
33
+ import dash
34
+ import dash_bootstrap_components as dbc
35
+ from dash import Input, Output, State, dcc
36
+
37
+ # ── JS payload ───────────────────────────────────────────────────────────────
38
+ # Extracted verbatim from app.py — kept in one place so both the original app
39
+ # and any new app share exactly the same toggle logic.
40
+ _TOGGLE_JS = """
41
+ function(n_clicks, current_theme) {
42
+ if (!n_clicks) {
43
+ return [window.dash_clientside.no_update, window.dash_clientside.no_update];
44
+ }
45
+ var new_theme = (current_theme === 'dark') ? 'light' : 'dark';
46
+ var is_light = (new_theme === 'light');
47
+
48
+ // Toggle CSS class on <body> — the light-theme CSS block handles the rest
49
+ document.body.classList.toggle('light-theme', is_light);
50
+
51
+ // Plotly graph layout overrides
52
+ var layout_light = {
53
+ paper_bgcolor: '#ffffff',
54
+ plot_bgcolor: '#f5f5f5',
55
+ 'font.color': '#212529',
56
+ 'title.font.color': '#212529',
57
+ 'xaxis.gridcolor': '#dee2e6',
58
+ 'xaxis.linecolor': '#adb5bd',
59
+ 'xaxis.tickcolor': '#adb5bd',
60
+ 'xaxis.tickfont.color': '#495057',
61
+ 'xaxis.title.font.color': '#212529',
62
+ 'yaxis.gridcolor': '#dee2e6',
63
+ 'yaxis.linecolor': '#adb5bd',
64
+ 'yaxis.tickcolor': '#adb5bd',
65
+ 'yaxis.tickfont.color': '#495057',
66
+ 'yaxis.title.font.color': '#212529',
67
+ 'legend.bgcolor': '#ffffff',
68
+ 'legend.bordercolor': '#dee2e6',
69
+ 'legend.font.color': '#212529',
70
+ 'coloraxis.colorbar.tickfont.color': '#212529',
71
+ 'coloraxis.colorbar.title.font.color': '#212529',
72
+ 'scene.bgcolor': '#f5f5f5',
73
+ 'scene.xaxis.backgroundcolor': '#ffffff',
74
+ 'scene.yaxis.backgroundcolor': '#ffffff',
75
+ 'scene.zaxis.backgroundcolor': '#ffffff',
76
+ 'scene.xaxis.gridcolor': '#dee2e6',
77
+ 'scene.yaxis.gridcolor': '#dee2e6',
78
+ 'scene.zaxis.gridcolor': '#dee2e6',
79
+ 'scene.xaxis.linecolor': '#adb5bd',
80
+ 'scene.yaxis.linecolor': '#adb5bd',
81
+ 'scene.zaxis.linecolor': '#adb5bd',
82
+ 'scene.xaxis.tickfont.color': '#495057',
83
+ 'scene.yaxis.tickfont.color': '#495057',
84
+ 'scene.zaxis.tickfont.color': '#495057',
85
+ 'scene.xaxis.title.font.color': '#212529',
86
+ 'scene.yaxis.title.font.color': '#212529',
87
+ 'scene.zaxis.title.font.color': '#212529'
88
+ };
89
+ var layout_dark = {
90
+ paper_bgcolor: '#282a36',
91
+ plot_bgcolor: '#282a36',
92
+ 'font.color': '#f8f8f2',
93
+ 'title.font.color': '#f8f8f2',
94
+ 'xaxis.gridcolor': '#44475a',
95
+ 'xaxis.linecolor': '#6272a4',
96
+ 'xaxis.tickcolor': '#6272a4',
97
+ 'xaxis.tickfont.color': '#f8f8f2',
98
+ 'xaxis.title.font.color': '#f8f8f2',
99
+ 'yaxis.gridcolor': '#44475a',
100
+ 'yaxis.linecolor': '#6272a4',
101
+ 'yaxis.tickcolor': '#6272a4',
102
+ 'yaxis.tickfont.color': '#f8f8f2',
103
+ 'yaxis.title.font.color': '#f8f8f2',
104
+ 'legend.bgcolor': '#44475a',
105
+ 'legend.bordercolor': '#6272a4',
106
+ 'legend.font.color': '#f8f8f2',
107
+ 'coloraxis.colorbar.tickfont.color': '#f8f8f2',
108
+ 'coloraxis.colorbar.title.font.color': '#f8f8f2',
109
+ 'scene.bgcolor': '#282a36',
110
+ 'scene.xaxis.backgroundcolor': '#282a36',
111
+ 'scene.yaxis.backgroundcolor': '#282a36',
112
+ 'scene.zaxis.backgroundcolor': '#282a36',
113
+ 'scene.xaxis.gridcolor': '#44475a',
114
+ 'scene.yaxis.gridcolor': '#44475a',
115
+ 'scene.zaxis.gridcolor': '#44475a',
116
+ 'scene.xaxis.linecolor': '#6272a4',
117
+ 'scene.yaxis.linecolor': '#6272a4',
118
+ 'scene.zaxis.linecolor': '#6272a4',
119
+ 'scene.xaxis.tickfont.color': '#f8f8f2',
120
+ 'scene.yaxis.tickfont.color': '#f8f8f2',
121
+ 'scene.zaxis.tickfont.color': '#f8f8f2',
122
+ 'scene.xaxis.title.font.color': '#f8f8f2',
123
+ 'scene.yaxis.title.font.color': '#f8f8f2',
124
+ 'scene.zaxis.title.font.color': '#f8f8f2'
125
+ };
126
+ var patch = is_light ? layout_light : layout_dark;
127
+
128
+ document.querySelectorAll('.js-plotly-plot').forEach(function(el) {
129
+ try { Plotly.relayout(el, patch); } catch(e) {}
130
+ });
131
+
132
+ return [new_theme, is_light ? '\\u{1F319} Donker' : '\\u2600\\uFE0F Licht'];
133
+ }
134
+ """
135
+
136
+
137
+ def _assets_src() -> str:
138
+ """Absolute path to the ``assets/`` folder inside this package."""
139
+ return os.path.join(os.path.dirname(__file__), "assets")
140
+
141
+
142
+ def _ensure_css_in_app(app: dash.Dash) -> None:
143
+ """Copy ``dracula.css`` into the app's assets folder if not already there.
144
+
145
+ Dash 4 auto-serves exactly one ``assets/`` folder per app. The simplest
146
+ portable approach is to copy the CSS there at startup.
147
+ """
148
+ src = os.path.join(_assets_src(), "dracula.css")
149
+ # Resolve the app's assets directory
150
+ try:
151
+ assets_folder = os.path.abspath(app.config.assets_folder)
152
+ except AttributeError:
153
+ assets_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
154
+
155
+ os.makedirs(assets_folder, exist_ok=True)
156
+ dest = os.path.join(assets_folder, "dracula.css")
157
+ if not os.path.exists(dest):
158
+ shutil.copy(src, dest)
159
+
160
+
161
+ def theme_toggle_button() -> dbc.Button:
162
+ """Return the pre-configured dark/light toggle button.
163
+
164
+ The button's ``id`` must be ``"theme-toggle-btn"`` — the clientside
165
+ callback registered by :func:`apply_theme` listens for clicks on that id.
166
+ """
167
+ return dbc.Button(
168
+ "☀️ Licht",
169
+ id="theme-toggle-btn",
170
+ color="secondary",
171
+ outline=True,
172
+ size="sm",
173
+ )
174
+
175
+
176
+ def apply_theme(
177
+ app: dash.Dash,
178
+ default_theme: str = "dark",
179
+ ) -> dcc.Store:
180
+ """Wire the Dracula theme into a Dash application.
181
+
182
+ 1. Copies ``dracula.css`` into the app's ``assets/`` folder so Dash
183
+ serves it automatically.
184
+ 2. Registers a ``clientside_callback`` on the app that toggles
185
+ ``body.light-theme`` and re-colours all Plotly graphs when the
186
+ ``#theme-toggle-btn`` button is clicked.
187
+
188
+ Parameters
189
+ ----------
190
+ app:
191
+ The ``dash.Dash`` instance to configure.
192
+ default_theme:
193
+ Either ``"dark"`` (default) or ``"light"``.
194
+
195
+ Returns
196
+ -------
197
+ dcc.Store
198
+ A hidden store component with ``id="theme-store"`` that **must** be
199
+ added to the app layout. It persists the current theme value so the
200
+ callback always has access to it.
201
+
202
+ Example
203
+ -------
204
+ ::
205
+
206
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
207
+ theme_store = apply_theme(app)
208
+
209
+ app.layout = dbc.Container([
210
+ theme_toggle_button(),
211
+ theme_store,
212
+ ...
213
+ ])
214
+ """
215
+ _ensure_css_in_app(app)
216
+
217
+ app.clientside_callback(
218
+ _TOGGLE_JS,
219
+ Output("theme-store", "data"),
220
+ Output("theme-toggle-btn", "children"),
221
+ Input("theme-toggle-btn", "n_clicks"),
222
+ State("theme-store", "data"),
223
+ prevent_initial_call=True,
224
+ )
225
+
226
+ return dcc.Store(id="theme-store", data=default_theme)
dash_dracula/colors.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ dash_dracula.colors
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ Official Dracula colour palette constants.
5
+ Source: https://draculatheme.com/contribute
6
+
7
+ All hex values are lowercase to match CSS convention.
8
+ """
9
+
10
+ # ── Official Dracula palette ─────────────────────────────────────────────────
11
+ # https://draculatheme.com/contribute
12
+ BACKGROUND = "#282a36" # main background
13
+ CURRENT_LINE = "#44475a" # current-line / selection highlight
14
+ SELECTION = "#44475a" # alias for CURRENT_LINE
15
+ FOREGROUND = "#f8f8f2" # default text
16
+ COMMENT = "#6272a4" # comments / secondary UI text
17
+
18
+ # Accent colours
19
+ RED = "#ff5555"
20
+ ORANGE = "#ffb86c"
21
+ YELLOW = "#f1fa8c"
22
+ GREEN = "#50fa7b"
23
+ CYAN = "#8be9fd"
24
+ PURPLE = "#bd93f9"
25
+ PINK = "#ff79c6"
26
+
27
+ # ── Plotly colorway (dark theme traces) ─────────────────────────────────────
28
+ # Order: most-distinct first so first few traces are always visually separated
29
+ COLORWAY = [CYAN, PURPLE, GREEN, ORANGE, PINK, RED, YELLOW]
30
+
31
+ # ── Neutral colorway (readable on both dark AND light backgrounds) ───────────
32
+ # Use these for data traces when your app supports a light-mode toggle.
33
+ NEUTRAL_COLORWAY = [
34
+ "#1f77b4", # Plotly default blue — readable on both backgrounds
35
+ "#7b2fbe", # mid-purple — readable on both backgrounds
36
+ "#2ca02c", # medium green — readable on both backgrounds
37
+ "#e09b00", # amber — readable on both backgrounds
38
+ "#c0357a", # dark pink — readable on both backgrounds
39
+ "#dc3545", # Bootstrap danger red
40
+ "#17becf", # teal
41
+ ]
42
+
43
+ # ── Print / PDF colours ──────────────────────────────────────────────────────
44
+ # Darkened Dracula variants — Dracula neon is unreadable on white paper.
45
+ PRINT_COLORS = {
46
+ "background": "#ffffff",
47
+ "surface": "#e8e8e8",
48
+ "foreground": "#000000",
49
+ "cyan": "#0055aa",
50
+ "purple": "#5533aa",
51
+ "green": "#006622",
52
+ "orange": "#cc5500",
53
+ "grid": "#999999",
54
+ "alt_row": "#f2f2f2",
55
+ # figure internals
56
+ "fig_dark": "#111111",
57
+ "fig_mid": "#444444",
58
+ "fig_grid": "#dddddd",
59
+ "fig_line": "#999999",
60
+ }
61
+
62
+ # ── Light-mode UI colours (CSS `body.light-theme` overrides) ─────────────────
63
+ LIGHT_COLORS = {
64
+ "background": "#f5f5f5",
65
+ "surface": "#ffffff",
66
+ "foreground": "#212529",
67
+ "comment": "#6c757d",
68
+ "cyan": "#0066cc",
69
+ "green": "#198754",
70
+ "orange": "#e06c00",
71
+ "pink": "#c0357a",
72
+ "purple": "#6610f2",
73
+ "red": "#dc3545",
74
+ "yellow": "#b58900",
75
+ "border": "#dee2e6",
76
+ "axis_line": "#adb5bd",
77
+ "tick_font": "#495057",
78
+ }
79
+
80
+ # ── Convenience dict of all official palette colours ────────────────────────
81
+ AS_DICT = {
82
+ "background": BACKGROUND,
83
+ "current_line": CURRENT_LINE,
84
+ "selection": SELECTION,
85
+ "foreground": FOREGROUND,
86
+ "comment": COMMENT,
87
+ "red": RED,
88
+ "orange": ORANGE,
89
+ "yellow": YELLOW,
90
+ "green": GREEN,
91
+ "cyan": CYAN,
92
+ "purple": PURPLE,
93
+ "pink": PINK,
94
+ }
dash_dracula/pdf.py ADDED
@@ -0,0 +1,161 @@
1
+ """
2
+ dash_dracula.pdf
3
+ ~~~~~~~~~~~~~~~~~
4
+ Utility for rendering Plotly figures to print-ready ReportLab ``Image``
5
+ objects. The output is **always light** (white background, dark text)
6
+ regardless of the current dashboard theme.
7
+
8
+ Requires the ``pdf`` extra::
9
+
10
+ pip install "dash-dracula-theme[pdf]"
11
+ # or: pip install reportlab kaleido
12
+
13
+ Usage::
14
+
15
+ from dash_dracula.pdf import fig_to_print_image
16
+
17
+ img = fig_to_print_image(my_figure, w_mm=170, h_mm=100)
18
+ # img is a reportlab.platypus.Image ready to include in a PDF
19
+ """
20
+
21
+
22
+ def fig_to_print_image(fig_dict_or_fig, w_mm: float = 170, h_mm: float = 100):
23
+ """Render a Plotly figure to a ReportLab ``Image``, fully overridden for print.
24
+
25
+ All Dracula dark-theme colours are stripped and replaced with a
26
+ ``simple_white`` template on a white background with dark fonts.
27
+ 3-D figures receive additional ``scene.bgcolor`` and axis-backgroundcolor
28
+ fixes that beat any baked-in template values.
29
+
30
+ Parameters
31
+ ----------
32
+ fig_dict_or_fig:
33
+ Either a Plotly ``go.Figure`` instance or a dict (e.g. from
34
+ ``dcc.Graph.figure``).
35
+ w_mm:
36
+ Output width in millimetres.
37
+ h_mm:
38
+ Output height in millimetres.
39
+
40
+ Returns
41
+ -------
42
+ reportlab.platypus.Image or None
43
+ ``None`` if rendering fails or the input is ``None``.
44
+ """
45
+ if fig_dict_or_fig is None:
46
+ return None
47
+ try:
48
+ import io
49
+
50
+ import plotly.graph_objects as go
51
+ import plotly.io as pio
52
+ from reportlab.lib.units import mm
53
+ from reportlab.platypus import Image
54
+
55
+ fig_obj = (
56
+ go.Figure(fig_dict_or_fig)
57
+ if isinstance(fig_dict_or_fig, dict)
58
+ else fig_dict_or_fig
59
+ )
60
+
61
+ DARK = "#111111"
62
+ MID = "#444444"
63
+ GRID = "#dddddd"
64
+ LINE = "#999999"
65
+ WHITE = "white"
66
+
67
+ axis_style_2d = dict(
68
+ title_font=dict(color=DARK, size=11),
69
+ tickfont=dict(color=MID, size=9),
70
+ linecolor=LINE,
71
+ gridcolor=GRID,
72
+ zerolinecolor=LINE,
73
+ )
74
+ axis_style_3d = dict(
75
+ title_font=dict(color=DARK, size=10),
76
+ tickfont=dict(color=MID, size=8),
77
+ linecolor=LINE,
78
+ gridcolor=GRID,
79
+ backgroundcolor=WHITE,
80
+ )
81
+
82
+ is_3d = bool(
83
+ fig_obj.data
84
+ and any(
85
+ (hasattr(t, "z") and hasattr(t, "surfacecolor"))
86
+ or t.type in ("surface", "scatter3d", "mesh3d")
87
+ for t in fig_obj.data
88
+ )
89
+ )
90
+
91
+ common = dict(
92
+ template="simple_white",
93
+ paper_bgcolor=WHITE,
94
+ font=dict(color=DARK, size=10),
95
+ title_font=dict(color=DARK, size=13),
96
+ legend=dict(
97
+ font=dict(color=DARK),
98
+ bgcolor=WHITE,
99
+ bordercolor=LINE,
100
+ borderwidth=1,
101
+ ),
102
+ )
103
+
104
+ if is_3d:
105
+ fig_obj.update_layout(
106
+ **common,
107
+ scene=dict(
108
+ bgcolor=WHITE,
109
+ xaxis=dict(**axis_style_3d),
110
+ yaxis=dict(**axis_style_3d),
111
+ zaxis=dict(**axis_style_3d),
112
+ ),
113
+ margin=dict(l=10, r=10, t=70, b=10),
114
+ )
115
+ # Direct attribute assignment beats any baked-in template values
116
+ try:
117
+ fig_obj.layout.template = None
118
+ fig_obj.layout.scene.bgcolor = WHITE
119
+ for _ax in (
120
+ fig_obj.layout.scene.xaxis,
121
+ fig_obj.layout.scene.yaxis,
122
+ fig_obj.layout.scene.zaxis,
123
+ ):
124
+ _ax.backgroundcolor = WHITE
125
+ _ax.gridcolor = GRID
126
+ _ax.linecolor = LINE
127
+ except Exception:
128
+ pass
129
+ else:
130
+ fig_obj.update_layout(
131
+ **common,
132
+ plot_bgcolor=WHITE,
133
+ xaxis=dict(**axis_style_2d),
134
+ yaxis=dict(**axis_style_2d),
135
+ margin=dict(l=75, r=20, t=60, b=70),
136
+ )
137
+ for ax in ["xaxis2", "xaxis3", "yaxis2", "yaxis3"]:
138
+ if getattr(fig_obj.layout, ax, None):
139
+ fig_obj.update_layout(**{ax: axis_style_2d})
140
+
141
+ # Fix colorbar and parcoords fonts
142
+ for trace in fig_obj.data:
143
+ if hasattr(trace, "colorbar") and trace.colorbar:
144
+ trace.colorbar.tickfont = dict(color=DARK)
145
+ if trace.colorbar.title:
146
+ trace.colorbar.title.font = dict(color=DARK)
147
+ if hasattr(trace, "labelfont"):
148
+ trace.labelfont = dict(color=DARK, size=10)
149
+ if hasattr(trace, "tickfont") and trace.type == "parcoords":
150
+ trace.tickfont = dict(color=MID, size=8)
151
+
152
+ png = pio.to_image(
153
+ fig_obj,
154
+ format="png",
155
+ width=int(w_mm * 3.78),
156
+ height=int(h_mm * 3.78),
157
+ scale=2,
158
+ )
159
+ return Image(io.BytesIO(png), width=w_mm * mm, height=h_mm * mm)
160
+ except Exception:
161
+ return None
@@ -0,0 +1,47 @@
1
+ """
2
+ dash_dracula.plotly_template
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Registers ``pio.templates["dracula"]`` — a Plotly layout template using the
5
+ official Dracula colour palette.
6
+
7
+ Call ``register()`` (or simply ``import dash_dracula``) before creating any
8
+ Plotly figures so that ``template="dracula"`` is available everywhere.
9
+ """
10
+ import plotly.graph_objects as go
11
+ import plotly.io as pio
12
+
13
+ from .colors import (
14
+ BACKGROUND, CURRENT_LINE, FOREGROUND, COMMENT, COLORWAY,
15
+ )
16
+
17
+ TEMPLATE_NAME = "dracula"
18
+
19
+ _AXIS_STYLE = dict(
20
+ gridcolor=CURRENT_LINE,
21
+ zerolinecolor=COMMENT,
22
+ linecolor=COMMENT,
23
+ tickcolor=COMMENT,
24
+ )
25
+
26
+
27
+ def register() -> None:
28
+ """Register ``pio.templates["dracula"]``. Safe to call multiple times."""
29
+ if TEMPLATE_NAME in pio.templates:
30
+ return
31
+ pio.templates[TEMPLATE_NAME] = go.layout.Template(
32
+ layout=go.Layout(
33
+ paper_bgcolor=BACKGROUND,
34
+ plot_bgcolor=BACKGROUND,
35
+ font=dict(color=FOREGROUND),
36
+ colorway=COLORWAY,
37
+ xaxis=_AXIS_STYLE,
38
+ yaxis=_AXIS_STYLE,
39
+ legend=dict(bgcolor=CURRENT_LINE, bordercolor=COMMENT),
40
+ title=dict(font=dict(color=FOREGROUND)),
41
+ )
42
+ )
43
+
44
+
45
+ def unregister() -> None:
46
+ """Remove the template from the registry (useful for testing)."""
47
+ pio.templates.pop(TEMPLATE_NAME, None)
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: dash-dracula-theme
3
+ Version: 0.1.0
4
+ Summary: Dracula dark/light theme for Plotly Dash applications
5
+ Author: hjeverts-VSM
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 hjeverts-VSM
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: dash>=4.0
32
+ Requires-Dist: plotly>=5.0
33
+ Requires-Dist: dash-bootstrap-components>=2.0
34
+ Provides-Extra: pdf
35
+ Requires-Dist: reportlab>=4.0; extra == "pdf"
36
+ Requires-Dist: kaleido>=0.2; extra == "pdf"
37
+ Dynamic: license-file
38
+
39
+ # Dracula for [Dash](https://dash.plotly.com/)
40
+
41
+ > A dark theme for [Plotly Dash](https://dash.plotly.com/) — pip-installable Python package
42
+ > that gives any Dash application the full Dracula colour experience, including a light-mode
43
+ > toggle and always-light PDF export.
44
+
45
+ | Dark mode | Light mode |
46
+ |---|---|
47
+ | ![Dark mode](screenshot_dark.png) | ![Light mode](screenshot_light.png) |
48
+
49
+ ## Install
50
+
51
+ All instructions can be found at [INSTALL.md](INSTALL.md).
52
+
53
+ ## Features
54
+
55
+ - **Dark mode** — full Dracula palette applied via CSS variables and a custom Plotly template
56
+ - **Light mode toggle** — single button switches the entire dashboard; all Plotly graphs
57
+ re-colour automatically via a clientside callback
58
+ - **PDF export** — `fig_to_print_image()` always produces print-ready figures on a white
59
+ background, regardless of the current dashboard theme
60
+ - **Official palette** — all 11 Dracula colours exposed as named Python constants
61
+ (`BACKGROUND`, `CYAN`, `PURPLE`, …)
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ import dash
67
+ import dash_bootstrap_components as dbc
68
+ import dash_dracula as dr
69
+
70
+ # 1. Create the app with Bootstrap DARKLY as base
71
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
72
+
73
+ # 2. Wire the Dracula theme (copies CSS, registers clientside toggle)
74
+ theme_store = dr.apply_theme(app) # returns dcc.Store for the layout
75
+
76
+ # 3. Use Dracula colours and template in your figures
77
+ fig = go.Figure(layout=dict(template="dracula"))
78
+ fig.add_scatter(y=[1, 2, 3], line_color=dr.CYAN)
79
+
80
+ # 4. Export a figure to a print-ready ReportLab Image (always light)
81
+ from dash_dracula.pdf import fig_to_print_image
82
+ img = fig_to_print_image(fig, w_mm=170, h_mm=100)
83
+ ```
84
+
85
+ See [sample/app.py](sample/app.py) for a complete minimal example.
86
+
87
+ ## Team
88
+
89
+ This theme is maintained by the following person(s) and a bunch of
90
+ [awesome contributors](https://github.com/dracula/dash/graphs/contributors).
91
+
92
+ | [hjeverts](https://github.com/hjeverts) |
93
+ |---|
94
+
95
+ ## Community
96
+
97
+ - [GitHub](https://github.com/dracula/dracula-theme/discussions) — Best for asking questions and discussing issues.
98
+ - [Discord](https://draculatheme.com/discord-invite) — Best for hanging out with the community.
99
+
100
+ ## License
101
+
102
+ [MIT License](LICENSE)
@@ -0,0 +1,11 @@
1
+ dash_dracula/__init__.py,sha256=qfK_WBbjn5x_axOcaXzDhYth8H6gxS2wBIuvPpD2bX0,2392
2
+ dash_dracula/bootstrap.py,sha256=Mq0cJzCTxg2wo1OBcSjkoVadYTkoW7l7LXqOTfryY8g,8272
3
+ dash_dracula/colors.py,sha256=hW7Rg3jfq7D1Of_uNp2R_PThfgNDL4CzABhSBk7rQ8A,3578
4
+ dash_dracula/pdf.py,sha256=nEA5ythfbF0OSamkp-QiVPW4PIduCqyqd_eXJnKqlbI,5338
5
+ dash_dracula/plotly_template.py,sha256=Jgi_RB9Csg5w8NlrlaLwHuYno0nljZHT9a1Fo8VgDso,1395
6
+ dash_dracula/assets/dracula.css,sha256=p6soy5CuCAqKwolXl1EKqgmhA0ugHMhBBnoLH-b0tqU,22471
7
+ dash_dracula_theme-0.1.0.dist-info/licenses/LICENSE,sha256=NUWfSilhzKdyXj0Ww-YxBxA5T50NXYYndGP-ruW6KHY,1090
8
+ dash_dracula_theme-0.1.0.dist-info/METADATA,sha256=p0PfySbGzsK1rFIyRGJUujP7rPhjRKGmdf3wU2kHuvo,4032
9
+ dash_dracula_theme-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ dash_dracula_theme-0.1.0.dist-info/top_level.txt,sha256=OxiE4rOyNPMh8CDwG3xEYY0M5UdbUI5BVKfhNoryeUg,13
11
+ dash_dracula_theme-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hjeverts-VSM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dash_dracula