@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/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
- const available = finiteValues(values);
44
- if (available.length === 0)
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 step = (domain.max - domain.min) / (safeCount - 1);
69
- return Array.from({ length: safeCount }, (_, index) => domain.min + step * index);
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 splitGeometrySegments = (data, domain, width, height, padding) => {
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
- if (datum.y === null || !Number.isFinite(datum.y)) {
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: padding +
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 width = options.width ?? defaultChartSize;
199
- const height = options.height ?? defaultChartSize;
200
- const padding = options.padding ?? defaultChartPadding;
201
- const calculatedDomain = getLumenChartDomain(data.map(datum => datum.y), options.includeZero ?? false);
202
- const domain = {
203
- max: options.domain?.max ?? calculatedDomain.max,
204
- min: options.domain?.min ?? calculatedDomain.min
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
- export const alignLumenChartSeries = (series, categories) => ({
248
- ...series,
249
- data: categories.map(category => series.data.find(datum => datum.x === category) ?? {
250
- x: category,
251
- y: null
252
- })
253
- });
254
- const getDatumValue = (series, category) => series.data.find(datum => datum.x === category)?.y ?? 0;
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 series) {
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 domain = layout === 'stacked' ?
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 value = getDatumValue(item, category);
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
+ };
@@ -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;AAiPvD,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,WAc9C,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"}
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
- while (lines[0]?.trim() === '')
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) => {