mirror of https://github.com/buster-so/buster.git
Attempt to resolve value bug
This commit is contained in:
parent
51ae682095
commit
514d2b6d13
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -167,7 +167,8 @@ export const BusterChartJSComponent = React.memo(
|
|||
animate,
|
||||
disableTooltip,
|
||||
xAxisTimeInterval,
|
||||
numberOfDataPoints
|
||||
numberOfDataPoints,
|
||||
trendlines
|
||||
});
|
||||
|
||||
const type = useMemo(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -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
|
||||
}),
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue