Skip to content

Commit afc93e4

Browse files
committed
Simplify function to get x labels
1 parent 30baeb2 commit afc93e4

4 files changed

Lines changed: 145 additions & 55 deletions

File tree

assets/js/dashboard/stats/graph/date-formatter.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import { parseUTCDate, formatMonthYYYY, formatDayShort } from '../../util/date'
2-
3-
const browserDateFormat = Intl.DateTimeFormat(navigator.language, {
4-
hour: 'numeric'
5-
})
6-
7-
const is12HourClock = function () {
8-
return browserDateFormat.resolvedOptions().hour12
9-
}
1+
import { parseUTCDate, formatMonthYYYY, formatDayShort, is12HourClock } from '../../util/date'
102

113
const monthIntervalFormatter = {
124
long(isoDate, options) {

assets/js/dashboard/stats/graph/main-graph.tsx

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
MetricFormatterShort
77
} from '../reports/metric-formatter'
88
import { DashboardPeriod } from '../../dashboard-time-periods'
9-
import dateFormatter from './date-formatter'
9+
import {
10+
formatMonthYYYY,
11+
formatDayShort,
12+
formatTime,
13+
is12HourClock,
14+
parseNaiveDate
15+
} from '../../util/date'
1016
import classNames from 'classnames'
1117
import { ChangeArrow } from '../reports/change-arrow'
1218
import { Metric } from '../../../types/query-api'
@@ -50,9 +56,9 @@ type MainGraphResponse = {
5056
}
5157
type GraphDatum = {
5258
value: number | null
53-
timeLabel: string | null
59+
timeLabel: string | null
5460
comparisonValue?: number | null
55-
comparisonTimeLabel?: string | null
61+
comparisonTimeLabel?: string | null
5662
change?: number | null
5763
}
5864

@@ -469,55 +475,46 @@ export const MainGraphContainer = React.forwardRef<
469475
})
470476

471477
const getXLabel = (
472-
xValue: '__blank__' | string,
478+
// in the format "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS"
479+
xValue: string,
473480
{
474481
shouldShowYear,
475482
period,
476483
interval
477-
}: { shouldShowYear: boolean; interval: string; period: DashboardPeriod }
478-
) => {
479-
if (xValue == '__blank__') return ''
480-
481-
if (interval === 'hour' && period !== 'day') {
482-
const date = dateFormatter({
483-
interval: 'day',
484-
longForm: false,
485-
period: period,
486-
shouldShowYear,
487-
isPeriodFull: false
488-
})(xValue)
489-
490-
const hour = dateFormatter({
491-
interval: interval,
492-
longForm: false,
493-
period: period,
494-
shouldShowYear,
495-
isPeriodFull: false
496-
})(xValue)
497-
498-
// Returns a combination of date and hour. This is because
499-
// small intervals like hour may return multiple days
500-
// depending on the queried period.
501-
return `${date}, ${hour}`
484+
}: {
485+
shouldShowYear: boolean
486+
/* "month" | "week" | "day" | "hour" | "minute" */
487+
interval: string
488+
period: DashboardPeriod
502489
}
503-
504-
if (interval === 'minute' && period !== 'realtime') {
505-
return dateFormatter({
506-
interval: 'hour',
507-
longForm: false,
508-
period: period,
509-
isPeriodFull: false,
510-
shouldShowYear: false
511-
})(xValue)
490+
) => {
491+
const parsedDate = parseNaiveDate(xValue)
492+
switch (interval) {
493+
case 'month':
494+
return formatMonthYYYY(parsedDate)
495+
case 'week':
496+
case 'day':
497+
return formatDayShort(parsedDate, shouldShowYear)
498+
case 'hour': {
499+
const time = formatTime(parsedDate, {
500+
use12HourClock: is12HourClock(),
501+
includeMinutes: false
502+
})
503+
if (period === 'day') {
504+
return time
505+
}
506+
// when viewing a period like Last 24h or Last 7 days, including the date is necessary for clarity
507+
return `${formatDayShort(parsedDate, shouldShowYear)}, ${time}`
508+
}
509+
case 'minute':
510+
if (period === 'realtime') return `${xValue}m`
511+
return formatTime(parsedDate, {
512+
use12HourClock: is12HourClock(),
513+
includeMinutes: true
514+
})
515+
default:
516+
return ''
512517
}
513-
514-
return dateFormatter({
515-
interval: interval,
516-
longForm: false,
517-
period: period,
518-
shouldShowYear,
519-
isPeriodFull: false
520-
})(xValue)
521518
}
522519

523520
const remapToGraphData = (

assets/js/dashboard/util/date.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import utc from 'dayjs/plugin/utc'
33

44
dayjs.extend(utc)
55

6+
const browserDateFormat = Intl.DateTimeFormat(navigator.language, {
7+
hour: 'numeric'
8+
})
9+
10+
export function is12HourClock () {
11+
return browserDateFormat.resolvedOptions().hour12
12+
}
13+
614
export function utcNow() {
715
return dayjs()
816
}
@@ -40,6 +48,13 @@ export function formatDay(date) {
4048
}
4149
}
4250

51+
export function formatTime(date, { use12HourClock, includeMinutes }) {
52+
if (use12HourClock) {
53+
return includeMinutes ? date.format('h:mma') : date.format('ha')
54+
}
55+
return date.format('HH:mm')
56+
}
57+
4358
export function formatDayShort(date, includeYear = false) {
4459
if (includeYear) {
4560
return date.format('D MMM YY')

assets/js/dashboard/util/date.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
dateForSite,
33
formatDayShort,
4+
formatTime,
5+
formatMonthYYYY,
46
formatISO,
57
nowForSite,
68
parseNaiveDate,
@@ -126,3 +128,87 @@ describe('formatting site-timezoned datetimes from database works flawlessly', (
126128
)
127129
})
128130
})
131+
132+
describe(formatMonthYYYY.name, () => {
133+
it('formats a date as "Month YYYY"', () => {
134+
expect(formatMonthYYYY(parseNaiveDate('2025-06-15'))).toEqual('June 2025')
135+
})
136+
137+
it('formats January correctly', () => {
138+
expect(formatMonthYYYY(parseNaiveDate('2024-01-01'))).toEqual(
139+
'January 2024'
140+
)
141+
})
142+
})
143+
144+
describe(formatDayShort.name, () => {
145+
it('formats without year by default', () => {
146+
expect(formatDayShort(parseNaiveDate('2025-06-05'))).toEqual('5 Jun')
147+
})
148+
149+
it('includes 2-digit year when requested', () => {
150+
expect(formatDayShort(parseNaiveDate('2025-06-05'), true)).toEqual(
151+
'5 Jun 25'
152+
)
153+
})
154+
})
155+
156+
describe(formatTime.name, () => {
157+
describe('12-hour clock', () => {
158+
it('formats hour without minutes as ha', () => {
159+
expect(
160+
formatTime(parseNaiveDate('2025-06-15 14:00:00'), {
161+
use12HourClock: true,
162+
includeMinutes: false
163+
})
164+
).toEqual('2pm')
165+
})
166+
167+
it('formats hour with minutes as h:mma', () => {
168+
expect(
169+
formatTime(parseNaiveDate('2025-06-15 14:30:00'), {
170+
use12HourClock: true,
171+
includeMinutes: true
172+
})
173+
).toEqual('2:30pm')
174+
})
175+
176+
it('formats midnight correctly', () => {
177+
expect(
178+
formatTime(parseNaiveDate('2025-06-15 00:00:00'), {
179+
use12HourClock: true,
180+
includeMinutes: false
181+
})
182+
).toEqual('12am')
183+
})
184+
})
185+
186+
describe('24-hour clock', () => {
187+
it('formats hour without minutes as HH:mm (not HH format because that would look weird) ', () => {
188+
expect(
189+
formatTime(parseNaiveDate('2025-06-15 14:00:00'), {
190+
use12HourClock: false,
191+
includeMinutes: false
192+
})
193+
).toEqual('14:00')
194+
})
195+
196+
it('formats hour with minutes as HH:mm', () => {
197+
expect(
198+
formatTime(parseNaiveDate('2025-06-15 14:30:00'), {
199+
use12HourClock: false,
200+
includeMinutes: true
201+
})
202+
).toEqual('14:30')
203+
})
204+
205+
it('pads single-digit hours', () => {
206+
expect(
207+
formatTime(parseNaiveDate('2025-06-15 09:00:00'), {
208+
use12HourClock: false,
209+
includeMinutes: false
210+
})
211+
).toEqual('09:00')
212+
})
213+
})
214+
})

0 commit comments

Comments
 (0)