@santi020k/lumen-core 3.0.1 → 4.0.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.
Files changed (147) hide show
  1. package/PHONE_FLAG_LICENSE.txt +21 -0
  2. package/README.md +363 -3
  3. package/THIRD_PARTY_NOTICES.md +16 -2
  4. package/dist/ai-surfaces.d.ts +25 -0
  5. package/dist/ai-surfaces.d.ts.map +1 -0
  6. package/dist/ai-surfaces.js +162 -0
  7. package/dist/amount-field.d.ts +22 -0
  8. package/dist/amount-field.d.ts.map +1 -0
  9. package/dist/amount-field.js +207 -0
  10. package/dist/attachments.d.ts +15 -0
  11. package/dist/attachments.d.ts.map +1 -0
  12. package/dist/attachments.js +123 -0
  13. package/dist/bullet-chart.d.ts +33 -0
  14. package/dist/bullet-chart.d.ts.map +1 -0
  15. package/dist/bullet-chart.js +80 -0
  16. package/dist/chart-activation.d.ts +32 -0
  17. package/dist/chart-activation.d.ts.map +1 -0
  18. package/dist/chart-activation.js +97 -0
  19. package/dist/chart-interaction.d.ts +12 -0
  20. package/dist/chart-interaction.d.ts.map +1 -0
  21. package/dist/chart-interaction.js +274 -0
  22. package/dist/chart-models.d.ts +156 -0
  23. package/dist/chart-models.d.ts.map +1 -0
  24. package/dist/chart-models.js +348 -0
  25. package/dist/chart-motion.d.ts +6 -0
  26. package/dist/chart-motion.d.ts.map +1 -0
  27. package/dist/chart-motion.js +107 -0
  28. package/dist/charts.d.ts +58 -3
  29. package/dist/charts.d.ts.map +1 -1
  30. package/dist/charts.js +247 -18
  31. package/dist/code.js +1 -1
  32. package/dist/combobox.d.ts +7 -0
  33. package/dist/combobox.d.ts.map +1 -0
  34. package/dist/combobox.js +253 -0
  35. package/dist/comparison-chart.d.ts +32 -0
  36. package/dist/comparison-chart.d.ts.map +1 -0
  37. package/dist/comparison-chart.js +64 -0
  38. package/dist/components.d.ts +210 -25
  39. package/dist/components.d.ts.map +1 -1
  40. package/dist/components.js +102 -26
  41. package/dist/dashboard.d.ts +16 -0
  42. package/dist/dashboard.d.ts.map +1 -0
  43. package/dist/dashboard.js +21 -0
  44. package/dist/data.d.ts.map +1 -1
  45. package/dist/data.js +21 -5
  46. package/dist/dates.d.ts +13 -0
  47. package/dist/dates.d.ts.map +1 -0
  48. package/dist/dates.js +37 -0
  49. package/dist/device-frame.d.ts +25 -0
  50. package/dist/device-frame.d.ts.map +1 -0
  51. package/dist/device-frame.js +50 -0
  52. package/dist/direction.d.ts +3 -0
  53. package/dist/direction.d.ts.map +1 -0
  54. package/dist/direction.js +10 -0
  55. package/dist/extended-charts.d.ts +95 -0
  56. package/dist/extended-charts.d.ts.map +1 -0
  57. package/dist/extended-charts.js +175 -0
  58. package/dist/figma.d.ts.map +1 -1
  59. package/dist/figma.js +10 -1
  60. package/dist/forms.d.ts.map +1 -1
  61. package/dist/forms.js +18 -4
  62. package/dist/icon-data.generated.d.ts +1 -1
  63. package/dist/icon-data.generated.d.ts.map +1 -1
  64. package/dist/icon-data.generated.js +1 -1
  65. package/dist/icons.js +3 -4
  66. package/dist/illustrations.generated.d.ts +1 -1
  67. package/dist/image-comparison.d.ts +14 -0
  68. package/dist/image-comparison.d.ts.map +1 -0
  69. package/dist/image-comparison.js +48 -0
  70. package/dist/index.d.ts +35 -4
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +34 -3
  73. package/dist/media-viewport.d.ts +24 -0
  74. package/dist/media-viewport.d.ts.map +1 -0
  75. package/dist/media-viewport.js +119 -0
  76. package/dist/media-workspace.d.ts +27 -0
  77. package/dist/media-workspace.d.ts.map +1 -0
  78. package/dist/media-workspace.js +83 -0
  79. package/dist/message-scroller.d.ts +14 -0
  80. package/dist/message-scroller.d.ts.map +1 -0
  81. package/dist/message-scroller.js +113 -0
  82. package/dist/motion-workflows.d.ts +24 -0
  83. package/dist/motion-workflows.d.ts.map +1 -0
  84. package/dist/motion-workflows.js +220 -0
  85. package/dist/motion.d.ts +12 -0
  86. package/dist/motion.d.ts.map +1 -0
  87. package/dist/motion.js +96 -0
  88. package/dist/native-input.d.ts +25 -0
  89. package/dist/native-input.d.ts.map +1 -0
  90. package/dist/native-input.js +188 -0
  91. package/dist/phone-flags.generated.d.ts +2 -0
  92. package/dist/phone-flags.generated.d.ts.map +1 -0
  93. package/dist/phone-flags.generated.js +263 -0
  94. package/dist/phone.d.ts +4 -0
  95. package/dist/phone.d.ts.map +1 -1
  96. package/dist/phone.js +6 -1
  97. package/dist/rich-text.d.ts +7 -0
  98. package/dist/rich-text.d.ts.map +1 -1
  99. package/dist/rich-text.js +13 -0
  100. package/dist/rive.d.ts +37 -0
  101. package/dist/rive.d.ts.map +1 -0
  102. package/dist/rive.js +129 -0
  103. package/dist/schedule.d.ts.map +1 -1
  104. package/dist/schedule.js +63 -23
  105. package/dist/tab-indicator.d.ts +3 -0
  106. package/dist/tab-indicator.d.ts.map +1 -0
  107. package/dist/tab-indicator.js +57 -0
  108. package/dist/theme-audit.d.ts +16 -0
  109. package/dist/theme-audit.d.ts.map +1 -0
  110. package/dist/theme-audit.js +57 -0
  111. package/dist/theme-builder.d.ts +7 -1
  112. package/dist/theme-builder.d.ts.map +1 -1
  113. package/dist/theme-builder.js +45 -23
  114. package/dist/theme-presets.generated.d.ts +217 -0
  115. package/dist/theme-presets.generated.d.ts.map +1 -0
  116. package/dist/theme-presets.generated.js +231 -0
  117. package/dist/theme.d.ts +11 -3
  118. package/dist/theme.d.ts.map +1 -1
  119. package/dist/theme.js +56 -6
  120. package/dist/virtual-collection.d.ts +14 -0
  121. package/dist/virtual-collection.d.ts.map +1 -0
  122. package/dist/virtual-collection.js +132 -0
  123. package/dist/virtual-list.d.ts +7 -0
  124. package/dist/virtual-list.d.ts.map +1 -0
  125. package/dist/virtual-list.js +135 -0
  126. package/dist/virtual-window.d.ts +19 -0
  127. package/dist/virtual-window.d.ts.map +1 -0
  128. package/dist/virtual-window.js +84 -0
  129. package/dist/visual-effects.d.ts +6 -0
  130. package/dist/visual-effects.d.ts.map +1 -0
  131. package/dist/visual-effects.js +44 -0
  132. package/dist/world-map-data.generated.d.ts +9 -0
  133. package/dist/world-map-data.generated.d.ts.map +1 -0
  134. package/dist/world-map-data.generated.js +240 -0
  135. package/dist/world-map-navigation.d.ts +16 -0
  136. package/dist/world-map-navigation.d.ts.map +1 -0
  137. package/dist/world-map-navigation.js +53 -0
  138. package/dist/world-map-pan.d.ts +3 -0
  139. package/dist/world-map-pan.d.ts.map +1 -0
  140. package/dist/world-map-pan.js +66 -0
  141. package/dist/world-map-zoom.d.ts +13 -0
  142. package/dist/world-map-zoom.d.ts.map +1 -0
  143. package/dist/world-map-zoom.js +61 -0
  144. package/dist/world-map.d.ts +71 -0
  145. package/dist/world-map.d.ts.map +1 -0
  146. package/dist/world-map.js +143 -0
  147. package/package.json +51 -4
package/dist/charts.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { createLumenChartActivationController, createLumenChartDatumActivation, createLumenHeatmapDatumActivation, createLumenRangeDatumActivation, parseLumenChartDatumActivation } from './chart-activation.js';
1
2
  export const lumenChartTones = [
2
3
  'series-1',
3
4
  'series-2',
@@ -25,11 +26,18 @@ const formatDefaultLumenChartSummary = (summary, formatValue) => {
25
26
  `Values range from ${formatValue(summary.minimum ?? 0)} to ${formatValue(summary.maximum ?? 0)}.${missing}`;
26
27
  };
27
28
  export const lumenChartLabels = Object.freeze({
29
+ count: 'Count',
30
+ density: 'Frequency density',
31
+ end: 'End',
32
+ invalidData: 'Chart data is invalid. Check the supplied values.',
33
+ start: 'Start',
28
34
  category: 'Category',
29
35
  chartData: 'Chart data',
30
36
  chartLegend: 'Chart legend',
31
37
  column: 'Column',
32
38
  empty: 'No chart data available.',
39
+ exploreData: 'Explore chart data',
40
+ formatDatumAction: (context) => `Open details: ${context}`,
33
41
  formatHeatmapSummary: (count) => count === 0 ?
34
42
  'No chart data available.' :
35
43
  `${count} available heatmap ${count === 1 ? 'cell' : 'cells'}.`,
@@ -88,9 +96,9 @@ export const getLumenChartDomain = (values, includeZero = true) => {
88
96
  max = Math.max(0, max);
89
97
  }
90
98
  if (min === max) {
91
- const offset = Math.abs(min || 1) * 0.1;
92
- min -= offset;
93
- max += offset;
99
+ const offset = Math.max(Number.MIN_VALUE, Math.abs(min || 1) * 0.1);
100
+ min = Math.max(-Number.MAX_VALUE, min - offset);
101
+ max = Math.min(Number.MAX_VALUE, max + offset);
94
102
  }
95
103
  return { max, min };
96
104
  };
@@ -103,9 +111,12 @@ const resolveLumenChartDomain = (values, requested, includeZero) => {
103
111
  };
104
112
  export const scaleLumenChartValue = (value, domain, rangeStart, rangeEnd) => {
105
113
  const domainSize = domain.max - domain.min;
106
- if (!Number.isFinite(value) || domainSize === 0)
114
+ if (!Number.isFinite(value) || !Number.isFinite(domain.min) || !Number.isFinite(domain.max) || domainSize === 0)
107
115
  return rangeStart;
108
- const ratio = (value - domain.min) / domainSize;
116
+ // A valid domain can span opposite finite extremes whose difference overflows.
117
+ const ratio = Number.isFinite(domainSize) ?
118
+ (value - domain.min) / domainSize :
119
+ (value / 2 - domain.min / 2) / (domain.max / 2 - domain.min / 2);
109
120
  return rangeStart + ratio * (rangeEnd - rangeStart);
110
121
  };
111
122
  export const getLumenChartTicks = (domain, count = 5) => {
@@ -199,6 +210,83 @@ export const getLumenChartAxisPadding = (labels, minimum = 44) => {
199
210
  const estimated = widestLabelWidth + chartAxisLabelGap;
200
211
  return Math.min(maximumChartAxisPadding, Math.max(safeMinimum, estimated));
201
212
  };
213
+ const fitLumenChartLabel = (label, maximumWidth) => {
214
+ if (estimateLumenChartAxisLabelWidth(label) <= maximumWidth)
215
+ return label;
216
+ const suffix = '…';
217
+ let result = '';
218
+ let width = estimateLumenChartAxisLabelWidth(suffix);
219
+ for (const character of label) {
220
+ width += Math.max(chartAxisLabelCharacterWidth, getLumenChartAxisCharacterWidth(character));
221
+ if (width > maximumWidth)
222
+ break;
223
+ result += character;
224
+ }
225
+ return result + suffix;
226
+ };
227
+ const getLumenChartTickAnchor = (index, count) => {
228
+ if (count === 1)
229
+ return 'middle';
230
+ if (index === 0)
231
+ return 'start';
232
+ return index === count - 1 ? 'end' : 'middle';
233
+ };
234
+ const selectLumenChartCategoryTicks = (candidates, gap) => {
235
+ const first = candidates[0];
236
+ const last = candidates.at(-1);
237
+ if (!first || !last)
238
+ return [];
239
+ if (candidates.length === 1)
240
+ return [first];
241
+ const lastStart = last.position - estimateLumenChartAxisLabelWidth(last.label);
242
+ const ticks = [first];
243
+ let previousEnd = first.position + estimateLumenChartAxisLabelWidth(first.label);
244
+ for (const candidate of candidates.slice(1, -1)) {
245
+ const halfWidth = estimateLumenChartAxisLabelWidth(candidate.label) / 2;
246
+ const labelStart = candidate.position - halfWidth;
247
+ const labelEnd = candidate.position + halfWidth;
248
+ if (labelStart < previousEnd + gap || labelEnd + gap > lastStart)
249
+ continue;
250
+ ticks.push(candidate);
251
+ previousEnd = labelEnd;
252
+ }
253
+ if (lastStart >= previousEnd + gap)
254
+ ticks.push(last);
255
+ return ticks;
256
+ };
257
+ const resolveLumenChartTickPositions = (count, end, positions, start) => {
258
+ const validPositions = positions?.length === count && positions.every((position, index) => (Number.isFinite(position) && position >= start && position <= end && position >= (positions[index - 1] ?? start)));
259
+ if (validPositions)
260
+ return positions;
261
+ return Array.from({ length: count }, (_, index) => (start + (count === 1 ? 0.5 : index / (count - 1)) * (end - start)));
262
+ };
263
+ /** Selects readable axis labels without changing or downsampling the underlying chart data. */
264
+ export const getLumenChartCategoryTicks = (labels, { end, minimumGap = 16, positions, start }) => {
265
+ if (labels.length === 0 || !Number.isFinite(start) || !Number.isFinite(end) || end < start)
266
+ return [];
267
+ const gap = Number.isFinite(minimumGap) ? Math.max(0, minimumGap) : 16;
268
+ const resolvedPositions = resolveLumenChartTickPositions(labels.length, end, positions, start);
269
+ const span = (resolvedPositions.at(-1) ?? end) - (resolvedPositions[0] ?? start);
270
+ const maximumWidth = Math.max(0, labels.length === 1 ? end - start : (span - gap) / 2);
271
+ const candidates = labels.map((label, index) => ({
272
+ index,
273
+ label: fitLumenChartLabel(label, maximumWidth),
274
+ position: resolvedPositions[index] ?? start,
275
+ textAnchor: getLumenChartTickAnchor(index, labels.length)
276
+ }));
277
+ return selectLumenChartCategoryTicks(candidates, gap);
278
+ };
279
+ /** Axis labels prefer xLabel; detail labels prefer an explicitly supplied full formatter. */
280
+ export const getLumenChartCategoryLabel = (series, category, formatCategory, context = 'axis') => {
281
+ if (context === 'detail' && formatCategory)
282
+ return formatCategory(category);
283
+ for (const item of series) {
284
+ const label = item.data.find(datum => datum.x === category)?.xLabel;
285
+ if (label !== undefined)
286
+ return label;
287
+ }
288
+ return formatCategory?.(category) ?? String(category);
289
+ };
202
290
  export const resolveLumenChartTone = (tone, index = 0) => tone ?? lumenChartTones[index % 8] ?? 'series-1';
203
291
  export const getLumenChartToneClassName = (tone, index = 0) => lumenChartToneClassNames[resolveLumenChartTone(tone, index)];
204
292
  export const getLumenPieChartVariantClassName = (variant) => lumenPieChartVariantClassNames[variant];
@@ -302,6 +390,8 @@ export const getLumenChartNumericX = (value, scale) => {
302
390
  if (typeof value === 'number')
303
391
  return Number.isFinite(value) ? value : null;
304
392
  if (scale === 'linear') {
393
+ if (!value.trim())
394
+ return null;
305
395
  const numeric = Number(value);
306
396
  return Number.isFinite(numeric) ? numeric : null;
307
397
  }
@@ -454,7 +544,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
454
544
  const layout = options.layout ?? 'grouped';
455
545
  const categories = getLumenChartCategories(series);
456
546
  const indexedSeries = series.map(indexLumenChartSeries);
457
- const values = series.flatMap(item => item.data.map(datum => datum.y));
547
+ const values = indexedSeries.flatMap(item => [...item.values()].map(datum => datum.y));
458
548
  const calculatedDomain = layout === 'stacked' ?
459
549
  getStackedDomain(series, categories) :
460
550
  getLumenChartDomain(values);
@@ -465,11 +555,16 @@ export const createLumenBarGeometry = (series, options = {}) => {
465
555
  const margin = orientation === 'horizontal' ?
466
556
  {
467
557
  bottom: 24,
468
- left: Math.max(64, Math.min(240, options.categoryWidth ?? 112)),
558
+ left: Math.max(64, Math.min(240, options.categoryWidth ?? getLumenChartAxisPadding(categories.map(category => getLumenChartCategoryLabel(series, category, options.formatCategory)), 112))),
469
559
  right: 20,
470
560
  top: 16
471
561
  } :
472
- { bottom: 52, left: 52, right: 16, top: 16 };
562
+ {
563
+ bottom: 52,
564
+ left: getLumenChartAxisPadding(getLumenChartTicks(domain).map(tick => (options.formatValue ?? String)(tick)), 52),
565
+ right: 16,
566
+ top: 16
567
+ };
473
568
  const plotWidth = Math.max(1, width - margin.left - margin.right);
474
569
  const plotHeight = Math.max(1, height - margin.top - margin.bottom);
475
570
  const categorySize = (orientation === 'horizontal' ? plotHeight : plotWidth) /
@@ -487,9 +582,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
487
582
  let negativeOffset = 0;
488
583
  categoryPositions.push({
489
584
  category,
490
- label: series
491
- .flatMap(item => item.data)
492
- .find(datum => datum.x === category)?.xLabel ?? category,
585
+ label: getLumenChartCategoryLabel(series, category, options.formatCategory),
493
586
  x: orientation === 'horizontal' ?
494
587
  margin.left - 8 :
495
588
  categoryStart + usableCategorySize / 2,
@@ -557,6 +650,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
557
650
  categories: categoryPositions,
558
651
  domain,
559
652
  height,
653
+ margin,
560
654
  marks,
561
655
  width
562
656
  };
@@ -622,8 +716,18 @@ const validateLumenChartDatumNumbers = (datum, context) => {
622
716
  }
623
717
  };
624
718
  const validateLumenChartDatumX = (datum, context) => {
625
- if (context.xScale === 'categorical')
719
+ if (context.xScale === 'categorical') {
720
+ const key = lumenChartCategoryKey(datum.x);
721
+ if (context.categories.has(key)) {
722
+ context.issues.push({
723
+ code: 'duplicate-category',
724
+ message: 'Category x values must be unique within a series. Use xLabel for repeated display labels.',
725
+ path: `${context.path}.x`
726
+ });
727
+ }
728
+ context.categories.add(key);
626
729
  return context.previousX;
730
+ }
627
731
  const numericX = getLumenChartNumericX(datum.x, context.xScale);
628
732
  if (numericX === null) {
629
733
  context.issues.push({
@@ -661,9 +765,11 @@ export const validateLumenChartSeries = (series, xScale = 'categorical') => {
661
765
  }
662
766
  seriesIds.add(item.id);
663
767
  const datumIds = new Set();
768
+ const categories = new Set();
664
769
  let previousX = Number.NEGATIVE_INFINITY;
665
770
  for (const [datumIndex, datum] of item.data.entries()) {
666
771
  previousX = validateLumenChartDatum(datum, {
772
+ categories,
667
773
  datumIds,
668
774
  issues,
669
775
  path: `${seriesPath}.data[${datumIndex}]`,
@@ -723,6 +829,68 @@ export const appendLumenChartDatum = (series, datum, maximumPoints = 500) => {
723
829
  data: data.slice(Math.max(0, data.length - safeMaximum))
724
830
  };
725
831
  };
832
+ const getLumenScatterNumericX = (value, scale) => {
833
+ const numeric = getLumenChartNumericX(value, scale === 'log' ? 'linear' : scale);
834
+ return scale === 'log' && numeric !== null && numeric <= 0 ? null : numeric;
835
+ };
836
+ const validScatterDomain = (domain, scale) => {
837
+ const finite = Number.isFinite(domain.min) && Number.isFinite(domain.max);
838
+ return finite && domain.max > domain.min && (scale !== 'log' || domain.min > 0);
839
+ };
840
+ const getLumenScatterXDomain = (data, scale, requested) => {
841
+ const values = data.map(datum => getLumenScatterNumericX(datum.x, scale));
842
+ const calculated = scale === 'log' && !values.some(value => value !== null) ?
843
+ { min: 1, max: 10 } :
844
+ getLumenChartDomain(values, false);
845
+ const domain = { min: requested?.min ?? calculated.min, max: requested?.max ?? calculated.max };
846
+ if (!validScatterDomain(domain, scale))
847
+ throw new RangeError('Scatter x domain must be finite, ordered, and positive for log scales');
848
+ return domain;
849
+ };
850
+ export const scaleLumenScatterX = (value, domain, rangeStart, rangeEnd, scale = 'linear') => {
851
+ if (scale !== 'log')
852
+ return scaleLumenChartValue(value, domain, rangeStart, rangeEnd);
853
+ if (value <= 0 || !Number.isFinite(value))
854
+ return Number.NaN;
855
+ return scaleLumenChartValue(Math.log10(value), { min: Math.log10(domain.min), max: Math.log10(domain.max) }, rangeStart, rangeEnd);
856
+ };
857
+ export const getLumenScatterXTicks = (domain, scale) => scale === 'log' ?
858
+ getLumenChartTicks({ min: Math.log10(domain.min), max: Math.log10(domain.max) }).map(value => 10 ** value) :
859
+ getLumenChartTicks(domain);
860
+ const scatterReferenceCoordinate = (value, fallback, project) => {
861
+ if (value === undefined)
862
+ return fallback;
863
+ return project(value);
864
+ };
865
+ const isLumenScatterReference = (value) => {
866
+ if (typeof value !== 'object' || value === null || !('id' in value) || !('label' in value))
867
+ return false;
868
+ if (typeof value.id !== 'string' || typeof value.label !== 'string')
869
+ return false;
870
+ return ['x', 'y', 'xEnd', 'yEnd'].every(key => {
871
+ const coordinate = Reflect.get(value, key);
872
+ return coordinate === undefined || (typeof coordinate === 'number' && Number.isFinite(coordinate));
873
+ });
874
+ };
875
+ export const createLumenScatterReferences = (references, geometry, scale = 'linear', padding = 44) => {
876
+ const result = [];
877
+ if (!Array.isArray(references))
878
+ return result;
879
+ const x = (value) => scaleLumenScatterX(value, geometry.xDomain, padding, geometry.width - padding, scale);
880
+ const y = (value) => scaleLumenChartValue(value, geometry.domain, geometry.height - padding, padding);
881
+ for (const reference of references) {
882
+ if (!isLumenScatterReference(reference))
883
+ continue;
884
+ const region = reference.xEnd !== undefined && reference.yEnd !== undefined;
885
+ const x1 = scatterReferenceCoordinate(reference.x, padding, x);
886
+ const y1 = scatterReferenceCoordinate(reference.y, padding, y);
887
+ const x2 = scatterReferenceCoordinate(reference.xEnd, reference.x === undefined ? geometry.width - padding : x1, x);
888
+ const y2 = scatterReferenceCoordinate(reference.yEnd, reference.y === undefined ? geometry.height - padding : y1, y);
889
+ if ([x1, x2, y1, y2].every(Number.isFinite))
890
+ result.push({ ...reference, x1, x2, y1, y2, region });
891
+ }
892
+ return result;
893
+ };
726
894
  const resolveLumenScatterGeometryOptions = (options) => ({
727
895
  height: options.height ?? 320,
728
896
  maximumRadius: options.maximumRadius ?? 18,
@@ -734,7 +902,7 @@ const resolveLumenScatterGeometryOptions = (options) => ({
734
902
  xScale: options.xScale ?? 'linear'
735
903
  });
736
904
  const createLumenScatterPoint = (datum, context) => {
737
- const numericX = getLumenChartNumericX(datum.x, context.xScale);
905
+ const numericX = getLumenScatterNumericX(datum.x, context.xScale);
738
906
  if (numericX === null || !isAvailableLumenChartY(datum.y))
739
907
  return null;
740
908
  const radius = datum.size === null || datum.size === undefined || !Number.isFinite(datum.size) || datum.size < 0 ?
@@ -746,20 +914,52 @@ const createLumenScatterPoint = (datum, context) => {
746
914
  seriesId: context.series.id,
747
915
  seriesLabel: context.series.label,
748
916
  tone: resolveLumenChartTone(datum.tone ?? context.series.tone, context.seriesIndex),
749
- xCoordinate: scaleLumenChartValue(numericX, context.xDomain, context.padding, context.width - context.padding),
917
+ xCoordinate: scaleLumenScatterX(numericX, context.xDomain, context.padding, context.width - context.padding, context.xScale),
750
918
  yCoordinate: scaleLumenChartValue(datum.y, context.domain, context.height - context.padding, context.padding)
751
919
  };
752
920
  };
921
+ const finiteLumenScatterBound = (value, fallback, scale) => (Number.isFinite(value) && (scale !== 'log' || value > 0) ? value : fallback);
922
+ // Automatic extents leave room for the complete bubble, including its stroke.
923
+ // Explicit bounds remain exact so consumers can intentionally crop a viewport.
924
+ const padLumenScatterDomain = (domain, values, requested, extent, radius, scale = 'linear') => {
925
+ if (values.length === 0 || extent <= 0)
926
+ return domain;
927
+ const project = (value) => scale === 'log' ? Math.log10(value) : value;
928
+ const fromScale = (value) => scale === 'log' ? 10 ** value : value;
929
+ const min = project(domain.min);
930
+ const max = project(domain.max);
931
+ const inset = Math.min(radius + 2, extent / 4);
932
+ const extra = (max - min) * inset / (extent - 2 * inset);
933
+ const projected = values.map(project);
934
+ const bounds = projected.reduce((result, value) => ({
935
+ min: Math.min(result.min, value), max: Math.max(result.max, value)
936
+ }), { min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY });
937
+ const paddedMin = fromScale(Math.min(min, bounds.min - extra));
938
+ const paddedMax = fromScale(Math.max(max, bounds.max + extra));
939
+ return {
940
+ min: requested?.min ?? finiteLumenScatterBound(paddedMin, domain.min, scale),
941
+ max: requested?.max ?? finiteLumenScatterBound(paddedMax, domain.max, scale)
942
+ };
943
+ };
753
944
  export const createLumenScatterGeometry = (series, options = {}) => {
754
945
  const { height, maximumRadius: requestedMaximumRadius, minimumRadius: requestedMinimumRadius, padding, requestedDomain, requestedXDomain, width, xScale } = resolveLumenScatterGeometryOptions(options);
755
946
  const data = series.flatMap(item => item.data);
756
- const projectedData = data.filter(datum => (getLumenChartNumericX(datum.x, xScale) !== null && isAvailableLumenChartY(datum.y)));
757
- const domain = resolveLumenChartDomain(projectedData.map(datum => datum.y), requestedDomain, false);
758
- const xDomain = getLumenChartXDomain(projectedData, xScale, requestedXDomain);
947
+ const projectedData = data.filter(datum => (getLumenScatterNumericX(datum.x, xScale) !== null && isAvailableLumenChartY(datum.y)));
948
+ const calculatedDomain = resolveLumenChartDomain(projectedData.map(datum => datum.y), requestedDomain, false);
949
+ if (!validScatterDomain(calculatedDomain, 'linear')) {
950
+ throw new RangeError('Scatter y domain must be finite and increasing');
951
+ }
952
+ const calculatedXDomain = getLumenScatterXDomain(projectedData, xScale, requestedXDomain);
759
953
  const sizes = projectedData.map(datum => (datum.size !== undefined && datum.size !== null && datum.size >= 0 ? datum.size : null));
760
954
  const sizeDomain = getLumenChartDomain(sizes, false);
761
955
  const minimumRadius = Math.max(1, requestedMinimumRadius);
762
956
  const maximumRadius = Math.max(minimumRadius, requestedMaximumRadius);
957
+ const yValues = projectedData.flatMap(datum => datum.y === null ? [] : [datum.y]);
958
+ const domain = padLumenScatterDomain(calculatedDomain, yValues, requestedDomain, height - padding * 2, maximumRadius);
959
+ const xDomain = padLumenScatterDomain(calculatedXDomain, projectedData.flatMap(datum => {
960
+ const value = getLumenScatterNumericX(datum.x, xScale);
961
+ return value === null ? [] : [value];
962
+ }), requestedXDomain, width - padding * 2, maximumRadius, xScale);
763
963
  const points = [];
764
964
  for (const [seriesIndex, item] of series.entries()) {
765
965
  for (const datum of item.data) {
@@ -788,6 +988,33 @@ const uniqueLumenChartCategories = (values) => {
788
988
  categories.set(lumenChartCategoryKey(value), value);
789
989
  return [...categories.values()];
790
990
  };
991
+ const isHeatmapRecord = (value) => {
992
+ if (Array.isArray(value))
993
+ return false;
994
+ return typeof value === 'object' && value !== null;
995
+ };
996
+ const isHeatmapCoordinate = (value) => typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value));
997
+ const isOptionalHeatmapLabel = (value) => value === undefined || typeof value === 'string';
998
+ const isHeatmapTone = (value) => {
999
+ if (value === undefined)
1000
+ return true;
1001
+ return lumenChartTones.some(tone => tone === value);
1002
+ };
1003
+ const isHeatmapDatum = (datum) => {
1004
+ if (!isHeatmapRecord(datum))
1005
+ return false;
1006
+ return isHeatmapCoordinate(datum.x) && isHeatmapCoordinate(datum.y) &&
1007
+ // Non-finite measurements are missing cells; coordinates still require finite numbers.
1008
+ (datum.value === null || typeof datum.value === 'number') &&
1009
+ [datum.id, datum.label, datum.xLabel, datum.yLabel].every(isOptionalHeatmapLabel) && isHeatmapTone(datum.tone);
1010
+ };
1011
+ /** Reject the whole decoded collection before adapters read or format cell fields. */
1012
+ export const normalizeLumenHeatmapData = (data) => {
1013
+ if (!Array.isArray(data))
1014
+ return [];
1015
+ const rows = Array.from(data);
1016
+ return rows.every(isHeatmapDatum) ? rows : [];
1017
+ };
791
1018
  const createLumenHeatmapCell = (datum, context) => {
792
1019
  const xIndex = context.xIndexes.get(lumenChartCategoryKey(datum.x));
793
1020
  const yIndex = context.yIndexes.get(lumenChartCategoryKey(datum.y));
@@ -804,7 +1031,8 @@ const createLumenHeatmapCell = (datum, context) => {
804
1031
  yCoordinate: yIndex * context.cellHeight
805
1032
  };
806
1033
  };
807
- export const createLumenHeatmapGeometry = (data, width = 640, height = 320) => {
1034
+ export const createLumenHeatmapGeometry = (rawData, width = 640, height = 320) => {
1035
+ const data = normalizeLumenHeatmapData(rawData);
808
1036
  const xCategories = uniqueLumenChartCategories(data.map(datum => datum.x));
809
1037
  const yCategories = uniqueLumenChartCategories(data.map(datum => datum.y));
810
1038
  const domain = getLumenChartDomain(data.map(datum => datum.value), false);
@@ -873,3 +1101,4 @@ export const createLumenRangeGeometry = (data, options = {}) => {
873
1101
  points: segments.flat()
874
1102
  };
875
1103
  };
1104
+ export * from './extended-charts.js';
package/dist/code.js CHANGED
@@ -320,7 +320,7 @@ const getMarkupTokenKind = (value, source, start) => {
320
320
  return 'symbol';
321
321
  const openingTagIndex = source.lastIndexOf('<', start);
322
322
  const closingTagIndex = source.lastIndexOf('>', start);
323
- if (openingTagIndex < closingTagIndex)
323
+ if (openingTagIndex <= closingTagIndex)
324
324
  return undefined;
325
325
  const beforeValue = source
326
326
  .slice(openingTagIndex + 1, start)
@@ -0,0 +1,7 @@
1
+ export interface LumenComboboxController {
2
+ close: () => void;
3
+ destroy: () => void;
4
+ }
5
+ /** Editable combobox. */
6
+ export declare const createLumenComboboxController: (root: HTMLElement) => LumenComboboxController;
7
+ //# sourceMappingURL=combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../src/combobox.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAyBD,yBAAyB;AACzB,eAAO,MAAM,6BAA6B,GAAI,MAAM,WAAW,KAAG,uBAwTjE,CAAA"}
@@ -0,0 +1,253 @@
1
+ let nextId = 0;
2
+ const ignoresKeyboard = (event, composing) => event.defaultPrevented || event.isComposing || composing ||
3
+ event.altKey || event.ctrlKey || event.metaKey;
4
+ const filterOption = (item, query, filterHidden, writeHidden) => {
5
+ const label = item.textContent.trim().toLowerCase();
6
+ const value = item.dataset.value?.toLowerCase() ?? '';
7
+ const hidden = Boolean(query) && !label.includes(query) && !value.includes(query);
8
+ if (hidden && !item.hidden) {
9
+ writeHidden(item, true);
10
+ }
11
+ else if (!hidden && filterHidden.has(item)) {
12
+ writeHidden(item, false);
13
+ }
14
+ };
15
+ /** Editable combobox. */
16
+ export const createLumenComboboxController = (root) => {
17
+ const input = root.querySelector('input[role="combobox"]');
18
+ const list = root.querySelector('[role="listbox"]');
19
+ const document = root.ownerDocument;
20
+ const view = document.defaultView;
21
+ if (!input || !list || !view)
22
+ return { close: () => undefined, destroy: () => undefined };
23
+ const abort = new view.AbortController();
24
+ const { signal } = abort;
25
+ let active;
26
+ let composing = false;
27
+ let filtering = false;
28
+ let destroyed = false;
29
+ const filterHidden = new Set();
30
+ let resetTimer;
31
+ const eventRoot = root.getRootNode();
32
+ const observerRef = {};
33
+ const preserveVisibility = (records) => {
34
+ for (const record of records) {
35
+ if (record.attributeName === 'hidden') {
36
+ for (const option of filterHidden)
37
+ if (option === record.target)
38
+ filterHidden.delete(option);
39
+ }
40
+ }
41
+ for (const option of filterHidden) {
42
+ if (!list.contains(option)) {
43
+ option.hidden = false;
44
+ filterHidden.delete(option);
45
+ }
46
+ }
47
+ };
48
+ if (!list.id)
49
+ list.id = `ui-combobox-list-${++nextId}`;
50
+ input.setAttribute('aria-controls', list.id);
51
+ input.setAttribute('aria-autocomplete', 'list');
52
+ const items = () => [...list.querySelectorAll('[role="option"]')];
53
+ const editable = () => !input.matches(':disabled') && !input.readOnly;
54
+ const available = (item) => !item.hidden &&
55
+ !item.matches(':disabled, [disabled]') && item.getAttribute('aria-disabled') !== 'true';
56
+ const activate = (item) => {
57
+ active = item;
58
+ for (const option of items())
59
+ option.setAttribute('aria-selected', String(option === item));
60
+ if (item) {
61
+ if (!item.id)
62
+ item.id = `ui-combobox-option-${++nextId}`;
63
+ input.setAttribute('aria-activedescendant', item.id);
64
+ }
65
+ else {
66
+ input.removeAttribute('aria-activedescendant');
67
+ }
68
+ };
69
+ const close = () => {
70
+ input.setAttribute('aria-expanded', 'false');
71
+ if (!list.hidden)
72
+ list.hidden = true;
73
+ list.dataset.state = 'closed';
74
+ activate();
75
+ };
76
+ const flushVisibility = () => {
77
+ preserveVisibility(observerRef.current?.takeRecords() ?? []);
78
+ };
79
+ const writeHidden = (item, hidden) => {
80
+ flushVisibility();
81
+ if (hidden)
82
+ filterHidden.add(item);
83
+ else
84
+ filterHidden.delete(item);
85
+ item.hidden = hidden;
86
+ preserveVisibility(observerRef.current?.takeRecords().slice(1) ?? []);
87
+ };
88
+ const refresh = () => {
89
+ flushVisibility();
90
+ const query = input.value.trim().toLowerCase();
91
+ for (const item of items()) {
92
+ item.tabIndex = -1;
93
+ if (!item.id)
94
+ item.id = `ui-combobox-option-${++nextId}`;
95
+ if (filtering)
96
+ filterOption(item, query, filterHidden, writeHidden);
97
+ }
98
+ if (active && (!list.contains(active) || !available(active)))
99
+ activate();
100
+ if (!editable())
101
+ close();
102
+ };
103
+ const open = () => {
104
+ if (!editable())
105
+ return;
106
+ refresh();
107
+ input.setAttribute('aria-expanded', 'true');
108
+ list.hidden = false;
109
+ list.dataset.state = 'open';
110
+ };
111
+ const select = (item) => {
112
+ if (!editable() || !list.contains(item) || !available(item))
113
+ return;
114
+ input.value = item.dataset.value ?? item.textContent.trim();
115
+ input.dispatchEvent(new view.Event('input', { bubbles: true }));
116
+ input.dispatchEvent(new view.Event('change', { bubbles: true }));
117
+ input.focus({ preventScroll: true });
118
+ close();
119
+ };
120
+ const navigate = (key) => {
121
+ open();
122
+ const options = items().filter(available);
123
+ const index = active ? options.indexOf(active) : -1;
124
+ let next = (index + 1) % options.length;
125
+ if (key === 'ArrowUp') {
126
+ next = index < 0 ? options.length - 1 : (index - 1 + options.length) % options.length;
127
+ }
128
+ activate(options[next]);
129
+ if (typeof active?.scrollIntoView === 'function')
130
+ active.scrollIntoView({ block: 'nearest' });
131
+ };
132
+ const keydown = (event) => {
133
+ if (ignoresKeyboard(event, composing) || !editable())
134
+ return;
135
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
136
+ event.preventDefault();
137
+ navigate(event.key);
138
+ return;
139
+ }
140
+ switch (event.key) {
141
+ case 'Escape':
142
+ if (list.hidden)
143
+ return;
144
+ event.preventDefault();
145
+ close();
146
+ break;
147
+ case 'Enter':
148
+ if (list.hidden || !active)
149
+ return;
150
+ event.preventDefault();
151
+ select(active);
152
+ break;
153
+ }
154
+ };
155
+ const optionFromEvent = (event) => (items().find(option => event.composedPath().includes(option)));
156
+ refresh();
157
+ close();
158
+ input.addEventListener('focus', open, { signal });
159
+ input.addEventListener('compositionstart', () => {
160
+ composing = true;
161
+ }, { signal });
162
+ input.addEventListener('compositionend', () => {
163
+ composing = false;
164
+ filtering = true;
165
+ activate();
166
+ open();
167
+ }, { signal });
168
+ input.addEventListener('input', () => {
169
+ if (composing)
170
+ return;
171
+ filtering = true;
172
+ activate();
173
+ open();
174
+ }, { signal });
175
+ input.addEventListener('keydown', keydown, { signal });
176
+ eventRoot.addEventListener('reset', event => {
177
+ if (event.target !== input.form)
178
+ return;
179
+ view.clearTimeout(resetTimer);
180
+ resetTimer = view.setTimeout(() => {
181
+ if (destroyed || event.defaultPrevented || !input.isConnected)
182
+ return;
183
+ composing = false;
184
+ filtering = true;
185
+ close();
186
+ refresh();
187
+ });
188
+ }, { capture: true, signal });
189
+ list.addEventListener('pointerdown', event => {
190
+ if (optionFromEvent(event))
191
+ event.preventDefault();
192
+ }, { signal });
193
+ list.addEventListener('click', event => {
194
+ if (event.defaultPrevented)
195
+ return;
196
+ const option = optionFromEvent(event);
197
+ if (option) {
198
+ event.preventDefault();
199
+ select(option);
200
+ }
201
+ }, { signal });
202
+ const isNode = (target) => {
203
+ try {
204
+ view.Node.prototype.contains.call(target, null);
205
+ return true;
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ };
211
+ root.addEventListener('focusout', event => {
212
+ if (!event.relatedTarget || !isNode(event.relatedTarget) || !root.contains(event.relatedTarget))
213
+ close();
214
+ }, { signal });
215
+ document.addEventListener('pointerdown', event => {
216
+ if (!event.composedPath().includes(root))
217
+ close();
218
+ }, { signal });
219
+ const observer = new view.MutationObserver(records => {
220
+ preserveVisibility(records);
221
+ if (!destroyed)
222
+ refresh();
223
+ });
224
+ observerRef.current = observer;
225
+ observer.observe(list, {
226
+ attributeFilter: ['disabled', 'aria-disabled', 'data-value', 'hidden'],
227
+ attributes: true,
228
+ characterData: true,
229
+ childList: true,
230
+ subtree: true
231
+ });
232
+ const disabledObserver = new view.MutationObserver(records => {
233
+ if (records.some(record => record.target.nodeName === 'FIELDSET' && record.target.contains(input)))
234
+ refresh();
235
+ });
236
+ disabledObserver.observe(eventRoot, { attributeFilter: ['disabled'], attributes: true, subtree: true });
237
+ observer.observe(input, { attributeFilter: ['disabled', 'readonly'], attributes: true });
238
+ return {
239
+ close,
240
+ destroy: () => {
241
+ destroyed = true;
242
+ view.clearTimeout(resetTimer);
243
+ abort.abort();
244
+ flushVisibility();
245
+ observer.disconnect();
246
+ disabledObserver.disconnect();
247
+ for (const option of filterHidden)
248
+ option.hidden = false;
249
+ filterHidden.clear();
250
+ close();
251
+ }
252
+ };
253
+ };