Label the time axis in local time while keeping the data in UTC
Lightweight Charts is timezone-agnostic: it reads epoch seconds as UTC and labels them as UTC, so the axis disagreed with the wall clock by the viewer's offset. tickMarkFormatter now formats axis ticks through the browser's own zone, and localization.timeFormatter does the same for the crosshair readout, which would otherwise contradict the axis. Not done by shifting the bar timestamps, which is the other common recipe for this. Every time in this codebase is epoch UTC by convention, and the chart's times feed trendline anchors, indexAt, hit testing and the values posted back for manual lines. An offset applied to the data would put every one of them out by that offset — the same class of bug that once priced a trendline 147 points from where it was drawn. Tick placement is still computed on UTC days, so the day-change divider sits at 00:00 UTC rather than local midnight, carrying the local date. Verified under America/Chicago: a bar at 13:03 UTC labels as 08:03 and the axis reads 05:30 through 08:00, with no console errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
374e255c95
commit
d1056ed486
2 changed files with 45 additions and 1 deletions
|
|
@ -1363,3 +1363,20 @@ Worth recording for the next person who reads a gap as a bug: the remaining
|
|||
pauses are the market, not the pipe. In thin pre-open tape /ES genuinely goes
|
||||
seconds without a price-changing trade, and then moves several ticks at once —
|
||||
which is what a "gap up" after a quiet spell actually is.
|
||||
|
||||
**The time axis reads local, the data stays UTC.** Lightweight Charts is
|
||||
timezone-agnostic: it reads epoch seconds as UTC and labels them as UTC, which
|
||||
is why the axis disagreed with the wall clock. Fixed with `tickMarkFormatter`
|
||||
for the axis and `localization.timeFormatter` for the crosshair, both going
|
||||
through the browser's own zone.
|
||||
|
||||
Deliberately *not* fixed by shifting the bar timestamps, which is the other
|
||||
common recipe. Every time in this codebase is epoch UTC by convention, and the
|
||||
chart's own times feed trendline anchors, `indexAt`, hit testing and the values
|
||||
posted back for manual lines — an offset applied to the data would put all of
|
||||
them out by the offset, which is exactly the class of bug that once priced a
|
||||
trendline 147 points away.
|
||||
|
||||
One limit worth knowing: tick *placement* is still computed on UTC days, so the
|
||||
day-change divider sits at 00:00 UTC rather than local midnight, labelled with
|
||||
the local date. The labels are right; the divider is in the UTC place.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,34 @@ class ConfluenceChart {
|
|||
vertLines: { color: 'rgba(128,128,128,.10)' },
|
||||
horzLines: { color: 'rgba(128,128,128,.10)' },
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
timeScale: {
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
// The library is timezone-agnostic: it reads epoch seconds as UTC and
|
||||
// labels them as UTC. Bars stay in UTC — that convention holds
|
||||
// everywhere in this codebase, and shifting the values themselves would
|
||||
// put every trendline anchor, hit test and alert an offset out. Only
|
||||
// the label is localised, via the browser's own zone.
|
||||
tickMarkFormatter: (time, tickMarkType) => {
|
||||
const at = new Date(time * 1000);
|
||||
if (tickMarkType === LightweightCharts.TickMarkType.Year) return String(at.getFullYear());
|
||||
if (tickMarkType === LightweightCharts.TickMarkType.Month) {
|
||||
return at.toLocaleString(undefined, { month: 'short' });
|
||||
}
|
||||
if (tickMarkType === LightweightCharts.TickMarkType.DayOfMonth) return String(at.getDate());
|
||||
return at.toLocaleTimeString(undefined, {
|
||||
hour12: false, hour: '2-digit', minute: '2-digit',
|
||||
...(tickMarkType === LightweightCharts.TickMarkType.TimeWithSeconds
|
||||
? { second: '2-digit' } : {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
// The crosshair readout, which would otherwise disagree with the axis.
|
||||
timeFormatter: time => new Date(time * 1000).toLocaleString(undefined, {
|
||||
hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
||||
}),
|
||||
},
|
||||
rightPriceScale: { borderVisible: false },
|
||||
});
|
||||
this.candles = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue