@deck.gl-community/infovis-layers 9.3.5 → 9.3.7

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 (126) hide show
  1. package/dist/index.cjs +2998 -114
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/index.d.ts +11 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +9 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/layers/animation-layer/animation-layer.d.ts +30 -0
  8. package/dist/layers/animation-layer/animation-layer.d.ts.map +1 -0
  9. package/dist/layers/animation-layer/animation-layer.js +53 -0
  10. package/dist/layers/animation-layer/animation-layer.js.map +1 -0
  11. package/dist/layers/animation-layer/animation.d.ts +86 -0
  12. package/dist/layers/animation-layer/animation.d.ts.map +1 -0
  13. package/dist/layers/animation-layer/animation.js +214 -0
  14. package/dist/layers/animation-layer/animation.js.map +1 -0
  15. package/dist/layers/block-layer/block-layer-fragment.glsl.d.ts +3 -0
  16. package/dist/layers/block-layer/block-layer-fragment.glsl.d.ts.map +1 -0
  17. package/dist/layers/block-layer/block-layer-fragment.glsl.js +41 -0
  18. package/dist/layers/block-layer/block-layer-fragment.glsl.js.map +1 -0
  19. package/dist/layers/block-layer/block-layer-uniforms.d.ts +27 -0
  20. package/dist/layers/block-layer/block-layer-uniforms.d.ts.map +1 -0
  21. package/dist/layers/block-layer/block-layer-uniforms.js +26 -0
  22. package/dist/layers/block-layer/block-layer-uniforms.js.map +1 -0
  23. package/dist/layers/block-layer/block-layer-vertex.glsl.d.ts +3 -0
  24. package/dist/layers/block-layer/block-layer-vertex.glsl.d.ts.map +1 -0
  25. package/dist/layers/block-layer/block-layer-vertex.glsl.js +67 -0
  26. package/dist/layers/block-layer/block-layer-vertex.glsl.js.map +1 -0
  27. package/dist/layers/block-layer/block-layer.d.ts +71 -0
  28. package/dist/layers/block-layer/block-layer.d.ts.map +1 -0
  29. package/dist/layers/block-layer/block-layer.js +108 -0
  30. package/dist/layers/block-layer/block-layer.js.map +1 -0
  31. package/dist/layers/fast-text-layer/fast-text-layer.d.ts +148 -0
  32. package/dist/layers/fast-text-layer/fast-text-layer.d.ts.map +1 -0
  33. package/dist/layers/fast-text-layer/fast-text-layer.js +862 -0
  34. package/dist/layers/fast-text-layer/fast-text-layer.js.map +1 -0
  35. package/dist/layers/fast-text-layer/fast-text-layer.test.d.ts +2 -0
  36. package/dist/layers/fast-text-layer/fast-text-layer.test.d.ts.map +1 -0
  37. package/dist/layers/fast-text-layer/fast-text-layer.test.js +300 -0
  38. package/dist/layers/fast-text-layer/fast-text-layer.test.js.map +1 -0
  39. package/dist/layers/fast-text-layer/fast-text-layout.d.ts +179 -0
  40. package/dist/layers/fast-text-layer/fast-text-layout.d.ts.map +1 -0
  41. package/dist/layers/fast-text-layer/fast-text-layout.js +665 -0
  42. package/dist/layers/fast-text-layer/fast-text-layout.js.map +1 -0
  43. package/dist/layers/fast-text-layer/font-atlas.d.ts +98 -0
  44. package/dist/layers/fast-text-layer/font-atlas.d.ts.map +1 -0
  45. package/dist/layers/fast-text-layer/font-atlas.js +337 -0
  46. package/dist/layers/fast-text-layer/font-atlas.js.map +1 -0
  47. package/dist/layers/fast-text-layer/index.d.ts +4 -0
  48. package/dist/layers/fast-text-layer/index.d.ts.map +1 -0
  49. package/dist/layers/fast-text-layer/index.js +4 -0
  50. package/dist/layers/fast-text-layer/index.js.map +1 -0
  51. package/dist/layers/time-delta-layer.d.ts +22 -60
  52. package/dist/layers/time-delta-layer.d.ts.map +1 -1
  53. package/dist/layers/time-delta-layer.js +23 -45
  54. package/dist/layers/time-delta-layer.js.map +1 -1
  55. package/dist/synchronized-views/synchronized-views.js +1 -1
  56. package/dist/synchronized-views/synchronized-views.js.map +1 -1
  57. package/dist/utils/format-utils.d.ts +27 -3
  58. package/dist/utils/format-utils.d.ts.map +1 -1
  59. package/dist/utils/format-utils.js +72 -40
  60. package/dist/utils/format-utils.js.map +1 -1
  61. package/dist/utils/utf8-string-view.d.ts +53 -0
  62. package/dist/utils/utf8-string-view.d.ts.map +1 -0
  63. package/dist/utils/utf8-string-view.js +169 -0
  64. package/dist/utils/utf8-string-view.js.map +1 -0
  65. package/dist/utils/utf8-string-view.test.d.ts +2 -0
  66. package/dist/utils/utf8-string-view.test.d.ts.map +1 -0
  67. package/dist/utils/utf8-string-view.test.js +88 -0
  68. package/dist/utils/utf8-string-view.test.js.map +1 -0
  69. package/dist/views/bounds-utils.d.ts +23 -0
  70. package/dist/views/bounds-utils.d.ts.map +1 -0
  71. package/dist/views/bounds-utils.js +65 -0
  72. package/dist/views/bounds-utils.js.map +1 -0
  73. package/dist/views/layer-bounds-filter.d.ts +48 -0
  74. package/dist/views/layer-bounds-filter.d.ts.map +1 -0
  75. package/dist/views/layer-bounds-filter.js +99 -0
  76. package/dist/views/layer-bounds-filter.js.map +1 -0
  77. package/dist/views/orthographic-utils.d.ts +30 -2
  78. package/dist/views/orthographic-utils.d.ts.map +1 -1
  79. package/dist/views/orthographic-utils.js +33 -14
  80. package/dist/views/orthographic-utils.js.map +1 -1
  81. package/dist/views/view-layout/build-views-from-view-layout.d.ts +37 -0
  82. package/dist/views/view-layout/build-views-from-view-layout.d.ts.map +1 -0
  83. package/dist/views/view-layout/build-views-from-view-layout.js +413 -0
  84. package/dist/views/view-layout/build-views-from-view-layout.js.map +1 -0
  85. package/dist/views/view-layout/index.d.ts +3 -0
  86. package/dist/views/view-layout/index.d.ts.map +1 -0
  87. package/dist/views/view-layout/index.js +3 -0
  88. package/dist/views/view-layout/index.js.map +1 -0
  89. package/dist/views/view-layout/view-layout-item.d.ts +74 -0
  90. package/dist/views/view-layout/view-layout-item.d.ts.map +1 -0
  91. package/dist/views/view-layout/view-layout-item.js +48 -0
  92. package/dist/views/view-layout/view-layout-item.js.map +1 -0
  93. package/dist/views/view-layout/view-layout.test.d.ts +2 -0
  94. package/dist/views/view-layout/view-layout.test.d.ts.map +1 -0
  95. package/dist/views/view-layout/view-layout.test.js +112 -0
  96. package/dist/views/view-layout/view-layout.test.js.map +1 -0
  97. package/dist/views/viewport-bounds-utils.d.ts +55 -0
  98. package/dist/views/viewport-bounds-utils.d.ts.map +1 -0
  99. package/dist/views/viewport-bounds-utils.js +70 -0
  100. package/dist/views/viewport-bounds-utils.js.map +1 -0
  101. package/package.json +11 -4
  102. package/src/index.ts +91 -2
  103. package/src/layers/animation-layer/animation-layer.ts +92 -0
  104. package/src/layers/animation-layer/animation.ts +328 -0
  105. package/src/layers/block-layer/block-layer-fragment.glsl.ts +41 -0
  106. package/src/layers/block-layer/block-layer-uniforms.ts +43 -0
  107. package/src/layers/block-layer/block-layer-vertex.glsl.ts +67 -0
  108. package/src/layers/block-layer/block-layer.ts +199 -0
  109. package/src/layers/fast-text-layer/fast-text-layer.test.ts +359 -0
  110. package/src/layers/fast-text-layer/fast-text-layer.ts +1208 -0
  111. package/src/layers/fast-text-layer/fast-text-layout.ts +1124 -0
  112. package/src/layers/fast-text-layer/font-atlas.ts +574 -0
  113. package/src/layers/fast-text-layer/index.ts +37 -0
  114. package/src/layers/time-delta-layer.ts +47 -62
  115. package/src/synchronized-views/synchronized-views.ts +1 -1
  116. package/src/utils/format-utils.ts +90 -40
  117. package/src/utils/utf8-string-view.test.ts +119 -0
  118. package/src/utils/utf8-string-view.ts +273 -0
  119. package/src/views/bounds-utils.ts +88 -0
  120. package/src/views/layer-bounds-filter.ts +160 -0
  121. package/src/views/orthographic-utils.ts +65 -18
  122. package/src/views/view-layout/build-views-from-view-layout.ts +547 -0
  123. package/src/views/view-layout/index.ts +18 -0
  124. package/src/views/view-layout/view-layout-item.ts +125 -0
  125. package/src/views/view-layout/view-layout.test.ts +129 -0
  126. package/src/views/viewport-bounds-utils.ts +104 -0
@@ -2,36 +2,47 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {CompositeLayer, type LayerProps} from '@deck.gl/core';
6
- import {LineLayer, TextLayer} from '@deck.gl/layers';
5
+ import {CompositeLayer, type DefaultProps, type Layer, type LayerProps} from '@deck.gl/core';
6
+ import {LineLayer, TextLayer, type TextLayerProps} from '@deck.gl/layers';
7
7
 
8
8
  // import {PathStyleExtension} from '@deck.gl/extensions';
9
9
 
10
10
  import {formatTimeMs} from '../utils/format-utils';
11
11
 
12
- export type TimeDeltaLayerProps = LayerProps & {
12
+ /** Properties supported by {@link TimeDeltaLayer}. */
13
+ export type TimeDeltaLayerProps = LayerProps & _TimeDeltaLayerProps;
14
+
15
+ type _TimeDeltaLayerProps = Pick<TextLayerProps, 'fontFamily' | 'fontSettings' | 'fontWeight'> & {
16
+ /** Legacy time label unit retained for compatibility. @defaultValue 'timestamp' */
13
17
  unit: 'timestamp' | 'milliseconds';
18
+ /** Minimum trace time represented by the surrounding view. @defaultValue 0 */
14
19
  minTimeMs: number;
20
+ /** Maximum trace time represented by the surrounding view. @defaultValue 100 */
15
21
  maxTimeMs: number;
16
- /** Start time in milliseconds since epoch */
22
+ /** Start time in milliseconds since epoch. @defaultValue 0 */
17
23
  startTimeMs: number;
18
- /** End time in milliseconds since epoch */
24
+ /** End time in milliseconds since epoch. @defaultValue 100 */
19
25
  endTimeMs: number;
20
- /** Optional: Y-coordinate for the axis line (default: 0) */
26
+ /** Y coordinate for the header interval guide. @defaultValue 0 */
21
27
  y?: number;
28
+ /** Whether to render the compact header label instead of full-height guide lines. @defaultValue false */
22
29
  header: boolean;
23
30
 
24
- /** Optional: RGBA color for axis and ticks (default: [0, 0, 0, 255]) */
31
+ /** Header label text size. @defaultValue 12 */
32
+ fontSize?: number;
33
+
34
+ /** RGBA color for interval guides and labels. @defaultValue [0, 0, 0, 255] */
25
35
  color?: [number, number, number, number];
26
- /** Minimum Y-coordinate for grid lines. @todo (ib) Remve and calculate from viewport? */
36
+ /** Minimum Y coordinate for full-height guide lines. @defaultValue -1e6 */
27
37
  yMin?: number;
28
- /** Maximum Y-coordinate for grid lines. @todo (ib) Remve and calculate from viewport? */
38
+ /** Maximum Y coordinate for full-height guide lines. @defaultValue 1e6 */
29
39
  yMax?: number;
30
40
  };
31
41
 
32
- export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
42
+ /** Renders a selected time interval as header or viewport guide lines. */
43
+ export class TimeDeltaLayer extends CompositeLayer<Required<_TimeDeltaLayerProps>> {
33
44
  static override layerName = 'TimeDeltaLayer';
34
- static override defaultProps: Required<Omit<TimeDeltaLayerProps, keyof LayerProps>> = {
45
+ static override defaultProps: DefaultProps<_TimeDeltaLayerProps> = {
35
46
  header: false,
36
47
  minTimeMs: 0,
37
48
  maxTimeMs: 100,
@@ -41,16 +52,16 @@ export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
41
52
  color: [0, 0, 0, 255],
42
53
  unit: 'timestamp',
43
54
  yMin: -1e6, // Should cover full viewport height in most cases
44
- yMax: 1e6 // Should cover full viewport height in most cases
55
+ yMax: 1e6, // Should cover full viewport height in most cases
56
+ fontSize: 12,
57
+ fontFamily: TextLayer.defaultProps.fontFamily,
58
+ fontSettings: TextLayer.defaultProps.fontSettings,
59
+ fontWeight: TextLayer.defaultProps.fontWeight
45
60
  };
46
61
 
47
- override renderLayers() {
62
+ override renderLayers(): Layer[] {
48
63
  const {startTimeMs, endTimeMs, color = [0, 0, 0, 255], yMin, yMax} = this.props;
49
64
 
50
- const timeDeltaPosition = [(startTimeMs + endTimeMs) / 2, 10];
51
- const timeDeltaMs = Math.abs(endTimeMs - startTimeMs);
52
- const timeDeltaLabel = formatTimeMs(timeDeltaMs, false);
53
-
54
65
  if (!this.props.header) {
55
66
  const timeLines = [
56
67
  {
@@ -76,50 +87,20 @@ export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
76
87
  ];
77
88
  }
78
89
 
79
- // // Tick marks
80
- // new LineLayer({
81
- // id: 'time-delta-marks',
82
- // data: tickLines,
83
- // getSourcePosition: (d) => d.sourcePosition,
84
- // getTargetPosition: (d) => d.targetPosition,
85
- // getColor: color,
86
- // getWidth: 1,
87
- // }),
88
-
89
- // TODO - triggers broader root-level file updates
90
- // new PathLayer({
91
- // id: 'dotted-path',
92
- // data: route,
93
- // getPath: d => d.path,
94
- // getWidth: 4,
95
- // getColor: [255, 0, 0],
96
-
97
- // // Enable rounded caps on each dash
98
- // rounded: true,
99
-
100
- // // Add the dash extension
101
- // extensions: [
102
- // new PathStyleExtension({
103
- // dash: true,
104
- // // highPrecisionDash: true, // uncomment for finer control at low zoom
105
- // capRounded: true // draw dash ends as semicircles
106
- // })
107
- // ],
108
-
109
- // // [dashLength, gapLength] in pixels
110
- // // small dash + equal or larger gap = dotted effect
111
- // getDashArray: () => [2, 6]
112
- // })
113
- // ]
114
- const HEADER_Y = 12;
90
+ const {y, fontSize, fontFamily, fontSettings, fontWeight} = this.props;
91
+
92
+ const timeDeltaPosition = [(startTimeMs + endTimeMs) / 2, y - 10];
93
+ const timeDeltaMs = Math.abs(endTimeMs - startTimeMs);
94
+ const timeDeltaLabel = formatTimeMs(timeDeltaMs, {space: false});
95
+
115
96
  const timeLines = [
116
97
  {
117
- sourcePosition: [startTimeMs, -100],
118
- targetPosition: [startTimeMs, HEADER_Y]
98
+ sourcePosition: [startTimeMs, y],
99
+ targetPosition: [startTimeMs, y - 7]
119
100
  },
120
101
  {
121
- sourcePosition: [endTimeMs, -100],
122
- targetPosition: [endTimeMs, HEADER_Y]
102
+ sourcePosition: [endTimeMs, y],
103
+ targetPosition: [endTimeMs, y - 7]
123
104
  }
124
105
  ];
125
106
 
@@ -140,8 +121,8 @@ export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
140
121
  id: 'header-time-delta-dotted-line',
141
122
  data: [
142
123
  {
143
- sourcePosition: [startTimeMs, HEADER_Y - 7],
144
- targetPosition: [endTimeMs, HEADER_Y - 7]
124
+ sourcePosition: [startTimeMs, y - 7],
125
+ targetPosition: [endTimeMs, y - 7]
145
126
  }
146
127
  ],
147
128
  getSourcePosition: d => d.sourcePosition,
@@ -157,12 +138,16 @@ export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
157
138
  data: [{position: timeDeltaPosition, text: timeDeltaLabel}],
158
139
  getPosition: d => d.position,
159
140
  getText: d => d.text,
160
- getSize: 12,
141
+ characterSet: '-0123456789.dhmsµ',
142
+ getSize: fontSize,
143
+ fontFamily,
144
+ fontSettings,
145
+ fontWeight,
161
146
  getColor: color,
162
147
  getTextAnchor: 'middle',
163
- getAlignmentBaseline: 'top',
148
+ getAlignmentBaseline: 'center',
164
149
  background: true,
165
- getBackgroundColor: [255, 255, 255, 255], // Solid green background
150
+ getBackgroundColor: [255 - color[0], 255 - color[1], 255 - color[2], 255],
166
151
  backgroundPadding: [4, 2] // Horizontal and vertical padding
167
152
  })
168
153
  ];
@@ -19,7 +19,7 @@ export const SYNCHRONIZED_VIEWS = [
19
19
  flipY: false,
20
20
  clear: true, // [1, 1, 1, 1],
21
21
  x: LEGEND_VIEW_WIDTH,
22
- width: `calc(100% - ${LEGEND_VIEW_WIDTH}px`,
22
+ width: `calc(100% - ${LEGEND_VIEW_WIDTH}px)`,
23
23
  y: HEADER_VIEW_HEIGHT,
24
24
  height: `calc(100% - ${HEADER_VIEW_HEIGHT}px)`,
25
25
  controller: {
@@ -3,78 +3,128 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  /**
6
- * Convert a time in microseconds to a human-readable string
7
- * @param us Time in microseconds
6
+ * Options for formatting millisecond duration labels.
8
7
  */
9
- export function formatTimeMs(timeMs: number, space: boolean = true): string {
8
+ export type FormatTimeMsOptions = {
9
+ /** Whether to include a space between the value and unit. @defaultValue true */
10
+ space?: boolean;
11
+ /** Number of significant digits used for fractional values. @defaultValue 5 */
12
+ roundDigits?: number;
13
+ };
14
+
15
+ /**
16
+ * Convert a time in milliseconds to a human-readable string.
17
+ * @param timeMs Time in milliseconds.
18
+ * @param options Formatting options, or the legacy `space` boolean argument.
19
+ * @returns Human-readable duration label.
20
+ */
21
+ export function formatTimeMs(timeMs: number, options: boolean | FormatTimeMsOptions = {}): string {
22
+ const normalizedOptions: FormatTimeMsOptions =
23
+ typeof options === 'boolean' ? {space: options} : options;
24
+ const {space = true, roundDigits = 5} = normalizedOptions;
10
25
  const sep = space ? ' ' : '';
11
26
  const us = timeMs * 1000;
12
27
  if (us === 0) {
13
28
  return '0s';
14
29
  }
15
30
  if (Math.abs(us) < 1000) {
16
- return `${floatToStr(us)}${sep}µs`;
31
+ return `${floatToStr(us, roundDigits)}${sep}µs`;
17
32
  }
18
33
  const ms = us / 1000;
19
34
  if (Math.abs(ms) < 1000) {
20
- return `${floatToStr(ms)}${sep} ms`;
35
+ return `${floatToStr(ms, roundDigits)}${sep}ms`;
21
36
  }
22
37
  const s = ms / 1000;
23
38
  if (Math.abs(s) < 60) {
24
- return `${floatToStr(s)}${sep} s`;
39
+ return `${floatToStr(s, roundDigits)}${sep}s`;
25
40
  }
26
41
  const m = s / 60;
27
42
  if (Math.abs(m) < 60) {
28
- return `${floatToStr(m)}${sep} min`;
43
+ return formatMinuteDuration(s);
29
44
  }
30
45
  const h = m / 60;
31
46
  if (Math.abs(h) < 24) {
32
- return `${floatToStr(h)}${sep} hrs`;
47
+ return formatHourDuration(s);
33
48
  }
34
- const d = h / 24;
35
- return `${floatToStr(d)}${sep} days`;
49
+ return formatDayDuration(s);
36
50
  }
37
51
 
52
+ /**
53
+ * Convert a millisecond time range to a human-readable string.
54
+ * @param startMs Range start in milliseconds.
55
+ * @param endMs Range end in milliseconds.
56
+ * @returns Human-readable start and end labels joined by a separator.
57
+ */
38
58
  export function formatTimeRangeMs(startMs: number, endMs: number): string {
39
59
  return `${formatTimeMs(startMs)} - ${formatTimeMs(endMs)}`;
40
60
  }
41
61
 
42
62
  /**
43
- * Convert a float to a string
63
+ * Convert a float to a string with a specified number of significant digits.
64
+ * @param f The float to convert.
65
+ * @param roundDigits The number of significant digits to round to (default is 5).
66
+ * @returns The float as a string, rounded to the specified precision.
44
67
  */
45
- function floatToStr(f: number, roundDigits: number = 5): string {
46
- if (Number.isInteger(f)) {
68
+ export function floatToStr(f: number, roundDigits: number = 5): string {
69
+ if (roundDigits < 1 || Number.isInteger(f)) {
47
70
  return f.toString();
48
71
  }
49
72
 
50
- for (let i = 1; i < roundDigits - 1; i++) {
51
- const rounded = parseFloat(f.toPrecision(i));
52
- if (rounded === f) {
53
- return rounded.toPrecision(i);
54
- }
73
+ // Round the float to the specified precision
74
+ const precision = f.toPrecision(roundDigits);
75
+ return parseFloat(precision).toString();
76
+ }
77
+
78
+ /**
79
+ * Format a duration between one minute and one hour with whole seconds.
80
+ * @param seconds Duration in seconds.
81
+ * @returns A compact minutes-and-seconds duration label.
82
+ */
83
+ function formatMinuteDuration(seconds: number): string {
84
+ const sign = seconds < 0 ? '-' : '';
85
+ const totalSeconds = Math.floor(Math.abs(seconds));
86
+ const minutes = Math.floor(totalSeconds / 60);
87
+ const remainingSeconds = totalSeconds % 60;
88
+
89
+ if (remainingSeconds === 0) {
90
+ return `${sign}${minutes}m`;
55
91
  }
56
92
 
57
- return f.toPrecision(roundDigits);
93
+ return `${sign}${minutes}m${remainingSeconds}s`;
58
94
  }
59
95
 
60
- // export function formatTimesUs(ticks: number[]): string {
61
- // // Try from 0 up to a reasonable max (e.g. 20)
62
- // for (let d = 0; d <= 20; d++) {
63
- // const seen = new Set<string>();
64
- // let allDistinct = true;
65
- // for (const t of ticks) {
66
- // // Format each tick with d decimals
67
- // const str = t.toFixed(d);
68
- // if (seen.has(str)) {
69
- // allDistinct = false;
70
- // break;
71
- // }
72
- // seen.add(str);
73
- // }
74
- // if (allDistinct) {
75
- // return d;
76
- // }
77
- // }
78
- // // Fallback if somehow not distinct even at 20 decimals
79
- // return 20;
80
- // }
96
+ /**
97
+ * Format a duration between one hour and one day with whole minutes.
98
+ * @param seconds Duration in seconds.
99
+ * @returns A compact hours-and-minutes duration label.
100
+ */
101
+ function formatHourDuration(seconds: number): string {
102
+ const sign = seconds < 0 ? '-' : '';
103
+ const totalMinutes = Math.floor(Math.abs(seconds) / 60);
104
+ const hours = Math.floor(totalMinutes / 60);
105
+ const remainingMinutes = totalMinutes % 60;
106
+
107
+ if (remainingMinutes === 0) {
108
+ return `${sign}${hours}h`;
109
+ }
110
+
111
+ return `${sign}${hours}h${remainingMinutes}m`;
112
+ }
113
+
114
+ /**
115
+ * Format a duration of at least one day with whole hours.
116
+ * @param seconds Duration in seconds.
117
+ * @returns A compact days-and-hours duration label.
118
+ */
119
+ function formatDayDuration(seconds: number): string {
120
+ const sign = seconds < 0 ? '-' : '';
121
+ const totalHours = Math.floor(Math.abs(seconds) / 3600);
122
+ const days = Math.floor(totalHours / 24);
123
+ const remainingHours = totalHours % 24;
124
+
125
+ if (remainingHours === 0) {
126
+ return `${sign}${days}d`;
127
+ }
128
+
129
+ return `${sign}${days}d${remainingHours}h`;
130
+ }
@@ -0,0 +1,119 @@
1
+ import * as arrow from 'apache-arrow';
2
+ import {describe, expect, it} from 'vitest';
3
+
4
+ import {
5
+ arrowFindUtf8,
6
+ getArrowUtf8ColumnSource,
7
+ getArrowUtf8RowView,
8
+ getUtf8ColumnSourceRowView,
9
+ makeUtf8StringView
10
+ } from './utf8-string-view';
11
+
12
+ describe('utf8-string-view', () => {
13
+ it('finds matching Utf8 rows without materializing a lookup map', () => {
14
+ const column = arrow.vectorFromArray(['alpha', null, '6149800612493239450'], new arrow.Utf8());
15
+
16
+ expect(arrowFindUtf8(column, makeUtf8StringView('alpha'))).toBe(0);
17
+ expect(arrowFindUtf8(column, makeUtf8StringView('6149800612493239450'))).toBe(2);
18
+ expect(arrowFindUtf8(column, makeUtf8StringView('alpha'), 1)).toBe(-1);
19
+ expect(arrowFindUtf8(column, makeUtf8StringView('missing'))).toBe(-1);
20
+ });
21
+
22
+ it('finds exact UTF-8 strings in Arrow columns', () => {
23
+ const column = buildArrowUtf8Vector([
24
+ 'span-a',
25
+ '6149800612493239450',
26
+ 'span-b',
27
+ '6149800612493239450'
28
+ ]);
29
+ const view = makeUtf8StringView('6149800612493239450');
30
+
31
+ expect(arrowFindUtf8(column, view)).toBe(1);
32
+ expect(arrowFindUtf8(column, view, 2)).toBe(3);
33
+ expect(arrowFindUtf8(column, view, 4)).toBe(-1);
34
+ });
35
+
36
+ it('falls back safely for nullable Arrow string columns', () => {
37
+ const column = arrow.vectorFromArray(['span-a', null, 'span-c'], new arrow.Utf8());
38
+
39
+ expect(arrowFindUtf8(column, makeUtf8StringView('span-c'))).toBe(2);
40
+ expect(arrowFindUtf8(column, makeUtf8StringView('missing'))).toBe(-1);
41
+ });
42
+
43
+ it('fills a reusable row view for direct Utf8 values', () => {
44
+ const column = buildArrowUtf8Vector(['alpha', '', 'gamma']);
45
+ const out = makeEmptyUtf8View();
46
+
47
+ expect(getArrowUtf8RowView(column, 0, out)).toBe(true);
48
+ expect(readUtf8ViewBytes(out)).toEqual([97, 108, 112, 104, 97]);
49
+ expect(getArrowUtf8RowView(column, 1, out)).toBe(true);
50
+ expect(readUtf8ViewBytes(out)).toEqual([]);
51
+ expect(getArrowUtf8RowView(column, 3, out)).toBe(false);
52
+ });
53
+
54
+ it('fills a reusable row view for nullable Utf8 values', () => {
55
+ const column = arrow.vectorFromArray(['span-a', null, 'span-c'], new arrow.Utf8());
56
+ const out = makeEmptyUtf8View();
57
+
58
+ expect(getArrowUtf8RowView(column, 1, out)).toBe(false);
59
+ expect(getArrowUtf8RowView(column, 2, out)).toBe(true);
60
+ expect(readUtf8ViewBytes(out)).toEqual([115, 112, 97, 110, 45, 99]);
61
+ });
62
+
63
+ it('fills a reusable row view for sliced Utf8 values', () => {
64
+ const column = buildArrowUtf8Vector(['skip', 'first', 'second']).slice(
65
+ 1,
66
+ 3
67
+ ) as arrow.Vector<arrow.Utf8>;
68
+ const out = makeEmptyUtf8View();
69
+
70
+ expect(getArrowUtf8RowView(column, 0, out)).toBe(true);
71
+ expect(readUtf8ViewBytes(out)).toEqual([102, 105, 114, 115, 116]);
72
+ expect(getArrowUtf8RowView(column, 1, out)).toBe(true);
73
+ expect(readUtf8ViewBytes(out)).toEqual([115, 101, 99, 111, 110, 100]);
74
+ expect(arrowFindUtf8(column, makeUtf8StringView('second'))).toBe(1);
75
+ });
76
+
77
+ it('fills normalized column source row views for sliced Utf8 values', () => {
78
+ const column = buildArrowUtf8Vector(['skip', 'first', 'second']).slice(
79
+ 1,
80
+ 3
81
+ ) as arrow.Vector<arrow.Utf8>;
82
+ const source = getArrowUtf8ColumnSource(column);
83
+ const out = makeEmptyUtf8View();
84
+
85
+ expect(source).not.toBeNull();
86
+ expect(getUtf8ColumnSourceRowView(source!, 0, out)).toBe(true);
87
+ expect(readUtf8ViewBytes(out)).toEqual([102, 105, 114, 115, 116]);
88
+ expect(getUtf8ColumnSourceRowView(source!, 1, out)).toBe(true);
89
+ expect(readUtf8ViewBytes(out)).toEqual([115, 101, 99, 111, 110, 100]);
90
+ });
91
+
92
+ it('fills a reusable row view for chunked Utf8 values', () => {
93
+ const leftTable = new arrow.Table({
94
+ name: arrow.vectorFromArray(['left'], new arrow.Utf8())
95
+ });
96
+ const rightTable = new arrow.Table({
97
+ name: arrow.vectorFromArray(['right', 'last'], new arrow.Utf8())
98
+ });
99
+ const column = leftTable.concat(rightTable).getChild('name') as arrow.Vector<arrow.Utf8>;
100
+ const out = makeEmptyUtf8View();
101
+
102
+ expect(getArrowUtf8RowView(column, 0, out)).toBe(true);
103
+ expect(readUtf8ViewBytes(out)).toEqual([108, 101, 102, 116]);
104
+ expect(getArrowUtf8RowView(column, 2, out)).toBe(true);
105
+ expect(readUtf8ViewBytes(out)).toEqual([108, 97, 115, 116]);
106
+ });
107
+ });
108
+
109
+ function makeEmptyUtf8View() {
110
+ return {data: new Uint8Array(), start: 0, end: 0};
111
+ }
112
+
113
+ function readUtf8ViewBytes(view: ReturnType<typeof makeEmptyUtf8View>): number[] {
114
+ return Array.from(view.data.subarray(view.start, view.end));
115
+ }
116
+
117
+ function buildArrowUtf8Vector(values: readonly string[]): arrow.Vector<arrow.Utf8> {
118
+ return arrow.vectorFromArray(values, new arrow.Utf8()) as arrow.Vector<arrow.Utf8>;
119
+ }