@santi020k/lumen-core 1.4.0 → 1.7.0
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/README.md +17 -0
- package/THIRD_PARTY_NOTICES.md +21 -2
- package/dist/charts.d.ts +119 -0
- package/dist/charts.d.ts.map +1 -1
- package/dist/charts.js +446 -49
- package/dist/code.d.ts.map +1 -1
- package/dist/code.js +22 -9
- package/dist/components.d.ts +32 -7
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +13 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +32 -10
- package/dist/error-state.d.ts +13 -0
- package/dist/error-state.d.ts.map +1 -0
- package/dist/error-state.js +3 -0
- package/dist/foundations.generated.d.ts +149 -24
- package/dist/foundations.generated.d.ts.map +1 -1
- package/dist/foundations.generated.js +148 -24
- package/dist/illustrations.generated.d.ts +77 -0
- package/dist/illustrations.generated.d.ts.map +1 -0
- package/dist/illustrations.generated.js +137 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/phone.d.ts +24 -0
- package/dist/phone.d.ts.map +1 -0
- package/dist/phone.js +90 -0
- package/dist/rich-text.d.ts +1 -0
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +11 -4
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +18 -10
- package/dist/tabs.d.ts +7 -0
- package/dist/tabs.d.ts.map +1 -0
- package/dist/tabs.js +6 -0
- package/dist/tokens.d.ts +48 -22
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +5 -21
- package/package.json +13 -4
package/dist/charts.js
CHANGED
|
@@ -36,15 +36,19 @@ const lumenPieChartVariantClassNames = {
|
|
|
36
36
|
};
|
|
37
37
|
const defaultChartSize = 100;
|
|
38
38
|
const defaultChartPadding = 4;
|
|
39
|
-
const finiteValues = (values) => values.filter((value) => value !== null && Number.isFinite(value));
|
|
40
39
|
export const hasLumenChartData = (series) => series.some(item => item.data.some(datum => datum.y !== null && Number.isFinite(datum.y)));
|
|
41
40
|
export const hasLumenPieData = (data) => data.some(datum => datum.y !== null && Number.isFinite(datum.y) && datum.y > 0);
|
|
42
41
|
export const getLumenChartDomain = (values, includeZero = true) => {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
let min = Number.POSITIVE_INFINITY;
|
|
43
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
if (value === null || !Number.isFinite(value))
|
|
46
|
+
continue;
|
|
47
|
+
min = Math.min(min, value);
|
|
48
|
+
max = Math.max(max, value);
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(min) || !Number.isFinite(max))
|
|
45
51
|
return { max: 1, min: 0 };
|
|
46
|
-
let min = Math.min(...available);
|
|
47
|
-
let max = Math.max(...available);
|
|
48
52
|
if (includeZero) {
|
|
49
53
|
min = Math.min(0, min);
|
|
50
54
|
max = Math.max(0, max);
|
|
@@ -56,6 +60,13 @@ export const getLumenChartDomain = (values, includeZero = true) => {
|
|
|
56
60
|
}
|
|
57
61
|
return { max, min };
|
|
58
62
|
};
|
|
63
|
+
const resolveLumenChartDomain = (values, requested, includeZero) => {
|
|
64
|
+
const calculated = getLumenChartDomain(values, includeZero);
|
|
65
|
+
return {
|
|
66
|
+
max: requested?.max ?? calculated.max,
|
|
67
|
+
min: requested?.min ?? calculated.min
|
|
68
|
+
};
|
|
69
|
+
};
|
|
59
70
|
export const scaleLumenChartValue = (value, domain, rangeStart, rangeEnd) => {
|
|
60
71
|
const domainSize = domain.max - domain.min;
|
|
61
72
|
if (!Number.isFinite(value) || domainSize === 0)
|
|
@@ -65,8 +76,29 @@ export const scaleLumenChartValue = (value, domain, rangeStart, rangeEnd) => {
|
|
|
65
76
|
};
|
|
66
77
|
export const getLumenChartTicks = (domain, count = 5) => {
|
|
67
78
|
const safeCount = Math.max(2, Math.round(count));
|
|
68
|
-
const
|
|
69
|
-
|
|
79
|
+
const span = domain.max - domain.min;
|
|
80
|
+
if (!Number.isFinite(span) || span <= 0)
|
|
81
|
+
return [domain.min, domain.max];
|
|
82
|
+
const roughStep = span / (safeCount - 1);
|
|
83
|
+
const magnitude = 10 ** Math.floor(Math.log10(roughStep));
|
|
84
|
+
const residual = roughStep / magnitude;
|
|
85
|
+
let niceFactor = 1;
|
|
86
|
+
if (residual >= 7.5)
|
|
87
|
+
niceFactor = 10;
|
|
88
|
+
else if (residual >= 3.5)
|
|
89
|
+
niceFactor = 5;
|
|
90
|
+
else if (residual >= 1.5)
|
|
91
|
+
niceFactor = 2;
|
|
92
|
+
const step = niceFactor * magnitude;
|
|
93
|
+
const first = Math.ceil(domain.min / step) * step;
|
|
94
|
+
const last = Math.floor(domain.max / step) * step;
|
|
95
|
+
const ticks = [];
|
|
96
|
+
for (let value = first; value <= last + step / 1000; value += step) {
|
|
97
|
+
ticks.push(Number(value.toPrecision(12)));
|
|
98
|
+
}
|
|
99
|
+
if (ticks.length >= 2)
|
|
100
|
+
return ticks;
|
|
101
|
+
return [domain.min, domain.max];
|
|
70
102
|
};
|
|
71
103
|
export const resolveLumenChartTone = (tone, index = 0) => tone ?? lumenChartTones[index % 8] ?? 'series-1';
|
|
72
104
|
export const getLumenChartToneClassName = (tone, index = 0) => lumenChartToneClassNames[resolveLumenChartTone(tone, index)];
|
|
@@ -167,14 +199,42 @@ export const createLumenPieGeometry = (data, options = {}) => {
|
|
|
167
199
|
total
|
|
168
200
|
};
|
|
169
201
|
};
|
|
170
|
-
const
|
|
202
|
+
export const getLumenChartNumericX = (value, scale) => {
|
|
203
|
+
if (typeof value === 'number')
|
|
204
|
+
return Number.isFinite(value) ? value : null;
|
|
205
|
+
if (scale === 'linear') {
|
|
206
|
+
const numeric = Number(value);
|
|
207
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
208
|
+
}
|
|
209
|
+
const timestamp = Date.parse(value);
|
|
210
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
211
|
+
};
|
|
212
|
+
const getLumenChartXDomain = (data, scale, requested) => {
|
|
213
|
+
const calculated = getLumenChartDomain(data.map(datum => getLumenChartNumericX(datum.x, scale)), false);
|
|
214
|
+
return {
|
|
215
|
+
max: requested?.max ?? calculated.max,
|
|
216
|
+
min: requested?.min ?? calculated.min
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
const getLumenLineXCoordinate = (datum, index, dataLength, padding, drawableWidth, xScale, xDomain) => {
|
|
220
|
+
if (xScale === 'categorical') {
|
|
221
|
+
const denominator = Math.max(1, dataLength - 1);
|
|
222
|
+
return padding + (dataLength === 1 ? 0.5 : index / denominator) * drawableWidth;
|
|
223
|
+
}
|
|
224
|
+
const numericX = getLumenChartNumericX(datum.x, xScale);
|
|
225
|
+
if (numericX === null || xDomain === undefined)
|
|
226
|
+
return null;
|
|
227
|
+
return scaleLumenChartValue(numericX, xDomain, padding, padding + drawableWidth);
|
|
228
|
+
};
|
|
229
|
+
const isAvailableLumenChartY = (value) => value !== null && Number.isFinite(value);
|
|
230
|
+
const splitGeometrySegments = (data, domain, width, height, padding, xScale, xDomain) => {
|
|
171
231
|
const drawableWidth = Math.max(0, width - padding * 2);
|
|
172
232
|
const drawableHeight = Math.max(0, height - padding * 2);
|
|
173
|
-
const denominator = Math.max(1, data.length - 1);
|
|
174
233
|
const segments = [];
|
|
175
234
|
let current = [];
|
|
176
235
|
for (const [index, datum] of data.entries()) {
|
|
177
|
-
|
|
236
|
+
const xCoordinate = getLumenLineXCoordinate(datum, index, data.length, padding, drawableWidth, xScale, xDomain);
|
|
237
|
+
if (!isAvailableLumenChartY(datum.y) || xCoordinate === null) {
|
|
178
238
|
if (current.length > 0)
|
|
179
239
|
segments.push(current);
|
|
180
240
|
current = [];
|
|
@@ -182,8 +242,7 @@ const splitGeometrySegments = (data, domain, width, height, padding) => {
|
|
|
182
242
|
}
|
|
183
243
|
current.push({
|
|
184
244
|
...datum,
|
|
185
|
-
xCoordinate
|
|
186
|
-
(data.length === 1 ? 0.5 : index / denominator) * drawableWidth,
|
|
245
|
+
xCoordinate,
|
|
187
246
|
yCoordinate: scaleLumenChartValue(datum.y, domain, padding + drawableHeight, padding)
|
|
188
247
|
});
|
|
189
248
|
}
|
|
@@ -194,39 +253,41 @@ const splitGeometrySegments = (data, domain, width, height, padding) => {
|
|
|
194
253
|
const segmentPath = (points) => points
|
|
195
254
|
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.xCoordinate.toFixed(3)} ${point.yCoordinate.toFixed(3)}`)
|
|
196
255
|
.join(' ');
|
|
256
|
+
const lumenLineAreaPath = (points, baseline) => {
|
|
257
|
+
const first = points[0];
|
|
258
|
+
const last = points[points.length - 1];
|
|
259
|
+
if (!first || !last)
|
|
260
|
+
return '';
|
|
261
|
+
return [
|
|
262
|
+
segmentPath(points),
|
|
263
|
+
'L',
|
|
264
|
+
last.xCoordinate.toFixed(3),
|
|
265
|
+
baseline.toFixed(3),
|
|
266
|
+
'L',
|
|
267
|
+
first.xCoordinate.toFixed(3),
|
|
268
|
+
baseline.toFixed(3),
|
|
269
|
+
'Z'
|
|
270
|
+
].join(' ');
|
|
271
|
+
};
|
|
197
272
|
export const createLumenLineGeometry = (data, options = {}) => {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const segments = splitGeometrySegments(data, domain, width, height, padding);
|
|
273
|
+
const { domain: requestedDomain, height = defaultChartSize, includeZero = false, padding = defaultChartPadding, width = defaultChartSize, xDomain: requestedXDomain, xScale = 'categorical' } = options;
|
|
274
|
+
const domainValues = xScale === 'categorical' ?
|
|
275
|
+
data.map(datum => datum.y) :
|
|
276
|
+
data
|
|
277
|
+
.filter(datum => getLumenChartNumericX(datum.x, xScale) !== null)
|
|
278
|
+
.map(datum => datum.y);
|
|
279
|
+
const domain = resolveLumenChartDomain(domainValues, requestedDomain, includeZero);
|
|
280
|
+
const xDomain = xScale === 'categorical' ? undefined : getLumenChartXDomain(data, xScale, requestedXDomain);
|
|
281
|
+
const segments = splitGeometrySegments(data, domain, width, height, padding, xScale, xDomain);
|
|
207
282
|
const baseline = scaleLumenChartValue(Math.max(domain.min, Math.min(domain.max, 0)), domain, height - padding, padding);
|
|
208
283
|
return {
|
|
209
284
|
areaPaths: segments
|
|
210
|
-
.map(points =>
|
|
211
|
-
const first = points[0];
|
|
212
|
-
const last = points[points.length - 1];
|
|
213
|
-
if (!first || !last)
|
|
214
|
-
return '';
|
|
215
|
-
return [
|
|
216
|
-
segmentPath(points),
|
|
217
|
-
'L',
|
|
218
|
-
last.xCoordinate.toFixed(3),
|
|
219
|
-
baseline.toFixed(3),
|
|
220
|
-
'L',
|
|
221
|
-
first.xCoordinate.toFixed(3),
|
|
222
|
-
baseline.toFixed(3),
|
|
223
|
-
'Z'
|
|
224
|
-
].join(' ');
|
|
225
|
-
})
|
|
285
|
+
.map(points => lumenLineAreaPath(points, baseline))
|
|
226
286
|
.filter(Boolean),
|
|
227
287
|
domain,
|
|
228
288
|
path: segments.map(segmentPath).join(' '),
|
|
229
|
-
points: segments.flat()
|
|
289
|
+
points: segments.flat(),
|
|
290
|
+
...(xDomain === undefined ? {} : { xDomain })
|
|
230
291
|
};
|
|
231
292
|
};
|
|
232
293
|
export const getLumenChartValueRatio = (value, domain) => {
|
|
@@ -244,19 +305,28 @@ export const getLumenChartCategories = (series) => {
|
|
|
244
305
|
}
|
|
245
306
|
return [...categories.values()];
|
|
246
307
|
};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
308
|
+
const lumenChartCategoryKey = (category) => `${typeof category}:${String(category)}`;
|
|
309
|
+
const indexLumenChartSeries = (series) => new Map(series.data.map(datum => [lumenChartCategoryKey(datum.x), datum]));
|
|
310
|
+
export const alignLumenChartSeries = (series, categories) => {
|
|
311
|
+
const indexed = indexLumenChartSeries(series);
|
|
312
|
+
return {
|
|
313
|
+
...series,
|
|
314
|
+
data: categories.map(category => indexed.get(lumenChartCategoryKey(category)) ?? {
|
|
315
|
+
x: category,
|
|
316
|
+
y: null
|
|
317
|
+
})
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
const getDatumValue = (series, category) => {
|
|
321
|
+
const value = series.get(lumenChartCategoryKey(category))?.y;
|
|
322
|
+
return value !== null && value !== undefined && Number.isFinite(value) ? value : 0;
|
|
323
|
+
};
|
|
255
324
|
const getStackedDomain = (series, categories) => {
|
|
325
|
+
const indexedSeries = series.map(indexLumenChartSeries);
|
|
256
326
|
const totals = categories.flatMap(category => {
|
|
257
327
|
let positive = 0;
|
|
258
328
|
let negative = 0;
|
|
259
|
-
for (const item of
|
|
329
|
+
for (const item of indexedSeries) {
|
|
260
330
|
const value = getDatumValue(item, category);
|
|
261
331
|
if (value >= 0)
|
|
262
332
|
positive += value;
|
|
@@ -279,10 +349,15 @@ export const createLumenBarGeometry = (series, options = {}) => {
|
|
|
279
349
|
const orientation = options.orientation ?? 'vertical';
|
|
280
350
|
const layout = options.layout ?? 'grouped';
|
|
281
351
|
const categories = getLumenChartCategories(series);
|
|
352
|
+
const indexedSeries = series.map(indexLumenChartSeries);
|
|
282
353
|
const values = series.flatMap(item => item.data.map(datum => datum.y));
|
|
283
|
-
const
|
|
354
|
+
const calculatedDomain = layout === 'stacked' ?
|
|
284
355
|
getStackedDomain(series, categories) :
|
|
285
356
|
getLumenChartDomain(values);
|
|
357
|
+
const domain = {
|
|
358
|
+
max: options.domain?.max ?? calculatedDomain.max,
|
|
359
|
+
min: options.domain?.min ?? calculatedDomain.min
|
|
360
|
+
};
|
|
286
361
|
const margin = orientation === 'horizontal' ?
|
|
287
362
|
{
|
|
288
363
|
bottom: 24,
|
|
@@ -319,7 +394,12 @@ export const createLumenBarGeometry = (series, options = {}) => {
|
|
|
319
394
|
height - 20
|
|
320
395
|
});
|
|
321
396
|
for (const [seriesIndex, item] of series.entries()) {
|
|
322
|
-
const
|
|
397
|
+
const indexed = indexedSeries[seriesIndex];
|
|
398
|
+
if (!indexed)
|
|
399
|
+
continue;
|
|
400
|
+
const value = indexed.get(lumenChartCategoryKey(category))?.y;
|
|
401
|
+
if (value === null || value === undefined || !Number.isFinite(value))
|
|
402
|
+
continue;
|
|
323
403
|
const tone = resolveLumenChartTone(item.tone, seriesIndex);
|
|
324
404
|
if (orientation === 'horizontal') {
|
|
325
405
|
const startValue = getBarStartValue(layout, value, positiveOffset, negativeOffset);
|
|
@@ -378,3 +458,320 @@ export const createLumenBarGeometry = (series, options = {}) => {
|
|
|
378
458
|
};
|
|
379
459
|
};
|
|
380
460
|
/* eslint-enable complexity */
|
|
461
|
+
export const summarizeLumenChart = (series) => {
|
|
462
|
+
let availablePointCount = 0;
|
|
463
|
+
let maximum = Number.NEGATIVE_INFINITY;
|
|
464
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
465
|
+
let missingPointCount = 0;
|
|
466
|
+
for (const item of series) {
|
|
467
|
+
for (const datum of item.data) {
|
|
468
|
+
if (datum.y === null || !Number.isFinite(datum.y)) {
|
|
469
|
+
missingPointCount += 1;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
availablePointCount += 1;
|
|
473
|
+
maximum = Math.max(maximum, datum.y);
|
|
474
|
+
minimum = Math.min(minimum, datum.y);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
availablePointCount,
|
|
479
|
+
maximum: Number.isFinite(maximum) ? maximum : null,
|
|
480
|
+
minimum: Number.isFinite(minimum) ? minimum : null,
|
|
481
|
+
missingPointCount,
|
|
482
|
+
seriesCount: series.length
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
export const formatLumenChartSummary = (series, formatValue = String) => {
|
|
486
|
+
const summary = summarizeLumenChart(series);
|
|
487
|
+
if (summary.availablePointCount === 0)
|
|
488
|
+
return 'No chart data available.';
|
|
489
|
+
const pointLabel = summary.availablePointCount === 1 ? 'point' : 'points';
|
|
490
|
+
const missing = summary.missingPointCount === 0 ?
|
|
491
|
+
'' :
|
|
492
|
+
` ${summary.missingPointCount} missing ${summary.missingPointCount === 1 ? 'value' : 'values'}.`;
|
|
493
|
+
return `${summary.seriesCount} series, ${summary.availablePointCount} ${pointLabel}. ` +
|
|
494
|
+
`Values range from ${formatValue(summary.minimum ?? 0)} to ${formatValue(summary.maximum ?? 0)}.${missing}`;
|
|
495
|
+
};
|
|
496
|
+
const validateLumenChartDatumId = (datum, context) => {
|
|
497
|
+
if (datum.id === undefined)
|
|
498
|
+
return;
|
|
499
|
+
if (context.datumIds.has(datum.id)) {
|
|
500
|
+
context.issues.push({
|
|
501
|
+
code: 'duplicate-datum-id',
|
|
502
|
+
message: `Datum id "${datum.id}" must be unique within its series.`,
|
|
503
|
+
path: `${context.path}.id`
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
context.datumIds.add(datum.id);
|
|
507
|
+
};
|
|
508
|
+
const validateLumenChartDatumNumbers = (datum, context) => {
|
|
509
|
+
if (datum.y !== null && !Number.isFinite(datum.y)) {
|
|
510
|
+
context.issues.push({
|
|
511
|
+
code: 'invalid-y',
|
|
512
|
+
message: 'Chart values must be finite numbers or null.',
|
|
513
|
+
path: `${context.path}.y`
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (datum.size === undefined || datum.size === null)
|
|
517
|
+
return;
|
|
518
|
+
if (!Number.isFinite(datum.size) || datum.size < 0) {
|
|
519
|
+
context.issues.push({
|
|
520
|
+
code: 'invalid-size',
|
|
521
|
+
message: 'Bubble sizes must be finite, non-negative numbers or null.',
|
|
522
|
+
path: `${context.path}.size`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const validateLumenChartDatumX = (datum, context) => {
|
|
527
|
+
if (context.xScale === 'categorical')
|
|
528
|
+
return context.previousX;
|
|
529
|
+
const numericX = getLumenChartNumericX(datum.x, context.xScale);
|
|
530
|
+
if (numericX === null) {
|
|
531
|
+
context.issues.push({
|
|
532
|
+
code: 'invalid-x',
|
|
533
|
+
message: `The ${context.xScale} x-axis requires finite numbers or valid ISO dates.`,
|
|
534
|
+
path: `${context.path}.x`
|
|
535
|
+
});
|
|
536
|
+
return context.previousX;
|
|
537
|
+
}
|
|
538
|
+
if (numericX < context.previousX) {
|
|
539
|
+
context.issues.push({
|
|
540
|
+
code: 'unsorted-x',
|
|
541
|
+
message: `The ${context.xScale} x-axis data must be sorted in ascending order.`,
|
|
542
|
+
path: `${context.path}.x`
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return numericX;
|
|
546
|
+
};
|
|
547
|
+
const validateLumenChartDatum = (datum, context) => {
|
|
548
|
+
validateLumenChartDatumId(datum, context);
|
|
549
|
+
validateLumenChartDatumNumbers(datum, context);
|
|
550
|
+
return validateLumenChartDatumX(datum, context);
|
|
551
|
+
};
|
|
552
|
+
export const validateLumenChartSeries = (series, xScale = 'categorical') => {
|
|
553
|
+
const issues = [];
|
|
554
|
+
const seriesIds = new Set();
|
|
555
|
+
for (const [seriesIndex, item] of series.entries()) {
|
|
556
|
+
const seriesPath = `series[${seriesIndex}]`;
|
|
557
|
+
if (seriesIds.has(item.id)) {
|
|
558
|
+
issues.push({
|
|
559
|
+
code: 'duplicate-series-id',
|
|
560
|
+
message: `Series id "${item.id}" must be unique.`,
|
|
561
|
+
path: `${seriesPath}.id`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
seriesIds.add(item.id);
|
|
565
|
+
const datumIds = new Set();
|
|
566
|
+
let previousX = Number.NEGATIVE_INFINITY;
|
|
567
|
+
for (const [datumIndex, datum] of item.data.entries()) {
|
|
568
|
+
previousX = validateLumenChartDatum(datum, {
|
|
569
|
+
datumIds,
|
|
570
|
+
issues,
|
|
571
|
+
path: `${seriesPath}.data[${datumIndex}]`,
|
|
572
|
+
previousX,
|
|
573
|
+
xScale
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return issues;
|
|
578
|
+
};
|
|
579
|
+
const selectLumenChartBucketDatum = (bucket, first, last, expectedRatio) => {
|
|
580
|
+
const missing = bucket.find(datum => !isAvailableLumenChartY(datum.y));
|
|
581
|
+
if (missing)
|
|
582
|
+
return missing;
|
|
583
|
+
const expected = !isAvailableLumenChartY(first.y) || !isAvailableLumenChartY(last.y) ?
|
|
584
|
+
0 :
|
|
585
|
+
first.y + (last.y - first.y) * expectedRatio;
|
|
586
|
+
let selected = bucket[0];
|
|
587
|
+
let largestDistance = Number.NEGATIVE_INFINITY;
|
|
588
|
+
for (const datum of bucket) {
|
|
589
|
+
const distance = isAvailableLumenChartY(datum.y) ? Math.abs(datum.y - expected) : 0;
|
|
590
|
+
if (distance <= largestDistance)
|
|
591
|
+
continue;
|
|
592
|
+
largestDistance = distance;
|
|
593
|
+
selected = datum;
|
|
594
|
+
}
|
|
595
|
+
return selected;
|
|
596
|
+
};
|
|
597
|
+
export const downsampleLumenChartData = (data, threshold) => {
|
|
598
|
+
const safeThreshold = Math.max(3, Math.floor(threshold));
|
|
599
|
+
if (data.length <= safeThreshold)
|
|
600
|
+
return [...data];
|
|
601
|
+
const sampled = [];
|
|
602
|
+
const first = data[0];
|
|
603
|
+
const last = data[data.length - 1];
|
|
604
|
+
if (!first || !last)
|
|
605
|
+
return [];
|
|
606
|
+
sampled.push(first);
|
|
607
|
+
const bucketSize = (data.length - 2) / (safeThreshold - 2);
|
|
608
|
+
for (let bucketIndex = 0; bucketIndex < safeThreshold - 2; bucketIndex += 1) {
|
|
609
|
+
const start = 1 + Math.floor(bucketIndex * bucketSize);
|
|
610
|
+
const end = Math.min(data.length - 1, 1 + Math.floor((bucketIndex + 1) * bucketSize));
|
|
611
|
+
const bucket = data.slice(start, Math.max(start + 1, end));
|
|
612
|
+
const expectedRatio = (bucketIndex + 1) / (safeThreshold - 1);
|
|
613
|
+
const selected = selectLumenChartBucketDatum(bucket, first, last, expectedRatio);
|
|
614
|
+
if (selected)
|
|
615
|
+
sampled.push(selected);
|
|
616
|
+
}
|
|
617
|
+
sampled.push(last);
|
|
618
|
+
return sampled;
|
|
619
|
+
};
|
|
620
|
+
export const appendLumenChartDatum = (series, datum, maximumPoints = 500) => {
|
|
621
|
+
const safeMaximum = Math.max(1, Math.floor(maximumPoints));
|
|
622
|
+
const data = [...series.data, datum];
|
|
623
|
+
return {
|
|
624
|
+
...series,
|
|
625
|
+
data: data.slice(Math.max(0, data.length - safeMaximum))
|
|
626
|
+
};
|
|
627
|
+
};
|
|
628
|
+
const resolveLumenScatterGeometryOptions = (options) => ({
|
|
629
|
+
height: options.height ?? 320,
|
|
630
|
+
maximumRadius: options.maximumRadius ?? 18,
|
|
631
|
+
minimumRadius: options.minimumRadius ?? 4,
|
|
632
|
+
padding: options.padding ?? 44,
|
|
633
|
+
requestedDomain: options.domain,
|
|
634
|
+
requestedXDomain: options.xDomain,
|
|
635
|
+
width: options.width ?? 640,
|
|
636
|
+
xScale: options.xScale ?? 'linear'
|
|
637
|
+
});
|
|
638
|
+
const createLumenScatterPoint = (datum, context) => {
|
|
639
|
+
const numericX = getLumenChartNumericX(datum.x, context.xScale);
|
|
640
|
+
if (numericX === null || !isAvailableLumenChartY(datum.y))
|
|
641
|
+
return null;
|
|
642
|
+
const radius = datum.size === null || datum.size === undefined || !Number.isFinite(datum.size) || datum.size < 0 ?
|
|
643
|
+
context.minimumRadius :
|
|
644
|
+
scaleLumenChartValue(datum.size, context.sizeDomain, context.minimumRadius, context.maximumRadius);
|
|
645
|
+
return {
|
|
646
|
+
...datum,
|
|
647
|
+
radius,
|
|
648
|
+
seriesId: context.series.id,
|
|
649
|
+
seriesLabel: context.series.label,
|
|
650
|
+
tone: resolveLumenChartTone(datum.tone ?? context.series.tone, context.seriesIndex),
|
|
651
|
+
xCoordinate: scaleLumenChartValue(numericX, context.xDomain, context.padding, context.width - context.padding),
|
|
652
|
+
yCoordinate: scaleLumenChartValue(datum.y, context.domain, context.height - context.padding, context.padding)
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
export const createLumenScatterGeometry = (series, options = {}) => {
|
|
656
|
+
const { height, maximumRadius: requestedMaximumRadius, minimumRadius: requestedMinimumRadius, padding, requestedDomain, requestedXDomain, width, xScale } = resolveLumenScatterGeometryOptions(options);
|
|
657
|
+
const data = series.flatMap(item => item.data);
|
|
658
|
+
const projectedData = data.filter(datum => (getLumenChartNumericX(datum.x, xScale) !== null && isAvailableLumenChartY(datum.y)));
|
|
659
|
+
const domain = resolveLumenChartDomain(projectedData.map(datum => datum.y), requestedDomain, false);
|
|
660
|
+
const xDomain = getLumenChartXDomain(projectedData, xScale, requestedXDomain);
|
|
661
|
+
const sizes = projectedData.map(datum => (datum.size !== undefined && datum.size !== null && datum.size >= 0 ? datum.size : null));
|
|
662
|
+
const sizeDomain = getLumenChartDomain(sizes, false);
|
|
663
|
+
const minimumRadius = Math.max(1, requestedMinimumRadius);
|
|
664
|
+
const maximumRadius = Math.max(minimumRadius, requestedMaximumRadius);
|
|
665
|
+
const points = [];
|
|
666
|
+
for (const [seriesIndex, item] of series.entries()) {
|
|
667
|
+
for (const datum of item.data) {
|
|
668
|
+
const point = createLumenScatterPoint(datum, {
|
|
669
|
+
domain,
|
|
670
|
+
height,
|
|
671
|
+
maximumRadius,
|
|
672
|
+
minimumRadius,
|
|
673
|
+
padding,
|
|
674
|
+
series: item,
|
|
675
|
+
seriesIndex,
|
|
676
|
+
sizeDomain,
|
|
677
|
+
width,
|
|
678
|
+
xDomain,
|
|
679
|
+
xScale
|
|
680
|
+
});
|
|
681
|
+
if (point)
|
|
682
|
+
points.push(point);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { domain, height, points, width, xDomain };
|
|
686
|
+
};
|
|
687
|
+
const uniqueLumenChartCategories = (values) => {
|
|
688
|
+
const categories = new Map();
|
|
689
|
+
for (const value of values)
|
|
690
|
+
categories.set(lumenChartCategoryKey(value), value);
|
|
691
|
+
return [...categories.values()];
|
|
692
|
+
};
|
|
693
|
+
const createLumenHeatmapCell = (datum, context) => {
|
|
694
|
+
const xIndex = context.xIndexes.get(lumenChartCategoryKey(datum.x));
|
|
695
|
+
const yIndex = context.yIndexes.get(lumenChartCategoryKey(datum.y));
|
|
696
|
+
if (xIndex === undefined || yIndex === undefined)
|
|
697
|
+
return null;
|
|
698
|
+
return {
|
|
699
|
+
...datum,
|
|
700
|
+
height: context.cellHeight,
|
|
701
|
+
ratio: datum.value === null || !Number.isFinite(datum.value) ?
|
|
702
|
+
0 :
|
|
703
|
+
getLumenChartValueRatio(datum.value, context.domain),
|
|
704
|
+
width: context.cellWidth,
|
|
705
|
+
xCoordinate: xIndex * context.cellWidth,
|
|
706
|
+
yCoordinate: yIndex * context.cellHeight
|
|
707
|
+
};
|
|
708
|
+
};
|
|
709
|
+
export const createLumenHeatmapGeometry = (data, width = 640, height = 320) => {
|
|
710
|
+
const xCategories = uniqueLumenChartCategories(data.map(datum => datum.x));
|
|
711
|
+
const yCategories = uniqueLumenChartCategories(data.map(datum => datum.y));
|
|
712
|
+
const domain = getLumenChartDomain(data.map(datum => datum.value), false);
|
|
713
|
+
const cellWidth = width / Math.max(1, xCategories.length);
|
|
714
|
+
const cellHeight = height / Math.max(1, yCategories.length);
|
|
715
|
+
const xIndexes = new Map(xCategories.map((category, index) => [lumenChartCategoryKey(category), index]));
|
|
716
|
+
const yIndexes = new Map(yCategories.map((category, index) => [lumenChartCategoryKey(category), index]));
|
|
717
|
+
const cells = data.flatMap(datum => {
|
|
718
|
+
const cell = createLumenHeatmapCell(datum, {
|
|
719
|
+
cellHeight,
|
|
720
|
+
cellWidth,
|
|
721
|
+
domain,
|
|
722
|
+
xIndexes,
|
|
723
|
+
yIndexes
|
|
724
|
+
});
|
|
725
|
+
return cell ? [cell] : [];
|
|
726
|
+
});
|
|
727
|
+
return { cells, domain, height, width, xCategories, yCategories };
|
|
728
|
+
};
|
|
729
|
+
const createLumenRangePoint = (datum, index, length, width, height, padding, domain) => {
|
|
730
|
+
if (datum.low === null ||
|
|
731
|
+
datum.high === null ||
|
|
732
|
+
!Number.isFinite(datum.low) ||
|
|
733
|
+
!Number.isFinite(datum.high))
|
|
734
|
+
return null;
|
|
735
|
+
const denominator = Math.max(1, length - 1);
|
|
736
|
+
const xCoordinate = padding + (length === 1 ? 0.5 : index / denominator) * (width - padding * 2);
|
|
737
|
+
return {
|
|
738
|
+
...datum,
|
|
739
|
+
highCoordinate: scaleLumenChartValue(datum.high, domain, height - padding, padding),
|
|
740
|
+
lowCoordinate: scaleLumenChartValue(datum.low, domain, height - padding, padding),
|
|
741
|
+
xCoordinate
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
export const createLumenRangeGeometry = (data, options = {}) => {
|
|
745
|
+
const { domain: requestedDomain, height = 320, includeZero = false, padding = 44, width = 640 } = options;
|
|
746
|
+
const values = data.flatMap(datum => (datum.low !== null &&
|
|
747
|
+
datum.high !== null &&
|
|
748
|
+
Number.isFinite(datum.low) &&
|
|
749
|
+
Number.isFinite(datum.high) ?
|
|
750
|
+
[datum.low, datum.high] :
|
|
751
|
+
[]));
|
|
752
|
+
const domain = resolveLumenChartDomain(values, requestedDomain, includeZero);
|
|
753
|
+
const segments = [];
|
|
754
|
+
let currentSegment = [];
|
|
755
|
+
for (const [index, datum] of data.entries()) {
|
|
756
|
+
const point = createLumenRangePoint(datum, index, data.length, width, height, padding, domain);
|
|
757
|
+
if (!point) {
|
|
758
|
+
if (currentSegment.length > 0)
|
|
759
|
+
segments.push(currentSegment);
|
|
760
|
+
currentSegment = [];
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
currentSegment.push(point);
|
|
764
|
+
}
|
|
765
|
+
if (currentSegment.length > 0)
|
|
766
|
+
segments.push(currentSegment);
|
|
767
|
+
const areaPath = segments.map(segment => {
|
|
768
|
+
const upper = segment.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.xCoordinate.toFixed(3)} ${point.highCoordinate.toFixed(3)}`);
|
|
769
|
+
const lower = [...segment].reverse().map(point => `L ${point.xCoordinate.toFixed(3)} ${point.lowCoordinate.toFixed(3)}`);
|
|
770
|
+
return [...upper, ...lower, 'Z'].join(' ');
|
|
771
|
+
}).join(' ');
|
|
772
|
+
return {
|
|
773
|
+
areaPath,
|
|
774
|
+
domain,
|
|
775
|
+
points: segments.flat()
|
|
776
|
+
};
|
|
777
|
+
};
|
package/dist/code.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../src/code.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC5B,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,wBAAwB;;;;;;;CAOkB,CAAA;
|
|
1
|
+
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../src/code.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC5B,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,wBAAwB;;;;;;;CAOkB,CAAA;AA0QvD,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,WAK9C,CAAA;AA0LD,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,iBAAa,KACZ,cAAc,EA0BhB,CAAA;AAeD,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,EAAE,iBAAa,WAA6E,CAAA"}
|
package/dist/code.js
CHANGED
|
@@ -214,16 +214,29 @@ const startsWithIdentifierPattern = /^[A-Za-z_$]/;
|
|
|
214
214
|
const startsWithNumberPattern = /^\d/;
|
|
215
215
|
const startsWithCommentPattern = /^(?:#|--|\/[/*]|<!--)/;
|
|
216
216
|
const startsWithStringPattern = /^["'`]/;
|
|
217
|
+
const trimBlankEdgeLines = (lines) => {
|
|
218
|
+
let start = 0;
|
|
219
|
+
let end = lines.length;
|
|
220
|
+
while (start < end && lines[start]?.trim() === '')
|
|
221
|
+
start += 1;
|
|
222
|
+
while (end > start && lines[end - 1]?.trim() === '')
|
|
223
|
+
end -= 1;
|
|
224
|
+
return lines.slice(start, end);
|
|
225
|
+
};
|
|
226
|
+
const getMinimumIndent = (lines) => {
|
|
227
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
if (line.trim() === '')
|
|
230
|
+
continue;
|
|
231
|
+
const indent = leadingWhitespacePattern.exec(line)?.[0].length ?? 0;
|
|
232
|
+
if (indent < minimum)
|
|
233
|
+
minimum = indent;
|
|
234
|
+
}
|
|
235
|
+
return Number.isFinite(minimum) ? minimum : 0;
|
|
236
|
+
};
|
|
217
237
|
export const normalizeLumenCode = (code) => {
|
|
218
|
-
const lines = code.replaceAll(/\r\n?/g, '\n').split('\n');
|
|
219
|
-
|
|
220
|
-
lines.shift();
|
|
221
|
-
while (lines.at(-1)?.trim() === '')
|
|
222
|
-
lines.pop();
|
|
223
|
-
const indentation = lines
|
|
224
|
-
.filter(line => line.trim() !== '')
|
|
225
|
-
.map(line => leadingWhitespacePattern.exec(line)?.[0].length ?? 0);
|
|
226
|
-
const trimBy = indentation.length > 0 ? Math.min(...indentation) : 0;
|
|
238
|
+
const lines = trimBlankEdgeLines(code.replaceAll(/\r\n?/g, '\n').split('\n'));
|
|
239
|
+
const trimBy = getMinimumIndent(lines);
|
|
227
240
|
return lines.map(line => line.slice(trimBy)).join('\n');
|
|
228
241
|
};
|
|
229
242
|
const getJavascriptTokenKind = (value) => {
|