add default for trendline

This commit is contained in:
Nate Kelley 2025-05-13 10:25:49 -06:00
parent 756f57c5f6
commit 38ea7378d3
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 180 additions and 119 deletions

View File

@ -10,7 +10,7 @@ export interface GoalLine {
export interface Trendline {
show: boolean; //OPTIONAL: default is true. this should only be used if the user explicitly requests a trendline
showTrendlineLabel: boolean; //OPTIONAL: default is true
trendlineLabel: string | null; //OPTIONAL: if showTrendlineLabel is true, this will be the label. default is "Slope".
trendlineLabel: string | null; //OPTIONAL: if showTrendlineLabel is true, this will be the label
type:
| 'average'
| 'linear_regression'
@ -20,7 +20,7 @@ export interface Trendline {
| 'min'
| 'max'
| 'median'; //default is linear trend
trendLineColor?: string | null | 'inherit'; //OPTIONAL: default is #000000
trendLineColor?: string | null | 'inherit'; //OPTIONAL: default is #000000, inherit will inherit the color from the line/bar
columnId: string;
trendlineLabelPositionOffset?: number; //OPTIONAL: default is 0.85. Goes from 0 to 1.
projection?: boolean; //OPTIONAL: default is false. if true, the trendline will be projected to the end of the chart.

View File

@ -121,11 +121,8 @@ export const DEFAULT_COLUMN_LABEL_FORMAT: Required<ColumnLabelFormat> = {
};
export const ENABLED_DOTS_ON_LINE = 3.5;
export const DEFAULT_CHART_CONFIG_ENTRIES = Object.entries(DEFAULT_CHART_CONFIG);
export const DEFAULT_BAR_ROUNDNESS = DEFAULT_COLUMN_SETTINGS.barRoundness;
export const MIN_DONUT_WIDTH = 15;
export const DEFAULT_DAY_OF_WEEK_FORMAT = 'ddd';
@ -134,7 +131,6 @@ export const DEFAULT_DATE_FORMAT_MONTH_OF_YEAR = 'MMMM';
export const DEFAULT_DATE_FORMAT_QUARTER = 'YYYY [Q]Q';
export const ENABLED_DOTS_ON_LINE_SIZE = 4;
export const DEFAULT_COLUMN_METADATA: ColumnMetaData[] = [];
export const DEFAULT_IBUSTER_METRIC: Required<IBusterMetric> = {
@ -171,3 +167,19 @@ export const DEFAULT_IBUSTER_METRIC: Required<IBusterMetric> = {
public_password: null,
versions: []
};
export const DEFAULT_TRENDLINE_CONFIG: Required<IBusterMetricChartConfig['trendlines'][number]> = {
id: 'DEFAULT_ID',
columnId: '',
show: true,
showTrendlineLabel: false,
trendlineLabel: null,
type: 'linear_regression',
trendLineColor: '#FF0000',
trendlineLabelPositionOffset: 0,
projection: false,
lineStyle: 'solid',
polynomialOrder: 2,
aggregateAllCategories: true,
offset: 0
};

View File

@ -1,11 +1,12 @@
'use client';
import { forwardRef, useCallback, useMemo, useState } from 'react';
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { PopoverRoot, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Input } from '@/components/ui/inputs';
import { useDebounceFn } from '@/hooks';
import { cva } from 'class-variance-authority';
import { cn } from '@/lib/classMerge';
interface ColorPickerProps {
value: string | null | undefined;
@ -16,6 +17,9 @@ interface ColorPickerProps {
size?: 'default' | 'small' | 'tall';
name?: string;
className?: string;
children?: React.ReactNode;
showInput?: boolean;
showPicker?: boolean;
}
const colorPickerWrapperVariants = cva('border p-0.5 rounded cursor-pointer shadow', {
@ -32,71 +36,105 @@ const colorPickerWrapperVariants = cva('border p-0.5 rounded cursor-pointer shad
}
});
const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(
(
{
disabled,
onChangeComplete,
size = 'default',
value: valueProp = '#000000',
onChange,
name,
className = '',
...props
const ColorPicker = ({
disabled,
onChangeComplete,
size = 'default',
value: valueProp = '#000000',
onChange,
name,
className = '',
children,
showInput = true,
showPicker = true,
...props
}: ColorPickerProps) => {
const [open, setOpen] = useState(false);
const [value, setValue] = useState(valueProp);
const parsedValue = useMemo(() => {
return value || '#000000';
}, [value]);
const { run: debouncedOnChangeComplete } = useDebounceFn(
(value: string) => {
onChangeComplete?.(value);
},
forwardedRef
) => {
const [open, setOpen] = useState(false);
const [value, setValue] = useState(valueProp);
{ wait: 150 }
);
const parsedValue = useMemo(() => {
return value || '#000000';
}, [value]);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e?.currentTarget?.value);
onChange?.(e?.currentTarget?.value);
debouncedOnChangeComplete?.(e?.currentTarget?.value);
},
[onChange, debouncedOnChangeComplete]
);
const { run: debouncedOnChangeComplete } = useDebounceFn(
(value: string) => {
onChangeComplete?.(value);
},
{ wait: 150 }
);
const handleHexColorPickerChange = useCallback(
(color: string) => {
setValue(color);
onChange?.(color);
debouncedOnChangeComplete?.(color);
},
[onChange, debouncedOnChangeComplete]
);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e?.currentTarget?.value);
onChange?.(e?.currentTarget?.value);
debouncedOnChangeComplete?.(e?.currentTarget?.value);
},
[onChange, debouncedOnChangeComplete]
);
useEffect(() => {
setValue(valueProp);
}, [valueProp]);
const handleHexColorPickerChange = useCallback(
(color: string) => {
setValue(color);
onChange?.(color);
debouncedOnChangeComplete?.(color);
},
[onChange, debouncedOnChangeComplete]
);
return (
<PopoverRoot onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild disabled={disabled}>
<div className={colorPickerWrapperVariants({ size, disabled })}>
<div className="h-full w-full rounded-sm" style={{ backgroundColor: parsedValue }} />
</div>
</PopoverTrigger>
<PopoverContent className="w-full" align="end" side="bottom">
<HexColorPicker color={parsedValue} onChange={handleHexColorPickerChange} />
<Input
className="mt-2.5"
maxLength={7}
onChange={handleInputChange}
value={parsedValue}
/>
</PopoverContent>
</PopoverRoot>
);
}
);
return (
<PopoverRoot onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild disabled={disabled}>
<div>
<ColorPickerInputBox parsedValue={parsedValue} size={size} disabled={disabled} />
</div>
</PopoverTrigger>
<PopoverContent className="w-full" align="end" side="bottom">
<div>
{showPicker && (
<HexColorPicker color={parsedValue} onChange={handleHexColorPickerChange} />
)}
{showInput && (
<Input
className="mt-2.5"
maxLength={7}
onChange={handleInputChange}
value={parsedValue}
/>
)}
{children && <div className={cn((showInput || showPicker) && 'mt-2.5')}>{children}</div>}
</div>
</PopoverContent>
</PopoverRoot>
);
};
ColorPicker.displayName = 'ColorPicker';
const ColorPickerInputBox = ({
parsedValue,
size,
disabled
}: {
parsedValue: string;
size: 'default' | 'small' | 'tall';
disabled: boolean | undefined;
}) => {
const backgroundStyle =
parsedValue === 'inherit'
? {
backgroundImage:
'linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet)'
}
: { backgroundColor: parsedValue };
return (
<div className={colorPickerWrapperVariants({ size, disabled })}>
<div className="h-full w-full rounded-sm" style={backgroundStyle} />
</div>
);
};
export { ColorPicker };

View File

@ -1,4 +1,4 @@
import { IBusterMetricChartConfig } from '@/api/asset_interfaces';
import { DEFAULT_TRENDLINE_CONFIG, IBusterMetricChartConfig } from '@/api/asset_interfaces';
import React, { useEffect, useMemo, useState } from 'react';
import type { ChartEncodes, ScatterAxis, Trendline } from '@/api/asset_interfaces/metric/charts';
import { v4 as uuidv4 } from 'uuid';
@ -85,19 +85,10 @@ export const EditTrendline: React.FC<{
const type = hasLinearRegression ? getNewType() : ('linear_regression' as const);
const newTrendline: Required<LoopTrendline> = {
...DEFAULT_TRENDLINE_CONFIG,
id: uuidv4(),
show: true,
showTrendlineLabel: false,
trendlineLabel: null,
type,
trendLineColor: '#FF0000',
columnId: selectedAxis.y[0] || '',
trendlineLabelPositionOffset: 0,
projection: false,
lineStyle: 'solid',
polynomialOrder: 2,
aggregateAllCategories: true,
offset: 0
columnId: selectedAxis.y[0] || ''
};
addNewTrendId(newTrendline.id);
@ -283,48 +274,52 @@ const TrendlineItemContent: React.FC<{
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlineColumnId
trend={trend}
columnMetadata={columnMetadata}
columnLabelFormats={columnLabelFormats}
yAxisEncodes={yAxisEncodes}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
{show && (
<>
<TrendlineColumnId
trend={trend}
columnMetadata={columnMetadata}
columnLabelFormats={columnLabelFormats}
yAxisEncodes={yAxisEncodes}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<EditTrendlineOption
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
yAxisEncodes={yAxisEncodes}
xAxisEncodes={xAxisEncodes}
columnLabelFormats={columnLabelFormats}
selectedChartType={selectedChartType}
/>
<EditTrendlineOption
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
yAxisEncodes={yAxisEncodes}
xAxisEncodes={xAxisEncodes}
columnLabelFormats={columnLabelFormats}
selectedChartType={selectedChartType}
/>
<TrendlineLineStyle
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlineLineStyle
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlinePolynomialOrder
{/* <TrendlinePolynomialOrder
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
/> */}
<TrendlineProjection
{/* <TrendlineProjection
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
/> */}
<TrendlineAggregateAllCategories
trend={trend}
categoryEncodes={categoryEncodes}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlineAggregateAllCategories
trend={trend}
categoryEncodes={categoryEncodes}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlineColorPicker
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
<TrendlineColorPicker
trend={trend}
onUpdateExisitingTrendline={onUpdateExisitingTrendline}
/>
</>
)}
</div>
{show && (

View File

@ -3,6 +3,7 @@ import React from 'react';
import { LabelAndInput } from '../../Common';
import { LoopTrendline } from './EditTrendline';
import { useMemoizedFn } from '@/hooks';
import { Switch } from '@/components/ui/switch';
export const TrendlineColorPicker = React.memo(
({
@ -17,14 +18,27 @@ export const TrendlineColorPicker = React.memo(
onUpdateExisitingTrendline({ ...trend, trendLineColor: hexColor });
});
const handleInheritColorChange = useMemoizedFn((checked: boolean) => {
onUpdateExisitingTrendline({ ...trend, trendLineColor: checked ? 'inherit' : '#000000' });
});
const isInheritColor = trend.trendLineColor === 'inherit';
return (
<LabelAndInput label="Color">
<div className="flex w-full items-center justify-end">
<ColorPicker
size="small"
value={trend.trendLineColor || 'black'}
onChangeComplete={onChangeComplete}
/>
showInput={!isInheritColor}
showPicker={!isInheritColor}
value={trend.trendLineColor || '#000000'}
onChangeComplete={onChangeComplete}>
<LabelAndInput label="Inherit color">
<div className="flex w-full items-center justify-end">
<Switch checked={isInheritColor} onCheckedChange={handleInheritColorChange} />
</div>
</LabelAndInput>
</ColorPicker>
</div>
</LabelAndInput>
);

View File

@ -2,6 +2,7 @@ import React from 'react';
import { LabelAndInput } from '../../Common';
import { LoopTrendline } from './EditTrendline';
import { useMemoizedFn } from '@/hooks';
import { Slider } from '@/components/ui/slider';
interface TrendlineOffsetProps {
trend: LoopTrendline;
@ -24,11 +25,12 @@ export const TrendlineOffset: React.FC<TrendlineOffsetProps> = React.memo(
return (
<LabelAndInput label="Label offset">
<div className="flex w-full justify-end">
<input
type="number"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
value={trend.offset ?? 0}
onChange={handleChange}
<Slider
value={[trend.offset ?? 0]}
min={-75}
max={75}
step={1}
onValueChange={(value) => onUpdateExisitingTrendline({ ...trend, offset: value[0] })}
/>
</div>
</LabelAndInput>