Skip to content

Commit

Permalink
feat: support new granularities when determining x-axis timestamp for…
Browse files Browse the repository at this point in the history
…mat (#1848)
  • Loading branch information
filipgutica authored Dec 12, 2024
1 parent fcabfa7 commit a4d46ee
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ import { Line, Bar } from 'vue-chartjs'
import composables from '../../composables'
import type { ChartLegendSortFn, ChartTooltipSortFn, EnhancedLegendItem, KChartData, LegendValues, TooltipEntry } from '../../types'
import type { GranularityValues, AbsoluteTimeRangeV4 } from '@kong-ui-public/analytics-utilities'
import { formatTime } from '@kong-ui-public/analytics-utilities'
import type { Chart, LegendItem } from 'chart.js'
import { ChartLegendPosition } from '../../enums'
import { formatByGranularity } from '../../utils'
const props = defineProps({
chartData: {
Expand Down Expand Up @@ -216,7 +216,7 @@ const { options } = composables.useLinechartOptions({
composables.useReportChartDataForSynthetics(toRef(props, 'chartData'), toRef(props, 'syntheticsDataKey'))
const formatTimestamp = (ts: number): string | number => {
return formatTime(ts, { short: ['daily', 'weekly'].includes(props.granularity) })
return formatByGranularity(new Date(ts), props.granularity, false)
}
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import type {
import {
Tooltip,
} from 'chart.js'
import { horizontalTooltipPositioning, tooltipBehavior, verticalTooltipPositioning } from '../utils'
import { millisecondsToHours } from 'date-fns'
import { formatByGranularity, horizontalTooltipPositioning, tooltipBehavior, verticalTooltipPositioning } from '../utils'
import { isNullOrUndef } from 'chart.js/helpers'
import type { ExternalTooltipContext, LineChartOptions } from '../types'
import { millisecondsToHours } from 'date-fns'

export default function useLinechartOptions(chartOptions: LineChartOptions) {

Expand All @@ -29,6 +29,10 @@ export default function useLinechartOptions(chartOptions: LineChartOptions) {
autoSkipPadding: 100,
source: 'auto',
maxRotation: 0,
callback: (value: number) => {
const tickValue = new Date(value)
return formatByGranularity(tickValue, chartOptions.granularity.value, dayBoundaryCrossed.value)
},
},
title: {
display: !isNullOrUndef(chartOptions.dimensionAxesTitle?.value),
Expand All @@ -38,6 +42,10 @@ export default function useLinechartOptions(chartOptions: LineChartOptions) {
weight: 'bold',
},
},
border: {
display: false,
},
stacked: chartOptions.stacked.value,
}))
const yAxesOptions = computed(() => ({
title: {
Expand All @@ -56,6 +64,10 @@ export default function useLinechartOptions(chartOptions: LineChartOptions) {
},
id: 'main-y-axis',
beginAtZero: true,
border: {
display: false,
},
stacked: chartOptions.stacked.value,
}))

const tooltipOptions = {
Expand Down Expand Up @@ -96,25 +108,12 @@ export default function useLinechartOptions(chartOptions: LineChartOptions) {
}
}

const xAxisGranularityUnit = computed(() => {
switch (chartOptions.granularity.value) {
case 'minutely':
return 'minute'
case 'hourly':
return 'hour'
case 'daily':
return 'day'
default:
return 'day'
}
})

const hourDisplayFormat = computed(() => {
return millisecondsToHours(Number(chartOptions.timeRangeMs.value)) >= 24 ? 'yyyy-MM-dd h:mm' : 'h:mm'
})

const dayDisplayFormat = computed(() => {
return ['daily', 'weekly'].includes(chartOptions.granularity.value) ? 'yyyy-MM-dd' : 'yyyy-MM-dd h:mm'
const dayBoundaryCrossed = computed(() => {
const timeRange = Number(chartOptions.timeRangeMs.value)
const now = new Date()
const start = new Date(now.getTime() - timeRange)
return millisecondsToHours(timeRange) > 24 || start.getDate() !== now.getDate()
})

const options = computed(() => {
Expand All @@ -135,29 +134,8 @@ export default function useLinechartOptions(chartOptions: LineChartOptions) {
easing: 'linear',
},
scales: {
x: {
border: {
display: false,
},
...xAxesOptions.value,
stacked: chartOptions.stacked.value,
time: {
tooltipFormat: 'h:mm:ss a',
unit: xAxisGranularityUnit.value,
displayFormats: {
minute: 'h:mm:ss a',
hour: hourDisplayFormat.value,
day: dayDisplayFormat.value,
},
},
},
y: {
border: {
display: false,
},
...yAxesOptions.value,
stacked: chartOptions.stacked.value,
},
x: xAxesOptions.value,
y: yAxesOptions.value,
},
responsive: true,
maintainAspectRatio: false,
Expand Down
18 changes: 7 additions & 11 deletions packages/analytics/analytics-chart/src/utils/commonOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,14 @@ export const hasMillisecondTimestamps = (chartData: KChartData) =>
(ds) => ds.data[0] && (ds.data[0] as ScatterDataPoint).x.toString().length >= 13,
)

/**
* Adjust the tooltip's horizontal position based on its width and cursor location relative to the chart center.
* This logic ensures consistent visual placement of a custom tooltip, as ChartJS offers limited direct control.
*/
export const horizontalTooltipPositioning = (position: Point, tooltipWidth: number, chartCenterX: number) => {
// We are manipulating an initial positioning logic that appears to be quite arbitrary.
// ChartJS offers limited documentation on this. The logic that follows has been tested across multiple scenarios
// and provides the most consistent visual output.
// The goal is to shift the tooltip to either the left or right in proportion to the tooltip's width,
// depending on the cursor's location relative to the chart's center.
// Additionally, we need to scale by the ratio of the tooltip width to chart width in order to
// adjust for any changes in the tooltip width.
// The original tooltip position tends to lean towards the center of the tooltip — this is one of the arbitrary aspects we are dealing with.
// It appears that the default position.x and position.y values don't consistently align with the tooltip.
// It's likely that these initial position.x and position.y values refer to the position of ChartJS' default tooltip,
// which is not visible as we're using a custom tooltip.
// Scaling factor that prevents the tooltip from shifting too far when it's wide, or too little when
// it's narrow. Ensuring that as the tooltip width changes, the horizontal offset is proportionally
// adjusted to maintain a visually balanced placement.
const withRatioScalingBase = 1150 // Found through trial and error.
const widthRatio = Math.min(tooltipWidth / withRatioScalingBase, 1) // Limit the ratio to a maximum of 1
// Define a scaling factor for when the tooltip is positioned to the right of the cursor.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest'
import { formatByGranularity } from './format-timestamps'

describe('formatByGranularity', () => {
const testDate = new Date('2024-12-10T15:30:45Z')

it('formats correctly for second-based granularities in UTC', () => {
expect(formatByGranularity(testDate, 'secondly', false, 'UTC')).toBe('3:30:45 PM')
expect(formatByGranularity(testDate, 'secondly', true, 'UTC')).toBe('2024-12-10 3:30:45 PM')
})

it('formats correctly for minute-based granularities in UTC', () => {
expect(formatByGranularity(testDate, 'minutely', false, 'UTC')).toBe('3:30 PM')
expect(formatByGranularity(testDate, 'hourly', true, 'UTC')).toBe('2024-12-10 3:30 PM')
})

it('formats correctly for twelveHourly granularity in UTC', () => {
expect(formatByGranularity(testDate, 'twelveHourly', false, 'UTC')).toBe('2024-12-10 3:30 PM')
})

it('formats correctly for daily granularity in UTC', () => {
expect(formatByGranularity(testDate, 'daily', false, 'UTC')).toBe('2024-12-10')
})

it('formats correctly for weekly granularity in UTC', () => {
expect(formatByGranularity(testDate, 'weekly', false, 'UTC')).toBe('2024 W50')
})

it('formats with default format for unknown granularities in UTC', () => {
// @ts-ignore - testing unknown granularity
expect(formatByGranularity(testDate, 'unknownGranularity', false, 'UTC')).toBe('2024-12-10 3:30:45 PM')
})

it('formats correctly for second-based granularities in America/New_York', () => {
expect(formatByGranularity(testDate, 'secondly', false, 'America/New_York')).toBe('10:30:45 AM')
expect(formatByGranularity(testDate, 'secondly', true, 'America/New_York')).toBe('2024-12-10 10:30:45 AM')
})

it('formats correctly for minute-based granularities in America/New_York', () => {
expect(formatByGranularity(testDate, 'minutely', false, 'America/New_York')).toBe('10:30 AM')
expect(formatByGranularity(testDate, 'hourly', true, 'America/New_York')).toBe('2024-12-10 10:30 AM')
})
})
31 changes: 31 additions & 0 deletions packages/analytics/analytics-chart/src/utils/format-timestamps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { GranularityValues } from '@kong-ui-public/analytics-utilities'
import { formatInTimeZone } from 'date-fns-tz'

const tz = Intl.DateTimeFormat().resolvedOptions().timeZone

export const formatByGranularity = (
tickValue: Date,
granularity: GranularityValues,
dayBoundaryCrossed: boolean,
timezone: string = tz,
) => {
if (['secondly', 'tenSecondly', 'thirtySecondly'].includes(granularity)) {
return formatInTimeZone(tickValue, timezone, dayBoundaryCrossed ? 'yyyy-MM-dd h:mm:ss a' : 'h:mm:ss a')
}
if (['minutely', 'fiveMinutely', 'tenMinutely', 'thirtyMinutely', 'hourly', 'twoHourly'].includes(granularity)) {
return formatInTimeZone(tickValue, timezone, dayBoundaryCrossed ? 'yyyy-MM-dd h:mm a' : 'h:mm a')
}
if (granularity === 'twelveHourly') {
return formatInTimeZone(tickValue, timezone, 'yyyy-MM-dd h:mm a')
}

if (granularity === 'daily') {
return formatInTimeZone(tickValue, timezone, 'yyyy-MM-dd')
}

if (granularity === 'weekly') {
return `${formatInTimeZone(tickValue, timezone, 'yyyy')} W${formatInTimeZone(tickValue, timezone, 'II')}`
}

return formatInTimeZone(tickValue, timezone, 'yyyy-MM-dd h:mm:ss a')
}
1 change: 1 addition & 0 deletions packages/analytics/analytics-chart/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './colors'
export * from './defaultLineOptions'
export * from './stackedBarUtil'
export * from './customColors'
export * from './format-timestamps'

0 comments on commit a4d46ee

Please sign in to comment.