Problem
SmartDateLabelProvider (used by DateTimeNumericAxis) documents formatDateWide and
formatDatePrecise as the extension points for customizing axis label text — both carry JSDoc
saying "This method can be overridden to customize ... label formatting". That's true for every
label range except one specific case: when the label range is ETradeChartLabelFormat.Months
and a tick crosses a month boundary without also crossing a year boundary. In that case,
formatSmartLabel builds the label itself by calling the internal formatUnixDateToHumanStringMMM
util directly — a hardcoded, always-English, 3-letter month abbreviation — instead of calling
this.formatDateWide, which it calls for every other branch (first label, year change, every other
label range).
Impact: any subclass overriding formatDateWide/formatDatePrecise to localize or customize
month names works everywhere except this one branch, where it silently reverts to hardcoded
English — even though the rest of the axis's labels correctly reflect the override.
Root cause
SmartDateLabelProvider.prototype.formatSmartLabel = function (format, valueInSeconds, prevValueInSeconds, prevPrevValueInSeconds, originalRawValue) {
var showWider = !this.firstLabel || this.showWiderDateOnFirstLabel;
this.firstLabel = false;
var wideDate = this.formatDateWide(format, valueInSeconds); // overridable, called here
var prevWideDate = prevValueInSeconds !== undefined
? this.formatDateWide(format, prevValueInSeconds)
: undefined;
var isNewDate = prevWideDate === undefined || wideDate !== prevWideDate;
if (format !== ETradeChartLabelFormat.Months) {
if (isNewDate && showWider) { return wideDate; }
return this.formatDatePrecise(format, valueInSeconds, originalRawValue); // overridable
}
else {
if (isNewDate && showWider) { return wideDate; } // year boundary: goes through the override
var prevPrevWideDate = /* ... */;
var prevPrevNewYear = prevPrevWideDate === undefined || prevWideDate !== prevPrevWideDate;
var newMonth = prevPrevNewYear ||
(0, date_1.formatUnixDateToHumanStringMMM)(valueInSeconds) !==
(0, date_1.formatUnixDateToHumanStringMMM)(prevValueInSeconds);
if (newMonth) {
// BUG: returns the raw util's output directly, never touching this.formatDateWide
// or this.formatDatePrecise — the override has no effect here.
return (0, date_1.formatUnixDateToHumanStringMMM)(valueInSeconds);
}
return this.formatDatePrecise(format, valueInSeconds, originalRawValue);
}
};
formatDateWide's own default branch (used for Months) returns just the year, so once the
wide-format string hasn't changed — i.e. we're still in the same year as the previous tick — the
code falls into the newMonth branch as soon as the month itself changes (which is every tick, on
any reasonably-spaced axis). That branch calls formatUnixDateToHumanStringMMM directly,
instead of routing through any overridable method, so a subclass override has no effect there.
Fix
formatDateWide's Months case already means "the year" (per its own JSDoc example, "For years:
2020"), so simply routing the newMonth branch through this.formatDateWide would collide with
that existing meaning. This adds a new, dedicated overridable method — defaulting to exactly
today's hardcoded behavior, so the change is fully backward compatible — and routes the newMonth
branch through it instead of calling the internal util directly:
--- a/Charting/Visuals/Axis/LabelProvider/SmartDateLabelProvider.js
+++ b/Charting/Visuals/Axis/LabelProvider/SmartDateLabelProvider.js
@@ formatDatePrecise (end of method) @@
return (0, number_1.formatNumber)(valueInSeconds, NumericFormat_1.ENumericFormat.Date_DDMMYY, 0);
}
};
+ /**
+ * Formats the month-only label shown when a tick crosses a month boundary but not a year
+ * boundary, at the `Months` label range (e.g. "Jan", "Feb"). This method can be overridden to
+ * customize or localize this specific label, independently of {@link formatDateWide} (whose
+ * own `Months` case returns the year, not the month) and {@link formatDatePrecise}.
+ *
+ * @param valueInSeconds - The data value converted to Unix seconds with dateOffset applied
+ * @returns The formatted month-only label string
+ *
+ * @example
+ * // Default: "Jan", "Feb", ...
+ */
+ SmartDateLabelProvider.prototype.formatMonthLabel = function (valueInSeconds) {
+ return (0, date_1.formatUnixDateToHumanStringMMM)(valueInSeconds);
+ };
SmartDateLabelProvider.prototype.toJSON = function () {
@@ formatSmartLabel, newMonth branch @@
var newMonth = prevPrevNewYear ||
(0, date_1.formatUnixDateToHumanStringMMM)(valueInSeconds) !==
(0, date_1.formatUnixDateToHumanStringMMM)(prevValueInSeconds);
if (newMonth) {
- return (0, date_1.formatUnixDateToHumanStringMMM)(valueInSeconds);
+ return this.formatMonthLabel(valueInSeconds);
}
return this.formatDatePrecise(format, valueInSeconds, originalRawValue);
}
--- a/Charting/Visuals/Axis/LabelProvider/SmartDateLabelProvider.d.ts
+++ b/Charting/Visuals/Axis/LabelProvider/SmartDateLabelProvider.d.ts
@@ class SmartDateLabelProvider @@
formatDatePrecise(labelRange: ETradeChartLabelFormat | string, valueInSeconds: number, rawValue?: number): string;
+ /**
+ * Formats the month-only label shown when a tick crosses a month boundary but not a year
+ * boundary, at the `Months` label range (e.g. "Jan", "Feb"). This method can be overridden to
+ * customize or localize this specific label, independently of {@link formatDateWide} (whose
+ * own `Months` case returns the year, not the month) and {@link formatDatePrecise}.
+ *
+ * @param valueInSeconds - The data value converted to Unix seconds with dateOffset applied
+ * @returns The formatted month-only label string
+ *
+ * @example
+ * // Default: "Jan", "Feb", ...
+ */
+ formatMonthLabel(valueInSeconds: number): string;
toJSON(): {
type: string;
options: Required<Omit<import("./LabelProvider").ILabelOptions, never>>;
This gives subclasses the same kind of override hook formatDateWide/formatDatePrecise already
provide, without changing any existing default output.
Testing
Verified by monkey-patching a 5.2.62 build with the exact change above and rerunning a
SmartDateLabelProvider subclass that overrides formatDateWide/formatDatePrecise to reverse
their output, on a DateTimeNumericAxis spanning several years of monthly ticks:
- Before the fix: year-boundary labels render reversed (e.g.
"5202"), confirming the override
is invoked there, while month-boundary labels render as plain, unreversed English ("Jan",
"Feb", ...), confirming formatDateWide is bypassed.
- After the fix: with the new
formatMonthLabel override added to the same subclass, month
labels are reversed too ("naJ", "beF", ...) — the override is now honored everywhere.
No race, no timing dependency, no change to default (non-overridden) output — deterministic every
run.
index.html
Problem
SmartDateLabelProvider(used byDateTimeNumericAxis) documentsformatDateWideandformatDatePreciseas the extension points for customizing axis label text — both carry JSDocsaying "This method can be overridden to customize ... label formatting". That's true for every
label range except one specific case: when the label range is
ETradeChartLabelFormat.Monthsand a tick crosses a month boundary without also crossing a year boundary. In that case,
formatSmartLabelbuilds the label itself by calling the internalformatUnixDateToHumanStringMMMutil directly — a hardcoded, always-English, 3-letter month abbreviation — instead of calling
this.formatDateWide, which it calls for every other branch (first label, year change, every otherlabel range).
Impact: any subclass overriding
formatDateWide/formatDatePreciseto localize or customizemonth names works everywhere except this one branch, where it silently reverts to hardcoded
English — even though the rest of the axis's labels correctly reflect the override.
Root cause
formatDateWide's own default branch (used forMonths) returns just the year, so once thewide-format string hasn't changed — i.e. we're still in the same year as the previous tick — the
code falls into the
newMonthbranch as soon as the month itself changes (which is every tick, onany reasonably-spaced axis). That branch calls
formatUnixDateToHumanStringMMMdirectly,instead of routing through any overridable method, so a subclass override has no effect there.
Fix
formatDateWide'sMonthscase already means "the year" (per its own JSDoc example, "For years:2020"), so simply routing the
newMonthbranch throughthis.formatDateWidewould collide withthat existing meaning. This adds a new, dedicated overridable method — defaulting to exactly
today's hardcoded behavior, so the change is fully backward compatible — and routes the
newMonthbranch through it instead of calling the internal util directly:
This gives subclasses the same kind of override hook
formatDateWide/formatDatePrecisealreadyprovide, without changing any existing default output.
Testing
Verified by monkey-patching a
5.2.62build with the exact change above and rerunning aSmartDateLabelProvidersubclass that overridesformatDateWide/formatDatePreciseto reversetheir output, on a
DateTimeNumericAxisspanning several years of monthly ticks:"5202"), confirming the overrideis invoked there, while month-boundary labels render as plain, unreversed English (
"Jan","Feb", ...), confirmingformatDateWideis bypassed.formatMonthLabeloverride added to the same subclass, monthlabels are reversed too (
"naJ","beF", ...) — the override is now honored everywhere.No race, no timing dependency, no change to default (non-overridden) output — deterministic every
run.
index.html