Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new granularities when determining x-axis timestamp format #1848

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some housekeeping... revisited this old code... thought I'd improve this explanation a bit

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')
})
})
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'
Loading