Skip to content

fix(charts): forward trendEnabled and thresholdZones, and pick the numeric KPI column (#1397) - #1473

Merged
alfredo1996 merged 1 commit into
release/1.5from
fix/issue-1397-forward-chart-options
Aug 6, 2026
Merged

fix(charts): forward trendEnabled and thresholdZones, and pick the numeric KPI column (#1397)#1473
alfredo1996 merged 1 commit into
release/1.5from
fix/issue-1397-forward-chart-options

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes #1397 · follow-up #1472

Important

The issue's diagnosis was wrong and its proposed fix was a no-op. Detail in this comment; summarised below. The acceptance criteria changed as a result.

What was broken

trendEnabled (single-value) and thresholdZones (gauge) were present in the widget editor, implemented in the component library, and silently discarded in between. Setting either did nothing, with no error — and the seeded reference dashboard shipped tiles demonstrating both, with the thresholdZones (3 bands) tile pixel-identical to default.

It was not the schema

The issue blames the plugin Zod schema .strip()ing unknown keys. Both schemas end in .passthrough(), which preserves them. Against the real schema:

gaugeSettingsSchema.parse({ thresholdZones: '[{"value":30}]', min: 0 })
→ survives? true  "[{\"value\":30}]"

The option arrives intact and dies at the plugin's explicit prop mappingGaugePluginComponent maps eight settings onto <GaugeChart> and thresholdZones isn't one. So the filed fix ("wire both through the plugin schemas") would have changed nothing visible. The keys are added to the schemas here for typing, but that is not what fixes it.

trendEnabled was never a forwardable prop

SingleValueChart takes trend?: { direction, label } — a computed object, not a boolean. It needs the previous period, which transformToValueData had already thrown away by collapsing the result to a single scalar. So the option and the KPI bug below share one root.

The worse half: the KPI showed a date

transformToValueData took the first column positionally. The editor says trend "requires 2 rows in the query result", so the natural query is label, value — and the KPI rendered

$2026-03

a date string with a currency prefix, as the headline metric. It now picks the first numeric column, falls back to a non-numeric one only when there is no numeric column (the legitimate "status text" case), and carries the second row's value as previous.

The ratchet — and why the proposed one wouldn't work

A single test asserting that every key returned by getChartOptions(type) exists in that plugin's settingsSchema

That would have passed while both bugs were live. The schema was never the seam, and it fails the other way too: a key in the schema is still dropped if the component doesn't forward it.

This PR asserts the seam that matters — is this option actually read by the plugin that advertises it — across all 20 chart types. It is source-based rather than importing getChartOptions, because that barrel pulls in @neo4j-nvl, a browser-only WebGL dependency that cannot load in the node test environment. The parsing it relies on is itself guarded by a test that fails if any chart type yields zero option keys, so a refactor can't make it pass vacuously.

Known limits, stated in the test: it catches "advertised but never wired", not "wired but misused", and not "wired to dead code".

Scope: 14, not 2

The ratchet found unforwarded options in 7 chart types. A first pass reported 29 — that over-counted, because table and form delegate rendering to TableRenderer / FormWidgetRenderer, which do read their options; the check now follows that one hop.

Fixed here: gauge.thresholdZones, single-value.trendEnabled.
Allowlisted and tracked in #1472: graph (3), json (3), line (2), map (2).
Allowlisted for a different reason: parameter-select.defaultValue / .syncToUrl are consumed by dashboard-level code outside the render path — syncToUrl works (#1388), defaultValue doesn't, but that's #1421.

The allowlist may only shrink: a separate test fails if an allowlisted key becomes read without being removed from the list.

Seeded tiles

No changes needed. page-single-value-v-81 and page-gauge-v-89 were authored correctly all along; only the plumbing between them and the chart was broken. They now demonstrate the real behaviour.

Tests

  • Unit — the forwarding ratchet, 23 cases across every chart type
  • Unit — 9 new transformToValueData cases: numeric-column selection, numeric strings, the no-numeric-column fallback, and previous extraction
  • jsdom — 7 cases asserting the props that actually reach the chart, including the $2026-03 regression

Verification

app 3540/3540 · typecheck 0 · lint 0

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional gauge threshold zones for displaying value bands with custom colors.
    • Added optional trend indicators to single-value charts, including direction and percentage changes.
  • Bug Fixes

    • Improved single-value metric selection to prioritize numeric values and handle numeric text reliably.
    • Prevented invalid trend percentages when the previous value is zero or unavailable.
    • Improved chart option forwarding so configured settings are applied consistently across visualizations.

…meric KPI column (#1397)

Both options were present in the widget editor, implemented in the
component library, and silently dropped in between. Setting either did
nothing, with no error.

The issue's diagnosis was wrong: it blamed the Zod schema stripping
unknown keys. Both schemas end in .passthrough(), which preserves them —
verified against the real schema. The value died at the plugin's explicit
prop mapping, which never forwarded it. Adding the key to the schema, the
fix as filed, would have changed nothing on screen.

trendEnabled could never have been forwarded as-is either: SingleValueChart
takes a computed { direction, label }, not a boolean, and it needs the
previous period — which transformToValueData had already discarded by
collapsing the result to one scalar.

That collapse is also the worse bug. It took the first column positionally,
so following the editor's own "requires 2 rows" instruction on a
`label, value` query rendered the date as the KPI: `$2026-03`. It now picks
the first numeric column, falling back to a non-numeric one only when the
result has none, and carries the second row's value as `previous`.

Adds a ratchet asserting every option a chart type advertises is read by
its plugin. The one proposed in the issue — comparing option keys against
settingsSchema — would have passed while both bugs were live, since the
schema was never the seam.

The ratchet found 14 unforwarded options, not 2. The remaining 10 are
allowlisted and tracked in #1472; the list may only shrink.

The seeded reference tiles needed no changes — they were authored
correctly and only the plumbing was broken.

Closes #1397

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alfredo1996 alfredo1996 added bug Something isn't working pkg:app Next.js application package area:widgets Widget system area:charts Chart rendering priority:P1 Ship-but-fix before release labels Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR forwards thresholdZones and trendEnabled through plugin settings, selects numeric single-value results, computes trends from prior rows, and adds source-based and integration tests for option forwarding.

Changes

Chart option forwarding and value trends

Layer / File(s) Summary
Gauge threshold option forwarding
app/src/plugins/gauge/settings.ts, app/src/plugins/gauge/component.tsx, app/src/plugins/__tests__/option-forwarding.test.tsx
The gauge schema accepts optional thresholdZones, and GaugeChart receives the configured value. Tests cover configured and unset options.
Single-value data transformation
app/src/plugins/single-value/transform.ts, app/src/plugins/transforms/__tests__/remaining.test.ts
transformToValueData returns structured data, selects the first numeric column, and exposes a numeric second-row value as previous.
Single-value trend forwarding and option coverage
app/src/plugins/single-value/settings.ts, app/src/plugins/single-value/component.tsx, app/src/plugins/__tests__/option-forwarding.test.tsx, app/src/lib/plugin/__tests__/chart-option-forwarding.test.ts, CHANGELOG.md
The schema accepts trendEnabled. The component computes and forwards trend direction and percentage data. Tests cover disabled, insufficient, upward, downward, neutral, and zero-baseline cases. A source-based ratchet checks chart option references and tracked exceptions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • alfredo1996/neoboard#1472 — The issue addresses the same thresholdZones, trendEnabled, and chart-option forwarding ratchet changes.

Possibly related PRs

  • alfredo1996/neoboard#612 — This PR also changes app/src/plugins/single-value/component.tsx, but addresses deprecated colorThresholds rather than trend and value handling.

Sequence Diagram(s)

sequenceDiagram
  participant PluginComponent
  participant transformToValueData
  participant SingleValueChart
  PluginComponent->>transformToValueData: Transform query rows
  transformToValueData-->>PluginComponent: Return value and previous
  PluginComponent->>PluginComponent: Compute trend
  PluginComponent->>SingleValueChart: Pass value and trend
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the chart option forwarding and numeric KPI selection fixes.
Linked Issues check ✅ Passed The changes satisfy issue #1397 by forwarding both options, correcting KPI selection, and adding the required ratchet and behavior tests.
Out of Scope Changes check ✅ Passed All changes support issue #1397, including implementation updates, targeted tests, changelog documentation, and option coverage tracking.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1397-forward-chart-options

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@app/src/plugins/__tests__/option-forwarding.test.tsx`:
- Around line 75-87: Update the ratchet in the option-forwarding test around
pluginSource() and the allowlist expiry check so it ignores comments and
declarations, then validates executable reads of each option or its chart-prop
mapping using an AST-based predicate where possible. Ensure thresholdZones and
trendEnabled only satisfy the check when their runtime forwarding logic is
present, not merely when they appear in settings schemas or comments.

In `@app/src/plugins/single-value/component.tsx`:
- Around line 46-55: Add a jsdom test covering the zero-baseline branch in the
trend calculation, using a zero previous value and changed/equal current values
as appropriate. Assert the resulting direction and verify that SingleValueChart
receives no Infinity% label, following the existing test structure and writing
the test before implementation.

In `@app/src/plugins/single-value/transform.ts`:
- Around line 41-43: Update transformToValueData around the first-row inspection
to verify that records[0] is a non-null record before calling Object.keys or
reading its values. When the first row is null or otherwise invalid, return the
existing fallback value; preserve the current numeric-key selection for valid
record rows.
- Around line 46-55: Update the value construction in the exported transform to
ensure boolean results from normalizeValue are converted to strings before
returning, keeping SingleValueData.value restricted to string | number. Preserve
numeric conversion and the existing zero fallback, and add a test covering an
enabled: true input.
🪄 Autofix

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: CHILL

Plan: Pro Plus

Run ID: 24752b8b-e699-450a-8748-ed75bc56c7ec

📥 Commits

Reviewing files that changed from the base of the PR and between 8dee489 and 40c6942.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • app/src/lib/plugin/__tests__/chart-option-forwarding.test.ts
  • app/src/plugins/__tests__/option-forwarding.test.tsx
  • app/src/plugins/gauge/component.tsx
  • app/src/plugins/gauge/settings.ts
  • app/src/plugins/single-value/component.tsx
  • app/src/plugins/single-value/settings.ts
  • app/src/plugins/single-value/transform.ts
  • app/src/plugins/transforms/__tests__/remaining.test.ts

Comment on lines +75 to +87
const el = screen.getByTestId("chart");
expect(el.getAttribute("data-trend-direction")).toBe("up");
expect(el.getAttribute("data-trend-label")).toBe("25.0%");
});

it("renders a downward trend when the value fell", () => {
render(
<SingleValueComponent
data={transformToValueData([...twoRows].reverse())}
settings={{ trendEnabled: true }}
/>,
);
expect(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the ratchet inspect executable option reads.

pluginSource() includes settings.ts and raw comments. thresholdZones and trendEnabled now occur in their schemas, so Line 131 passes even if their chart prop mapping is removed. Comments can also satisfy the current word match.

Exclude declarations and comments from this check. Assert a concrete settings read or chart-prop mapping, preferably with an AST-based predicate. Apply the same predicate to the allowlist expiry check.

🤖 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 `@app/src/plugins/__tests__/option-forwarding.test.tsx` around lines 75 - 87,
Update the ratchet in the option-forwarding test around pluginSource() and the
allowlist expiry check so it ignores comments and declarations, then validates
executable reads of each option or its chart-prop mapping using an AST-based
predicate where possible. Ensure thresholdZones and trendEnabled only satisfy
the check when their runtime forwarding logic is present, not merely when they
appear in settings schemas or comments.

Comment on lines +46 to +55
if (previous === 0) {
return {
direction,
label: direction === "neutral" ? "no change" : undefined,
};
}
const pct = Math.abs(delta / previous) * 100;
return {
direction,
label: direction === "neutral" ? "no change" : `${pct.toFixed(1)}%`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test the zero-baseline trend branch.

When previous === 0, this branch omits a percentage label for changes and returns "no change" for equal values. The supplied tests do not cover this behavior.

Add a jsdom test with a zero previous value. Assert the direction and that no Infinity% label reaches SingleValueChart.

As per coding guidelines, “Every new behavior, bug fix, and edge case must have a test, written before implementation, following Red → Green → Refactor.”

🤖 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 `@app/src/plugins/single-value/component.tsx` around lines 46 - 55, Add a jsdom
test covering the zero-baseline branch in the trend calculation, using a zero
previous value and changed/equal current values as appropriate. Assert the
resulting direction and verify that SingleValueChart receives no Infinity%
label, following the existing test structure and writing the test before
implementation.

Source: Coding guidelines

Comment on lines 41 to +43
const first = records[0];
const values = Object.values(first);
return normalizeValue(values[0]) ?? 0;
const keys = Object.keys(first);
const numericKey = keys.find((k) => toNumber(first[k]) !== null);

Copy link
Copy Markdown

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

Guard non-record rows before inspecting keys.

toRecords() returns arrays unchanged. transformToValueData([null]) assigns null to first, then Line 42 calls Object.keys(first) and throws. This can prevent the widget from rendering for malformed connector output.

Validate or filter rows before reading keys. Return the existing fallback value when the first row is not a record.

🤖 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 `@app/src/plugins/single-value/transform.ts` around lines 41 - 43, Update
transformToValueData around the first-row inspection to verify that records[0]
is a non-null record before calling Object.keys or reading its values. When the
first row is null or otherwise invalid, return the existing fallback value;
preserve the current numeric-key selection for valid record rows.

Comment on lines +46 to +55
const raw = key === undefined ? undefined : first[key];
const value =
(numericKey ? toNumber(raw) : (normalizeValue(raw) ?? 0)) ?? 0;

const previous =
records.length > 1 && key !== undefined
? (toNumber(records[1][key]) ?? undefined)
: undefined;

return { value: value as string | number, previous };

Copy link
Copy Markdown

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

Keep SingleValueData.value within its declared type.

For { enabled: true }, normalizeValue(raw) returns true. Line 55 casts that boolean to string | number without converting it. Callers of this exported transform can therefore receive a runtime value excluded by SingleValueData.

Convert boolean fallback values to strings, or widen the interface and downstream chart contract. Add a boolean fallback test.

🤖 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 `@app/src/plugins/single-value/transform.ts` around lines 46 - 55, Update the
value construction in the exported transform to ensure boolean results from
normalizeValue are converted to strings before returning, keeping
SingleValueData.value restricted to string | number. Preserve numeric conversion
and the existing zero fallback, and add a test covering an enabled: true input.

@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@alfredo1996
alfredo1996 merged commit 7daaddc into release/1.5 Aug 6, 2026
15 checks passed
@alfredo1996
alfredo1996 deleted the fix/issue-1397-forward-chart-options branch August 6, 2026 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:charts Chart rendering area:widgets Widget system bug Something isn't working pkg:app Next.js application package priority:P1 Ship-but-fix before release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants