From 77d640da46bdcba7df2cbab54d4cb28a7c2e0ca3 Mon Sep 17 00:00:00 2001 From: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:06:28 -0400 Subject: [PATCH] fix(eclwatch): add a multi-scale 24-hour tick formatter adds a 24h multiscale tick formatting function to chart/Axis, exposes a tickFormatFunc property in timeline/ReactTimelineSeries, and uses both in eclwatch/WUTimeline Signed-off-by: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> --- packages/chart/src/Axis.ts | 17 +++++++++++-- packages/chart/src/index.ts | 1 + packages/chart/src/timeFormats.ts | 26 ++++++++++++++++++++ packages/eclwatch/src/WUTimeline.ts | 3 ++- packages/timeline/src/ReactTimelineSeries.ts | 14 +++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 packages/chart/src/timeFormats.ts diff --git a/packages/chart/src/Axis.ts b/packages/chart/src/Axis.ts index 7d852ebe98..32b12bf728 100644 --- a/packages/chart/src/Axis.ts +++ b/packages/chart/src/Axis.ts @@ -22,7 +22,7 @@ export class Axis extends SVGWidget { protected parser; protected parserInvert; - protected formatter: (date: Date) => string; + protected formatter: ((d: Date | any) => string) | null; protected d3Scale; protected d3Axis; protected d3Guides; @@ -30,6 +30,7 @@ export class Axis extends SVGWidget { protected svg; protected svgAxis; protected svgGuides; + protected _tickFormatFunc?: (d: any) => string; constructor(drawStartPosition: "origin" | "center" = "origin") { super(); @@ -91,6 +92,13 @@ export class Axis extends SVGWidget { return this.format(this.parse(d)); } + tickFormatFunc(fn?: (d: any) => string): this | ((d: any) => string) | undefined { + if (!arguments.length) return this._tickFormatFunc; + this._tickFormatFunc = fn; + this.updateScale(); + return this; + } + scalePos(d) { let retVal = this.d3Scale(this.parse(d)); if (this.type() === "ordinal") { @@ -211,7 +219,11 @@ export class Axis extends SVGWidget { } this.parser = this.timePattern_exists() ? d3TimeParse(this.timePattern()) : null; this.parserInvert = this.timePattern_exists() ? d3TimeFormat(this.timePattern()) : null; - this.formatter = this.tickFormat_exists() ? d3TimeFormat(this.tickFormat()) : null; + if (this._tickFormatFunc) { + this.formatter = this._tickFormatFunc; + } else { + this.formatter = this.tickFormat_exists() ? d3TimeFormat(this.tickFormat()) : null; + } break; default: } @@ -677,6 +689,7 @@ export interface Axis { tickFormat(_: string): this; tickFormat_exists(): boolean; tickFormat_reset(): void; + tickFormatFunc(fn?: (d: any) => string): this | ((d: any) => string) | undefined; tickLength(): number; tickLength(_: number): this; tickLength_exists(): boolean; diff --git a/packages/chart/src/index.ts b/packages/chart/src/index.ts index ae5cea8258..f0226db5e9 100644 --- a/packages/chart/src/index.ts +++ b/packages/chart/src/index.ts @@ -25,3 +25,4 @@ export * from "./Summary.ts"; export * from "./SummaryC.ts"; export * from "./WordCloud.ts"; export * from "./XYAxis.ts"; +export * from "./timeFormats.ts"; diff --git a/packages/chart/src/timeFormats.ts b/packages/chart/src/timeFormats.ts new file mode 100644 index 0000000000..0e4177d0f8 --- /dev/null +++ b/packages/chart/src/timeFormats.ts @@ -0,0 +1,26 @@ +import { timeFormat } from "d3-time-format"; + +/** + * Adaptive 24-hour multi-scale tick formatter. + * Order of precedence (first predicate that matches): + * milliseconds -> seconds -> minutes -> hours -> day -> month -> year. + */ +export function multiScale24Hours(): (d: Date) => string { + const fmtMs = timeFormat(".%L"); + const fmtSec = timeFormat(":%S"); + const fmtMin = timeFormat("%H:%M"); + const fmtHour = timeFormat("%H:00"); + const fmtDay = timeFormat("%b %d"); + const fmtMonth = timeFormat("%b"); + const fmtYear = timeFormat("%Y"); + + return (d: Date): string => { + if (d.getMilliseconds() !== 0) return fmtMs(d); + if (d.getSeconds() !== 0) return fmtSec(d); + if (d.getMinutes() !== 0) return fmtMin(d); + if (d.getHours() !== 0) return fmtHour(d); + if (d.getDate() !== 1) return fmtDay(d); + if (d.getMonth() !== 0) return fmtMonth(d); + return fmtYear(d); + }; +} diff --git a/packages/eclwatch/src/WUTimeline.ts b/packages/eclwatch/src/WUTimeline.ts index 39b7fae4a0..5bfc1693bb 100644 --- a/packages/eclwatch/src/WUTimeline.ts +++ b/packages/eclwatch/src/WUTimeline.ts @@ -1,6 +1,7 @@ import { Palette } from "@hpcc-js/common"; import { Scope, Workunit, WsWorkunits } from "@hpcc-js/comms"; import { ReactTimelineSeries } from "@hpcc-js/timeline"; +import { multiScale24Hours } from "@hpcc-js/chart"; import { hashSum, RecursivePartial } from "@hpcc-js/util"; import "../src/WUGraph.css"; @@ -20,7 +21,7 @@ export class WUTimeline extends ReactTimelineSeries { .colorColumn("color") .seriesColumn("series") .timePattern("%Y-%m-%dT%H:%M:%S.%LZ") - .tickFormat("%H:%M") + .tickFormatFunc(multiScale24Hours()) .tooltipTimeFormat("%H:%M:%S.%L") .tooltipHTML(d => { return d[columns.length].calcTooltip(); diff --git a/packages/timeline/src/ReactTimelineSeries.ts b/packages/timeline/src/ReactTimelineSeries.ts index 310056fb7f..74e1246953 100644 --- a/packages/timeline/src/ReactTimelineSeries.ts +++ b/packages/timeline/src/ReactTimelineSeries.ts @@ -73,6 +73,19 @@ export class ReactTimelineSeries extends ReactAxisGanttSeries { } } + tickFormatFunc(fn?: (d: any) => string): this | ((d: any) => string) | undefined { + if (!arguments.length) { + return this._axisLabelFormatter; + } + this._axisLabelFormatter = fn; + + // Delegate to underlying Axis instances using the proper method + this._topAxis.tickFormatFunc(fn); + this._bottomAxis.tickFormatFunc(fn); + + return this; + } + tooltipHTML(callback) { this._tooltipHTML = callback; this.tooltip().tooltipHTML(this._tooltipHTML); @@ -118,6 +131,7 @@ export interface ReactTimelineSeries { timePattern_exists(): boolean; tooltipTimeFormat(): string; tooltipTimeFormat(_: string): this; + tickFormatFunc(fn?: (d: any) => string): this | ((d: any) => string) | undefined; } ReactTimelineSeries.prototype.publish("timePattern", "%Y-%m-%d", "string", "Time pattern used for parsing datetime strings on each data row", null, { optional: true }); ReactTimelineSeries.prototype.publish("tooltipTimeFormat", "%Y-%m-%d", "string", "Time format used in the default html tooltip");