@pond-ts/charts 0.56.2 → 0.58.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 (78) hide show
  1. package/CHANGELOG.md +1218 -1
  2. package/dist/AreaChart.d.ts +12 -1
  3. package/dist/AreaChart.js +131 -13
  4. package/dist/BarChart.d.ts +84 -9
  5. package/dist/BarChart.js +295 -40
  6. package/dist/BarList.d.ts +85 -5
  7. package/dist/BarList.js +25 -4
  8. package/dist/BoxList.d.ts +70 -3
  9. package/dist/BoxList.js +21 -7
  10. package/dist/BoxPlot.d.ts +2 -1
  11. package/dist/BoxPlot.js +101 -9
  12. package/dist/Candlestick.d.ts +13 -1
  13. package/dist/Candlestick.js +89 -3
  14. package/dist/ChartContainer.d.ts +79 -48
  15. package/dist/ChartContainer.js +482 -60
  16. package/dist/ChartRow.d.ts +9 -2
  17. package/dist/ChartRow.js +86 -12
  18. package/dist/HeatMap.d.ts +176 -0
  19. package/dist/HeatMap.js +344 -0
  20. package/dist/Layers.d.ts +5 -1
  21. package/dist/Layers.js +1014 -253
  22. package/dist/Legend.js +8 -4
  23. package/dist/LineChart.d.ts +18 -1
  24. package/dist/LineChart.js +165 -4
  25. package/dist/ListTable.d.ts +30 -3
  26. package/dist/ListTable.js +381 -23
  27. package/dist/ScatterChart.d.ts +3 -2
  28. package/dist/ScatterChart.js +68 -4
  29. package/dist/XAxis.js +40 -22
  30. package/dist/YAxis.d.ts +28 -1
  31. package/dist/YAxis.js +24 -2
  32. package/dist/annotations.d.ts +74 -0
  33. package/dist/annotations.js +97 -7
  34. package/dist/area.d.ts +34 -1
  35. package/dist/area.js +88 -1
  36. package/dist/bars.d.ts +178 -5
  37. package/dist/bars.js +504 -46
  38. package/dist/box.d.ts +2 -2
  39. package/dist/box.js +158 -40
  40. package/dist/brush.d.ts +142 -0
  41. package/dist/brush.js +179 -0
  42. package/dist/child-index.d.ts +27 -0
  43. package/dist/child-index.js +57 -0
  44. package/dist/context.d.ts +871 -36
  45. package/dist/cursors.d.ts +161 -0
  46. package/dist/cursors.js +503 -0
  47. package/dist/decimate.d.ts +78 -1
  48. package/dist/decimate.js +157 -0
  49. package/dist/heat.d.ts +163 -0
  50. package/dist/heat.js +659 -0
  51. package/dist/index.d.ts +13 -4
  52. package/dist/index.js +25 -2
  53. package/dist/line.d.ts +137 -0
  54. package/dist/line.js +328 -0
  55. package/dist/ohlc.d.ts +16 -1
  56. package/dist/ohlc.js +93 -4
  57. package/dist/scatter.d.ts +17 -9
  58. package/dist/scatter.js +221 -33
  59. package/dist/select.d.ts +13 -5
  60. package/dist/select.js +14 -6
  61. package/dist/selection-fixtures.d.ts +174 -0
  62. package/dist/selection-fixtures.js +569 -0
  63. package/dist/selection-stories.d.ts +73 -0
  64. package/dist/selection-stories.js +301 -0
  65. package/dist/selectors.d.ts +316 -0
  66. package/dist/selectors.js +391 -0
  67. package/dist/span.d.ts +122 -0
  68. package/dist/span.js +203 -0
  69. package/dist/sweep.d.ts +154 -0
  70. package/dist/sweep.js +282 -0
  71. package/dist/theme.d.ts +517 -11
  72. package/dist/theme.js +220 -39
  73. package/dist/tracker.d.ts +6 -0
  74. package/dist/tracker.js +6 -0
  75. package/dist/tradingAxis.fixture.d.ts +78 -0
  76. package/dist/tradingAxis.fixture.js +215 -0
  77. package/dist/useChartLegend.js +18 -3
  78. package/package.json +3 -3
@@ -3,8 +3,15 @@ import { type CursorMode } from './context.js';
3
3
  export interface ChartRowProps {
4
4
  /** Row height in CSS pixels. */
5
5
  height: number;
6
- /** Cursor presentation for this row, overriding the container's default
7
- * ({@link ChartContainerProps.cursor}). Omit to inherit. See {@link CursorMode}. */
6
+ /**
7
+ * Cursor presentation for this row, overriding the container's default
8
+ * ({@link ChartContainerProps.cursor}). Omit to inherit. See {@link CursorMode}.
9
+ *
10
+ * @deprecated Mount a cursor component **inside the row** instead
11
+ * (`<ChartRow><CrosshairCursor /> …</ChartRow>`) — the per-row override with
12
+ * the same nearest-mount-wins semantics. Works for one more minor; a mounted
13
+ * cursor in the row overrides this prop.
14
+ */
8
15
  cursor?: CursorMode;
9
16
  children?: ReactNode;
10
17
  }
package/dist/ChartRow.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { Children, Fragment, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { scaleLinear, scaleLog } from 'd3-scale';
4
4
  import { isDev } from './dev.js';
5
+ import { useIndexedChildren } from './child-index.js';
5
6
  import { logAxisWarning, needsExtents, resolveYDomain } from './domain.js';
6
7
  import { resolveAxisFormat } from './format.js';
7
8
  import { resolveYTickCount } from './yticks.js';
8
9
  import { placeAxisSlots } from './slots.js';
9
10
  import { useSlotKey } from './use-slot-key.js';
11
+ import { LegacyCursor } from './cursors.js';
10
12
  import { YAxis } from './YAxis.js';
11
13
  import { ContainerContext, RowContext, } from './context.js';
12
14
  /** Sentinel id for the implicit axis a row gets when no `<YAxis>` is declared. */
@@ -104,6 +106,18 @@ export function ChartRow({ height, cursor, children }) {
104
106
  const { registerRow } = container;
105
107
  useEffect(() => registerRow(rowKey), [registerRow, rowKey]);
106
108
  const isFirstRow = container.firstRowKey === rowKey;
109
+ // Deprecation notice for the legacy `cursor` prop (dev, once per row): the
110
+ // per-row override is now a cursor component mounted inside the row. The
111
+ // prop keeps working via the shim rendered below.
112
+ const warnedCursorRef = useRef(false);
113
+ useEffect(() => {
114
+ if (!isDev || cursor === undefined || warnedCursorRef.current)
115
+ return;
116
+ warnedCursorRef.current = true;
117
+ console.warn(`[pond-charts] <ChartRow cursor="${cursor}"> is deprecated (it keeps ` +
118
+ 'working this minor, removed next) — mount the cursor component ' +
119
+ 'inside the row instead (docs/rfcs/interaction.md §9).');
120
+ }, [cursor]);
107
121
  // Keyed by a stable per-instance id (Map preserves insertion order; setting an
108
122
  // existing key updates in place). So a re-register on a prop change keeps the
109
123
  // entry's slot — the axis-default (first axis) and layer z-order stay stable
@@ -222,6 +236,7 @@ export function ChartRow({ height, cursor, children }) {
222
236
  // One y-scale per axis. A layer counts toward an axis when its (late-resolved)
223
237
  // axis id matches; `resolveYDomain` handles the auto-fit + empty/flat/inverted
224
238
  // edges. yExtent() is O(points), so only walk the layers when a bound auto-fits.
239
+ const { k: yk, ty: yty } = container.yTransform;
225
240
  const yScales = useMemo(() => {
226
241
  const map = new Map();
227
242
  for (const ax of effectiveAxes) {
@@ -239,10 +254,27 @@ export function ChartRow({ height, cursor, children }) {
239
254
  // surface every consumer uses (see `YScale`), so choosing between them
240
255
  // here is the whole of log support — no draw layer branches on it.
241
256
  const base = ax.scale === 'log' ? scaleLog() : scaleLinear();
242
- map.set(ax.id, base.domain([lo, hi]).range([height, topHeader]));
257
+ const s = base.domain([lo, hi]).range([height, topHeader]);
258
+ // 2-D pan/zoom is carried as a **pixel** transform (`k`, `ty`) so one
259
+ // gesture serves every axis in the row whatever its units, and all of them
260
+ // zoom by the same factor — which is what fixes the aspect ratio. But it is
261
+ // applied by narrowing the **domain** to the window that transform makes
262
+ // visible, not by stretching the range.
263
+ //
264
+ // That distinction is not cosmetic. Stretching the range leaves the tick
265
+ // generator working on the FULL domain, so ticks outside the view get
266
+ // clamped onto the plot edge and pile up — 350 and 400 printed on top of
267
+ // each other in the first cut. Narrowing the domain means ticks, padding
268
+ // and every downstream reader see an ordinary axis over the visible
269
+ // window, and none of them need to know a transform exists.
270
+ if (yk !== 1 || yty !== 0) {
271
+ const at = (px) => +s.invert((px - yty) / yk);
272
+ s.domain([at(height), at(topHeader)]);
273
+ }
274
+ map.set(ax.id, s);
243
275
  }
244
276
  return map;
245
- }, [effectiveAxes, layerList, height, defaultAxisId, topHeader]);
277
+ }, [effectiveAxes, layerList, height, defaultAxisId, topHeader, yk, yty]);
246
278
  // Dev-mode diagnostics for a `scale="log"` axis (see `logAxisWarning`). Three
247
279
  // things about *where* this sits are load-bearing, each of them a bug the
248
280
  // first version shipped:
@@ -369,9 +401,14 @@ export function ChartRow({ height, cursor, children }) {
369
401
  // Inject each direct child's JSX position so axes register their declaration
370
402
  // order (the default-axis source). `<Layers>` receives an index too (harmless
371
403
  // — it's not an axis) and injects its own into the draw layers.
372
- const indexedChildren = Children.map(children, (child, index) => isValidElement(child)
373
- ? cloneElement(child, { index })
374
- : child);
404
+ //
405
+ // A fragment child costs more here than it does in `<Layers>`: the axes
406
+ // inside it lose the index *and* the `child.type === YAxis` sort below cannot
407
+ // see through it, so they fall into `plotEls` and render in the middle of the
408
+ // row instead of in a gutter. Hence the same warning on both.
409
+ const indexedChildren = useIndexedChildren(children, '<ChartRow>', 'the axes inside it lose their declaration order (the default-axis pick ' +
410
+ 'and slot order within a side) and are placed in the plot rather than a ' +
411
+ 'gutter, because the side sort cannot see through a fragment');
375
412
  // Place axes by their `side`, not by JSX author position — so a `side="right"`
376
413
  // axis always renders right of the plot (and a left axis left), **consistent
377
414
  // with the side-based gutter reservation above**. (Author position only
@@ -383,20 +420,57 @@ export function ChartRow({ height, cursor, children }) {
383
420
  const leftAxisEls = [];
384
421
  const plotEls = [];
385
422
  const rightAxisEls = [];
423
+ let axisInsideWrapper = false;
386
424
  for (const child of indexedChildren ?? []) {
387
425
  if (isValidElement(child) && child.type === YAxis) {
388
426
  const side = child.props.side ?? 'left';
389
427
  (side === 'right' ? rightAxisEls : leftAxisEls).push(child);
390
428
  }
391
429
  else {
430
+ // A `<Selector>`/`<MultiSelector>` is a legitimate row child now that it
431
+ // wraps its scope (RFC A10.1) — but it must wrap the row's `<Layers>`,
432
+ // NOT its axes: the sort above matches on `child.type`, so an axis
433
+ // nested inside any wrapper is invisible to it and lands in the plot
434
+ // column. The fragment warning cannot catch this one (a selector is a
435
+ // real element, not a fragment), and the failure is silent, so look one
436
+ // level down for the mistake the docs could invite.
437
+ // A fragment is skipped here: `useIndexedChildren` already warns about
438
+ // it and names the same gutter consequence, so checking it too would
439
+ // print two warnings for one mistake.
440
+ if (isDev &&
441
+ isValidElement(child) &&
442
+ child.type !== Fragment &&
443
+ !axisInsideWrapper) {
444
+ const nested = child.props.children;
445
+ if (nested !== undefined) {
446
+ for (const g of Children.toArray(nested)) {
447
+ if (isValidElement(g) && g.type === YAxis) {
448
+ axisInsideWrapper = true;
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ }
392
454
  plotEls.push(child);
393
455
  }
394
456
  }
395
- return (_jsx(RowContext.Provider, { value: frame, children: _jsxs("div", { style: {
396
- display: 'flex',
397
- flexDirection: 'row',
398
- width: `${container.width}px`,
399
- height: `${height}px`,
400
- }, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] }) }));
457
+ const warnedAxisWrapperRef = useRef(false);
458
+ useEffect(() => {
459
+ if (!isDev || !axisInsideWrapper || warnedAxisWrapperRef.current)
460
+ return;
461
+ warnedAxisWrapperRef.current = true;
462
+ console.warn('[pond-charts] a <YAxis> is nested inside another element in this ' +
463
+ '<ChartRow>, so it renders in the plot column instead of a gutter — ' +
464
+ '<ChartRow> places axes by matching its own children, and cannot see ' +
465
+ 'through a wrapper. A row-scoped <Selector>/<MultiSelector> should ' +
466
+ "wrap the row's <Layers>, leaving each <YAxis> a direct child of the " +
467
+ '<ChartRow>.');
468
+ }, [axisInsideWrapper]);
469
+ return (_jsxs(RowContext.Provider, { value: frame, children: [cursor !== undefined && (_jsx(LegacyCursor, { mode: cursor, showTime: container.cursorTime, snap: container.crosshairSnap })), _jsxs("div", { style: {
470
+ display: 'flex',
471
+ flexDirection: 'row',
472
+ width: `${container.width}px`,
473
+ height: `${height}px`,
474
+ }, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] })] }));
401
475
  }
402
476
  //# sourceMappingURL=ChartRow.js.map
@@ -0,0 +1,176 @@
1
+ import { ValueSeries } from 'pond-ts';
2
+ import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
+ import type { DecimateOption } from './decimate.js';
4
+ import type { Orientation } from './bars.js';
5
+ import { type HeatNoData, type HeatScale } from './heat.js';
6
+ export interface HeatMapProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
7
+ /**
8
+ * The source series. A **`TimeSeries`** puts time intervals on x, a
9
+ * **`ValueSeries`** puts value intervals on x — inferred, no axis-kind prop,
10
+ * the same rule `<BarChart>` uses.
11
+ *
12
+ * Because the cell spans are the ordinary bin spans, the whole of pond's
13
+ * binning machinery applies unchanged: `aggregate` over a trading calendar
14
+ * with sessions, `Sequence.calendar` day/week/month buckets, `byColumn` value
15
+ * bands. The heat map inherits all of it by having no opinion about x.
16
+ */
17
+ series: TimeSeries<S> | ValueSeries<VS>;
18
+ /**
19
+ * The numeric columns forming the **rows**, bottom → top — one row per
20
+ * column. Give one column for a single-row **stripe**; a stripe is just
21
+ * `columns.length === 1`, drawn by the same path.
22
+ *
23
+ * The y dimension must be columns, which is the layer's one real constraint.
24
+ * A month-of-year grid means a column per month; a per-city grid means a
25
+ * column per city (`pivotByGroup`'s long→wide output, or `partitionBy`
26
+ * reshaped). That keeps the second dimension in the data model, where pond's
27
+ * own reshaping operators can produce it, rather than inventing a
28
+ * chart-level pivot.
29
+ *
30
+ * Row `0` is at the **bottom**, matching the band-axis convention; reverse
31
+ * the list to read top-down.
32
+ */
33
+ columns: readonly string[];
34
+ /**
35
+ * The colour ramp, low → high. The value domain splits into `colors.length`
36
+ * equal bands and a cell takes its band's colour (see {@link bandedColor}).
37
+ * A diverging ramp is just a ramp whose middle is pale.
38
+ */
39
+ colors: readonly string[];
40
+ /**
41
+ * Pin the colour domain as `[lo, hi]`. **Omitted ⇒ the finite extent across
42
+ * the whole grid**, so every row is read against one scale and rows are
43
+ * comparable to each other.
44
+ *
45
+ * Pin it when two charts must be read against each other, or when the window
46
+ * is a slice of a longer record and the colours should not re-mean themselves
47
+ * as it moves — a colour scale has no tick labels to reveal that it moved.
48
+ */
49
+ domain?: readonly [number, number];
50
+ /**
51
+ * Which axis carries the **bins**. `'vertical'` (the default) puts them on
52
+ * **x** with the columns as rows down y; `'horizontal'` transposes — bins run
53
+ * down **y** and the columns become the categories along x.
54
+ *
55
+ * The transpose is cheaper here than for `<BarChart>`, because a heat map has
56
+ * two *position* axes and no value axis: nothing has to change which scale it
57
+ * is measured against, only which one is horizontal on the canvas.
58
+ *
59
+ * Reach for `'horizontal'` when the binned dimension is the long one and the
60
+ * columns are few — a gene-expression matrix (thousands of gene buckets, a
61
+ * handful of samples) is the canonical case, and it is the orientation that
62
+ * literature draws. Note that the bins still come from the **key** axis, so
63
+ * the genes must be the series' rows and the samples its columns; the
64
+ * ordinary binning operators (`byColumn`, `aggregate`) then bucket them.
65
+ */
66
+ orientation?: Orientation;
67
+ /** Semantic identifier — picks geometry defaults off `theme.bar[as]`. */
68
+ as?: string;
69
+ /** Which `<YAxis>` (by `id`) this layer scales against. */
70
+ axis?: string;
71
+ /** Px inset around each cell. **Omitted ⇒ `0`**, tiling flush. */
72
+ gap?: number;
73
+ /**
74
+ * How value maps onto the ramp's bands. **Omitted ⇒ `'linear'`** — equal-width
75
+ * bands across the domain.
76
+ *
77
+ * `'log'` gives equal-*ratio* bands, which is what a quantity spanning orders
78
+ * of magnitude needs. US measles incidence runs from ~2,900 per 100k before
79
+ * the vaccine to under 1 after it; linear banding over eight colours puts
80
+ * everything below ~360 into a single band — the entire post-1965 record,
81
+ * which is the half of that chart carrying the finding.
82
+ *
83
+ * Bands on `log1p` of the offset from the domain's floor, so a value **at**
84
+ * the floor is a real band rather than `-Infinity`. Zero is the case that
85
+ * needs it: an incidence grid is mostly zeros once a disease is eliminated,
86
+ * and those cells are the point.
87
+ */
88
+ scale?: HeatScale;
89
+ /**
90
+ * How a cell with no value is drawn. **Omitted ⇒ `'blank'`** — nothing is
91
+ * painted and the background shows through, which is right when a hole simply
92
+ * means "outside the record".
93
+ *
94
+ * `'hatch'` draws diagonal lines in the theme's grid colour. Reach for it when
95
+ * *missing* and *low* would otherwise be indistinguishable — on a pale ramp
96
+ * "draw nothing" reads as the bottom of the scale, so a state with no
97
+ * surveillance yet looks exactly like a state reporting zero cases. No ramp
98
+ * colour can be mistaken for hatching, which is why it is the convention.
99
+ *
100
+ * Suppressed while decimated: an aggregated cell is not a hole.
101
+ */
102
+ noData?: HeatNoData;
103
+ /**
104
+ * Viewport decimation — **on by default**, and a perf knob rather than a
105
+ * rendering-style one.
106
+ *
107
+ * Once the visible cells are denser than ~2 per device pixel they overlap and
108
+ * overpaint each other, so what you see is already one cell per column picked
109
+ * by draw order. Decimation replaces that with the **mean** per pixel column
110
+ * — what the overdrawn picture resolves to at that size — from `O(W·G)` rects
111
+ * instead of `O(V·G)`. A 20,000-bin grid over an 800px plot goes from ~48ms
112
+ * to a fraction of it.
113
+ *
114
+ * `{ threshold }` moves the cells-per-pixel gate (default `2`). `false` draws
115
+ * every visible cell — reach for it if you are screenshotting at a device
116
+ * pixel ratio the gate can't see, not to "keep the data honest": undecimated
117
+ * at this density is the less honest picture.
118
+ *
119
+ * While decimated, per-cell selection and hover outlines are suppressed (a
120
+ * sub-pixel ring isn't visible anyway) and interaction still reads the source
121
+ * grid.
122
+ */
123
+ decimate?: DecimateOption;
124
+ /**
125
+ * Stable identity — **gates selection + hover**, as every layer's does. Both
126
+ * channels are sets, and **every** cell a member names outlines (bin `key` —
127
+ * or the stable per-bin `mark` — plus the row `label`), so a multi-cell pin or
128
+ * a drag-sweep hover lights all of it. A cell in both reads as selected.
129
+ */
130
+ id?: string;
131
+ /** @internal Declaration position, injected by `Layers`. Do not set. */
132
+ index?: number;
133
+ }
134
+ /**
135
+ * A **heat-map draw layer**: a grid of cells, bins along x and the series'
136
+ * columns down y, colour carrying the aggregate ([PND-HEATMAP]).
137
+ *
138
+ * ```tsx
139
+ * // A stripe — one column.
140
+ * <HeatMap series={hourly} columns={['count']} colors={ramp} id="load" />
141
+ *
142
+ * // A grid — one column per row.
143
+ * <HeatMap series={byCity} columns={['London', 'Paris', 'Berlin']} colors={ramp} />
144
+ * ```
145
+ *
146
+ * **No reader of its own.** It builds on `stacksFromColumns`, whose output is
147
+ * already a heat map's data shape — bin spans, named rows, a row-major value
148
+ * grid. That covers all four shapes pond can express today (`TimeSeries` or
149
+ * `ValueSeries` × one column or many), and the stripe is simply `G === 1`, so
150
+ * there is one draw path rather than two.
151
+ *
152
+ * **The readout is the point.** A cell carries its value, so hover and click
153
+ * report it and the readout pill takes the cell's own colour. The bar-based
154
+ * workaround this replaces cannot: its bars are a constant-height column
155
+ * carrying no value, so the number has to be looked up out-of-band.
156
+ *
157
+ * **Styling.** Colour is data and comes from `colors`, not the theme. Geometry
158
+ * and the selected-cell treatment are borrowed from
159
+ * `theme.bar[as] ?? theme.bar.default` rather than a new `theme.heat` slot:
160
+ * `ChartTheme`'s slots are required, so adding one is breaking for every custom
161
+ * theme, and the M5 "theme tokens optional-with-default" gate has to land
162
+ * first. Borrowing defers that decision instead of pre-empting it.
163
+ *
164
+ * **Pair it with `<ChartContainer cursor="none">`.** The container's default is
165
+ * the shared vertical line, and on a grid that is a *second, weaker* cursor
166
+ * competing with the one that already works: the cell under the pointer takes an
167
+ * outline, which says both axes at once. The line says only x, and a heat map's
168
+ * x position is rarely the question. The pointer's own crosshair shape plus the
169
+ * cell outline is the whole affordance.
170
+ *
171
+ * **Not built:** a grouped two-level x axis, and cell value labels. The former
172
+ * is axis work that would serve bars equally; the latter is small and
173
+ * independent.
174
+ */
175
+ export declare function HeatMap<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, columns, colors, domain, orientation, as: semantic, axis, gap, scale, noData, decimate, id, index, }: HeatMapProps<S, VS>): null;
176
+ //# sourceMappingURL=HeatMap.d.ts.map