pond-ts 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -1
- package/dist/core/calendar.js +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,11 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
|
|
9
9
|
include new features and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.57.0...HEAD
|
|
12
|
+
[0.57.0]: https://github.com/pond-ts/pond/compare/v0.56.2...v0.57.0
|
|
13
|
+
[0.56.2]: https://github.com/pond-ts/pond/compare/v0.56.1...v0.56.2
|
|
14
|
+
[0.56.1]: https://github.com/pond-ts/pond/compare/v0.56.0...v0.56.1
|
|
15
|
+
[0.56.0]: https://github.com/pond-ts/pond/compare/v0.55.0...v0.56.0
|
|
12
16
|
[0.55.0]: https://github.com/pond-ts/pond/compare/v0.54.0...v0.55.0
|
|
13
17
|
[0.54.0]: https://github.com/pond-ts/pond/compare/v0.53.1...v0.54.0
|
|
14
18
|
[0.53.1]: https://github.com/pond-ts/pond/compare/v0.53.0...v0.53.1
|
|
@@ -56,6 +60,289 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
56
60
|
|
|
57
61
|
## [Unreleased]
|
|
58
62
|
|
|
63
|
+
## [0.57.0] — 2026-08-07
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **charts: `<Zone>` — a shaded y-span annotation.** The fourth annotation
|
|
68
|
+
mark, and the value-axis counterpart of `<Region>`: a band between two y
|
|
69
|
+
values, spanning the full plot width. The mark for a **classification of the
|
|
70
|
+
value axis** — US EPA AQI categories, heart-rate / power zones, SLO bands,
|
|
71
|
+
control-chart spec limits — where a reading only means something read against
|
|
72
|
+
a scale.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
{
|
|
76
|
+
AQI_CATEGORIES.map((c) => (
|
|
77
|
+
<Zone key={c.role} from={c.from} to={c.to} axis="aqi" role={c.role} />
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Colour comes from `theme.annotation.roles[role]`, so a zone _set_ styles as
|
|
83
|
+
one palette in the theme rather than N colours at the call site. Bounds are
|
|
84
|
+
order-free, clamped to the plot (a band past the axis domain is cut, one
|
|
85
|
+
fully outside culls), and accept `±Infinity` for genuinely open-ended bands
|
|
86
|
+
(`to={Infinity}` — AQI's "Hazardous", a `ZoneTime.openEnded` zone), so a
|
|
87
|
+
whole category table can be rendered and the axis decides what shows.
|
|
88
|
+
|
|
89
|
+
Three defaults deliberately **invert** the rest of the annotation family,
|
|
90
|
+
because a zone spans the full width and a set tiles the row:
|
|
91
|
+
`selectable={false}` (else every mousemove lights a band and its hit area
|
|
92
|
+
eats the plot's clicks), `edges={false}` (contiguous sets share every
|
|
93
|
+
interior boundary — edges-on draws each twice), and no auto-label (the bounds
|
|
94
|
+
are already legible on the y axis; the useful label is a name). `<Zone>` has
|
|
95
|
+
no `onChange` — drag-to-edit zones await a consumer.
|
|
96
|
+
|
|
97
|
+
Guide: [From a CSV to a banded chart](https://pond-ts.org/docs/how-to-guides/air-quality-bands).
|
|
98
|
+
|
|
99
|
+
- **charts: `annotation.dash` — an optional dash pattern for the annotation
|
|
100
|
+
register**, per-register or per-role (`{ color, fillOpacity?, dash? }`), same
|
|
101
|
+
shape as `LineStyle.dash`. Applies to marker / baseline lines and region /
|
|
102
|
+
zone boundaries; fills are never dashed. A dashed reference line reads as
|
|
103
|
+
_placed_ rather than _measured_ — the job the annotation register exists to
|
|
104
|
+
do, and one colour alone can't always carry.
|
|
105
|
+
|
|
106
|
+
- **charts: threshold-banded bars** — `<BarChart thresholds={[1, 2]}>` colours
|
|
107
|
+
one bar **along its length** against a ladder (neutral → warning → alarm), so
|
|
108
|
+
a long bar shows how far through the ladder it travelled rather than only
|
|
109
|
+
which band it ended in. Band fills come from the new
|
|
110
|
+
**`BarStyle.bands`** on the resolved theme role, overridable per chart with
|
|
111
|
+
**`<BarChart bandColors>`** — breakpoints are data, colour stays in the theme,
|
|
112
|
+
the same split `colors` already applies to a stack's group fills.
|
|
113
|
+
|
|
114
|
+
The shape was previously expressible as N `<BarChart>` layers drawn
|
|
115
|
+
outermost-first, each clipped to a band, compositing the gradient by
|
|
116
|
+
overpainting. That produces the same pixels and loses what matters: N layers
|
|
117
|
+
means N hit targets, N `SelectInfo.mark` identities and N legend rows for
|
|
118
|
+
something the reader sees as one bar. Banding is **draw-only** — the hit rect
|
|
119
|
+
is untouched, so a banded bar stays one bar. It also measures **27–49%
|
|
120
|
+
cheaper** than the layered workaround at 8–400 categories
|
|
121
|
+
(`scripts/perf-bandbar.mjs`, which interleaves the arms and prints the
|
|
122
|
+
ratios), and costs nothing when unused.
|
|
123
|
+
|
|
124
|
+
Applies to any single-value bar — `series`, `bins`, `categories`, both
|
|
125
|
+
orientations. Negatives band symmetrically on the magnitude, so a ± diverging
|
|
126
|
+
scale needs no negative breakpoints. Ignored (with a dev warning) on a
|
|
127
|
+
multi-group stack, and yields to `binColors` when both are set. Suppresses
|
|
128
|
+
envelope decimation for the same reason `binColors` does.
|
|
129
|
+
|
|
130
|
+
- **charts: `<YAxis hide>`** — keep the scale, draw no gutter, reserve no
|
|
131
|
+
width. A `<YAxis>` does two jobs — it _holds the scale_ (`min`/`max`/`scale`/
|
|
132
|
+
`pad`) and it _renders a gutter_ — and there was no way to ask for the first
|
|
133
|
+
without the second. A caller could express "auto domain, no gutter" (omit the
|
|
134
|
+
axis) or "explicit domain, with a gutter", but not the pairing a fixed-domain
|
|
135
|
+
chart needs. Omitting the axis is not equivalent: the row supplies an implicit
|
|
136
|
+
auto-domain axis, which is exactly what must not be given up. `width={0}`
|
|
137
|
+
isn't either — the labels still draw, over the plot.
|
|
138
|
+
|
|
139
|
+
Gridlines are unaffected: they belong to the plot, not the gutter, and
|
|
140
|
+
`<ChartContainer grid>` already governs them.
|
|
141
|
+
|
|
142
|
+
- **charts: `<ChartContainer maxBandWidth>` + `bandAlign`** — cap the **slot
|
|
143
|
+
pitch** on a category x axis and place the resulting block
|
|
144
|
+
(`'start'` (default) / `'center'` / `'end'`). A band scale otherwise spreads
|
|
145
|
+
its categories across the full plot width, so three categories in a 900px
|
|
146
|
+
panel become three 300px bars and thirty become thirty 30px ones — the same
|
|
147
|
+
chart in the same panel reading as two different charts depending on how many
|
|
148
|
+
categories the data returned. Fine for a fixed domain; wrong for a **live**
|
|
149
|
+
one, where bar width becomes a variable that moves on its own and a reader
|
|
150
|
+
can't compare the chart to itself a minute ago.
|
|
151
|
+
|
|
152
|
+
`maxBandWidth` caps the **slot**; `<BarChart gap>` still insets the bar
|
|
153
|
+
within it — one knob for pitch, one for ink. Omitting `maxBandWidth` is the
|
|
154
|
+
previous fill behaviour exactly, and a cap too loose to bind degrades back to
|
|
155
|
+
it rather than clipping. There is deliberately no `bandAlign: 'fill'`: "fill"
|
|
156
|
+
is what omitting the cap means, and a `fill` alongside a pitch cap would be a
|
|
157
|
+
contradiction rather than a choice.
|
|
158
|
+
|
|
159
|
+
**Vertical / x-axis categories only** — a `orientation="horizontal"`
|
|
160
|
+
categorical chart puts its categories on the y axis as unit slots, a
|
|
161
|
+
different mechanism this does not cap.
|
|
162
|
+
|
|
163
|
+
- **charts: themed emphasis on the category path** — `BarStyle.hover` /
|
|
164
|
+
`.highlight` now apply to `categories` and horizontal bars, which routed
|
|
165
|
+
through the transposed stacked draw path and read neither. New
|
|
166
|
+
`BarStyle.selectedOutline` (the selected bar's stroke, where the default is
|
|
167
|
+
its own fill) and `BarStyle.emphasisOpacity` (the alpha a live bar pops to,
|
|
168
|
+
previously hard-coded `1`) make the emphasis tunable rather than fixed.
|
|
169
|
+
|
|
170
|
+
The _behaviour_ was defensible; the problem was that the theme accepted
|
|
171
|
+
values it would not use. `bar.hover` / `.highlight` were typed, settable and
|
|
172
|
+
documented as the emphasis channel, and silently did nothing on the most
|
|
173
|
+
common categorical chart, so a theme author set them, saw no change, and
|
|
174
|
+
could not tell whether they were wrong about the colour or the mechanism.
|
|
175
|
+
The one genuine exclusion stays and is now the only one: a `binColors` bar
|
|
176
|
+
keeps its own colour under hover/selection, because swapping a
|
|
177
|
+
zone-coloured or direction-coloured bar to a single highlight hue erases
|
|
178
|
+
what the colour encodes.
|
|
179
|
+
|
|
180
|
+
### Fixed
|
|
181
|
+
|
|
182
|
+
- **charts: a negative segment in a multi-group stack is no longer silently
|
|
183
|
+
dropped** ([PND-SIGNSTACK]). Positives stack **up** from the baseline and
|
|
184
|
+
negatives stack **down** from it — two running totals per bin — so the
|
|
185
|
+
**signed stacked histogram** (net flow by category, inflow/outflow, buy/sell
|
|
186
|
+
pressure by venue) renders correctly. `stackValueExtent` grew the matching
|
|
187
|
+
negative half; both had to move together, since an extent stopping at `0`
|
|
188
|
+
would clip the segments the draw path now emits.
|
|
189
|
+
|
|
190
|
+
**This is a visible behaviour change** for any existing chart feeding
|
|
191
|
+
negative values into a multi-group stack — but such a chart was previously
|
|
192
|
+
rendering _wrongly_: the dropped segments did not clamp, warn or throw, every
|
|
193
|
+
remaining segment stacked up as though they had never been in the data, and a
|
|
194
|
+
mixed-sign series came out as a confident, wrong, all-positive chart. An
|
|
195
|
+
all-positive stack is bit-identical to before. Splitting into two layers was
|
|
196
|
+
not a workaround either — the negative layer was still `G > 1`, so it was
|
|
197
|
+
dropped too.
|
|
198
|
+
|
|
199
|
+
### Changed
|
|
200
|
+
|
|
201
|
+
- **charts (docs): `<BarChart bins>` now states that it selects a _value_
|
|
202
|
+
axis** ([PND-TICKUNIT]), so a time-bucketed histogram fed through `bins` gets
|
|
203
|
+
the decimal 1-2-5 tick ladder rather than the duration ladder a clock
|
|
204
|
+
subdivides by — labelling e.g. 11:40 and 13:20 at a ~100-minute step. The
|
|
205
|
+
natural reading ("I have pre-binned buckets, so I'll pass `bins`") is exactly
|
|
206
|
+
what forecloses the time axis, and nothing at the call site said so. The prop
|
|
207
|
+
docs now point a time-keyed caller at `<BarChart series columns>`, where the
|
|
208
|
+
clock ticks are native. No behaviour change.
|
|
209
|
+
|
|
210
|
+
## [0.56.2] — 2026-08-05
|
|
211
|
+
|
|
212
|
+
### Fixed
|
|
213
|
+
|
|
214
|
+
- **charts (tests only, no shipped change):** the log-axis rendered-label test
|
|
215
|
+
is no longer an exact-set assertion over every digit-bearing node in the
|
|
216
|
+
render tree. It passed on Node 22 and failed on CI's Node 18 with one extra
|
|
217
|
+
element, blocking the publish twice. The discrepancy is **unreproduced and
|
|
218
|
+
still open** — recorded as `[PND-LOGTICK-N18]` in `PND_CHARTS_PLAN.md` with
|
|
219
|
+
everything measured about it. The assertion now checks that every chosen tick
|
|
220
|
+
renders in order, which is the wiring this test exists to cover; the tick
|
|
221
|
+
_selection_ it was really about is pinned deterministically by the
|
|
222
|
+
`yTickValues` unit tests.
|
|
223
|
+
|
|
224
|
+
## [0.56.1] — 2026-08-05
|
|
225
|
+
|
|
226
|
+
### Fixed
|
|
227
|
+
|
|
228
|
+
- **charts (tests only, no shipped change):** a log-axis test asserted on
|
|
229
|
+
rendered label _text_, parsing numbers back out of the DOM to infer scale
|
|
230
|
+
behaviour. It passed locally and failed in CI on a value it could not have
|
|
231
|
+
produced there, which blocked the v0.56.0 publish. The root cause was never
|
|
232
|
+
reproduced; rather than guess at it, the assertion now compares the rendered
|
|
233
|
+
labels against the ticks the axis is specified to draw, formatted through the
|
|
234
|
+
same formatter — deterministic regardless of locale, formatting or DOM
|
|
235
|
+
differences, and the numeric guarantee itself was already pinned directly by
|
|
236
|
+
the `yTickValues` unit tests. The published artifact is identical to what
|
|
237
|
+
v0.56.0 would have been.
|
|
238
|
+
|
|
239
|
+
## [0.56.0] — 2026-08-05
|
|
240
|
+
|
|
241
|
+
### Added
|
|
242
|
+
|
|
243
|
+
- **charts:** **`<YAxis scale="log">` — a base-10 logarithmic y axis.** Every y
|
|
244
|
+
scale was `scaleLinear`, so data spanning orders of magnitude was
|
|
245
|
+
undrawable: on a linear axis everything below the top decade collapses onto
|
|
246
|
+
the baseline. Set `scale="log"` and the axis maps by ratio, ticking the
|
|
247
|
+
decades. `format` still formats the **value**, so a readout says `1.2 PB`
|
|
248
|
+
rather than its logarithm — the transform is in the scale, not in the data,
|
|
249
|
+
which is what keeps it transparent to every draw layer, annotation and
|
|
250
|
+
cursor readout.
|
|
251
|
+
|
|
252
|
+
A log domain cannot contain zero, and d3 maps a non-positive value to
|
|
253
|
+
**`NaN`** — a coordinate the canvas silently _drops_, which is why every
|
|
254
|
+
consequence below is about something failing invisibly rather than throwing.
|
|
255
|
+
So the axis is deliberate about it:
|
|
256
|
+
- **Domain policy matches the linear axis exactly.** Auto-fit takes the
|
|
257
|
+
smallest **positive** extent (one zero sample can't collapse the axis, and
|
|
258
|
+
a `BarChart` — whose extent always widens to include zero — can still share
|
|
259
|
+
it); a positive explicit `min`/`max` is honoured verbatim and never
|
|
260
|
+
discarded, with the _auto-fit_ side moving if the domain would otherwise
|
|
261
|
+
invert; a fully auto-fit domain is `.nice()`d out to whole powers of ten, so
|
|
262
|
+
the extremes get headroom instead of sitting clipped on the plot edge. A
|
|
263
|
+
non-positive bound has no position and is refused in favour of the data.
|
|
264
|
+
`pad` is applied multiplicatively, adding the same fraction of a decade at
|
|
265
|
+
both ends.
|
|
266
|
+
- **A value with no position on the axis renders as a gap.** Previously the
|
|
267
|
+
gap test was `Number.isFinite(value)`, and `0` is finite — so the coordinate
|
|
268
|
+
became `NaN`, the canvas dropped the path op without breaking the path, and
|
|
269
|
+
the two neighbours were joined by a straight line _over_ the missing data.
|
|
270
|
+
Lines, area fills and outlines, and band envelopes now all break there.
|
|
271
|
+
- **Layers that reach for a baseline rest on the axis floor.** `AreaChart`
|
|
272
|
+
resolves an out-of-domain `baseline` there (writing `baseline={0}` is
|
|
273
|
+
natural and correct on a linear axis), and a **stacked** bar layer starts
|
|
274
|
+
its first segment there — starting at zero made the bottom segment of every
|
|
275
|
+
stack both invisible and unhittable. Unchanged on a linear axis, where zero
|
|
276
|
+
clamped into the domain _is_ zero.
|
|
277
|
+
- **The dev-mode warning names only unambiguous mistakes**: a refused
|
|
278
|
+
`min`/`max`, negative data, or an axis with no positive data at all. It
|
|
279
|
+
deliberately says nothing about an extent of exactly `[0, hi]`, which a
|
|
280
|
+
line touching zero and a bar layer on strictly positive data both report
|
|
281
|
+
identically — warning there fired on _every_ bar chart on a log axis. It
|
|
282
|
+
warns once per distinct complaint rather than on every repaint.
|
|
283
|
+
|
|
284
|
+
- **charts:** **pan and zoom now yield whole-millisecond view ranges.** A
|
|
285
|
+
wheel-zoom derives its range from pixel positions through `xScale.invert()`,
|
|
286
|
+
so the result was fractional by construction — an ordinary scroll produced
|
|
287
|
+
`1.7e12 + 0.37`. The epoch millisecond is this model's atomic unit and
|
|
288
|
+
consumers are entitled to assume it; one did, and a calendar `cursorSequence`
|
|
289
|
+
threw on a plain scroll. `zoomRange` / `panRange` round both ends, and never
|
|
290
|
+
collapse a positive span to zero width in doing so. (Core's fractional-instant
|
|
291
|
+
fix covers the same crash from the other side; this closes the class.)
|
|
292
|
+
|
|
293
|
+
- **charts:** **`AreaStyle.flatFill` — stacked areas that read as slabs.** An
|
|
294
|
+
area's fill has always graded to transparent at the baseline, which is right
|
|
295
|
+
for the elevation form and wrong for a stack: every band showed the one
|
|
296
|
+
beneath it through the fade, so a stacked area was not really drawable. Set
|
|
297
|
+
`flatFill` and the fill is flat; omitted, the gradient is unchanged, so no
|
|
298
|
+
existing theme shifts. The docs theme's `seq1…seq8` area roles set it, since
|
|
299
|
+
stacking is what they exist for.
|
|
300
|
+
|
|
301
|
+
- **docs theme:** **a sequential ramp — `seq1…seq8` — for charts with more
|
|
302
|
+
series than the categorical set has hues.** `--pond-viz-1…5` were, and
|
|
303
|
+
remain, the categorical set; a chart needing more slots (an eight-source
|
|
304
|
+
stack, a wall of climate stripes) now steps **tonally** through the brand
|
|
305
|
+
teal instead of introducing competing hues. Eight steps, evenly spaced
|
|
306
|
+
(~ΔL\* 9 in CIELAB), defined for light and dark, each mode's ramp containing
|
|
307
|
+
that mode's `--pond-viz-1` exactly. Exposed as `line` / `area` / `bar` theme
|
|
308
|
+
roles on `docsTheme` (Storybook) and the docs site's `useSiteChartTheme`,
|
|
309
|
+
and as an array from the site's `useSequentialRamp()`. Dev-only: the ramp
|
|
310
|
+
lives in the `docs-theme.fixture.ts` Storybook fixture and the website's
|
|
311
|
+
CSS, both excluded from the published `@pond-ts/charts` build — the library
|
|
312
|
+
still ships no palette.
|
|
313
|
+
|
|
314
|
+
### Fixed
|
|
315
|
+
|
|
316
|
+
- **core:** **a fractional epoch millisecond no longer crashes calendar
|
|
317
|
+
math.** `Temporal.Instant` refuses a non-integer epoch ms outright
|
|
318
|
+
(`epoch milliseconds must be an integer`), and `toPlainDateStart` passed
|
|
319
|
+
whatever it was given straight through — so realizing a `Sequence.calendar`
|
|
320
|
+
over a fractional range threw, and in a React app the exception unmounted the
|
|
321
|
+
page. A fraction is not a caller error: a chart's wheel-zoom derives its view
|
|
322
|
+
range from pixel positions via `xScale.invert()`, so an ordinary scroll
|
|
323
|
+
produces `1.7e12 + 0.37`. The instant is now floored to the millisecond
|
|
324
|
+
containing it — the epoch millisecond is this model's atomic unit and
|
|
325
|
+
calendar boundaries are themselves whole milliseconds, so the bucket
|
|
326
|
+
containing `t` and the one containing `t + 0.37` are necessarily the same,
|
|
327
|
+
and integer inputs are untouched. (`Math.floor`, not `Math.trunc`: pre-1970
|
|
328
|
+
they disagree, and `-5.5` lies inside the millisecond spanning `[-6, -5)`.)
|
|
329
|
+
|
|
330
|
+
- **charts:** toggling **`<ChartContainer grid>`** now repaints immediately.
|
|
331
|
+
`Layers`' draw callback read `container.grid` but didn't depend on it, so
|
|
332
|
+
switching gridlines off changed nothing until an unrelated dependency moved —
|
|
333
|
+
in practice you had to pan or zoom a little to force the update. The same
|
|
334
|
+
omission covered `sessionDividers` and `xKind`.
|
|
335
|
+
|
|
336
|
+
- **charts:** the log axis's dev-mode warning no longer requires **node's
|
|
337
|
+
ambient types**. It was guarded by a bare `process.env.NODE_ENV`, which
|
|
338
|
+
typechecks only when a tool happens to resolve `@types/node` from a parent
|
|
339
|
+
`node_modules` — so `tsc` inside the package passed while running the _same_
|
|
340
|
+
tsconfig from a consumer's directory failed with `TS2591: Cannot find name
|
|
341
|
+
'process'`. That took out the docs site's TypeDoc step, and would equally hit
|
|
342
|
+
any consumer typechecking the package's sources. The guard now lives in
|
|
343
|
+
`src/dev.ts` behind a local declaration and a `typeof` check, so a browser
|
|
344
|
+
bundle with no `process` global doesn't throw at import either.
|
|
345
|
+
|
|
59
346
|
## [0.55.0] — 2026-08-04
|
|
60
347
|
|
|
61
348
|
### Added
|
package/dist/core/calendar.js
CHANGED
|
@@ -43,7 +43,24 @@ export function parseTimestampString(value, options = {}) {
|
|
|
43
43
|
return Temporal.Instant.from(value).epochMilliseconds;
|
|
44
44
|
}
|
|
45
45
|
export function toPlainDateStart(instantMs, timeZone, unit, weekStartsOn) {
|
|
46
|
-
|
|
46
|
+
// **Floor to the containing millisecond.** `Temporal.Instant` refuses a
|
|
47
|
+
// fractional epoch ms outright — `fromEpochMilliseconds(1577836800000.37)`
|
|
48
|
+
// throws `epoch milliseconds must be an integer` — and a fraction is not a
|
|
49
|
+
// caller error here. A chart's wheel-zoom derives its view range from pixel
|
|
50
|
+
// positions through `xScale.invert()`, so a perfectly ordinary gesture hands
|
|
51
|
+
// us `1.7e12 + 0.37`; realizing a calendar sequence over that range then
|
|
52
|
+
// crashed the page outright.
|
|
53
|
+
//
|
|
54
|
+
// Flooring is the only sane reading rather than a papering-over: the epoch
|
|
55
|
+
// millisecond *is* the atomic unit of this model, so an over-precise input
|
|
56
|
+
// can only mean the millisecond containing it, and calendar boundaries are
|
|
57
|
+
// themselves whole milliseconds — so the bucket containing `t` and the one
|
|
58
|
+
// containing `t + 0.37` are necessarily the same. Integer inputs are
|
|
59
|
+
// untouched.
|
|
60
|
+
//
|
|
61
|
+
// `Math.floor`, not `Math.trunc`: before 1970 they disagree, and `-5.5` lies
|
|
62
|
+
// inside the millisecond spanning `[-6, -5)`, which is `-6`.
|
|
63
|
+
const zoned = Temporal.Instant.fromEpochMilliseconds(Math.floor(instantMs)).toZonedDateTimeISO(timeZone);
|
|
47
64
|
const date = zoned.toPlainDate();
|
|
48
65
|
if (unit === 'day') {
|
|
49
66
|
return date;
|