diff --git a/CHANGELOG.md b/CHANGELOG.md index ca20010b..9a1ed602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/packages/core/src/types/spec.ts b/packages/core/src/types/spec.ts index 3c3f25f1..0256d536 100644 --- a/packages/core/src/types/spec.ts +++ b/packages/core/src/types/spec.ts @@ -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; } // --------------------------------------------------------------------------- diff --git a/packages/engine/src/charts/scatter/__tests__/compute.test.ts b/packages/engine/src/charts/scatter/__tests__/compute.test.ts index c7625d6e..8e0dc6f8 100644 --- a/packages/engine/src/charts/scatter/__tests__/compute.test.ts +++ b/packages/engine/src/charts/scatter/__tests__/compute.test.ts @@ -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'; // --------------------------------------------------------------------------- @@ -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); + }); +}); diff --git a/packages/engine/src/charts/scatter/index.ts b/packages/engine/src/charts/scatter/index.ts index bbc2eacf..f6e5d518 100644 --- a/packages/engine/src/charts/scatter/index.ts +++ b/packages/engine/src/charts/scatter/index.ts @@ -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;