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,12 @@ export const timeseriesChartSchema = {
stacked: {
type: 'boolean',
},
threshold: {
type: 'object',
additionalProperties: {
type: 'number',
},
},
chartDatasetColors: chartDatasetColorsSchema,
syntheticsDataKey,
chartTitle,
Expand Down
Loading