mirror of https://github.com/buster-so/buster.git
Merge pull request #106 from buster-so/staging
fix(visualization): add x axis time unit.
This commit is contained in:
commit
80f82856b1
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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'.
|
||||
|
|
|
@ -19,6 +19,7 @@ export const DEFAULT_CHART_CONFIG: IBusterThreadMessageChartConfig = {
|
|||
y2AxisShowAxisTitle: true,
|
||||
y2AxisStartAxisAtZero: true,
|
||||
y2AxisScaleType: 'linear',
|
||||
xAxisTimeInterval: null,
|
||||
xAxisShowAxisLabel: true,
|
||||
xAxisShowAxisTitle: true,
|
||||
xAxisAxisTitle: null,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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'> &
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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(() => {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue