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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/chart-factory-table.js +1400 -0
- package/dist/chart-factory.css +2368 -0
- package/dist/chart-factory.js +11398 -0
- package/dist/chart-factory.min.js +7 -0
- package/docs/API.md +1815 -0
- package/docs/chart-guide.md +872 -0
- package/docs/color-guide.md +134 -0
- package/docs/conformance-matrix.md +864 -0
- package/docs/theming.md +107 -0
- package/index.d.ts +10453 -0
- package/package.json +83 -0
- package/src/core/base.css +508 -0
- package/src/core/builder-metadata.generated.mjs +406 -0
- package/src/core/d3-base.js +249 -0
- package/src/core/d3-chart-factory.js +18547 -0
- package/src/core/d3-data.js +0 -0
- package/src/core/d3-suggest.js +565 -0
- package/src/core/example-nav.css +62 -0
- package/src/core/example-nav.js +249 -0
- package/src/core/palettes.js +310 -0
- package/src/core/theme-toggle.js +126 -0
- package/src/core/tokens.css +792 -0
- package/src/table/d3-table.js +1570 -0
- package/src/table/table.css +899 -0
- package/src/table/tokens.css +157 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart Factory - Shared Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common utilities used across all D3 visualization components.
|
|
5
|
+
*
|
|
6
|
+
* @requires d3 v7+ (peer dependency)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as d3 from 'd3';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get CSS variable value with fallback
|
|
13
|
+
* @param {string} name - CSS variable name (e.g., '--font-family')
|
|
14
|
+
* @param {string} fallback - Fallback value if variable not found
|
|
15
|
+
* @returns {string} The CSS variable value or fallback
|
|
16
|
+
*/
|
|
17
|
+
export function getCSSVar(name, fallback = '') {
|
|
18
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
19
|
+
return value || fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Text measurement utility class
|
|
24
|
+
* Creates a hidden DOM element to measure text width accurately
|
|
25
|
+
*/
|
|
26
|
+
export class TextMeasurer {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.element = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the hidden measurement element
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
init() {
|
|
36
|
+
if (!this.element) {
|
|
37
|
+
this.element = d3.select('body')
|
|
38
|
+
.append('div')
|
|
39
|
+
.style('position', 'absolute')
|
|
40
|
+
.style('visibility', 'hidden')
|
|
41
|
+
.style('white-space', 'nowrap')
|
|
42
|
+
.style('font-family', getCSSVar('--font-family', 'sans-serif'))
|
|
43
|
+
.style('font-size', getCSSVar('--font-size-lg', '13px'));
|
|
44
|
+
}
|
|
45
|
+
return this.element;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Measure the width of text
|
|
50
|
+
* @param {string} text - Text to measure
|
|
51
|
+
* @param {string} fontSize - Font size (optional)
|
|
52
|
+
* @param {string} fontWeight - Font weight (optional)
|
|
53
|
+
* @param {Object} options - Additional options
|
|
54
|
+
* @param {string} options.letterSpacing - Letter spacing
|
|
55
|
+
* @param {string} options.textTransform - Text transform (e.g., 'uppercase')
|
|
56
|
+
* @param {string} options.fontFamily - Font family override (else the
|
|
57
|
+
* base --font-family; lets callers measure cells whose family a
|
|
58
|
+
* theme retargets via a token, e.g. --table-primary-font)
|
|
59
|
+
* @returns {number} Width in pixels
|
|
60
|
+
*/
|
|
61
|
+
measure(text, fontSize = null, fontWeight = null, options = {}) {
|
|
62
|
+
const el = this.init();
|
|
63
|
+
if (fontSize) el.style('font-size', fontSize);
|
|
64
|
+
if (fontWeight) el.style('font-weight', fontWeight);
|
|
65
|
+
|
|
66
|
+
// Reset optional styles
|
|
67
|
+
el.style('font-family', options.fontFamily || getCSSVar('--font-family', 'sans-serif'));
|
|
68
|
+
el.style('letter-spacing', options.letterSpacing || 'normal');
|
|
69
|
+
el.style('text-transform', options.textTransform || 'none');
|
|
70
|
+
|
|
71
|
+
el.text(text);
|
|
72
|
+
return el.node().getBoundingClientRect().width;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clean up the measurement element
|
|
77
|
+
*/
|
|
78
|
+
destroy() {
|
|
79
|
+
if (this.element) {
|
|
80
|
+
this.element.remove();
|
|
81
|
+
this.element = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a D3 sequential color scale
|
|
88
|
+
* @param {string} type - Scale type name (orange, blue, green, etc.)
|
|
89
|
+
* @param {Array} domain - [min, max] domain values
|
|
90
|
+
* @returns {Function} D3 color scale function
|
|
91
|
+
*/
|
|
92
|
+
export function createColorScale(type, domain) {
|
|
93
|
+
// Endpoints/stops are design tokens (themeable via --scale-* in
|
|
94
|
+
// tokens.css); the hex literals are fallbacks for token-less embeds.
|
|
95
|
+
const t = (name, fallback) => getCSSVar(name, fallback);
|
|
96
|
+
const scales = {
|
|
97
|
+
orange: d3.scaleSequential()
|
|
98
|
+
.domain(domain)
|
|
99
|
+
.interpolator(d3.interpolateRgb(t('--scale-orange-lo', '#fff5eb'), t('--scale-orange-hi', '#ff6b35'))),
|
|
100
|
+
|
|
101
|
+
orangeRed: d3.scaleSequential()
|
|
102
|
+
.domain(domain)
|
|
103
|
+
.interpolator(d3.interpolateRgb(t('--scale-orange-lo', '#fff5eb'), t('--scale-orange-red-hi', '#d7191c'))),
|
|
104
|
+
|
|
105
|
+
blue: d3.scaleSequential()
|
|
106
|
+
.domain(domain)
|
|
107
|
+
.interpolator(d3.interpolateRgb(t('--scale-blue-lo', '#e0f3f8'), t('--scale-blue-hi', '#4575b4'))),
|
|
108
|
+
|
|
109
|
+
teal: d3.scaleSequential()
|
|
110
|
+
.domain(domain)
|
|
111
|
+
.interpolator(d3.interpolateRgb(t('--scale-blue-lo', '#e0f3f8'), t('--scale-teal-hi', '#1a9988'))),
|
|
112
|
+
|
|
113
|
+
aqua: d3.scaleSequential()
|
|
114
|
+
.domain(domain)
|
|
115
|
+
.interpolator(d3.interpolateRgbBasis([
|
|
116
|
+
t('--scale-aqua-1', '#a4d7e1'), t('--scale-aqua-2', '#4ec1d4'),
|
|
117
|
+
t('--scale-aqua-3', '#0095b7'), t('--scale-aqua-4', '#005e8b')])),
|
|
118
|
+
|
|
119
|
+
grayscale: d3.scaleSequential()
|
|
120
|
+
.domain(domain)
|
|
121
|
+
.interpolator(d3.interpolateRgb(t('--scale-gray-lo', '#f5f5f5'), t('--scale-gray-hi', '#333333'))),
|
|
122
|
+
|
|
123
|
+
purple: d3.scaleSequential()
|
|
124
|
+
.domain(domain)
|
|
125
|
+
.interpolator(d3.interpolateRgbBasis([
|
|
126
|
+
t('--scale-purple-1', '#f3e5f5'), t('--scale-purple-2', '#ce93d8'),
|
|
127
|
+
t('--scale-purple-3', '#ab47bc'), t('--scale-purple-4', '#7b1fa2')])),
|
|
128
|
+
|
|
129
|
+
green: d3.scaleSequential()
|
|
130
|
+
.domain(domain)
|
|
131
|
+
.interpolator(d3.interpolateRgbBasis([
|
|
132
|
+
t('--scale-green-1', '#e8f5e9'), t('--scale-green-2', '#a5d6a7'),
|
|
133
|
+
t('--scale-green-3', '#66bb6a'), t('--scale-green-4', '#2e7d32')])),
|
|
134
|
+
|
|
135
|
+
diverging: d3.scaleDiverging()
|
|
136
|
+
.domain([domain[0], (domain[0] + domain[1]) / 2, domain[1]])
|
|
137
|
+
.interpolator(d3.interpolateRdBu)
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return scales[type] || scales.orange;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Shared canvas-based text measurer. Returns a `measure(text, weight?, size?,
|
|
145
|
+
* family?)` function bound to one reused 2d context. This is THE text
|
|
146
|
+
* measurement utility — builders must not create their own canvases.
|
|
147
|
+
*/
|
|
148
|
+
let _measureCtx = null;
|
|
149
|
+
export function createTextMeasurer(defaults = {}) {
|
|
150
|
+
if (!_measureCtx) {
|
|
151
|
+
_measureCtx = document.createElement('canvas').getContext('2d');
|
|
152
|
+
}
|
|
153
|
+
const baseFamily = defaults.family || getCSSVar('--font-family', 'system-ui, sans-serif');
|
|
154
|
+
const baseSize = defaults.size || getCSSVar('--font-size-md', '13px');
|
|
155
|
+
const baseWeight = defaults.weight || 'normal';
|
|
156
|
+
return function measure(text, weight = baseWeight, size = baseSize, family = baseFamily) {
|
|
157
|
+
_measureCtx.font = `${weight} ${size} ${family}`;
|
|
158
|
+
return _measureCtx.measureText(String(text)).width;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculate luminance for contrast detection
|
|
164
|
+
* @param {string} color - CSS color string
|
|
165
|
+
* @returns {number} Luminance value (0-1)
|
|
166
|
+
*/
|
|
167
|
+
export function getLuminance(color) {
|
|
168
|
+
const rgb = d3.rgb(color);
|
|
169
|
+
const r = rgb.r / 255;
|
|
170
|
+
const g = rgb.g / 255;
|
|
171
|
+
const b = rgb.b / 255;
|
|
172
|
+
const [rs, gs, bs] = [r, g, b].map(val =>
|
|
173
|
+
val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4)
|
|
174
|
+
);
|
|
175
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get contrasting text color (black or white) based on background
|
|
180
|
+
* @param {string} bgColor - Background color
|
|
181
|
+
* @returns {string} '#000' or '#fff'
|
|
182
|
+
*/
|
|
183
|
+
export function getContrastColor(bgColor) {
|
|
184
|
+
return getLuminance(bgColor) > 0.5 ? '#000' : '#fff';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format a numeric value based on format type
|
|
189
|
+
* @param {*} value - Value to format
|
|
190
|
+
* @param {string} format - Format type (decimal, decimal2, percentage, currency, integer)
|
|
191
|
+
* @returns {string} Formatted value
|
|
192
|
+
*/
|
|
193
|
+
export function formatValue(value, format) {
|
|
194
|
+
if (value === null || value === undefined) return '';
|
|
195
|
+
|
|
196
|
+
switch (format) {
|
|
197
|
+
case 'decimal':
|
|
198
|
+
const num = parseFloat(value);
|
|
199
|
+
return isNaN(num) ? '0.0' : num.toFixed(1);
|
|
200
|
+
case 'decimal2':
|
|
201
|
+
const num2 = parseFloat(value);
|
|
202
|
+
return isNaN(num2) ? '0.00' : num2.toFixed(2);
|
|
203
|
+
case 'percentage':
|
|
204
|
+
return `${value}%`;
|
|
205
|
+
case 'currency':
|
|
206
|
+
return `$${parseFloat(value).toLocaleString()}`;
|
|
207
|
+
case 'integer':
|
|
208
|
+
return parseInt(value).toLocaleString();
|
|
209
|
+
default:
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply staggered entrance animation to a D3 selection
|
|
216
|
+
* @param {d3.Selection} selection - D3 selection to animate
|
|
217
|
+
* @param {number} duration - Animation duration in ms
|
|
218
|
+
* @param {number} staggerDelay - Delay between each element in ms
|
|
219
|
+
* @param {Object} fromStyles - Starting styles
|
|
220
|
+
* @param {Object} toStyles - Ending styles
|
|
221
|
+
*/
|
|
222
|
+
export function staggeredEntrance(selection, duration, staggerDelay, fromStyles, toStyles) {
|
|
223
|
+
// Apply initial styles
|
|
224
|
+
Object.entries(fromStyles).forEach(([key, value]) => {
|
|
225
|
+
selection.style(key, value);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Animate to final styles
|
|
229
|
+
selection.transition()
|
|
230
|
+
.delay((d, i) => i * staggerDelay)
|
|
231
|
+
.duration(duration)
|
|
232
|
+
.ease(d3.easeCubicOut)
|
|
233
|
+
.call(transition => {
|
|
234
|
+
Object.entries(toStyles).forEach(([key, value]) => {
|
|
235
|
+
transition.style(key, value);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Default export with all utilities
|
|
241
|
+
export default {
|
|
242
|
+
getCSSVar,
|
|
243
|
+
TextMeasurer,
|
|
244
|
+
createColorScale,
|
|
245
|
+
getLuminance,
|
|
246
|
+
getContrastColor,
|
|
247
|
+
formatValue,
|
|
248
|
+
staggeredEntrance
|
|
249
|
+
};
|