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: add threshold line to timeseries charts [MA-3442] #1825

Merged
merged 8 commits into from
Dec 4, 2024
2 changes: 2 additions & 0 deletions packages/analytics/analytics-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ yarn add @kong-ui-public/analytics-chart
- `stacked` option applies to timeseries charts as well as vertical/horizontal bar charts.
- `fill` only applies to time series line chart
- `chartTypes` defined [here](https://github.com/Kong/public-ui-components/blob/main/packages/analytics/analytics-utilities/src/types/chart-types.ts)
- `threshold` is optional
- A key / value pair of type `Record<ExploreAggregations: number>` will draw a dotted threshold line on a timeseries chart at the provided Y-axis value.
- `chartDatasetColors` are optional
- If no colors are provided, the default color palette will be used
- If custom colors are needed you may provide a custom color palette in the form of:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
:legend-position="legendPosition"
:show-annotations="showAnnotationsToggle"
:show-legend-values="showLegendValuesToggle"
:threshold="threshold"
:timeseries-zoom="timeSeriesZoomToggle"
tooltip-title="tooltip title"
@zoom-time-range="eventLog += 'Zoomed to ' + JSON.stringify($event) + '\n'"
Expand Down Expand Up @@ -257,7 +258,7 @@ import {
CsvExportModal,
CsvExportButton,
} from '../../src'
import type { AnalyticsExploreRecord, ExploreResultV4, QueryResponseMeta } from '@kong-ui-public/analytics-utilities'
import type { AnalyticsExploreRecord, ExploreAggregations, ExploreResultV4, QueryResponseMeta } from '@kong-ui-public/analytics-utilities'
import type { AnalyticsChartColors, AnalyticsChartOptions, ChartType } from '../../src/types'
import { isValidJson, rand } from '../utils/utils'
import { lookupDatavisColor } from '../../src/utils'
Expand All @@ -266,7 +267,6 @@ import type { SandboxNavigationItem } from '@kong-ui-public/sandbox-layout'
import { generateMultipleMetricTimeSeriesData, generateSingleMetricTimeSeriesData } from '@kong-ui-public/analytics-utilities'
import CodeText from '../CodeText.vue'
import { INJECT_QUERY_PROVIDER } from '../../src/constants'
import useEvaluateFeatureFlag from '../../src/composables/useEvauluateFeatureFlag'

enum Metrics {
TotalRequests = 'TotalRequests',
Expand Down Expand Up @@ -324,6 +324,10 @@ const serviceDimensionValues = ref(new Set([
'service1', 'service2', 'service3', 'service4', 'service5',
]))

const threshold = {
'request_count': 1250,
} as Record<ExploreAggregations, number>

const exportModalVisible = ref(false)
const setModalVisibility = (val: boolean) => {
exportModalVisible.value = val
Expand Down Expand Up @@ -379,6 +383,7 @@ const analyticsChartOptions = computed<AnalyticsChartOptions>(() => {
return {
type: chartType.value,
stacked: stackToggle.value,
threshold,
// chartDatasetColors: colorPalette.value,
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
:metric-unit="computedMetricUnit"
:stacked="chartOptions.stacked"
:synthetics-data-key="syntheticsDataKey"
:threshold="threshold"
:time-range-ms="timeRangeMs"
:tooltip-title="tooltipTitle"
:type="(chartOptions.type as ('timeseries_line' | 'timeseries_bar'))"
Expand Down Expand Up @@ -183,6 +184,11 @@ const props = defineProps({
required: false,
default: true,
},
threshold: {
type: Object as PropType<Record<ExploreAggregations, number>>,
required: false,
default: undefined,
},
timeseriesZoom: {
type: Boolean,
required: false,
Expand All @@ -207,6 +213,7 @@ const computedChartData = computed(() => {
{
fill: props.chartOptions.stacked,
colorPalette: props.chartOptions.chartDatasetColors || defaultStatusCodeColors,
threshold: props.chartOptions.threshold || undefined,
},
toRef(props, 'chartData'),
).value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const htmlLegendPlugin = {
// @ts-ignore - ChartJS types are incomplete
legendItems.value = chart.options.plugins.legend.labels.generateLabels(chart)
.map(e => ({ ...e, value: props.legendValues && props.legendValues[e.text] }))
.filter(e => !e.value.isThreshold)
.sort(props.chartLegendSortFn)
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export default function useChartLegendValues(chartData: Ref<KChartData>, chartTy
})) || `${approxNum(raw, { capital: true, ...(metricUnit.value === 'usd' && { prefix: '$' }) })} ${metricUnit.value}`
}

return { ...a, [v.label as string]: { raw, formatted } }
return {
...a,
[v.label as string]: {
raw,
formatted,
isThreshold: v.isThreshold,
},
}
}, {})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { ExploreResultV4, AnalyticsExploreRecord } from '@kong-ui-public/analytics-utilities'
import type { ExploreAggregations, ExploreResultV4, AnalyticsExploreRecord } from '@kong-ui-public/analytics-utilities'
import { defaultLineOptions, lookupDatavisColor, datavisPalette, BORDER_WIDTH, NO_BORDER } from '../utils'
import type { Ref } from 'vue'
import { computed } from 'vue'
import type { Dataset, KChartData, ExploreToDatasetDeps, DatasetLabel } from '../types'
import { parseISO } from 'date-fns'
import { isNullOrUndef } from 'chart.js/helpers'
import composables from '../composables'
import { KUI_COLOR_BACKGROUND_NEUTRAL } from '@kong/design-tokens'

type MetricThreshold = Record<ExploreAggregations, number>

const range = (start: number, stop: number, step: number = 1): number[] =>
Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step)
Expand Down Expand Up @@ -180,6 +183,30 @@ export default function useExploreResultToTimeDataset(
// sort by total, descending
datasets.sort((a, b) => (Number(a.total) < Number(b.total) ? -1 : 1))

// Draw threshold lines, if any
if (deps.threshold) {
for (const key of Object.keys(deps.threshold)) {
const thresholdValue = deps.threshold[key as keyof MetricThreshold]

if (thresholdValue) {
datasets.push({
type: 'line',
rawMetric: key,
isThreshold: true,
label: i18n.t('chartLabels.threshold'),
borderColor: KUI_COLOR_BACKGROUND_NEUTRAL,
borderWidth: 1.25,
borderDash: [12, 8],
fill: false,
order: -1, // Display above all other datasets
stack: 'custom', // Never stack this dataset
data: zeroFilledTimeSeries.map(ts => {
return { x: ts, y: thresholdValue }
}),
} as Dataset)
}
}
}
return {
datasets,
colorMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,4 +726,56 @@ describe('useVitalsExploreDatasets', () => {
expect(result.value.datasets[3].backgroundColor).toEqual('#ffd5b1')
expect(result.value.datasets[4].backgroundColor).toEqual('#ffb6b6')
})

it('displays a static threshold line on timeseries charts', () => {
const exploreResult: ComputedRef<ExploreResultV4> = computed(() => ({
data: [
{
timestamp: START_FOR_DAILY_QUERY.toISOString(),
event: {
metric1: 2,
metric2: 1,
},
} as GroupByResult,
{
timestamp: END_FOR_DAILY_QUERY.toISOString(),
event: {
metric1: 2,
metric2: 1,
},
} as GroupByResult,
],
meta: {
start_ms: Math.trunc(START_FOR_DAILY_QUERY.getTime()),
end_ms: Math.trunc(END_FOR_DAILY_QUERY.getTime()),
granularity_ms: 86400000,
metric_names: ['metric1', 'metric2'] as any as ExploreAggregations[],
display: {},
query_id: '',
metric_units: { metric1: 'units' } as MetricUnit,
},
}))

const result = useExploreResultToTimeDataset(
{
fill: false,
threshold: { 'request_count': 320 } as Record<ExploreAggregations, number>,
},
exploreResult,
)

expect(result.value.datasets[2].label).toEqual('Alert threshold')
expect(result.value.datasets[2].data).toEqual(
[
{
x: START_FOR_DAILY_QUERY.getTime(),
y: 320,
},
{
x: END_FOR_DAILY_QUERY.getTime(),
y: 320,
},
],
)
})
})
3 changes: 2 additions & 1 deletion packages/analytics/analytics-chart/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@
"cost": "Costs",
"llm_cache_embeddings_latency_average": "Embeddings Latency (avg)",
"llm_cache_fetch_latency_average": "Fetch Latency (avg)",
"llm_latency_average": "LLM Latency (avg)"
"llm_latency_average": "LLM Latency (avg)",
"threshold": "Alert threshold"
},
"metricAxisTitles": {
"request_count": "Request Count",
Expand Down
16 changes: 14 additions & 2 deletions packages/analytics/analytics-chart/src/types/chart-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import type { ChartData, ChartDataset, LegendItem } from 'chart.js'
import type { ChartMetricDisplay } from '../enums'
import type { ChartTooltipSortFn } from './chartjs-options'
import type { ChartType, SimpleChartType } from './chart-types'
import type { ExploreAggregations } from '@kong-ui-public/analytics-utilities'

// Chart.js extendend interfaces
export type Dataset = ChartDataset & { rawDimension: string, rawMetric?: string, total?: number, lineTension?: number, fill?: boolean }
export type Dataset = ChartDataset & { rawDimension: string,
rawMetric?: string,
total?: number,
lineTension?: number,
fill?: boolean,
isThreshold?: boolean
}

export interface KChartData extends ChartData {
datasets: Dataset[]
Expand All @@ -31,7 +38,8 @@ export interface AnalyticsChartColors {

export interface LegendValueEntry {
raw: number,
formatted: string
formatted: string,
isThreshold?: boolean,
}

/**
Expand Down Expand Up @@ -83,6 +91,10 @@ export interface AnalyticsChartOptions {
* Sort tooltip entries
*/
chartTooltipSortFn?: ChartTooltipSortFn,
/**
* A static or dynamic metric threshold to be displayed on a timeseries chart
*/
threshold?: Record<ExploreAggregations, number>,
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AnalyticsChartColors } from './chart-data'
import type { ExploreAggregations } from '@kong-ui-public/analytics-utilities'

/**
* Interace representing the various options
Expand All @@ -13,4 +14,5 @@ import type { AnalyticsChartColors } from './chart-data'
export interface ExploreToDatasetDeps {
colorPalette?: AnalyticsChartColors | string[]
fill?: boolean
threshold?: Record<ExploreAggregations, number>
}
6 changes: 6 additions & 0 deletions packages/analytics/analytics-utilities/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export function formatTime(ts: number, options: TimeFormatOptions = {}) {
}
}

/**
* Formatted display for a start and end time range
* @param start Date from
* @param end Date to
* @returns Human-readable date range string
*/
export function formatTimeRange(start: Date, end: Date) {
return `${formatTime(start.getTime())} - ${formatTime(end.getTime(), { includeTZ: true })}`
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type { DashboardConfig, DashboardRendererContext, TileConfig, TileDefinit
import { DashboardRenderer } from '../../src'
import { inject, ref } from 'vue'
import { ChartMetricDisplay } from '@kong-ui-public/analytics-chart'
import type { ExploreAggregations } from '@kong-ui-public/analytics-utilities'
import type { SandboxNavigationItem } from '@kong-ui-public/sandbox-layout'
import { SandboxLayout } from '@kong-ui-public/sandbox-layout'
import '@kong-ui-public/sandbox-layout/dist/style.css'
Expand Down Expand Up @@ -164,6 +165,9 @@ const dashboardConfig: DashboardConfig = {
chart: {
type: 'timeseries_line',
chartTitle: 'Timeseries line chart of mock data',
threshold: {
'request_count': 3200,
} as Record<ExploreAggregations, number>,
},
query: {
datasource: 'basic',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const options = computed((): AnalyticsChartOptions => ({
type: props.chartOptions.type,
stacked: props.chartOptions.stacked ?? false,
chartDatasetColors: props.chartOptions.chartDatasetColors,
threshold: props.chartOptions.threshold,
}))

const exploreLink = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ export const timeseriesChartSchema = {
stacked: {
type: 'boolean',
},
threshold: {
type: 'object',
properties: Object.fromEntries(
exploreAggregations.map((key) => [key, { type: 'number' }]),
Copy link
Contributor

Choose a reason for hiding this comment

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

This sort of dynamic definition is going to prevent TS inference from working. I don't have context on what this is trying to do; at the least trying to make this dynamic will be a journey with the way the current dashboard types are set up. Maybe just splat it out for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what it yields:

Screenshot 2024-12-04 at 08 44 04

I assume it's preferable to dynamically build the allowed keys rather than spelling out each one, and having to maintain this list separate from exploreAggregations.

We can simply make it {type: string}, {type: number}, but that seems too imprecise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dropped the properties and just ended up doing

additionalProperties: {
      type: 'number',
 },

),
required: exploreAggregations,
mihai-peteu marked this conversation as resolved.
Show resolved Hide resolved
additionalProperties: false,
},
chartDatasetColors: chartDatasetColorsSchema,
syntheticsDataKey,
chartTitle,
Expand Down
Loading