Skip to content

Commit 84c26b1

Browse files
committed
feat: improved time scale/tooltip/cursor formatting
1 parent c48fef8 commit 84c26b1

File tree

4 files changed

+5463
-5477
lines changed

4 files changed

+5463
-5477
lines changed

examples/simple/src/useDemoConfig.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ function makeSeries(
191191
) {
192192
const start = 0;
193193
const startDate = new Date();
194+
// startDate.setFullYear(2020);
194195
startDate.setUTCHours(0);
195196
startDate.setUTCMinutes(0);
196197
startDate.setUTCSeconds(0);

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"@svgr/rollup": "^5.4.0",
8181
"@testing-library/react": "^12.0.0",
8282
"@types/d3-delaunay": "^6.0.0",
83+
"@types/d3-time-format": "^4.0.0",
8384
"@types/jest": "^26.0.4",
8485
"@typescript-eslint/eslint-plugin": "^4.8.1",
8586
"@typescript-eslint/parser": "^4.8.1",
@@ -135,6 +136,7 @@
135136
"d3-scale": "^3.3.0",
136137
"d3-shape": "^2.1.0",
137138
"d3-time": "^2.1.1",
139+
"d3-time-format": "^4.1.0",
138140
"ts-toolbelt": "^9.6.0"
139141
}
140142
}

src/utils/buildAxis.linear.ts

Lines changed: 141 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ import {
1111
ScaleBand,
1212
} from 'd3-scale'
1313

14+
import {
15+
timeSecond,
16+
timeMinute,
17+
timeHour,
18+
timeDay,
19+
timeMonth,
20+
timeWeek,
21+
timeYear,
22+
} from 'd3-time'
23+
24+
import { timeFormat } from 'd3-time-format'
25+
1426
import {
1527
Axis,
1628
AxisBand,
@@ -73,16 +85,16 @@ export default function buildAxisLinear<TDatum>(
7385
// Give the scale a home
7486
return options.scaleType === 'time' || options.scaleType === 'localTime'
7587
? buildTimeAxis(
76-
isPrimary,
77-
options,
78-
series,
79-
allDatums,
80-
isVertical,
81-
range,
82-
outerRange
83-
)
88+
isPrimary,
89+
options,
90+
series,
91+
allDatums,
92+
isVertical,
93+
range,
94+
outerRange
95+
)
8496
: options.scaleType === 'linear' || options.scaleType === 'log'
85-
? buildLinearAxis(
97+
? buildLinearAxis(
8698
isPrimary,
8799
options,
88100
series,
@@ -91,11 +103,11 @@ export default function buildAxisLinear<TDatum>(
91103
range,
92104
outerRange
93105
)
94-
: options.scaleType === 'band'
95-
? buildBandAxis(isPrimary, options, series, isVertical, range, outerRange)
96-
: (() => {
97-
throw new Error('Invalid scale type')
98-
})()
106+
: options.scaleType === 'band'
107+
? buildBandAxis(isPrimary, options, series, isVertical, range, outerRange)
108+
: (() => {
109+
throw new Error('Invalid scale type')
110+
})()
99111
}
100112

101113
function buildTimeAxis<TDatum>(
@@ -111,10 +123,13 @@ function buildTimeAxis<TDatum>(
111123

112124
let isInvalid = false
113125

114-
series = isPrimary ? series : series
115-
.filter(s => s.secondaryAxisId === options.id)
126+
series = isPrimary
127+
? series
128+
: series.filter(s => s.secondaryAxisId === options.id)
116129

117-
allDatums = isPrimary ? allDatums : allDatums.filter(d => d.secondaryAxisId === options.id)
130+
allDatums = isPrimary
131+
? allDatums
132+
: allDatums.filter(d => d.secondaryAxisId === options.id)
118133

119134
// Now set the range
120135
const scale = scaleFn(range)
@@ -125,6 +140,87 @@ function buildTimeAxis<TDatum>(
125140
return value
126141
})
127142

143+
// Here, we find the maximum context (in descending order from year
144+
// down to millisecond) needed to understand the
145+
// dates in this dataset. If the min/max dates span multiples of
146+
// any of the time units OR if the max date resides in a different
147+
// unit boundary than today's, we use that unit as context.
148+
149+
const unitScale = [
150+
'millisecond',
151+
'second',
152+
'minute',
153+
'hour',
154+
'day',
155+
'month',
156+
'year',
157+
] as const
158+
159+
let autoFormatStr: string
160+
161+
if (minValue && maxValue) {
162+
if (
163+
timeYear.count(minValue, maxValue) > 0 ||
164+
+timeYear.floor(maxValue) < +timeYear()
165+
) {
166+
autoFormatStr = '%b %-d, %Y %-I:%M:%S.%L %p'
167+
} else if (
168+
timeMonth.count(minValue, maxValue) > 0 ||
169+
+timeMonth.floor(maxValue) < +timeMonth()
170+
) {
171+
autoFormatStr = '%b %-d, %-I:%M:%S.%L %p'
172+
} else if (
173+
timeDay.count(minValue, maxValue) > 0 ||
174+
+timeDay.floor(maxValue) < +timeDay()
175+
) {
176+
autoFormatStr = '%b %-d, %-I:%M:%S.%L %p'
177+
} else if (
178+
timeHour.count(minValue, maxValue) > 0 ||
179+
+timeHour.floor(maxValue) < +timeHour()
180+
) {
181+
autoFormatStr = '%-I:%M:%S.%L %p'
182+
} else if (
183+
timeMinute.count(minValue, maxValue) > 0 ||
184+
+timeMinute.floor(maxValue) < +timeMinute()
185+
) {
186+
autoFormatStr = '%-I:%M:%S.%L'
187+
} else if (
188+
timeSecond.count(minValue, maxValue) > 0 ||
189+
+timeSecond.floor(maxValue) < +timeSecond()
190+
) {
191+
autoFormatStr = '%L'
192+
}
193+
}
194+
195+
const contextFormat = (format: string, date: Date) => {
196+
if (timeSecond(date) < date) {
197+
// milliseconds - Do not remove any context
198+
return timeFormat(format)(date)
199+
}
200+
if (timeMinute(date) < date) {
201+
// seconds - remove potential milliseconds
202+
return timeFormat(format.replace(/\.%L.*?(\s|$)/, ' '))(date)
203+
}
204+
if (timeHour(date) < date) {
205+
// minutes - remove potential seconds and milliseconds
206+
return timeFormat(format.replace(/:%S.*?(\s|$)/, ' '))(date)
207+
}
208+
if (timeDay(date) < date) {
209+
// hours - remove potential minutes and seconds and milliseconds
210+
return timeFormat(format.replace(/:%M.*?(\s|$)/, ' '))(date)
211+
}
212+
if (timeMonth(date) < date) {
213+
// days - remove potential hours, minutes, seconds and milliseconds
214+
return timeFormat(format.replace(/%I.*/, ''))(date)
215+
}
216+
if (timeYear(date) < date) {
217+
// months - remove potential days, hours, minutes, seconds and milliseconds
218+
return timeFormat(format.replace(/%e, .*?(\s|$)/, ''))(date)
219+
}
220+
// years
221+
return timeFormat('%Y')(date)
222+
}
223+
128224
let shouldNice = options.shouldNice
129225

130226
// see https://stackoverflow.com/a/2831422
@@ -195,13 +291,13 @@ function buildTimeAxis<TDatum>(
195291
])
196292
}
197293

198-
const defaultFormat = scale.tickFormat()
199-
200294
const formatters = {} as AxisTime<TDatum>['formatters']
201295

296+
const defaultFormat = scale.tickFormat()
297+
202298
const scaleFormat = (value: Date) =>
203299
options.formatters?.scale?.(value, { ...formatters, scale: undefined }) ??
204-
defaultFormat(value)
300+
contextFormat(autoFormatStr, value)
205301

206302
const tooltipFormat = (value: Date) =>
207303
options.formatters?.tooltip?.(value, {
@@ -211,7 +307,7 @@ function buildTimeAxis<TDatum>(
211307

212308
const cursorFormat = (value: Date) =>
213309
options.formatters?.cursor?.(value, { ...formatters, cursor: undefined }) ??
214-
tooltipFormat(value)
310+
scaleFormat(value)
215311

216312
Object.assign(formatters, {
217313
default: defaultFormat,
@@ -247,32 +343,35 @@ function buildLinearAxis<TDatum>(
247343

248344
let isInvalid = false
249345

250-
series = isPrimary ? series : series
251-
.filter(s => s.secondaryAxisId === options.id)
346+
series = isPrimary
347+
? series
348+
: series.filter(s => s.secondaryAxisId === options.id)
252349

253-
allDatums = isPrimary ? allDatums : allDatums.filter(d => d.secondaryAxisId === options.id)
350+
allDatums = isPrimary
351+
? allDatums
352+
: allDatums.filter(d => d.secondaryAxisId === options.id)
254353

255354
if (options.stacked) {
256355
stackSeries(series, options)
257356
}
258357

259358
let [minValue, maxValue] = options.stacked
260359
? extent(
261-
series
262-
.map(s =>
263-
s.datums.map(datum => {
264-
const value = options.getValue(datum.originalDatum)
265-
datum[isPrimary ? 'primaryValue' : 'secondaryValue'] = value
266-
return datum.stackData ?? []
267-
})
268-
)
269-
.flat(2) as unknown as number[]
270-
)
360+
series
361+
.map(s =>
362+
s.datums.map(datum => {
363+
const value = options.getValue(datum.originalDatum)
364+
datum[isPrimary ? 'primaryValue' : 'secondaryValue'] = value
365+
return datum.stackData ?? []
366+
})
367+
)
368+
.flat(2) as unknown as number[]
369+
)
271370
: extent(allDatums, datum => {
272-
const value = options.getValue(datum.originalDatum)
273-
datum[isPrimary ? 'primaryValue' : 'secondaryValue'] = value
274-
return value
275-
})
371+
const value = options.getValue(datum.originalDatum)
372+
datum[isPrimary ? 'primaryValue' : 'secondaryValue'] = value
373+
return value
374+
})
276375

277376
let shouldNice = options.shouldNice
278377

@@ -496,8 +595,8 @@ function stackSeries<TDatum>(
496595

497596
const stacked = stacker(
498597
Array.from({
499-
length: series.sort((a, b) => b.datums.length - a.datums.length)[0]
500-
.datums.length,
598+
length: series.sort((a, b) => b.datums.length - a.datums.length)[0].datums
599+
.length,
501600
})
502601
)
503602

@@ -578,11 +677,11 @@ function buildSeriesBandScale<TDatum>(
578677
.round(false)
579678
.paddingOuter(
580679
options.outerSeriesBandPadding ??
581-
(options.outerBandPadding ? options.outerBandPadding / 2 : 0)
680+
(options.outerBandPadding ? options.outerBandPadding / 2 : 0)
582681
)
583682
.paddingInner(
584683
options.innerSeriesBandPadding ??
585-
(options.innerBandPadding ? options.innerBandPadding / 2 : 0)
684+
(options.innerBandPadding ? options.innerBandPadding / 2 : 0)
586685
)
587686

588687
const scale = (seriesIndex: number) =>

0 commit comments

Comments
 (0)