@santi020k/lumen-core 3.0.0 → 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 +379 -4
  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 +68 -3
  29. package/dist/charts.d.ts.map +1 -1
  30. package/dist/charts.js +328 -29
  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) => {
@@ -134,6 +145,148 @@ export const getLumenChartTicks = (domain, count = 5) => {
134
145
  return ticks;
135
146
  return [domain.min, domain.max];
136
147
  };
148
+ const chartAxisLabelCharacterWidth = 7;
149
+ const chartAxisLabelNarrowCharacterWidth = 4;
150
+ const chartAxisLabelWideCharacterWidth = 11;
151
+ const chartAxisLabelGap = 16;
152
+ const maximumChartAxisPadding = 240;
153
+ const chartAxisLabelNarrowCharacters = new Set([
154
+ ' ', '.', ',', ':', ';', '!', '|', '\'', 'i', 'j', 'l', 'I'
155
+ ]);
156
+ const chartAxisLabelWideCharacters = new Set(['M', 'W', '@', '%', '&', '#']);
157
+ const chartAxisCombiningCodePointRanges = [
158
+ [0x0300, 0x036F],
159
+ [0x1AB0, 0x1AFF],
160
+ [0x1DC0, 0x1DFF],
161
+ [0x20D0, 0x20FF],
162
+ [0xFE00, 0xFE0F],
163
+ [0xFE20, 0xFE2F],
164
+ [0x1F3FB, 0x1F3FF]
165
+ ];
166
+ const chartAxisWideCodePointRanges = [
167
+ [0x1100, 0x115F],
168
+ [0x2329, 0x232A],
169
+ [0x2E80, 0xA4CF],
170
+ [0xAC00, 0xD7A3],
171
+ [0xF900, 0xFAFF],
172
+ [0xFE10, 0xFE19],
173
+ [0xFE30, 0xFE6F],
174
+ [0xFF00, 0xFF60],
175
+ [0xFFE0, 0xFFE6],
176
+ [0x1F300, 0x1FAFF],
177
+ [0x20000, 0x3FFFD]
178
+ ];
179
+ const isLumenChartCodePointInRanges = (codePoint, ranges) => ranges.some(([start, end]) => codePoint >= start && codePoint <= end);
180
+ const isLumenChartCombiningCodePoint = (codePoint) => (codePoint === 0x200D ||
181
+ isLumenChartCodePointInRanges(codePoint, chartAxisCombiningCodePointRanges));
182
+ const isLumenChartWideCodePoint = (codePoint) => (isLumenChartCodePointInRanges(codePoint, chartAxisWideCodePointRanges));
183
+ const getLumenChartAxisCharacterWidth = (character) => {
184
+ const codePoint = character.codePointAt(0) ?? 0;
185
+ if (isLumenChartCombiningCodePoint(codePoint))
186
+ return 0;
187
+ if (isLumenChartWideCodePoint(codePoint))
188
+ return chartAxisLabelWideCharacterWidth;
189
+ if (chartAxisLabelWideCharacters.has(character))
190
+ return 10;
191
+ if (chartAxisLabelNarrowCharacters.has(character))
192
+ return chartAxisLabelNarrowCharacterWidth;
193
+ return chartAxisLabelCharacterWidth;
194
+ };
195
+ const estimateLumenChartAxisLabelWidth = (label) => {
196
+ let width = 0;
197
+ for (const character of label) {
198
+ width += getLumenChartAxisCharacterWidth(character);
199
+ }
200
+ return Math.max(label.length * chartAxisLabelCharacterWidth, width);
201
+ };
202
+ /**
203
+ * Reserves enough SVG space for formatted value-axis labels when text measurement is unavailable
204
+ * during server rendering. The estimate targets Lumen's 0.6875rem chart label style and stays
205
+ * bounded so unusually verbose formatters cannot consume the complete plot.
206
+ */
207
+ export const getLumenChartAxisPadding = (labels, minimum = 44) => {
208
+ const safeMinimum = Math.max(0, minimum);
209
+ const widestLabelWidth = labels.reduce((widest, label) => Math.max(widest, estimateLumenChartAxisLabelWidth(label)), 0);
210
+ const estimated = widestLabelWidth + chartAxisLabelGap;
211
+ return Math.min(maximumChartAxisPadding, Math.max(safeMinimum, estimated));
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
+ };
137
290
  export const resolveLumenChartTone = (tone, index = 0) => tone ?? lumenChartTones[index % 8] ?? 'series-1';
138
291
  export const getLumenChartToneClassName = (tone, index = 0) => lumenChartToneClassNames[resolveLumenChartTone(tone, index)];
139
292
  export const getLumenPieChartVariantClassName = (variant) => lumenPieChartVariantClassNames[variant];
@@ -237,6 +390,8 @@ export const getLumenChartNumericX = (value, scale) => {
237
390
  if (typeof value === 'number')
238
391
  return Number.isFinite(value) ? value : null;
239
392
  if (scale === 'linear') {
393
+ if (!value.trim())
394
+ return null;
240
395
  const numeric = Number(value);
241
396
  return Number.isFinite(numeric) ? numeric : null;
242
397
  }
@@ -250,24 +405,24 @@ const getLumenChartXDomain = (data, scale, requested) => {
250
405
  min: requested?.min ?? calculated.min
251
406
  };
252
407
  };
253
- const getLumenLineXCoordinate = (datum, index, dataLength, padding, drawableWidth, xScale, xDomain) => {
408
+ const getLumenLineXCoordinate = (datum, index, dataLength, paddingLeft, drawableWidth, xScale, xDomain) => {
254
409
  if (xScale === 'categorical') {
255
410
  const denominator = Math.max(1, dataLength - 1);
256
- return padding + (dataLength === 1 ? 0.5 : index / denominator) * drawableWidth;
411
+ return paddingLeft + (dataLength === 1 ? 0.5 : index / denominator) * drawableWidth;
257
412
  }
258
413
  const numericX = getLumenChartNumericX(datum.x, xScale);
259
414
  if (numericX === null || xDomain === undefined)
260
415
  return null;
261
- return scaleLumenChartValue(numericX, xDomain, padding, padding + drawableWidth);
416
+ return scaleLumenChartValue(numericX, xDomain, paddingLeft, paddingLeft + drawableWidth);
262
417
  };
263
418
  const isAvailableLumenChartY = (value) => value !== null && Number.isFinite(value);
264
- const splitGeometrySegments = (data, domain, width, height, padding, xScale, xDomain) => {
265
- const drawableWidth = Math.max(0, width - padding * 2);
266
- const drawableHeight = Math.max(0, height - padding * 2);
419
+ const splitGeometrySegments = (data, domain, width, height, paddingBottom, paddingLeft, paddingRight, paddingTop, xScale, xDomain) => {
420
+ const drawableWidth = Math.max(0, width - paddingLeft - paddingRight);
421
+ const drawableHeight = Math.max(0, height - paddingTop - paddingBottom);
267
422
  const segments = [];
268
423
  let current = [];
269
424
  for (const [index, datum] of data.entries()) {
270
- const xCoordinate = getLumenLineXCoordinate(datum, index, data.length, padding, drawableWidth, xScale, xDomain);
425
+ const xCoordinate = getLumenLineXCoordinate(datum, index, data.length, paddingLeft, drawableWidth, xScale, xDomain);
271
426
  if (!isAvailableLumenChartY(datum.y) || xCoordinate === null) {
272
427
  if (current.length > 0)
273
428
  segments.push(current);
@@ -277,7 +432,7 @@ const splitGeometrySegments = (data, domain, width, height, padding, xScale, xDo
277
432
  current.push({
278
433
  ...datum,
279
434
  xCoordinate,
280
- yCoordinate: scaleLumenChartValue(datum.y, domain, padding + drawableHeight, padding)
435
+ yCoordinate: scaleLumenChartValue(datum.y, domain, paddingTop + drawableHeight, paddingTop)
281
436
  });
282
437
  }
283
438
  if (current.length > 0)
@@ -303,8 +458,13 @@ const lumenLineAreaPath = (points, baseline) => {
303
458
  'Z'
304
459
  ].join(' ');
305
460
  };
461
+ const resolveLumenLinePadding = (value, fallback) => value ?? fallback;
306
462
  export const createLumenLineGeometry = (data, options = {}) => {
307
- const { domain: requestedDomain, height = defaultChartSize, includeZero = false, padding = defaultChartPadding, width = defaultChartSize, xDomain: requestedXDomain, xScale = 'categorical' } = options;
463
+ const { domain: requestedDomain, height = defaultChartSize, includeZero = false, padding = defaultChartPadding, paddingBottom: requestedPaddingBottom, paddingLeft: requestedPaddingLeft, paddingRight: requestedPaddingRight, paddingTop: requestedPaddingTop, width = defaultChartSize, xDomain: requestedXDomain, xScale = 'categorical' } = options;
464
+ const paddingBottom = resolveLumenLinePadding(requestedPaddingBottom, padding);
465
+ const paddingLeft = resolveLumenLinePadding(requestedPaddingLeft, padding);
466
+ const paddingRight = resolveLumenLinePadding(requestedPaddingRight, padding);
467
+ const paddingTop = resolveLumenLinePadding(requestedPaddingTop, padding);
308
468
  const domainValues = xScale === 'categorical' ?
309
469
  data.map(datum => datum.y) :
310
470
  data
@@ -312,8 +472,8 @@ export const createLumenLineGeometry = (data, options = {}) => {
312
472
  .map(datum => datum.y);
313
473
  const domain = resolveLumenChartDomain(domainValues, requestedDomain, includeZero);
314
474
  const xDomain = xScale === 'categorical' ? undefined : getLumenChartXDomain(data, xScale, requestedXDomain);
315
- const segments = splitGeometrySegments(data, domain, width, height, padding, xScale, xDomain);
316
- const baseline = scaleLumenChartValue(Math.max(domain.min, Math.min(domain.max, 0)), domain, height - padding, padding);
475
+ const segments = splitGeometrySegments(data, domain, width, height, paddingBottom, paddingLeft, paddingRight, paddingTop, xScale, xDomain);
476
+ const baseline = scaleLumenChartValue(Math.max(domain.min, Math.min(domain.max, 0)), domain, height - paddingBottom, paddingTop);
317
477
  return {
318
478
  areaPaths: segments
319
479
  .map(points => lumenLineAreaPath(points, baseline))
@@ -384,7 +544,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
384
544
  const layout = options.layout ?? 'grouped';
385
545
  const categories = getLumenChartCategories(series);
386
546
  const indexedSeries = series.map(indexLumenChartSeries);
387
- const values = series.flatMap(item => item.data.map(datum => datum.y));
547
+ const values = indexedSeries.flatMap(item => [...item.values()].map(datum => datum.y));
388
548
  const calculatedDomain = layout === 'stacked' ?
389
549
  getStackedDomain(series, categories) :
390
550
  getLumenChartDomain(values);
@@ -395,11 +555,16 @@ export const createLumenBarGeometry = (series, options = {}) => {
395
555
  const margin = orientation === 'horizontal' ?
396
556
  {
397
557
  bottom: 24,
398
- 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))),
399
559
  right: 20,
400
560
  top: 16
401
561
  } :
402
- { 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
+ };
403
568
  const plotWidth = Math.max(1, width - margin.left - margin.right);
404
569
  const plotHeight = Math.max(1, height - margin.top - margin.bottom);
405
570
  const categorySize = (orientation === 'horizontal' ? plotHeight : plotWidth) /
@@ -417,9 +582,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
417
582
  let negativeOffset = 0;
418
583
  categoryPositions.push({
419
584
  category,
420
- label: series
421
- .flatMap(item => item.data)
422
- .find(datum => datum.x === category)?.xLabel ?? category,
585
+ label: getLumenChartCategoryLabel(series, category, options.formatCategory),
423
586
  x: orientation === 'horizontal' ?
424
587
  margin.left - 8 :
425
588
  categoryStart + usableCategorySize / 2,
@@ -487,6 +650,7 @@ export const createLumenBarGeometry = (series, options = {}) => {
487
650
  categories: categoryPositions,
488
651
  domain,
489
652
  height,
653
+ margin,
490
654
  marks,
491
655
  width
492
656
  };
@@ -552,8 +716,18 @@ const validateLumenChartDatumNumbers = (datum, context) => {
552
716
  }
553
717
  };
554
718
  const validateLumenChartDatumX = (datum, context) => {
555
- 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);
556
729
  return context.previousX;
730
+ }
557
731
  const numericX = getLumenChartNumericX(datum.x, context.xScale);
558
732
  if (numericX === null) {
559
733
  context.issues.push({
@@ -591,9 +765,11 @@ export const validateLumenChartSeries = (series, xScale = 'categorical') => {
591
765
  }
592
766
  seriesIds.add(item.id);
593
767
  const datumIds = new Set();
768
+ const categories = new Set();
594
769
  let previousX = Number.NEGATIVE_INFINITY;
595
770
  for (const [datumIndex, datum] of item.data.entries()) {
596
771
  previousX = validateLumenChartDatum(datum, {
772
+ categories,
597
773
  datumIds,
598
774
  issues,
599
775
  path: `${seriesPath}.data[${datumIndex}]`,
@@ -653,6 +829,68 @@ export const appendLumenChartDatum = (series, datum, maximumPoints = 500) => {
653
829
  data: data.slice(Math.max(0, data.length - safeMaximum))
654
830
  };
655
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
+ };
656
894
  const resolveLumenScatterGeometryOptions = (options) => ({
657
895
  height: options.height ?? 320,
658
896
  maximumRadius: options.maximumRadius ?? 18,
@@ -664,7 +902,7 @@ const resolveLumenScatterGeometryOptions = (options) => ({
664
902
  xScale: options.xScale ?? 'linear'
665
903
  });
666
904
  const createLumenScatterPoint = (datum, context) => {
667
- const numericX = getLumenChartNumericX(datum.x, context.xScale);
905
+ const numericX = getLumenScatterNumericX(datum.x, context.xScale);
668
906
  if (numericX === null || !isAvailableLumenChartY(datum.y))
669
907
  return null;
670
908
  const radius = datum.size === null || datum.size === undefined || !Number.isFinite(datum.size) || datum.size < 0 ?
@@ -676,20 +914,52 @@ const createLumenScatterPoint = (datum, context) => {
676
914
  seriesId: context.series.id,
677
915
  seriesLabel: context.series.label,
678
916
  tone: resolveLumenChartTone(datum.tone ?? context.series.tone, context.seriesIndex),
679
- 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),
680
918
  yCoordinate: scaleLumenChartValue(datum.y, context.domain, context.height - context.padding, context.padding)
681
919
  };
682
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
+ };
683
944
  export const createLumenScatterGeometry = (series, options = {}) => {
684
945
  const { height, maximumRadius: requestedMaximumRadius, minimumRadius: requestedMinimumRadius, padding, requestedDomain, requestedXDomain, width, xScale } = resolveLumenScatterGeometryOptions(options);
685
946
  const data = series.flatMap(item => item.data);
686
- const projectedData = data.filter(datum => (getLumenChartNumericX(datum.x, xScale) !== null && isAvailableLumenChartY(datum.y)));
687
- const domain = resolveLumenChartDomain(projectedData.map(datum => datum.y), requestedDomain, false);
688
- 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);
689
953
  const sizes = projectedData.map(datum => (datum.size !== undefined && datum.size !== null && datum.size >= 0 ? datum.size : null));
690
954
  const sizeDomain = getLumenChartDomain(sizes, false);
691
955
  const minimumRadius = Math.max(1, requestedMinimumRadius);
692
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);
693
963
  const points = [];
694
964
  for (const [seriesIndex, item] of series.entries()) {
695
965
  for (const datum of item.data) {
@@ -718,6 +988,33 @@ const uniqueLumenChartCategories = (values) => {
718
988
  categories.set(lumenChartCategoryKey(value), value);
719
989
  return [...categories.values()];
720
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
+ };
721
1018
  const createLumenHeatmapCell = (datum, context) => {
722
1019
  const xIndex = context.xIndexes.get(lumenChartCategoryKey(datum.x));
723
1020
  const yIndex = context.yIndexes.get(lumenChartCategoryKey(datum.y));
@@ -734,7 +1031,8 @@ const createLumenHeatmapCell = (datum, context) => {
734
1031
  yCoordinate: yIndex * context.cellHeight
735
1032
  };
736
1033
  };
737
- export const createLumenHeatmapGeometry = (data, width = 640, height = 320) => {
1034
+ export const createLumenHeatmapGeometry = (rawData, width = 640, height = 320) => {
1035
+ const data = normalizeLumenHeatmapData(rawData);
738
1036
  const xCategories = uniqueLumenChartCategories(data.map(datum => datum.x));
739
1037
  const yCategories = uniqueLumenChartCategories(data.map(datum => datum.y));
740
1038
  const domain = getLumenChartDomain(data.map(datum => datum.value), false);
@@ -803,3 +1101,4 @@ export const createLumenRangeGeometry = (data, options = {}) => {
803
1101
  points: segments.flat()
804
1102
  };
805
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"}