Custom High Precision Date Labels

Demonstrates how to customize specific label formats on a High Precision Date Axis, using external libraries like date-fns.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    DateTimeNumericAxis,
3    EllipsePointMarker,
4    MouseWheelZoomModifier,
5    ZoomExtentsModifier,
6    ZoomPanModifier,
7    XyDataSeries,
8    NumericAxis,
9    FastLineRenderableSeries,
10    SciChartSurface,
11    EAutoRange,
12    SmartDateLabelProvider,
13    EResamplingMode,
14    EDatePrecision,
15    EHighPrecisionLabelMode,
16    ETradeChartLabelFormat,
17    NumberRange,
18    RubberBandXyZoomModifier,
19    NativeTextAnnotation,
20    EHorizontalAnchorPoint,
21    ECoordinateMode,
22} from "scichart";
23import { format, fromUnixTime } from "date-fns";
24
25const toUTC = (date: Date) => {
26    // SciChart internally uses UTC dates, so convert to UTC
27    return new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
28};
29
30export const drawExample = async (rootElement: string | HTMLDivElement) => {
31    const { wasmContext, sciChartSurface } = await SciChartSurface.create(rootElement);
32
33    const startDate = new Date("2025-01-01T00:00:00Z");
34    const startTimeSeconds = startDate.getTime() / 1000;
35
36    const xAxis = new DateTimeNumericAxis(wasmContext, {
37        axisTitle: "Time",
38        datePrecision: EDatePrecision.Nanoseconds, // Very important -> 1 x-value increment == 1 nanosecond
39        highPrecisionLabelMode: EHighPrecisionLabelMode.Suffix,
40        dateOffset: startTimeSeconds, // in seconds
41        labelStyle: {
42            color: "#FFFFFF", // make them stand out a bit
43        },
44
45        showWiderDateOnFirstLabel: true,
46        showYearOnWiderDate: true,
47
48        splitWideDateWithComma: false,
49
50        showSecondsOnWideDate: true, // Usually these 2 should be opposites: when wide date shows seconds, precise date shouldn't.
51        showSecondsOnPreciseDate: true,
52    });
53    sciChartSurface.xAxes.add(xAxis);
54
55    const labelProvider = xAxis.labelProvider as SmartDateLabelProvider;
56
57    // 1. Capture the original SciChart implementations
58    const defaultFormatDateWide = labelProvider.formatDateWide.bind(labelProvider);
59    const defaultFormatDatePrecise = labelProvider.formatDatePrecise.bind(labelProvider);
60
61    // 2. Define the Custom "date-fns" implementations
62    // Wide dates -> e.g. "Jan 01, 2025 12:00:00"
63    const customFormatDateWide = (labelRange: ETradeChartLabelFormat | string, valueInSeconds: number) => {
64        const date = toUTC(fromUnixTime(valueInSeconds));
65        if (
66            labelRange === ETradeChartLabelFormat.Nanoseconds ||
67            labelRange === ETradeChartLabelFormat.Microseconds ||
68            labelRange === ETradeChartLabelFormat.MilliSeconds
69        ) {
70            return format(date, "MMM dd, yyyy HH:mm:ss");
71        } else if (labelRange === ETradeChartLabelFormat.Seconds || labelRange === ETradeChartLabelFormat.Minutes) {
72            return format(date, "MMM dd, yyyy");
73        } else if (labelRange === ETradeChartLabelFormat.Days) {
74            return format(date, "MMM yyyy");
75        } else {
76            return format(date, "yyyy");
77        }
78    };
79
80    // Precise dates -> e.g. "12:59:59" or "03s12345ns"
81    const customFormatDatePrecise = (
82        labelRange: ETradeChartLabelFormat | string,
83        valueInSeconds: number,
84        rawValue?: number
85    ) => {
86        const date = toUTC(fromUnixTime(valueInSeconds));
87
88        // High precision logic
89        if (
90            labelRange === ETradeChartLabelFormat.Nanoseconds ||
91            labelRange === ETradeChartLabelFormat.Microseconds ||
92            labelRange === ETradeChartLabelFormat.MilliSeconds
93        ) {
94            const mode = labelProvider.highPrecisionLabelMode;
95
96            if (mode === EHighPrecisionLabelMode.Suffix && rawValue !== undefined) {
97                const tps = labelProvider.datePrecision;
98                const wholeSeconds = Math.floor(rawValue / tps);
99                const ticksWithinSecond = rawValue - wholeSeconds * tps;
100                const subSecondOffset = ticksWithinSecond / tps;
101
102                const seconds = date.getUTCSeconds();
103                const secondsStr = seconds.toString().padStart(2, "0");
104
105                if (labelRange === ETradeChartLabelFormat.Nanoseconds) {
106                    const ns = Math.round(subSecondOffset * 1_000_000_000);
107                    return `${secondsStr}:${ns}ns`;
108                }
109                if (labelRange === ETradeChartLabelFormat.Microseconds) {
110                    const us = Math.round(subSecondOffset * 1_000_000);
111                    return `${secondsStr}:${us}µs`;
112                }
113                const ms = Math.round(subSecondOffset * 1_000);
114                return `${secondsStr}:${ms}ms`;
115            }
116            return format(date, "ss.SSS");
117        }
118
119        if (labelRange === ETradeChartLabelFormat.Seconds) return format(date, "HH:mm:ss");
120        if (labelRange === ETradeChartLabelFormat.Minutes) return format(date, "HH:mm");
121        if (labelRange === ETradeChartLabelFormat.Days || labelRange === ETradeChartLabelFormat.Months)
122            return format(date, "dd");
123
124        return format(date, "dd/MM/yy");
125    };
126
127    // 3. Logic to toggle between them
128    const setUseDateFns = (useCustom: boolean) => {
129        if (useCustom) {
130            labelProvider.formatDateWide = customFormatDateWide;
131            labelProvider.formatDatePrecise = customFormatDatePrecise;
132        } else {
133            labelProvider.formatDateWide = defaultFormatDateWide;
134            labelProvider.formatDatePrecise = defaultFormatDatePrecise;
135        }
136        // Force redraw to apply changes to existing labels
137        sciChartSurface.invalidateElement();
138    };
139
140    // Apply custom by default initially
141    setUseDateFns(true);
142
143    sciChartSurface.yAxes.add(
144        new NumericAxis(wasmContext, {
145            axisTitle: "Signal (mV)",
146            autoRange: EAutoRange.Always,
147            growBy: new NumberRange(0.1, 0.1),
148        })
149    );
150
151    // Data Generation
152    const xValues: number[] = [];
153    const yValues: number[] = [];
154    type Generator = (i: number) => number;
155
156    const addCluster = (startOffsetNano: number, count: number, generator: Generator) => {
157        for (let i = 0; i < count; i++) {
158            const x = startOffsetNano + i * 10_000_000;
159            const noise = (Math.random() - 0.5) * 0.05;
160            const y = generator(i) + noise;
161
162            xValues.push(x);
163            yValues.push(y);
164        }
165    };
166
167    const ONE_SEC = 1_000_000_000; // Because we use `datePrecision = Nanoseconds`
168    const ONE_MIN = ONE_SEC * 60;
169
170    // 1. Damped Ringing
171    addCluster(0, 2000, (i) => Math.sin(i * 0.1) * Math.exp(-i * 0.002) * 2);
172
173    // 2. Frequency Chirp
174    addCluster(1 * ONE_MIN, 2000, (i) => {
175        const freq = 0.01 + i * 0.00005;
176        return Math.sin(i * freq);
177    });
178
179    // 3. Amplitude Modulated Packet
180    addCluster(3 * ONE_MIN, 2000, (i) => {
181        const carrier = Math.sin(i * 0.1);
182        const envelope = Math.sin(i * 0.03); // Slow envelope
183        return carrier * envelope * 1.5;
184    });
185
186    // 4. Noisy "Heartbeat"
187    addCluster(4 * ONE_MIN, 2000, (i) => {
188        // spike every 200 points
189        if (i % 200 < 20) return 1.0 + Math.random();
190        return Math.random() * 0.2;
191    });
192
193    const lineSeries = new FastLineRenderableSeries(wasmContext, {
194        dataSeries: new XyDataSeries(wasmContext, {
195            xValues,
196            yValues,
197            containsNaN: false,
198            isSorted: true,
199            dataSeriesName: "Sensor A",
200        }),
201        stroke: "#50C7E0",
202        strokeThickness: 2,
203        resamplingMode: EResamplingMode.None,
204        pointMarker: new EllipsePointMarker(wasmContext, {
205            width: 4,
206            height: 4,
207            fill: "#2e3a59",
208            stroke: "#50C7E0",
209            strokeThickness: 1,
210        }),
211    });
212    lineSeries.rolloverModifierProps.tooltipLabelX = "X";
213    sciChartSurface.renderableSeries.add(lineSeries);
214
215    sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier(), new ZoomExtentsModifier(), new ZoomPanModifier());
216    sciChartSurface.zoomExtents();
217
218    sciChartSurface.annotations.add(
219        new NativeTextAnnotation({
220            xCoordinateMode: ECoordinateMode.Relative,
221            yCoordinateMode: ECoordinateMode.Relative,
222            x1: 0.5,
223            y1: 0.08,
224            horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
225            text: `Zoom in & out while toggling the switch to see different axis label formats
226            \nSee how you can also customize date formatting at lines 60, 80 in drawExample.ts!`,
227            lineSpacing: 10,
228            fontSize: 16,
229            opacity: 0.7,
230            textColor: "#FFFFFF",
231        })
232    );
233
234    return {
235        wasmContext,
236        sciChartSurface,
237        controls: { setUseDateFns },
238    };
239};
240

High-Precision DateTimeNumericAxis with Custom Label Formatting - in Vanilla JS

Overview

This example demonstrates how to use DateTimeNumericAxis with nanosecond-precision data and the built-in SmartDateLabelProvider, while safely overriding date formatting logic using date-fns.

It highlights how SciChart dynamically switches between wide and precise date labels as you zoom, and how you can fully customize that behavior without breaking cursor labels, tick calculations, or high-precision rendering.

Use the toggle above the chart to switch between SciChart’s default formatting and a custom date-fns implementation.

Key Concepts Demonstrated

DateTimeNumericAxis with Nanosecond Precision

The X-axis is configured with:

  • datePrecision = EDatePrecision.Nanoseconds
  • A fixed dateOffset (Unix seconds)

This allows raw X values to represent nanosecond ticks, while still displaying full calendar dates and times on the axis. Each increment of 1 on the X-axis equals 1 nanosecond, making this suitable for high-frequency sensor data, trading systems, or scientific signals.

SmartDateLabelProvider

The SmartDateLabelProvider automatically adapts label formats based on the visible time range:

  • Wide labels provide context (e.g. Jan 01, 2025 12:00:00)
  • Precise labels show incremental detail (e.g. 59s345ms, 123456ns)

As you zoom in and out, the provider dynamically switches between: Nanoseconds, Microseconds, Milliseconds, Seconds, Minutes, Days / Months.

This logic is driven by internal label thresholds and the current visible range.

Custom Formatting with date-fns

This demo overrides two key methods on the SmartDateLabelProvider:

  • formatDateWide
  • formatDatePrecise

Instead of replacing the entire label provider, the original SciChart implementations are preserved and can be restored instantly. This ensures:

  • Cursor and rollover labels remain correct
  • Tick spacing and delta calculations are unaffected
  • High-precision math stays intact

The toggle switches between:

  • Default SciChart formatting
  • Custom date-fns formatting for both wide and precise labels

This pattern is recommended when integrating third-party date libraries.

When to Use This Approach

Use DateTimeNumericAxis with SmartDateLabelProvider when:

  • You need sub-millisecond or nanosecond resolution
  • You want automatic, readable date formatting
  • You need full control over label appearance
  • Cursor and tooltip accuracy is critical

This pattern is ideal for high-frequency trading, telemetry, scientific measurement, and real-time monitoring applications.

Related Documentation:

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.