Skip to content

Scatter chart#4146

Open
Kiarokh wants to merge 4 commits into
mainfrom
scatter-chart
Open

Scatter chart#4146
Kiarokh wants to merge 4 commits into
mainfrom
scatter-chart

Conversation

@Kiarokh

@Kiarokh Kiarokh commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Added support for scatter charts, including new example data and demo variants.
    • Scatter points can now be displayed in both landscape and portrait orientations.
    • Added a customizable dot size option for scatter charts.
  • Bug Fixes

    • Improved axis rendering and label placement for scatter charts.
    • Updated tooltip and value display so scatter coordinates appear consistently.
    • Better handling of negative values and overlapping points in scatter plots.

Review:

  • Commits are atomic
  • Commits have the correct type for the changes made
  • Commits with breaking changes are marked as such

Browsers tested:

(Check any that applies, it's ok to leave boxes unchecked if testing something didn't seem relevant.)

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

Kiarokh and others added 4 commits June 26, 2026 09:08
Pull the inline type of the private `range` field out into a named
`AxisRange` interface so the shape can be referenced and reused. No
change to behavior or public API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `type="scatter"`, which plots each item as a dot at an `[x, y]`
coordinate using both axes as value axes. Unlike the evenly-spaced `dot`
type, points are positioned absolutely, so they can sit close together,
overlap, or stack directly on top of each other.

Each item's `value` tuple is read as `[x, y]`. Both axes are auto-ranged
(anchored through zero, so negative and zero-crossing data work) and each
draws its own grid, zero line, and value badges. `orientation` transposes
the plot, and the type honors `displayAxisLabels`, `displayItemText`, and
`displayItemValue`. The dot diameter is exposed via the new
`--chart-scatter-item-size` custom property.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `scatter` example (positive data, dot-size and orientation
controls) and a `scatter-negative` example (data in all four quadrants,
exercising the zero lines and negative ranges on both axes), and wire
them into the chart's docs via `@exampleComponent`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `scatter` to the type selector in the axis-labels example. Because
the example's temperature data is one-dimensional, feed scatter a 2D
dataset (with matching axis labels) when it is selected, so the labels,
item text, and value toggles can be exercised on a real scatter plot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Kiarokh Kiarokh requested a review from a team as a code owner June 26, 2026 09:37
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds scatter-chart support to the chart component, including new tuple semantics, axis-range math, scatter item positioning, scatter-specific styles, example datasets and components, and updated chart documentation references.

Changes

Scatter chart support

Layer / File(s) Summary
Scatter contract
src/components/chart/chart.tsx, src/components/chart/chart.types.ts
Extends the chart type union with scatter, adds AxisRange, and documents [number, number] values as ranges or x/y coordinates.
Range math
src/components/chart/chart.tsx
Reworks axis increments, normalization, and scatter range recalculation across both axes.
Scatter rendering
src/components/chart/chart.tsx
Adds scatter axis rendering, point positioning, class selection, and formatting branches.
Scatter styles
src/components/chart/chart.scss, src/components/chart/partial-styles/*
Re-exports the scatter partial and adds scatter-specific dot, axis, and label styling.
Examples
src/components/chart/chart.tsx, src/components/chart/examples/chart-axis-labels.tsx, src/components/chart/examples/chart-items-scatter*.ts, src/components/chart/examples/chart-type-scatter*.tsx
Adds scatter datasets and example components, and updates the axis-label example and chart docs list to include scatter.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChartTypeScatterExample
  participant limel-select
  participant limel-input-field
  participant limel-chart

  User->>limel-select: choose orientation
  limel-select->>ChartTypeScatterExample: emit change
  ChartTypeScatterExample->>limel-chart: set orientation

  User->>limel-input-field: change dot size
  limel-input-field->>ChartTypeScatterExample: emit change
  ChartTypeScatterExample->>limel-chart: set --chart-scatter-item-size
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

feature, usability

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adding support for a new scatter chart type.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scatter-chart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-4146/

@Kiarokh

Kiarokh commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI-generated review from 6 parallel agents, re-synthesized against the current branch. Treat as input, not a verdict — agents can be wrong or miss context.

Consolidated PR Review

PR Summary

Adds a scatter chart type to limel-chart: each item is an absolutely-positioned dot at an [x, y] coordinate (reusing the existing value: [number, number] tuple), with both axes auto-ranged and negative-safe — each drawing its own grid, zero line, and badges — orientation transposing the plot, and the display-label props honored. 12 files, +764 / −43, in four atomic commits (refactor → feat → docs → docs).

Merge Readiness — READY TO MERGE ✅

Strictly better than main in every dimension the agents examined — no regressions, no new security/perf/compatibility breaks, and the negative-safe increment math even fixes a latent NaN that main produced for all-negative data. An initial pass flagged two [Medium] items (the axisIncrement behaviour and a zero-span NaN) and one documentation nit; all three were fixed in-branch before this review — see Addressed during development below. No open High/Medium items remain.

  • Blockers: None.
  • Non-blocking nits: in the per-dimension collapsibles below.

Dimensions

1. Backward Compatibility — SAFE ✅

No backward-incompatible changes; everything is additive or behavior-equivalent for existing types.

What works well: the 'scatter' union member only widens the public type, ChartItem is unchanged, and --chart-scatter-item-size is the only new public surface; the renderAxisesrenderAxisLines and calculateAxisIncrement(number[]) refactors were verified numerically identical for bar/dot/line/area/pie, and the :not([type='scatter']) axis-rule scoping plus the new .horizontal-lines/.vertical-lines rules can't match non-scatter charts. The [x,y]-vs-[start,end] tuple ambiguity is now documented on ChartItem.value.

Issues: None.

2. Code Quality — GOOD ✅

Cohesive helpers and correct edge handling; both previously-flagged [Medium] items are resolved, leaving only Low nits.

What works well: the new private helpers (calculateNiceStep/axisSpan/calculateAxisRange/normalize) are single-purpose and well-commented; calculateNiceStep fixed a Math.log10(negative) → NaN hazard; normalize now guards a zero-span axis.

Issues: None.

Minor nits (4)
  • 🔵 getXValue/getYValue (chart.tsx) plot a scalar value at (v, v) on the diagonal; the docs now emphasize the [x, y] tuple but don't call out this scalar fallback, so it remains an undocumented surprise.
  • 🔵 A point whose value equals the rounded axis max normalizes to exactly 100%, so the centered dot half-overflows the plot edge (chart.tsx normalize + _scatter.scss) — cosmetic.
  • 🔵 SCSS duplication: .axises.horizontal-lines/.vertical-lines repeat the landscape/portrait .axis-line bodies, and _scatter.scss's axis-label placement repeats _layout-for-charts-with-x-y-axises.scss — a shared mixin would cut the copy-paste.
  • 🔵 limel-chart has no spec/e2e tests, so the new math helpers ship untested (the component has none, so just a note).

3. Architecture — GOOD ✅

Scatter fits the existing CSS-variable + per-type-branch architecture coherently; the axisIncrement/two-axis concern from the first pass is resolved (scatter now auto-derives both axes and no longer mutates the prop field).

What works well: reusing the shared range field for value[1] so the existing axis-line machinery works unchanged is sound and well-documented; AxisRange.increment is the right home for a per-axis step; the structure extends cleanly to a future bubble-size or second two-axis type.

Issues: None.

Minor nits (2)
  • 🔵 The orientation→screen-axis flip is encoded in two places that must stay in lockstep — TS picks which range feeds horizontal-lines/vertical-lines, CSS maps offset-x/-y to edges (chart.tsx renderScatterAxises, _scatter.scss); consistent today, inherent to the CSS-variable design.
  • 🔵 Scatter is special-cased across ~6 methods (consistent with how pie/doughnut/line already branch), worth watching if a third tuple-consuming type ever arrives.

4. Security — SAFE ✅

No new security issues; presentational component with consistently escaped output.

What works well: item.text and the (x, y) value render only as escaped JSX text nodes through <td>, limel-badge, and limel-tooltip-content (no innerHTML anywhere); the new offset/size CSS variables are component-derived numbers. The item.color → --limel-chart-item-color write is the same pre-existing pattern every chart type uses (scatter only adds an early-return in front of it), and custom-property values can't break out of the declaration via the DOM style API.

Issues: None.

5. Observability — GOOD ✅

For a presentational component with no logging by design, scatter improves accessible-DOM observability and never crashes.

What works well: scatter keeps the full accessible <table> (caption, <th> headers, per-item text/value cells), renders the value as the semantically-correct (x, y), and deliberately keeps the value label visible at y=0 by disabling has-value-zero; empty items and the all-at-origin degenerate case now both degrade to a stable, finite render.

Issues: None.

Minor nits (2)
  • 🔵 A non-tuple value on a scatter chart is silently misread as a diagonal point with no dev-time signal (same root as the Code Quality scalar nit); a dev-mode warning when a scatter item's value isn't a 2-tuple would close the gap.
  • 🔵 Arbitrary NaN/Infinity coordinates still propagate to NaN% offsets and the dot silently disappears (the all-values-zero case is now guarded; literal NaN/Infinity data is not) — consistent with main's unguarded handling of bad values.

6. Performance — GOOD ✅

No performance regressions; the scatter render path is, if anything, slightly cheaper per item than the line/area branches.

What works well: getScatterItemStyle skips the line/area next-item lookup; recompute only fires on @Watch/load, not per render; mix-blend-mode: hard-light matches the existing dot type; no new listeners, timers, or retained references.

Issues: None.

Minor nits (2)
  • 🔵 recalculateScatterRanges makes several O(n) passes and spreads the value arrays into Math.max/min — a pre-existing pattern, negligible at realistic sizes, and recompute is rare (chart.tsx).
  • 🔵 renderItems still calls calculateSizeAndOffset for scatter items even though getScatterItemStyle discards the result (chart.tsx) — dead O(1) work per item, not worth optimizing.

Addressed during development

Caught in an initial pass and already fixed in this branch — listed for the record, not as open work:

  1. ✅ [was 🟡 Medium] axisIncrement on a two-axis chart — scatter now auto-derives both axes independently and ignores axisIncrement (the same stance it takes on maxValue, since one scalar prop can't sensibly drive two axes of different scale), and no longer mutates this.axisIncrement. Documented in the scatter example. (chart.tsx recalculateScatterRanges, chart-type-scatter.tsx)
  2. ✅ [was 🟡 Medium] Zero-span axis → NaN positionsnormalize now guards range.totalRange === 0 and centers the point, so a lone point or all-at-origin data renders a visible dot instead of a blank NaN-positioned one. (chart.tsx normalize)
  3. ✅ [was 🔵 Low] Tuple semantics footgunChartItem.value and the scatter example now spell out that a [number, number] is [x, y] for scatter but [start, end] elsewhere. (chart.types.ts, chart-type-scatter.tsx)

Top Recommendations

None open — the two [Medium] items above are already resolved in-branch. Remaining items are all [Low] nits in the per-dimension collapsibles; none block merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/chart/chart.tsx`:
- Around line 597-601: The chart range state is not being recomputed when the
reactive type prop changes, which leaves secondaryAxisRange null for scatter
renders. Add a `@Watch`('type') handler on the Chart component that triggers the
same range reset/recalculation flow used by handleChange and componentWillLoad,
so switching to scatter at runtime recomputes ranges before renderScatterAxises
and getScatterItemStyle run. Use the existing recalculateRangeData,
secondaryAxisRange, and handleChange logic as the reference points when wiring
this up.

In `@src/components/chart/examples/chart-items-scatter.ts`:
- Line 10: The scatter chart example is still typed with the generic ChartItem[]
shape, so it can accept non-scatter scalar values and drift from the intended
[x, y] tuple contract. Update the chartItems declaration in the scatter example
to use the same specialized tuple typing as chart-items-scatter-negative.ts,
specifically Array<ChartItem<[number, number]>>, so the compiler enforces
scatter-only data.

In `@src/components/chart/examples/chart-type-scatter.tsx`:
- Around line 101-103: The handleDotSizeChange handler currently assigns
+event.detail directly, which can turn empty or invalid input into NaN and
propagate an invalid rem value; update this method to validate the parsed value
before mutating dotSize. In chart-type-scatter.tsx, keep the existing dotSize
unchanged unless the parsed number from event.detail is finite and greater than
zero, so the downstream rendering logic does not receive NaN.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dc5010f3-4ed6-45a1-92af-82b791b2ae61

📥 Commits

Reviewing files that changed from the base of the PR and between 74a6496 and ce44c83.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (12)
  • src/components/chart/chart.scss
  • src/components/chart/chart.tsx
  • src/components/chart/chart.types.ts
  • src/components/chart/examples/chart-axis-labels.tsx
  • src/components/chart/examples/chart-items-scatter-negative.ts
  • src/components/chart/examples/chart-items-scatter.ts
  • src/components/chart/examples/chart-type-scatter-negative.tsx
  • src/components/chart/examples/chart-type-scatter.tsx
  • src/components/chart/partial-styles/_axises.scss
  • src/components/chart/partial-styles/_display-item-text.scss
  • src/components/chart/partial-styles/_layout-for-charts-with-x-y-axises.scss
  • src/components/chart/partial-styles/_scatter.scss

Comment on lines 597 to 601
handleChange() {
this.range = null;
this.secondaryAxisRange = null;
this.recalculateRangeData();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add @Watch('type') to avoid a null-deref when type changes to scatter at runtime.

type is a reactive @Prop, but range recalculation only runs in componentWillLoad and on items/axisIncrement/maxValue changes. If type is switched to scatter without those props also changing, secondaryAxisRange is never computed and stays null. The subsequent render reaches getScatterItemStylenormalize(value, this.secondaryAxisRange) (and renderScatterAxisesrenderAxisLines(null)), dereferencing null.totalRange/null.minValue and throwing.

🛡️ Proposed fix
 `@Watch`('items')
 `@Watch`('axisIncrement')
 `@Watch`('maxValue')
+@Watch('type')
 handleChange() {
     this.range = null;
     this.secondaryAxisRange = null;
     this.recalculateRangeData();
 }

Confirm no other mechanism recomputes ranges on type change:

#!/bin/bash
rg -nP "`@Watch`\(|componentWillUpdate|componentWillRender|recalculateRangeData" src/components/chart/chart.tsx
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chart/chart.tsx` around lines 597 - 601, The chart range state
is not being recomputed when the reactive type prop changes, which leaves
secondaryAxisRange null for scatter renders. Add a `@Watch`('type') handler on the
Chart component that triggers the same range reset/recalculation flow used by
handleChange and componentWillLoad, so switching to scatter at runtime
recomputes ranges before renderScatterAxises and getScatterItemStyle run. Use
the existing recalculateRangeData, secondaryAxisRange, and handleChange logic as
the reference points when wiring this up.

* or are placed exactly on top of each other, to demonstrate that the scatter
* type does not space items out the way a dot chart does.
*/
export const chartItems: ChartItem[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Specialize the dataset type to scatter tuples.

ChartItem[] still accepts scalar values, so this file can drift away from the scatter-only [x, y] contract without compiler help. Match src/components/chart/examples/chart-items-scatter-negative.ts and type this as Array<ChartItem<[number, number]>>.

Suggested change
-export const chartItems: ChartItem[] = [
+export const chartItems: Array<ChartItem<[number, number]>> = [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const chartItems: ChartItem[] = [
export const chartItems: Array<ChartItem<[number, number]>> = [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chart/examples/chart-items-scatter.ts` at line 10, The scatter
chart example is still typed with the generic ChartItem[] shape, so it can
accept non-scatter scalar values and drift from the intended [x, y] tuple
contract. Update the chartItems declaration in the scatter example to use the
same specialized tuple typing as chart-items-scatter-negative.ts, specifically
Array<ChartItem<[number, number]>>, so the compiler enforces scatter-only data.

Comment on lines +101 to +103
private handleDotSizeChange = (event: CustomEvent<string>) => {
this.dotSize = +event.detail;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid dot sizes before updating state.

Clearing the input or entering an invalid number makes +event.detail become NaN, which then reaches Line 67 as NaNrem. Keep the previous value unless the parsed number is finite and positive.

Suggested change
     private handleDotSizeChange = (event: CustomEvent<string>) => {
-        this.dotSize = +event.detail;
+        const nextDotSize = Number(event.detail);
+
+        if (Number.isFinite(nextDotSize) && nextDotSize > 0) {
+            this.dotSize = nextDotSize;
+        }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private handleDotSizeChange = (event: CustomEvent<string>) => {
this.dotSize = +event.detail;
};
private handleDotSizeChange = (event: CustomEvent<string>) => {
const nextDotSize = Number(event.detail);
if (Number.isFinite(nextDotSize) && nextDotSize > 0) {
this.dotSize = nextDotSize;
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chart/examples/chart-type-scatter.tsx` around lines 101 - 103,
The handleDotSizeChange handler currently assigns +event.detail directly, which
can turn empty or invalid input into NaN and propagate an invalid rem value;
update this method to validate the parsed value before mutating dotSize. In
chart-type-scatter.tsx, keep the existing dotSize unchanged unless the parsed
number from event.detail is finite and greater than zero, so the downstream
rendering logic does not receive NaN.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant