Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this

### Features

- **scatter:** add `mark.trendline` to opt out of the auto regression line (`mark: { type: 'point', trendline: false }`). It defaulted to always-on and competed with author-drawn reference lines — a manual x=y diagonal in a separate layer produced two diagonals. Default behavior is unchanged.
- **scatter:** bubble size scale now honors `encoding.size.scale.domain` and `encoding.size.scale.range`. An explicit domain wider than the data keeps the largest datum below the max radius (so dense clusters stop blobbing together) and an explicit range caps the radii outright. The scale also clamps, so an author-narrowed domain never produces out-of-range radii.
- **endpoint-labels:** right-side per-series label column for multi-series line/area charts. Renders a chip+bar swatch (matching the redesigned legend), the colored series name, and a muted formatted value below. Auto-takes over from the traditional legend on ≥2-series line/area charts unless the user forces `legend: { show: true }`. Bidirectional collision sweep keeps labels stacked without overlap. Optional open-ring marker terminates the line at the chart's right edge; decorative point marks at the same coordinate are auto-suppressed to avoid double-circles.
- **legend:** redesigned categorical swatch as a rounded chip with a colored bar through its midline. Shared between the traditional legend and the endpoint-labels column so a chart never shows two swatch idioms.
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/types/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ export interface MarkDef {
tooltip?: boolean | null;
/** Clip marks to the chart area. */
clip?: boolean;
/**
* Overlay a least-squares regression line on scatter (point) marks.
* Defaults to true. Set `mark: { type: 'point', trendline: false }` to
* suppress it when the chart already carries its own reference line (e.g. a
* manual x=y diagonal in a separate layer), which would otherwise produce
* two competing diagonals.
*/
trendline?: boolean;
}

// ---------------------------------------------------------------------------
Expand Down
24 changes: 24 additions & 0 deletions packages/engine/src/charts/scatter/__tests__/compute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import type { NormalizedChartSpec } from '../../../compiler/types';
import { computeScales } from '../../../layout/scales';
import { computeScatterMarks } from '../compute';
import { scatterRenderer } from '../index';
import { computeTrendLine } from '../trendline';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -462,3 +463,26 @@ describe('computeTrendLine', () => {
expect(computeTrendLine([])).toBeNull();
});
});

describe('trendline opt-out', () => {
it('renders a trend line by default', () => {
const spec = makeBasicScatterSpec();
const scales = computeScales(spec, chartArea, spec.data);
const marks = scatterRenderer(spec, scales, chartArea, fullStrategy, undefined as never);

expect(marks.some((m) => m.type === 'line')).toBe(true);
});

it('suppresses the trend line when mark.trendline is false', () => {
const base = makeBasicScatterSpec();
const spec: NormalizedChartSpec = {
...base,
markDef: { type: 'point', trendline: false },
};
const scales = computeScales(spec, chartArea, spec.data);
const marks = scatterRenderer(spec, scales, chartArea, fullStrategy, undefined as never);

expect(marks.some((m) => m.type === 'line')).toBe(false);
expect(marks.every((m) => m.type === 'point')).toBe(true);
});
});
15 changes: 10 additions & 5 deletions packages/engine/src/charts/scatter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ export const scatterRenderer: ChartRenderer = (spec, scales, chartArea, strategy
const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy);
const marks: Mark[] = [...pointMarks];

// Add trend line if there are enough points
const trendLine = computeTrendLine(pointMarks);
if (trendLine) {
// Trend line goes behind points
marks.unshift(trendLine);
// Regression trend line renders by default but is opt-out via
// `mark: { type: 'point', trendline: false }`. Rendering it unconditionally
// competed with author-drawn reference lines (e.g. a manual x=y diagonal),
// so a chart could show two diagonals.
if (spec.markDef?.trendline !== false) {
const trendLine = computeTrendLine(pointMarks);
if (trendLine) {
// Trend line goes behind points
marks.unshift(trendLine);
}
}

return marks;
Expand Down
Loading