chart-factory 0.1.2

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.
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "chart-factory",
3
+ "version": "0.1.2",
4
+ "description": "Token-driven, responsive D3 chart library: area, line, slope, bar, scatter, dot charts and sortable tables that fill their container by default.",
5
+ "keywords": [
6
+ "d3",
7
+ "charts",
8
+ "chart",
9
+ "visualization",
10
+ "dataviz",
11
+ "data-visualization",
12
+ "svg",
13
+ "design-tokens",
14
+ "responsive-charts"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/jolsufka/chart-factory.git"
19
+ },
20
+ "homepage": "https://github.com/jolsufka/chart-factory#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/jolsufka/chart-factory/issues"
23
+ },
24
+ "type": "module",
25
+ "license": "MIT",
26
+ "author": "Jacob Olsufka",
27
+ "main": "./src/core/d3-chart-factory.js",
28
+ "module": "./src/core/d3-chart-factory.js",
29
+ "unpkg": "./dist/chart-factory.min.js",
30
+ "jsdelivr": "./dist/chart-factory.min.js",
31
+ "exports": {
32
+ ".": "./src/core/d3-chart-factory.js",
33
+ "./base": "./src/core/d3-base.js",
34
+ "./table": "./src/table/d3-table.js",
35
+ "./core/tokens.css": "./src/core/tokens.css",
36
+ "./core/base.css": "./src/core/base.css",
37
+ "./table/tokens.css": "./src/table/tokens.css",
38
+ "./table/table.css": "./src/table/table.css",
39
+ "./styles.css": "./dist/chart-factory.css",
40
+ "./dist/*": "./dist/*"
41
+ },
42
+ "style": "./dist/chart-factory.css",
43
+ "sideEffects": [
44
+ "./src/core/d3-chart-factory.js",
45
+ "*.css"
46
+ ],
47
+ "files": [
48
+ "src",
49
+ "dist",
50
+ "index.d.ts",
51
+ "docs",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "scripts": {
56
+ "build": "node build.mjs",
57
+ "watch": "node build.mjs --watch",
58
+ "serve": "node serve.mjs",
59
+ "conformance": "node scripts/conformance.mjs",
60
+ "test": "node scripts/bundle-smoke.mjs && playwright test",
61
+ "test:update": "UPDATE_GOLDENS=1 playwright test regression",
62
+ "prepare": "npm run build",
63
+ "bundle-smoke": "node scripts/bundle-smoke.mjs"
64
+ },
65
+ "peerDependencies": {
66
+ "d3": "^7.0.0",
67
+ "d3-hexbin": ">=0.2.0",
68
+ "d3-sankey": ">=0.12.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "d3-hexbin": {
72
+ "optional": true
73
+ },
74
+ "d3-sankey": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "devDependencies": {
79
+ "@playwright/test": "^1.61.1",
80
+ "esbuild": "^0.24.0"
81
+ },
82
+ "types": "./index.d.ts"
83
+ }
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Chart Factory - Shared Base Styles
3
+ *
4
+ * Common styles used across all D3 visualization components.
5
+ * Import after tokens.css:
6
+ *
7
+ * @import 'chart-factory/core/tokens.css';
8
+ * @import 'chart-factory/core/base.css';
9
+ */
10
+
11
+ /* ===========================
12
+ CONTAINER
13
+ =========================== */
14
+
15
+ .d3-container {
16
+ display: block;
17
+ width: 100%;
18
+ max-width: var(--container-max-width, 660px);
19
+ box-sizing: border-box;
20
+ background: var(--color-bg-container);
21
+ padding: var(--container-padding, var(--space-8));
22
+ box-shadow: var(--shadow-container);
23
+ text-align: left;
24
+ }
25
+
26
+ /* Charts fill their container; svg viewBox scaling is applied by the factory */
27
+ .d3-container svg,
28
+ .chart-container svg {
29
+ display: block;
30
+ }
31
+
32
+ /* ===========================
33
+ CHART ELEMENT DEFAULTS
34
+ ===========================
35
+ The canonical look for factory-generated SVG elements. Historically these
36
+ rules were copy-pasted into every example page's <style> block, which meant
37
+ package consumers loading only tokens.css + base.css got browser-default
38
+ black axes and gridlines. They belong here. Scoped under `svg` so generic
39
+ class names (.line, .dot) don't leak onto host-page HTML. */
40
+
41
+ svg .line {
42
+ fill: none;
43
+ stroke-width: var(--line-stroke-width);
44
+ }
45
+
46
+ svg .axis-x,
47
+ svg .axis-y {
48
+ font-family: var(--font-family);
49
+ font-size: var(--font-size-sm);
50
+ color: var(--axis-color);
51
+ }
52
+
53
+ svg .axis-x path,
54
+ svg .axis-y path,
55
+ svg .axis-x line,
56
+ svg .axis-y line {
57
+ stroke: var(--color-border-light, #e5e5e5);
58
+ }
59
+
60
+ svg .axis-y .domain {
61
+ display: none;
62
+ }
63
+
64
+ svg .grid line {
65
+ stroke: var(--color-border-light, #e5e5e5);
66
+ stroke-opacity: var(--grid-stroke-opacity);
67
+ }
68
+
69
+ svg .grid path {
70
+ stroke-width: 0;
71
+ }
72
+
73
+ /* Fallback fill only — dots that set their own fill attribute (per-category
74
+ coloring in the Dot family) keep it. */
75
+ svg .dot:not([fill]) {
76
+ fill: var(--chart-primary);
77
+ }
78
+
79
+ /* Legend swatches must never be squished by a tight flex row (would turn a
80
+ circular dot into an oval) — pin their box regardless of available space. */
81
+ .legend-dot,
82
+ .legend-swatch {
83
+ flex: none;
84
+ }
85
+
86
+ /* ===========================
87
+ TITLE / SUBTITLE / FOOTER
88
+ =========================== */
89
+
90
+ .d3-title {
91
+ font-family: var(--font-family);
92
+ font-size: var(--font-size-title);
93
+ font-weight: var(--font-weight-bold);
94
+ margin: 0 0 var(--space-2) 0;
95
+ color: var(--color-black);
96
+ letter-spacing: var(--letter-spacing-tight);
97
+ }
98
+
99
+ .d3-subtitle {
100
+ font-family: var(--font-family);
101
+ font-size: var(--font-size-subtitle);
102
+ color: var(--color-text-subtle);
103
+ margin: 0 0 var(--space-6) 0;
104
+ line-height: var(--line-height-normal);
105
+ font-weight: var(--font-weight-normal);
106
+ }
107
+
108
+ .d3-footer {
109
+ margin-top: 0;
110
+ padding-top: var(--space-4);
111
+ border-top: 1px solid var(--color-border-footer);
112
+ font-family: var(--font-family);
113
+ font-size: var(--font-size-xs);
114
+ color: var(--color-text-faint);
115
+ text-transform: uppercase;
116
+ letter-spacing: var(--letter-spacing-wider);
117
+ font-weight: var(--font-weight-semibold);
118
+ }
119
+
120
+ /* ===========================
121
+ TOOLTIPS (Shared)
122
+ =========================== */
123
+
124
+ /* Chart hover tooltip — the node TooltipManager creates ('.d3-tooltip').
125
+ Positioning (fixed/left/top/opacity/z-index) is managed by JS; this class
126
+ owns the appearance. */
127
+ .d3-tooltip {
128
+ position: fixed;
129
+ background: var(--color-bg-container);
130
+ border: 1px solid var(--color-border-tooltip);
131
+ border-radius: 6px;
132
+ padding: var(--tooltip-padding, 10px 13px);
133
+ box-shadow: var(--shadow-tooltip);
134
+ z-index: var(--tooltip-z-index, 9999);
135
+ pointer-events: none;
136
+ font-family: var(--font-family);
137
+ font-size: var(--tooltip-font-size, 12px);
138
+ line-height: 1.45;
139
+ min-width: var(--tooltip-min-width, 130px);
140
+ max-width: 320px;
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: var(--tooltip-row-gap, 4px);
144
+ }
145
+
146
+ /* Standard tooltip shell rows (TooltipManager.rows).
147
+ Hierarchy: title > values > labels > notes.
148
+ Alignment discipline: text labels flush LEFT, numeric values flush RIGHT. */
149
+ .d3-tooltip .d3-tooltip-title {
150
+ font-size: var(--tooltip-title-font-size, 15px);
151
+ font-weight: var(--font-weight-semibold);
152
+ color: var(--color-black);
153
+ letter-spacing: var(--letter-spacing-tight);
154
+ }
155
+
156
+ .d3-tooltip .d3-tooltip-label {
157
+ color: var(--color-text-subtle);
158
+ }
159
+
160
+ .d3-tooltip .d3-tooltip-value {
161
+ font-weight: var(--font-weight-semibold);
162
+ color: var(--color-black);
163
+ font-variant-numeric: tabular-nums;
164
+ }
165
+
166
+ .d3-tooltip .d3-tooltip-row {
167
+ display: flex;
168
+ align-items: baseline;
169
+ justify-content: space-between;
170
+ }
171
+
172
+ /* In swatched rows the label is the middle of three flex children —
173
+ grow it so it hugs the swatch on the left instead of centering.
174
+ (No flex gap: the swatch's own margin spaces it from the label; the
175
+ value's margin guarantees the minimum label-to-value separation.) */
176
+ .d3-tooltip .d3-tooltip-row .d3-tooltip-label {
177
+ flex: 1 1 auto;
178
+ text-align: left;
179
+ }
180
+
181
+ .d3-tooltip .d3-tooltip-row .d3-tooltip-value {
182
+ text-align: right;
183
+ margin-left: var(--space-4);
184
+ }
185
+
186
+ .d3-tooltip .d3-tooltip-note {
187
+ font-size: var(--font-size-sm);
188
+ color: var(--color-text-muted);
189
+ }
190
+
191
+ .d3-tooltip .d3-tooltip-footer {
192
+ border-top: 1px solid var(--color-border-row);
193
+ padding-top: var(--tooltip-row-gap, 3px);
194
+ margin-top: 1px;
195
+ }
196
+
197
+ .d3-tooltip .d3-tooltip-bar {
198
+ height: 3px;
199
+ border-radius: 2px;
200
+ margin-top: 2px;
201
+ }
202
+
203
+ .d3-tooltip .d3-tooltip-swatch {
204
+ display: inline-block;
205
+ width: 8px;
206
+ height: 8px;
207
+ border-radius: 50%;
208
+ margin-right: var(--space-1);
209
+ flex: none;
210
+ }
211
+
212
+ .d3-tooltip-chart {
213
+ display: flex;
214
+ align-items: flex-end;
215
+ gap: var(--tooltip-bar-gap);
216
+ height: var(--tooltip-chart-height);
217
+ position: relative;
218
+ box-sizing: border-box;
219
+ }
220
+
221
+ .d3-tooltip-bar {
222
+ flex: 1;
223
+ background-color: var(--color-text-muted);
224
+ border-radius: var(--tooltip-bar-border-radius);
225
+ min-width: var(--tooltip-bar-min-width);
226
+ position: relative;
227
+ max-height: var(--tooltip-chart-height);
228
+ }
229
+
230
+ .d3-tooltip-bar-label {
231
+ position: absolute;
232
+ bottom: calc(-1 * var(--tooltip-label-offset-y));
233
+ left: 50%;
234
+ transform: translateX(-50%);
235
+ font-size: var(--font-size-xs);
236
+ color: var(--color-text-muted);
237
+ white-space: nowrap;
238
+ }
239
+
240
+ .d3-tooltip-bar-value {
241
+ position: absolute;
242
+ top: calc(-1 * var(--tooltip-value-offset-y));
243
+ left: 50%;
244
+ transform: translateX(-50%);
245
+ font-size: var(--font-size-sm);
246
+ font-weight: var(--font-weight-semibold);
247
+ color: var(--color-black);
248
+ white-space: nowrap;
249
+ }
250
+
251
+ /* ===========================
252
+ RESPONSIVE
253
+ =========================== */
254
+
255
+ @media (max-width: 768px) {
256
+ .d3-container {
257
+ padding: var(--container-padding-mobile);
258
+ }
259
+
260
+ .d3-title {
261
+ font-size: var(--title-font-size-mobile);
262
+ }
263
+ }
264
+
265
+ /* ===========================
266
+ CHART SHELL (universal config: title / subtitle / legend / footer)
267
+ Rendered by the responsive wrapper around the chart svg — one
268
+ implementation for every builder, token-driven, dark-mode-live.
269
+ =========================== */
270
+ .d3-shell-title {
271
+ font-family: var(--font-family);
272
+ font-size: var(--shell-title-size, 15px);
273
+ font-weight: var(--font-weight-bold);
274
+ color: var(--color-black);
275
+ letter-spacing: var(--letter-spacing-tight);
276
+ margin: 0 0 2px 0;
277
+ }
278
+
279
+ .d3-shell-subtitle {
280
+ font-family: var(--font-family);
281
+ font-size: var(--shell-subtitle-size, 11px);
282
+ color: var(--color-text-subtle);
283
+ margin: 0 0 var(--space-3) 0;
284
+ }
285
+
286
+ .d3-shell--featured.d3-shell-title {
287
+ font-size: var(--shell-featured-title-size, 20px);
288
+ margin-bottom: var(--space-1);
289
+ }
290
+
291
+ .d3-shell--featured.d3-shell-subtitle {
292
+ font-size: var(--shell-featured-subtitle-size, 14px);
293
+ margin-bottom: var(--space-4);
294
+ }
295
+
296
+ .d3-shell-legend {
297
+ margin-bottom: var(--space-3);
298
+ }
299
+
300
+ .d3-shell-footer {
301
+ font-family: var(--font-family);
302
+ font-size: var(--font-size-xs);
303
+ color: var(--color-text-faint);
304
+ text-transform: uppercase;
305
+ letter-spacing: var(--letter-spacing-wide);
306
+ margin-top: var(--space-4);
307
+ padding-top: var(--space-3);
308
+ border-top: 1px solid var(--color-border-row);
309
+ display: flex;
310
+ justify-content: space-between;
311
+ align-items: center;
312
+ }
313
+
314
+ /* Chart story (story: config): narrative summary module rendered by the
315
+ wrapper below the svg, above the footer. Variants replicate the picked
316
+ story-module mockups: panel (accent-tinted takeaway), bubble (card with
317
+ an up-pointing tail — the chart is speaking), band (full-width dark
318
+ stat lede). All colors flow through the --story-* tokens; the `accent`
319
+ option overrides --story-accent inline on the module. */
320
+ .chart-story {
321
+ font-family: var(--font-family);
322
+ font-size: var(--story-font-size);
323
+ line-height: var(--story-line-height);
324
+ color: var(--color-text-primary);
325
+ margin-top: var(--space-3);
326
+ }
327
+
328
+ .chart-story-body { margin: 0; }
329
+
330
+ /* Optional uppercase lead-in (bubble and band variants) */
331
+ .chart-story-overline {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: var(--space-1);
335
+ font-size: var(--font-size-sm);
336
+ font-weight: var(--font-weight-bold);
337
+ letter-spacing: var(--letter-spacing-wider);
338
+ text-transform: uppercase;
339
+ color: var(--color-text-subtle);
340
+ margin-bottom: 3px;
341
+ }
342
+
343
+ .chart-story-icon { display: inline-flex; flex: none; }
344
+ .chart-story-icon svg { display: block; }
345
+
346
+ /* panel — soft accent wash + accent hairline; icon and bold accent label
347
+ lead the prose inline */
348
+ .chart-story--panel {
349
+ display: flex;
350
+ gap: var(--space-2);
351
+ align-items: flex-start;
352
+ background: color-mix(in srgb, var(--story-accent) var(--story-panel-bg-alpha), transparent);
353
+ border: 1px solid color-mix(in srgb, var(--story-accent) var(--story-panel-border-alpha), transparent);
354
+ border-radius: 8px;
355
+ padding: 11px 14px;
356
+ }
357
+ .chart-story--panel .chart-story-icon { margin-top: 2px; }
358
+ .chart-story-label {
359
+ color: var(--story-accent);
360
+ font-weight: var(--font-weight-bold);
361
+ }
362
+
363
+ /* bubble — container-background card whose rotated-square tail points up
364
+ at the chart */
365
+ .chart-story--bubble {
366
+ position: relative;
367
+ margin-top: calc(var(--space-3) + 7px); /* room for the tail */
368
+ background: var(--color-bg-container);
369
+ border: 1px solid var(--color-border-light);
370
+ border-radius: 10px;
371
+ padding: 11px 15px;
372
+ box-shadow: var(--shadow-tooltip);
373
+ }
374
+ .chart-story--bubble::before {
375
+ content: "";
376
+ position: absolute;
377
+ top: -7px;
378
+ left: 34px;
379
+ width: 12px;
380
+ height: 12px;
381
+ background: var(--color-bg-container);
382
+ border-left: 1px solid var(--color-border-light);
383
+ border-top: 1px solid var(--color-border-light);
384
+ transform: rotate(45deg);
385
+ }
386
+
387
+ /* band — full-width dark lede strip; spans the CHART CONTAINER (not any
388
+ outer card the page may have); <b>/<strong> pick up the accent */
389
+ .chart-story--band {
390
+ background: var(--story-band-bg);
391
+ color: var(--story-band-text);
392
+ border-radius: 6px;
393
+ padding: 13px 16px 14px;
394
+ }
395
+ .chart-story--band .chart-story-overline { color: var(--story-band-label); }
396
+ .chart-story--band .chart-story-body b,
397
+ .chart-story--band .chart-story-body strong { color: var(--story-accent); }
398
+
399
+ /* ===========================
400
+ CHART STATES (state: 'loading' | 'empty' | 'error')
401
+ ===========================
402
+ The state box takes the chart's exact footprint (height set inline by
403
+ the wrapper), so the real chart swaps in with zero layout jump. Loading
404
+ skeletons breathe on a constant 5s cadence with one straight shimmer
405
+ band (9s cycle: ~3.3s crossing + ~5.7s rest) clipped to the shapes. */
406
+
407
+ .d3-state {
408
+ position: relative;
409
+ width: 100%;
410
+ overflow: hidden;
411
+ display: flex;
412
+ flex-direction: column;
413
+ align-items: center;
414
+ justify-content: center;
415
+ gap: var(--space-2);
416
+ text-align: center;
417
+ }
418
+
419
+ .d3-state-msg {
420
+ font-family: var(--font-family);
421
+ font-size: var(--font-size-md);
422
+ color: var(--color-text-muted);
423
+ }
424
+
425
+ .d3-state-glyph {
426
+ font-size: 22px;
427
+ line-height: 1;
428
+ color: var(--color-text-faint);
429
+ }
430
+
431
+ .d3-state--error .d3-state-glyph { color: var(--chart-red); opacity: 0.7; }
432
+ .d3-state--error .d3-state-msg { color: var(--color-text-primary); }
433
+
434
+ /* Loading fallback: three pulsing dots */
435
+ .d3-state-dots { display: flex; gap: 5px; }
436
+ .d3-state-dots span {
437
+ width: 7px;
438
+ height: 7px;
439
+ border-radius: 50%;
440
+ background: var(--color-text-faint);
441
+ animation: d3-state-pulse 1.2s ease-in-out infinite;
442
+ }
443
+ .d3-state-dots span:nth-child(2) { animation-delay: 0.2s; }
444
+ .d3-state-dots span:nth-child(3) { animation-delay: 0.4s; }
445
+ @keyframes d3-state-pulse {
446
+ 0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); }
447
+ 40% { opacity: 1; transform: scale(1); }
448
+ }
449
+
450
+ /* Loading skeleton: breathing columns. The shimmer is ONE band swept
451
+ across the whole set — each column's ::after is sized/offset in
452
+ container coordinates (via the --sw-* custom properties the wrapper
453
+ sets) so the gradient lines up seamlessly across all columns. */
454
+ .d3-state--columns { display: block; }
455
+ .d3-skel-col {
456
+ position: absolute;
457
+ bottom: 8%;
458
+ border-radius: 3px 3px 0 0;
459
+ background: var(--color-bg-highlight);
460
+ overflow: hidden;
461
+ transition: height 5s ease-in-out;
462
+ }
463
+ .d3-skel-col::after {
464
+ content: '';
465
+ position: absolute;
466
+ top: 0;
467
+ bottom: 0;
468
+ left: var(--sw-left);
469
+ width: var(--sw-width);
470
+ background: linear-gradient(90deg, transparent 42%,
471
+ color-mix(in srgb, var(--color-text-faint) 16%, transparent) 50%,
472
+ transparent 58%);
473
+ animation: d3-skel-sweep 9s linear infinite;
474
+ transform: translateX(-100%);
475
+ }
476
+ @keyframes d3-skel-sweep {
477
+ 0% { transform: translateX(-100%); }
478
+ 37% { transform: translateX(100%); }
479
+ 100% { transform: translateX(100%); }
480
+ }
481
+
482
+ /* Rows/trend skeletons are SVG (shimmer clipped to the shapes via SMIL);
483
+ the svg just needs to fill the state box. */
484
+ .d3-state--rows svg, .d3-state--trend svg { display: block; width: 100%; height: 100%; }
485
+
486
+ /* ============================================
487
+ Facet helper (ChartFactory.facet) — minimal small-multiples chrome.
488
+ The grid itself is inline (display:grid on the caller's container);
489
+ these style the panels the helper creates inside it.
490
+ ============================================ */
491
+ .d3-facet-panel { min-width: 0; }
492
+ .d3-facet-chart { display: block; width: 100%; }
493
+
494
+ /* Pulsing anomaly rings (Line.createPulseAnomalies) — library-owned so npm
495
+ consumers get the pulse without copying example CSS. The builder sets
496
+ animation: none inline when animate: false; users who prefer reduced
497
+ motion get static rings automatically. */
498
+ svg circle.anomaly-pulse {
499
+ animation: d3-anomaly-pulse var(--chart-animation-duration, 2s) ease-in-out infinite;
500
+ }
501
+ @keyframes d3-anomaly-pulse {
502
+ 0% { opacity: 0.6; r: var(--anomaly-pulse-radius, 4px); }
503
+ 50% { opacity: 0; r: calc(var(--anomaly-pulse-radius, 4px) * 4); }
504
+ 100% { opacity: 0.6; r: var(--anomaly-pulse-radius, 4px); }
505
+ }
506
+ @media (prefers-reduced-motion: reduce) {
507
+ svg circle.anomaly-pulse { animation: none; }
508
+ }