Add year to X axis of multi-year graph (#2607)

* Add year to X axis of multi-year graph

* Remove hardcoded condition for rendering year string, render if multiple years present in graph view

* Apply new argument contract to dateFormatter usage in graph-util.js

* Better defensive runtime type guard when deriving hasMultipleYears

* Remove console log

* Remove unrelated JSDoc change

---------

Co-authored-by: Vini Brasil <vini@hey.com>
This commit is contained in:
Aviral 2023-02-14 12:54:22 +01:00 committed by GitHub
parent 0026487fb5
commit 0db52ff977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 26 deletions

View File

@ -1,18 +1,21 @@
import {parseUTCDate, formatMonthYYYY, formatDay, formatDayShort} from '../../util/date' import { parseUTCDate, formatMonthYYYY, formatDay, formatDayShort } from '../../util/date'
const browserDateFormat = Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }) const browserDateFormat = Intl.DateTimeFormat(navigator.language, { hour: 'numeric' })
const is12HourClock = function() { const is12HourClock = function () {
return browserDateFormat.resolvedOptions().hour12 return browserDateFormat.resolvedOptions().hour12
} }
const parseISODate = function(isoDate) { const parseISODate = function (isoDate) {
const date = parseUTCDate(isoDate) const date = parseUTCDate(isoDate)
const minutes = date.getMinutes(); const minutes = date.getMinutes();
return { date, minutes } const year = date.getFullYear()
return { date, minutes, year }
} }
const formatHours = function(isoDate) { const getYearString = (options, year) => options.shouldShowYear ? ` ${year}` : ''
const formatHours = function (isoDate) {
const monthIndex = 1 const monthIndex = 1
const dateParts = isoDate.split(/[^0-9]/); const dateParts = isoDate.split(/[^0-9]/);
dateParts[monthIndex] = dateParts[monthIndex] - 1 dateParts[monthIndex] = dateParts[monthIndex] - 1
@ -37,9 +40,9 @@ const weekIntervalFormatter = {
const formatted = this.short(isoDate, options) const formatted = this.short(isoDate, options)
return options.isBucketPartial ? `Partial week of ${formatted}` : `Week of ${formatted}` return options.isBucketPartial ? `Partial week of ${formatted}` : `Week of ${formatted}`
}, },
short(isoDate, _options) { short(isoDate, options) {
const { date } = parseISODate(isoDate) const { date, year } = parseISODate(isoDate)
return formatDayShort(date) return `${formatDayShort(date)}${getYearString(options, year)}`
} }
} }
@ -48,9 +51,9 @@ const dateIntervalFormatter = {
const { date } = parseISODate(isoDate) const { date } = parseISODate(isoDate)
return formatDay(date) return formatDay(date)
}, },
short(isoDate, _options) { short(isoDate, options) {
const { date } = parseISODate(isoDate) const { date, year } = parseISODate(isoDate)
return formatDayShort(date) return `${formatDayShort(date)}${getYearString(options, year)}`
} }
} }
@ -60,7 +63,7 @@ const hourIntervalFormatter = {
}, },
short(isoDate, _options) { short(isoDate, _options) {
const formatted = formatHours(isoDate) const formatted = formatHours(isoDate)
if (is12HourClock()) { if (is12HourClock()) {
return formatted.replace(' ', '').toLowerCase() return formatted.replace(' ', '').toLowerCase()
} else { } else {
@ -109,19 +112,22 @@ const factory = {
* The preferred date and time format in the dashboard depends on the selected * The preferred date and time format in the dashboard depends on the selected
* interval and period. For example, in real-time view only the time is necessary, * interval and period. For example, in real-time view only the time is necessary,
* while other intervals require dates to be displayed. * while other intervals require dates to be displayed.
* @param {Object} config - Configuration object for determining formatter.
* *
* @param {string} interval - The interval of the query, e.g. `minute`, `hour` * @param {string} config.interval - The interval of the query, e.g. `minute`, `hour`
* @param {boolean} longForm - Whether the formatted result should be in long or * @param {boolean} config.longForm - Whether the formatted result should be in long or
* short form. * short form.
* @param {string} period - The period of the query, e.g. `12mo`, `day` * @param {string} config.period - The period of the query, e.g. `12mo`, `day`
* @param {boolean} isPeriodFull - Indicates whether the interval has been cut * @param {boolean} config.isPeriodFull - Indicates whether the interval has been cut
* off by the requested date range or not. If false, the returned formatted date * off by the requested date range or not. If false, the returned formatted date
* indicates this cut off, e.g. `Partial week of November 8`. * indicates this cut off, e.g. `Partial week of November 8`.
* @param {boolean} config.shouldShowYear - Should the year be appended to the date?
* Defaults to false. Rendering year string is a newer opt-in feature to be enabled where needed.
*/ */
export default function dateFormatter(interval, longForm, period, isPeriodFull) { export default function dateFormatter({ interval, longForm, period, isPeriodFull, shouldShowYear = false }) {
const displayMode = longForm ? 'long' : 'short' const displayMode = longForm ? 'long' : 'short'
const options = { period: period, interval: interval, isBucketPartial: !isPeriodFull } const options = { period: period, interval: interval, isBucketPartial: !isPeriodFull, shouldShowYear }
return function(isoDate, _index, _ticks) { return function (isoDate, _index, _ticks) {
return factory[interval][displayMode](isoDate, options) return factory[interval][displayMode](isoDate, options)
} }
} }

View File

@ -39,14 +39,18 @@ const renderBucketLabel = function(query, graphData, label, comparison = false)
let isPeriodFull = graphData.full_intervals?.[label] let isPeriodFull = graphData.full_intervals?.[label]
if (comparison) isPeriodFull = true if (comparison) isPeriodFull = true
const formattedLabel = dateFormatter(graphData.interval, true, query.period, isPeriodFull)(label) const formattedLabel = dateFormatter({
interval: graphData.interval, longForm: true, period: query.period, isPeriodFull,
})(label)
if (query.period === 'realtime') { if (query.period === 'realtime') {
return dateFormatter(graphData.interval, true, query.period)(label) return dateFormatter({
interval: graphData.interval, longForm: true, period: query.period,
})(label)
} }
if (graphData.interval === 'hour' || graphData.interval == 'minute') { if (graphData.interval === 'hour' || graphData.interval == 'minute') {
const date = dateFormatter("date", true, query.period)(label) const date = dateFormatter({ interval: "date", longForm: true, period: query.period })(label)
return `${date}, ${formattedLabel}` return `${date}, ${formattedLabel}`
} }

View File

@ -89,9 +89,29 @@ class LineGraph extends React.Component {
ticks: { ticks: {
maxTicksLimit: 8, maxTicksLimit: 8,
callback: function (val, _index, _ticks) { callback: function (val, _index, _ticks) {
// realtime graph labels are not date strings
const hasMultipleYears = typeof graphData.labels[0] !== 'string' ? false :
graphData.labels
// date format: 'yyyy-mm-dd'; maps to -> 'yyyy'
.map(date => date.split('-')[0])
// reject any year that appears at a previous index, unique years only
.filter((value, index, list) => list.indexOf(value) === index)
.length > 1
if (graphData.interval === 'hour' && query.period !== 'day') { if (graphData.interval === 'hour' && query.period !== 'day') {
const date = dateFormatter("date", false, query.period)(this.getLabelForValue(val)) const date = dateFormatter({
const hour = dateFormatter(graphData.interval, false, query.period)(this.getLabelForValue(val)) interval: "date",
longForm: false,
period: query.period,
shouldShowYear: hasMultipleYears,
})(this.getLabelForValue(val))
const hour = dateFormatter({
interval: graphData.interval,
longForm: false,
period: query.period,
shouldShowYear: hasMultipleYears,
})(this.getLabelForValue(val))
// Returns a combination of date and hour. This is because // Returns a combination of date and hour. This is because
// small intervals like hour may return multiple days // small intervals like hour may return multiple days
@ -100,10 +120,14 @@ class LineGraph extends React.Component {
} }
if (graphData.interval === 'minute' && query.period !== 'realtime') { if (graphData.interval === 'minute' && query.period !== 'realtime') {
return dateFormatter("hour", false, query.period)(this.getLabelForValue(val)) return dateFormatter({
interval: "hour", longForm: false, period: query.period,
})(this.getLabelForValue(val))
} }
return dateFormatter(graphData.interval, false, query.period)(this.getLabelForValue(val)) return dateFormatter({
interval: graphData.interval, longForm: false, period: query.period, shouldShowYear: hasMultipleYears,
})(this.getLabelForValue(val))
}, },
color: this.props.darkTheme ? 'rgb(243, 244, 246)' : undefined color: this.props.darkTheme ? 'rgb(243, 244, 246)' : undefined
} }