diff --git a/timeserieschart/schemas/time-series.cue b/timeserieschart/schemas/time-series.cue index 80bdc7c38..e5b076e42 100644 --- a/timeserieschart/schemas/time-series.cue +++ b/timeserieschart/schemas/time-series.cue @@ -66,6 +66,7 @@ spec: close({ lineStyle?: #lineStyle areaOpacity?: #areaOpacity format?: common.#format + stack?: bool }] #lineStyle: "solid" | "dashed" | "dotted" diff --git a/timeserieschart/sdk/go/time-series.go b/timeserieschart/sdk/go/time-series.go index 3f63ba1c9..b68cb34b7 100644 --- a/timeserieschart/sdk/go/time-series.go +++ b/timeserieschart/sdk/go/time-series.go @@ -127,6 +127,7 @@ type QuerySettingsItem struct { LineStyle string `json:"lineStyle,omitempty" yaml:"lineStyle,omitempty"` AreaOpacity float64 `json:"areaOpacity,omitempty" yaml:"areaOpacity,omitempty"` Format *common.Format `json:"format,omitempty" yaml:"format,omitempty"` + Stack *bool `json:"stack,omitempty" yaml:"stack,omitempty"` } type Option func(plugin *Builder) error diff --git a/timeserieschart/src/QuerySettingsEditor.tsx b/timeserieschart/src/QuerySettingsEditor.tsx index 9de8b8d3a..8e86da7b9 100644 --- a/timeserieschart/src/QuerySettingsEditor.tsx +++ b/timeserieschart/src/QuerySettingsEditor.tsx @@ -19,6 +19,7 @@ import { MenuItem, Slider, Stack, + Switch, TextField, ToggleButton, ToggleButtonGroup, @@ -217,6 +218,24 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R }); }; + const addStack = (i: number): void => { + updateQuerySettings(i, (qs) => { + qs.stack = false; + }); + }; + + const removeStack = (i: number): void => { + updateQuerySettings(i, (qs) => { + qs.stack = undefined; + }); + }; + + const handleStackChange = (i: number, checked: boolean): void => { + updateQuerySettings(i, (qs) => { + qs.stack = checked; + }); + }; + const handleFormatChange = (i: number, format?: FormatOptions): void => { updateQuerySettings(i, (qs) => { qs.format = format; @@ -287,6 +306,9 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R onAddFormat={() => addFormat(i)} onRemoveFormat={() => removeFormat(i)} onFormatChange={(format) => handleFormatChange(i, format)} + onAddStack={() => addStack(i)} + onRemoveStack={() => removeStack(i)} + onStackChange={(checked) => handleStackChange(i, checked)} /> )) )} @@ -319,10 +341,13 @@ interface QuerySettingsInputProps { onAddFormat: () => void; onRemoveFormat: () => void; onFormatChange: (format?: FormatOptions) => void; + onAddStack: () => void; + onRemoveStack: () => void; + onStackChange: (checked: boolean) => void; } function QuerySettingsInput({ - querySettings: { queryIndex, colorMode, colorValue, lineStyle, areaOpacity, format }, + querySettings: { queryIndex, colorMode, colorValue, lineStyle, areaOpacity, format, stack }, availableQueryIndexes, onQueryIndexChange, onColorModeChange, @@ -340,6 +365,9 @@ function QuerySettingsInput({ onAddFormat, onRemoveFormat, onFormatChange, + onAddStack, + onRemoveStack, + onStackChange, }: QuerySettingsInputProps): ReactElement { // current query index should also be selectable const selectableQueryIndexes = availableQueryIndexes.concat(queryIndex).sort((a, b) => a - b); @@ -354,8 +382,9 @@ function QuerySettingsInput({ if (!lineStyle) options.push({ key: 'lineStyle', label: 'Line Style', action: onAddLineStyle }); if (areaOpacity === undefined) options.push({ key: 'opacity', label: 'Opacity', action: onAddAreaOpacity }); if (format === undefined) options.push({ key: 'format', label: 'Format', action: onAddFormat }); + if (stack === undefined) options.push({ key: 'stack', label: 'Stack', action: onAddStack }); return options; - }, [colorMode, lineStyle, areaOpacity, format, onAddColor, onAddLineStyle, onAddAreaOpacity, onAddFormat]); + }, [colorMode, lineStyle, areaOpacity, format, stack, onAddColor, onAddLineStyle, onAddAreaOpacity, onAddFormat, onAddStack]); const handleAddMenuClick = (event: React.MouseEvent): void => { if (availableOptions.length === 1 && availableOptions[0]) { @@ -472,6 +501,18 @@ function QuerySettingsInput({ )} + {/* Stack section */} + {stack !== undefined && ( + + onStackChange(e.target.checked)} + slotProps={{ input: { 'aria-label': 'stack override' } }} + /> + + + )} + {/* Add Options Button - only show if there are available options */} {availableOptions.length > 0 && ( <> diff --git a/timeserieschart/src/TimeSeriesChartBase.tsx b/timeserieschart/src/TimeSeriesChartBase.tsx index b1f759a23..58cdf1129 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -37,6 +37,7 @@ import { TooltipComponent, } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; +import { getCommonTimeScale } from '@perses-dev/core'; import { ChartInstance, ChartInstanceFocusOpts, @@ -48,7 +49,6 @@ import { enableDataZoom, FormatOptions, getClosestTimestamp, - getCommonTimeScale, getFormattedAxis, getPointInGrid, OnEventsType, diff --git a/timeserieschart/src/TimeSeriesChartPanel.tsx b/timeserieschart/src/TimeSeriesChartPanel.tsx index e3e6b511c..90e6d939b 100644 --- a/timeserieschart/src/TimeSeriesChartPanel.tsx +++ b/timeserieschart/src/TimeSeriesChartPanel.tsx @@ -42,8 +42,8 @@ import { DEFAULT_LEGEND, StepOptions, formatValue, - getTimeSeriesValues, } from '@perses-dev/components'; +import { getTimeSeriesValues } from '@perses-dev/core'; import { TimeSeries, TimeSeriesData, TimeSeriesValueTuple } from '@perses-dev/spec'; import { TimeSeriesChartOptions, diff --git a/timeserieschart/src/VisualOptionsEditor.tsx b/timeserieschart/src/VisualOptionsEditor.tsx index 5026b48df..6bab71caa 100644 --- a/timeserieschart/src/VisualOptionsEditor.tsx +++ b/timeserieschart/src/VisualOptionsEditor.tsx @@ -74,7 +74,7 @@ export function VisualOptionsEditor({ value, onChange }: VisualOptionsEditorProp onChange={(__, newValue) => { const updatedValue: TimeSeriesChartVisualOptions = { ...value, - stack: newValue.id === 'none' ? undefined : newValue.id, // stack is optional so remove property when 'None' is selected + stack: newValue.id === 'none' ? undefined : newValue.id, }; // stacked area chart preset to automatically set area under a curve shading if (newValue.id === 'all' && !value.areaOpacity) { diff --git a/timeserieschart/src/time-series-chart-model.ts b/timeserieschart/src/time-series-chart-model.ts index d0458b541..ca221a626 100644 --- a/timeserieschart/src/time-series-chart-model.ts +++ b/timeserieschart/src/time-series-chart-model.ts @@ -46,6 +46,7 @@ export interface QuerySettingsOptions { lineStyle?: LineStyleType; areaOpacity?: number; format?: FormatOptions; + stack?: boolean; } export type TimeSeriesChartOptionsEditorProps = OptionsEditorProps; diff --git a/timeserieschart/src/utils/data-transform.ts b/timeserieschart/src/utils/data-transform.ts index 4048737aa..a2450fc44 100644 --- a/timeserieschart/src/utils/data-transform.ts +++ b/timeserieschart/src/utils/data-transform.ts @@ -13,6 +13,7 @@ import type { YAXisComponentOption } from 'echarts'; import { LineSeriesOption, BarSeriesOption } from 'echarts/charts'; +import { getCommonTimeScale } from '@perses-dev/core'; import { OPTIMIZED_MODE_SERIES_LIMIT, LegacyTimeSeries, @@ -20,7 +21,6 @@ import { EChartsValues, TimeSeriesOption, StepOptions, - getCommonTimeScale, } from '@perses-dev/components'; import { useTimeSeriesQueries, PanelData } from '@perses-dev/plugin-system'; import { TimeScale, TimeSeries, TimeSeriesData, TimeSeriesValueTuple } from '@perses-dev/spec'; @@ -69,11 +69,13 @@ export function getTimeSeries( visual: TimeSeriesChartVisualOptions, timeScale: TimeScale, paletteColor: string, - querySettings?: { lineStyle?: LineStyleType; areaOpacity?: number }, + querySettings?: { lineStyle?: LineStyleType; areaOpacity?: number; stack?: boolean }, yAxisIndex?: number ): TimeSeriesOption { const lineWidth = visual.lineWidth ?? DEFAULT_LINE_WIDTH; const pointRadius = visual.pointRadius ?? DEFAULT_POINT_RADIUS; + const shouldStack = + querySettings?.stack !== undefined ? querySettings.stack : visual.stack === 'all'; // Shows datapoint symbols when selected time range is roughly 15 minutes or less const minuteMs = 60000; @@ -90,7 +92,7 @@ export function getTimeSeries( datasetIndex, name: formattedName, color: paletteColor, - stack: visual.stack === 'all' ? visual.stack : undefined, + stack: shouldStack ? 'all' : undefined, yAxisIndex: yAxisIndex, label: { show: false, @@ -106,7 +108,7 @@ export function getTimeSeries( name: formattedName, connectNulls: visual.connectNulls ?? DEFAULT_CONNECT_NULLS, color: paletteColor, - stack: visual.stack === 'all' ? visual.stack : undefined, + stack: shouldStack ? 'all' : undefined, yAxisIndex: yAxisIndex, sampling: 'lttb', progressiveThreshold: OPTIMIZED_MODE_SERIES_LIMIT, // https://echarts.apache.org/en/option.html#series-lines.progressiveThreshold