Attempt to resolve value bug

This commit is contained in:
Nate Kelley 2025-05-12 14:58:11 -06:00
parent 51ae682095
commit 514d2b6d13
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 196 additions and 86 deletions

View File

@ -23,6 +23,9 @@ export interface Trendline {
trendLineColor?: string | null | 'inherit'; //OPTIONAL: default is #000000
trendlineLabelPositionOffset?: number; //OPTIONAL: default is 0.85. Goes from 0 to 1.
columnId: string;
projection?: boolean; //OPTIONAL: default is false. if true, the trendline will be projected to the end of the chart.
lineStyle?: 'solid' | 'dotted' | 'dashed' | 'dashdot';
polynomialOrder?: number;
aggregateAllCategories?: boolean; //OPTIONAL: default is true. if true, the trendline will be calculated for all categories. if false, the trendline will be calculated for the category specified in the columnId.
id?: string;
}

View File

@ -32,8 +32,6 @@ export const BusterChartJS: React.FC<BusterChartComponentProps> = ({
const { lineGroupType, pieMinimumSlicePercentage, barGroupType, datasetOptions } = props;
console.log(props.trendlines);
const onChartReady = useMemoizedFn(() => {
setChartMounted(true);
if (chartRef.current) onChartMounted?.(chartRef.current);

View File

@ -167,7 +167,8 @@ export const BusterChartJSComponent = React.memo(
animate,
disableTooltip,
xAxisTimeInterval,
numberOfDataPoints
numberOfDataPoints,
trendlines
});
const type = useMemo(() => {

View File

@ -73,7 +73,7 @@ export interface TrendlineOptions {
label?: TrendlineLabelOptions;
}
type AggregateMultiple = TrendlineOptions & { yAxisKey: string; yAxisID: string };
export type AggregateMultiple = TrendlineOptions & { yAxisKey: string; yAxisID: string };
/** Plugin-level options */
export interface TrendlinePluginOptions {
@ -607,12 +607,10 @@ const trendlinePlugin: Plugin<'line'> = {
if (aggregateConfig.label?.display) {
queueTrendlineLabel(
ctx,
chartArea,
xScale,
yScale,
fitter,
aggregateConfig,
defaultColor,
labelPositions,
labelDrawingQueue
);
@ -667,18 +665,15 @@ const trendlinePlugin: Plugin<'line'> = {
// Draw only the trendline first (not labels)
drawTrendlinePath(ctx, chartArea, xScale, yScale, fitter, opts, defaultColor);
// Queue label for later drawing if needed
if (opts.label?.display) {
const labelIndices = { datasetIndex, trendlineIndex };
queueTrendlineLabel(
ctx,
chartArea,
xScale,
yScale,
fitter,
opts,
defaultColor,
labelPositions,
labelDrawingQueue,
labelIndices
@ -694,15 +689,26 @@ const trendlinePlugin: Plugin<'line'> = {
}
};
// Helper function to check if two rectangles overlap
function doRectsOverlap(
rect1: { x: number; y: number; width: number; height: number },
rect2: { x: number; y: number; width: number; height: number }
): boolean {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
// Helper function to queue a label for later drawing
function queueTrendlineLabel(
ctx: CanvasRenderingContext2D,
chartArea: { bottom: number },
xScale: any,
yScale: any,
fitter: BaseFitter,
opts: TrendlineOptions,
defaultColor: string,
labelPositions: Array<{ x: number; y: number; width: number; height: number }> = [],
labelDrawingQueue: Array<{
ctx: CanvasRenderingContext2D;
@ -717,7 +723,6 @@ function queueTrendlineLabel(
let minX = opts.projection ? (xScale.min as number) : fitter.minx;
const maxX = opts.projection ? (xScale.max as number) : fitter.maxx;
const maxYFitter = fitter.maxx;
// For logarithmic trendlines, ensure minX is positive
if (opts.type === 'logarithmic_regression' && minX <= 0) {
@ -737,7 +742,9 @@ function queueTrendlineLabel(
// Handle text as either string or callback function
let textContent: string;
console.log('lbl.text', typeof lbl.text, lbl.text);
if (typeof lbl.text === 'function') {
console.log('lbl.text', lbl);
// Call the function with the slope value
textContent = lbl.text({
slope,
@ -769,7 +776,7 @@ function queueTrendlineLabel(
if (labelIndices) {
// Use dataset index and trendline index to create a staggered effect
// The formula below creates an increasing offset for each label
const baseOffset = 8; // Base offset in pixels
const baseOffset = 0; // Base offset in pixels
const additionalOffset = baseOffset * (labelIndices.datasetIndex + labelIndices.trendlineIndex);
offsetY -= additionalOffset;
}
@ -793,9 +800,18 @@ function queueTrendlineLabel(
height: labelHeight
};
// Check if this label overlaps with any existing labels
for (const existingLabel of labelPositions) {
if (doRectsOverlap(labelRect, existingLabel)) {
// Label would overlap, so skip adding it
return;
}
}
// Store this label's position for future collision detection
labelPositions.push(labelRect);
console.log('labelText', labelText);
// Queue the label for drawing later (to ensure it's on top of all lines)
labelDrawingQueue.push({
ctx,

View File

@ -1,67 +0,0 @@
import type { BusterChartProps } from '@/api/asset_interfaces';
import { TrendlineOptions } from '../../core/plugins/chartjs-plugin-trendlines';
import { TypeToLabel } from '../useTrendlines/config';
import { formatLabel } from '@/lib/columnFormatter';
export const createTrendlineOnSeries = ({
trendlines,
yAxisKey,
color,
columnLabelFormats
}: {
trendlines: BusterChartProps['trendlines'];
yAxisKey: string;
color: string;
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>;
}): TrendlineOptions[] | undefined => {
if (!trendlines || trendlines.length === 0) return undefined;
const relevantTrendlines = trendlines.filter(
({ columnId, aggregateAllCategories }) => columnId === yAxisKey && !aggregateAllCategories
);
return relevantTrendlines.map(
({ type, show, trendlineLabel, trendLineColor, showTrendlineLabel, columnId, ...rest }) => {
return {
type,
show,
colorMax: trendLineColor === 'inherit' ? color : trendLineColor,
colorMin: trendLineColor === 'inherit' ? color : trendLineColor,
label: showTrendlineLabel
? {
display: true,
text: (v) => {
if (!trendlineLabel) {
let value: number | null = null;
if (type === 'average') {
value = v.averageY;
} else if (type === 'median') {
value = v.medianY;
} else if (type === 'max') {
value = v.maxY;
} else if (type === 'min') {
value = v.minY;
}
const formattedValue = value
? formatLabel(value, columnLabelFormats[columnId])
: '';
const trendlineLabel = TypeToLabel[type];
const labelContent =
trendlineLabel && formattedValue
? `${trendlineLabel}: ${formattedValue}`
: trendlineLabel;
return labelContent;
}
return trendlineLabel;
}
}
: undefined
} satisfies TrendlineOptions;
}
);
};

View File

@ -0,0 +1,133 @@
import type { BusterChartProps, ChartEncodes } from '@/api/asset_interfaces';
import {
AggregateMultiple,
TrendlineOptions,
TrendlinePluginOptions
} from '../../core/plugins/chartjs-plugin-trendlines';
import { TypeToLabel } from '../useTrendlines/config';
import { formatLabel } from '@/lib/columnFormatter';
export const createTrendlineOnSeries = ({
trendlines,
yAxisKey,
datasetColor,
columnLabelFormats,
useAggregateTrendlines
}: {
trendlines: BusterChartProps['trendlines'];
yAxisKey: string;
datasetColor?: string;
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>;
useAggregateTrendlines?: boolean;
}): TrendlineOptions[] | undefined => {
if (!trendlines || trendlines.length === 0) return undefined;
const relevantTrendlines = trendlines.filter(({ columnId, aggregateAllCategories }) =>
columnId === yAxisKey && useAggregateTrendlines
? aggregateAllCategories
: !aggregateAllCategories
);
if (relevantTrendlines.length === 0) return undefined;
if (useAggregateTrendlines) console.log('relevantTrendlines', relevantTrendlines);
return relevantTrendlines
.map(
({
type,
show,
trendlineLabel,
lineStyle,
polynomialOrder,
trendLineColor,
showTrendlineLabel,
columnId,
projection,
trendlineLabelPositionOffset,
...rest
}) => {
return {
type,
show,
projection,
lineStyle,
polynomialOrder,
colorMax: trendLineColor === 'inherit' ? datasetColor || '#000000' : trendLineColor,
colorMin: trendLineColor === 'inherit' ? datasetColor || '#000000' : trendLineColor,
label: showTrendlineLabel
? {
positionRatio: trendlineLabelPositionOffset,
display: true,
text: (v) => {
let value: number | null = null;
if (type === 'average') {
value = v.averageY;
} else if (type === 'median') {
value = v.medianY;
} else if (type === 'max') {
value = v.maxY;
} else if (type === 'min') {
value = v.minY;
}
const formattedValue = value
? formatLabel(value, columnLabelFormats[columnId])
: '';
const trendlineLabel = TypeToLabel[type];
const labelContent =
trendlineLabel && formattedValue
? `${trendlineLabel}: ${formattedValue}`
: trendlineLabel;
console.log('labelContent', { labelContent, v, value, formattedValue });
return labelContent;
}
}
: undefined
} satisfies TrendlineOptions;
}
)
.filter((trendline) => trendline.show);
};
export const createAggregrateTrendlines = ({
trendlines,
columnLabelFormats,
selectedAxis
}: {
selectedAxis: ChartEncodes;
trendlines: BusterChartProps['trendlines'];
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>;
}): TrendlinePluginOptions | undefined => {
if (!trendlines || trendlines.length === 0) return undefined;
const trendlineOptions = trendlines.reduce<AggregateMultiple[]>((acc, trendline) => {
const result = createTrendlineOnSeries({
trendlines: [trendline],
yAxisKey: trendline.columnId,
columnLabelFormats,
useAggregateTrendlines: true
});
if (result && result[0]) {
const isYAxis = selectedAxis.y.includes(trendline.columnId);
acc.push({
...result[0],
yAxisID: isYAxis ? 'y' : 'y2',
yAxisKey: trendline.columnId
});
}
return acc;
}, []);
console.log('trendlineOptions', trendlineOptions);
return {
aggregateMultiple: trendlineOptions
};
};

View File

@ -6,10 +6,8 @@ import { DEFAULT_CHART_CONFIG, DEFAULT_COLUMN_LABEL_FORMAT } from '@/api/asset_i
import { addOpacityToColor } from '@/lib/colors';
import { isDateColumnType } from '@/lib/messages';
import { createDayjsDate } from '@/lib/date';
import { lineSeriesBuilder_labels } from './lineSeriesBuilder';
import { formatLabelForDataset } from '../../../commonHelpers';
import { TrendlineOptions } from '../../core/plugins/chartjs-plugin-trendlines';
import { createTrendlineOnSeries } from './createTrendlineOnSeries';
import { createTrendlineOnSeries } from './createTrendlines';
declare module 'chart.js' {
interface BubbleDataPoint {
@ -82,7 +80,7 @@ export const scatterSeriesBuilder_data = ({
xAxisKeys,
trendline: createTrendlineOnSeries({
trendlines,
color,
datasetColor: color,
yAxisKey: dataset.dataKey,
columnLabelFormats
}),

View File

@ -253,18 +253,46 @@ export const ScatterWithTrendline_NumericalXAxisPolynomialRegression: Story = {
trendlines: [
{
type: 'polynomial_regression',
show: false,
showTrendlineLabel: true,
trendlineLabel: 'HUH?',
trendLineColor: 'brown',
columnId: 'revenue',
aggregateAllCategories: true,
lineStyle: 'dotted',
trendlineLabelPositionOffset: 0.15
},
{
type: 'max',
show: true,
showTrendlineLabel: true,
trendlineLabel: null,
trendLineColor: 'inherit',
columnId: 'revenue',
aggregateAllCategories: true
},
{
type: 'min',
show: false,
showTrendlineLabel: true,
trendlineLabel: null,
trendLineColor: 'inherit',
columnId: 'revenue'
},
{
type: 'max',
type: 'median',
show: false,
showTrendlineLabel: true,
trendlineLabel: null,
trendLineColor: 'red',
trendLineColor: 'inherit',
columnId: 'revenue'
},
{
type: 'average',
show: false,
showTrendlineLabel: true,
trendlineLabel: null,
trendLineColor: 'inherit',
columnId: 'revenue'
}
],