Merge pull request #106 from buster-so/staging

fix(visualization): add x axis time unit.
This commit is contained in:
dal 2025-02-06 11:59:45 -08:00 committed by GitHub
commit 80f82856b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 233 additions and 20 deletions

View File

@ -367,7 +367,7 @@ pub async fn modify_visualization_agent(
let (
format_labels_result,
configure_charts_result,
global_styling_result,
mut global_styling_result,
stylize_columns_result,
) = tokio::join!(
async {
@ -400,6 +400,29 @@ pub async fn modify_visualization_agent(
}
);
let time_unit = match configure_charts_result.clone() {
Some(mut result) => {
// Try to get and remove from bar_line_chart first
if let Some(Value::String(time_unit)) = result.bar_line_chart.as_object_mut().and_then(|obj| obj.remove("x_axis_time_unit")) {
time_unit
} else {
// If not found in bar_line_chart, try combo_chart
if let Some(Value::String(time_unit)) = result.combo_chart.as_object_mut().and_then(|obj| obj.remove("x_axis_time_unit")) {
time_unit
} else {
String::new()
}
}
},
None => String::new(),
};
if !time_unit.is_empty() {
global_styling_result = Some(json!({
"xAxisTimeInterval": time_unit
}));
}
// Transform format_labels_result into columnLabelFormats
let column_label_formats = match format_labels_result {
Some(result) => result,

View File

@ -1,4 +1,6 @@
pub fn bar_line_chart_system_prompt() -> String {
// time_unit: 'day' | 'week' | 'month' | 'quarter' | 'year' | null; //OPTIONAL: default is null. This will only apply if the x axis is a date column. This will convert the date to the specified time unit.
String::from(
r#" ## TYPESCRIPT CONFIGS
@ -8,6 +10,7 @@ pub fn bar_line_chart_system_prompt() -> String {
type BarAndLineAxis = {
x: string[]; //the column names to use for the x axis. If multiple columns are provided, they will be grouped together and summed. The LLM should NEVER set multiple x axis columns. Only the user can set this.
y: string[]; //the column names to use for the y axis. These columns MUST be numerical column type and should NEVER be ID columns. If there is not matching, then this can be empty. If multiple columns are provided, they will be grouped together and summed. The LLM should NEVER set multiple y axis columns. Only the user can set this.
x_axis_time_unit?: 'day' | 'week' | 'month' | 'quarter' | 'year' | null; // OPTIONAL: default is null if not selected. If the sql query is grouping by a specific interval, you MUST match the interval to the x_axis_time_unit.
category?: string[]; // OPTIONAL: the column names to use for the category axis. This is optional and should only be used if the user needs to group or stack their data. This should likely not be the same as the x axis column. The category is ALMOST ALWAYS a string type column.
tooltip?: string[] | null; //OPTIONAL: if null the y axis will automatically be used
};

View File

@ -9,6 +9,7 @@ type ComboChartAxis = {
x: string[]; //the column names to use for the x axis. If multiple columns are provided, they will be grouped together and summed. The LLM should NEVER set multiple x axis columns. Only the user can set this.
y: string[]; //the column names to use for the y axis. These columns MUST be numerical column type and should NEVER be ID columns. If there is not matching, then this can be empty. If multiple columns are provided, they will be grouped together and summed. The LLM should NEVER set multiple y axis columns. Only the user can set this.
y2?: string[]; //the column names to use for the second y axis. These columns MUST be numerical column type and should NEVER be ID columns. The y2 axis is often called the "right axes". The y2 is also what makes this a dual axes chart.
x_axis_time_unit?: 'day' | 'week' | 'month' | 'quarter' | 'year' | null; // OPTIONAL: default is null if not selected. If the sql query is grouping by a specific interval, you MUST match the interval to the x_axis_time_unit.
category?: string[] | null; //the column names to use for the category axis. This is optional and should only be used if the user needs to group or stack their data. This should likely not be the same as the x axis column. The category is ALMOST ALWAYS a string type column.
tooltip?: string[] | null; //if null the y axis will automatically be used
};

View File

@ -22,7 +22,7 @@ type BusterChartLabelFormatCurrency = {
} & ColumnLabelFormatBase;
type BusterChartLabelFormatDate = {
dateFormat?: 'auto' | string; //OPTIONAL: default is 'LL'. This will only apply if the format is set to 'date'. This will convert the date to the specified format. This MUST BE IN dayjs format. If you determine that a column type is a date column, you should specify it's date format here.
dateFormat?: 'auto' | string; //OPTIONAL: The default to 'auto'. Only specify the day.js string format if the user asks for it.
useRelativeTime?: boolean;
isUTC?: boolean;
// This is useful if a date column is actually returned as a number. For example, if the column is the day of the week (1-7) or month of the year (1-12), then you should set this to 'day_of_week' or 'month_of_year'.

View File

@ -19,6 +19,7 @@ export const DEFAULT_CHART_CONFIG: IBusterThreadMessageChartConfig = {
y2AxisShowAxisTitle: true,
y2AxisStartAxisAtZero: true,
y2AxisScaleType: 'linear',
xAxisTimeInterval: null,
xAxisShowAxisLabel: true,
xAxisShowAxisTitle: true,
xAxisAxisTitle: null,

View File

@ -1,4 +1,3 @@
import { IBusterThreadMessage } from '@/context/Threads/interfaces';
import React, { useMemo } from 'react';
import { DropZone, SelectAxisDropzones, SelectAxisItem } from './SelectAxisDragContainer';
import { IBusterThreadMessageChartConfig } from '@/api/buster_rest';
@ -19,7 +18,7 @@ import { SelectAxisEmptyState } from './SelectAxisEmptyState';
export const SelectAxis: React.FC<
Required<YAxisConfig> &
Required<XAxisConfig> &
Required<Omit<XAxisConfig, 'xAxisTimeInterval'>> &
Required<CategoryAxisStyleConfig> &
Required<Y2AxisConfig> &
ISelectAxisContext

View File

@ -17,7 +17,7 @@ import React from 'react';
export interface ISelectAxisContext
extends Required<YAxisConfig>,
Required<Y2AxisConfig>,
Required<XAxisConfig>,
Required<Omit<XAxisConfig, 'xAxisTimeInterval'>>,
Required<CategoryAxisStyleConfig> {
selectedAxis: ChartEncodes | null;
columnLabelFormats: IBusterThreadMessageChartConfig['columnLabelFormats'];

View File

@ -24,7 +24,6 @@ export const StylingAppVisualize: React.FC<
colors: string[];
disableTooltip: IBusterThreadMessageChartConfig['disableTooltip'];
} & Required<YAxisConfig> &
Required<XAxisConfig> &
Required<CategoryAxisStyleConfig> &
Required<Y2AxisConfig> &
Omit<ISelectAxisContext, 'selectedAxis'> &

View File

@ -0,0 +1,160 @@
'use client';
import { ColumnMetaData, DEFAULT_COLUMN_SETTINGS } from '@/api/buster_rest';
import {
BusterChart,
BusterChartProps,
ChartType,
IColumnLabelFormat,
ViewType
} from '@/components/charts';
import { faker } from '@faker-js/faker';
import { Button, Checkbox, Select, Slider } from 'antd';
import { useMemo, useState } from 'react';
const chartData = [
{ date: '2024-01-01', sales: 100 },
{ date: '2024-02-01', sales: 200 },
{ date: '2024-03-01', sales: 300 },
{ date: '2024-04-01', sales: 250 },
{ date: '2024-05-01', sales: 400 },
{ date: '2024-06-01', sales: 350 },
{ date: '2024-07-01', sales: 450 },
{ date: '2024-08-01', sales: 500 },
{ date: '2024-09-01', sales: 550 },
{ date: '2024-10-01', sales: 600 },
{ date: '2024-11-01', sales: 650 },
{ date: '2024-12-01', sales: 700 }
];
const barAndLineAxis = {
x: ['date'],
y: ['sales'],
category: []
};
const pieConfig = {
x: ['date'],
y: ['sales']
};
const scatterAxis = {
x: ['date'],
y: ['sales']
};
const comboConfig = {
x: ['date'],
y: ['sales'],
y2: []
};
const columnLabelFormats: Record<string, IColumnLabelFormat> = {
sales: {
style: 'currency',
currency: 'USD',
columnType: 'number'
// displayName: 'This is a display that is really long just for testing to make sure it works'
},
date: { style: 'date', dateFormat: undefined, columnType: 'date' }
};
const columnSettings: BusterChartProps['columnSettings'] = {
sales: {
...DEFAULT_COLUMN_SETTINGS,
// columnVisualization: 'bar',
// showDataLabels: false,
// barRoundness: 7,
// showDataLabelsAsPercentage: false,
lineSymbolSize: 3
// lineStyle: 'line',
// lineType: 'normal',
// lineWidth: 2
}
};
const columnMetadata: ColumnMetaData[] = Object.entries({
...chartData[0]
}).map(([key, value]) => ({
name: key,
min_value: 100,
max_value: 190,
unique_values: 1000,
simple_type: typeof value === 'number' ? 'number' : 'text',
type: typeof value as 'text'
}));
export default function ChartjsFixedLine() {
const [numberOfPoints, setNumberOfPoints] = useState(10);
const [useGeneratedData, setUseGeneratedData] = useState<'static' | 'generated' | 'multi-year'>(
'static'
);
const [rerenderNumber, setRerenderNumber] = useState(1);
const data = useMemo(() => {
if (useGeneratedData === 'generated') {
return Array.from({ length: numberOfPoints }, (_, index) => ({
date: faker.date.past({ years: 1 }).toISOString(),
sales: faker.number.int({ min: 590 + index, max: index + 10 + 590 })
}));
}
if (useGeneratedData === 'multi-year') {
return Array.from({ length: numberOfPoints }, (_, index) => ({
date: faker.date.past({ years: 5 }).toISOString(),
sales: faker.number.int({ min: 590 + index, max: index + 10 + 590 })
}));
}
return chartData;
}, [numberOfPoints, useGeneratedData]);
return (
<div className="flex h-[1000px] w-[75vw] flex-col rounded bg-white">
<div className="h-[500px] w-full p-3">
<BusterChart
key={rerenderNumber}
data={data}
selectedChartType={ChartType.Line}
selectedView={ViewType.Chart}
loading={false}
barAndLineAxis={barAndLineAxis}
pieChartAxis={pieConfig}
scatterAxis={scatterAxis}
comboChartAxis={comboConfig}
columnLabelFormats={columnLabelFormats}
columnSettings={columnSettings}
metricColumnId="sales"
columnMetadata={columnMetadata}
renderType="chartjs"
/>
</div>
<div className="flex w-full flex-col space-y-2 p-3">
<Select
value={useGeneratedData}
onChange={setUseGeneratedData}
options={[
{ label: 'Use 12 Month Interval Data', value: 'static' },
{ label: 'Use Multi-Year Data', value: 'multi-year' },
{ label: 'Use Random Same Year Data', value: 'generated' }
]}
/>
<div className="flex w-full items-center space-x-2">
<div>{numberOfPoints}</div>
<Slider
className="w-full"
value={numberOfPoints}
onChange={setNumberOfPoints}
min={1}
max={100}
/>
</div>
<Button onClick={() => setRerenderNumber(rerenderNumber + 1)}>
Rerender {rerenderNumber}
</Button>
</div>
</div>
);
}

View File

@ -64,6 +64,7 @@ export const BusterChartJSComponent = React.memo(
goalLines,
lineGroupType,
disableTooltip,
xAxisTimeInterval,
//TODO
xAxisDataZoom,
useRapidResizeObserver = false,
@ -158,7 +159,8 @@ export const BusterChartJSComponent = React.memo(
y2AxisStartAxisAtZero,
yAxisScaleType,
animate,
disableTooltip
disableTooltip,
xAxisTimeInterval
});
const type = useMemo(() => {

View File

@ -54,6 +54,7 @@ interface UseOptionsProps {
goalLinesAnnotations: AnnotationPluginOptions['annotations'];
trendlineAnnotations: AnnotationPluginOptions['annotations'];
disableTooltip: boolean;
xAxisTimeInterval: BusterChartProps['xAxisTimeInterval'];
}
export const useOptions = ({
@ -92,21 +93,22 @@ export const useOptions = ({
animate,
goalLinesAnnotations,
trendlineAnnotations,
disableTooltip
disableTooltip,
xAxisTimeInterval
}: UseOptionsProps) => {
const xAxis = useXAxis({
columnLabelFormats,
columnSettings,
selectedAxis,
selectedChartType,
columnMetadata,
xAxisLabelRotation,
xAxisShowAxisLabel,
gridLines,
xAxisAxisTitle,
xAxisShowAxisTitle,
lineGroupType,
barGroupType
barGroupType,
xAxisTimeInterval
});
const yAxis = useYAxis({

View File

@ -4,7 +4,8 @@ import {
ChartType,
BusterChartProps,
IColumnLabelFormat,
ComboChartAxis
ComboChartAxis,
XAxisConfig
} from '@/components/charts/interfaces';
import { useMemoizedFn } from 'ahooks';
import { useMemo } from 'react';
@ -19,7 +20,6 @@ export const useXAxis = ({
columnLabelFormats,
selectedAxis,
selectedChartType,
columnMetadata,
columnSettings,
xAxisLabelRotation,
xAxisShowAxisLabel,
@ -27,12 +27,12 @@ export const useXAxis = ({
xAxisShowAxisTitle,
gridLines,
lineGroupType,
barGroupType
barGroupType,
xAxisTimeInterval
}: {
columnLabelFormats: NonNullable<BusterChartConfigProps['columnLabelFormats']>;
selectedAxis: ChartEncodes;
selectedChartType: ChartType;
columnMetadata: NonNullable<BusterChartProps['columnMetadata']>;
xAxisLabelRotation: NonNullable<BusterChartProps['xAxisLabelRotation']>;
xAxisShowAxisLabel: NonNullable<BusterChartProps['xAxisShowAxisLabel']>;
gridLines: NonNullable<BusterChartProps['gridLines']>;
@ -41,6 +41,7 @@ export const useXAxis = ({
lineGroupType: BusterChartProps['lineGroupType'];
barGroupType: BusterChartProps['barGroupType'];
columnSettings: BusterChartProps['columnSettings'];
xAxisTimeInterval: BusterChartProps['xAxisTimeInterval'];
}): DeepPartial<ScaleChartOptions<'bar'>['scales']['x']> | undefined => {
const isScatterChart = selectedChartType === ChartType.Scatter;
const isPieChart = selectedChartType === ChartType.Pie;
@ -112,7 +113,7 @@ export const useXAxis = ({
isSupportedChartForAxisTitles: isSupportedType
});
const useTicketCallback = useMemo(() => {
const useTickCallback = useMemo(() => {
if (type === 'time') {
const isSingleXAxis = selectedAxis.x.length === 1;
const columnLabelFormat = xAxisColumnFormats[selectedAxis.x[0]];
@ -144,10 +145,24 @@ export const useXAxis = ({
};
}, [xAxisLabelRotation]);
const timeUnit = useMemo(() => {
if (type === 'time' && xAxisTimeInterval) {
const arrayOfValidTimeUnits: XAxisConfig['xAxisTimeInterval'][] = [
'day',
'week',
'month',
'quarter',
'year'
];
const isValidTimeUnit = arrayOfValidTimeUnits.includes(xAxisTimeInterval);
return isValidTimeUnit ? xAxisTimeInterval : false;
}
return false;
}, [type, xAxisTimeInterval]);
const memoizedXAxisOptions: DeepPartial<ScaleChartOptions<'bar'>['scales']['x']> | undefined =
useMemo(() => {
if (isPieChart) return undefined;
return {
type,
offset: !isScatterChart,
@ -156,13 +171,19 @@ export const useXAxis = ({
text: title
},
stacked,
time: {
unit: xAxisTimeInterval ? xAxisTimeInterval : false
},
ticks: {
...rotation,
sampleSize: type === 'time' ? 24 : undefined,
display: xAxisShowAxisLabel,
callback: useTicketCallback ? tickCallback : null,
autoSkip: true,
autoSkipPadding: 3,
align: 'center'
align: 'center',
autoSkipPadding: 5, // 17,
includeBounds: true,
callback: useTickCallback ? tickCallback : null,
source: 'auto'
},
display: true,
border: {
@ -171,10 +192,11 @@ export const useXAxis = ({
grid
} as DeepPartial<ScaleChartOptions<'bar'>['scales']['x']>;
}, [
timeUnit,
title,
isScatterChart,
isPieChart,
useTicketCallback,
useTickCallback,
xAxisShowAxisLabel,
stacked,
type,

View File

@ -16,6 +16,7 @@ export type Y2AxisConfig = {
};
export type XAxisConfig = {
xAxisTimeInterval?: 'day' | 'week' | 'month' | 'quarter' | 'year' | null; //OPTIONAL: default is null. Will only apply to combo and line charts
xAxisShowAxisLabel?: boolean; //OPTIONAL: default is true.
xAxisShowAxisTitle?: boolean; //OPTIONAL: default is true.
xAxisAxisTitle?: string | null; //OPTIONAL: default is null. If null the axis title will be a concatenation of all the x columns applied to the axis.