feat: chart improvements + accessibility + fixes - #185
Conversation
Add shared formatNumber() and buildTooltipFormatter() to chart-utils.ts: - Decimal places config (0-6, -1 for automatic) - Comma/compact/percent formatting - Prefix/suffix support - Consistent tooltip formatting across all ECharts chart types Add decimalPlaces option to single-value, bar, line, and pie chart schemas. Update SingleValueChart to use the shared formatter. Closes #138 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add enableDataZoom prop to BaseChart that injects ECharts DataZoom (type: 'inside') on both x and y axes. Users can scroll-to-zoom to explore large datasets. Disabled by default (opt-in via chart options). - New prop: enableDataZoom on BaseChartProps - DataZoom injected into merged options when enabled - Chart option exposed in chart-options-schema for bar/line - 3 new tests for DataZoom behavior Closes #134 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add buildCategoryAxisLabel() utility to chart-utils.ts: - Auto-rotate 30° at 8+ categories, 45° at 15+ - Truncate labels >15 chars with ellipsis (U+2026) - Tooltip shows full text on hover - Configurable rotation override via chart option Wire into BarChart component with axisLabelRotation prop. Add "Axis Label Rotation" option to chart-options-schema. Closes #137 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add parseReferenceLines() and buildMarkLineFromRefs() to chart-utils.ts.
Wire markLine into BarChart — reference lines attach to first series.
Register MarkLineComponent in ECharts and update all test mocks.
- JSON config: [{"value":50,"label":"Target","color":"#ff0000"}]
- Chart option added to bar and line in chart-options-schema
- 7 new tests for reference line parsing
Closes #136
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- groupTopN() utility: groups slices beyond top N into "Other" - Donut center text: shows total or custom text via ECharts graphic element - Register GraphicComponent in ECharts - New chart options: topN, donutCenterText - 5 new tests for groupTopN Closes #139 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…trast #103 — ECharts ARIA: - Add ariaDescription prop to BaseChart for custom screen reader descriptions - Wire aria.label.description into merged ECharts options - Add role="img" and tabIndex={0} to chart container for keyboard focus #101 — Accessibility audit fixes: - CodePreview: fix contrast by removing /50 opacity on language label - JsonViewer: replace div with button for expand/collapse, add aria-expanded #102 — Keyboard navigation: - Chart containers now focusable via tabIndex={0} - JsonViewer nodes navigable via keyboard (native button focus) Closes #103, Closes #101, Closes #102 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug 1 — CodeMirror bracket wrapping: - Add closeBrackets() extension and closeBracketsKeymap to query editor - Selecting text and typing ( now wraps as (text) instead of replacing - Both Cypher and SQL bracket facets are now activated Bug 2 — Radar chart uniform shape: - Use single global max across all indicators instead of per-indicator max - Values like 172 vs 9 now show actual relative differences - Explicit max column values preserved when provided - 4 new/updated tests for global max behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New "Chart Catalog" dashboard with 12 pages, one per chart type: - Bar: vertical, horizontal, stacked, show values, styling, click, colorblind + 6 palettes - Line: default, smooth+area, stepped, show points, colorblind + 6 palettes - Pie: default, donut, rose, labels inside, click, colorblind + 6 palettes - Single Value: prefix/suffix, comma, compact, styling, trend - Table: default, sorting+filters, selection, click - Gauge: default, no pointer, half gauge, styling + 6 palettes - Radar: default, circle, filled+values, colorblind + 6 palettes - Sankey: horizontal, vertical + 6 palettes - Treemap: default, with values + 6 palettes - Sunburst: default, no labels + 6 palettes - Content: markdown (with tables), JSON viewer, iframe - Detail: click action target page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When onClick is set, CrossFilterTag renders as a <button>. The remove control inside was also a <button>, creating invalid nested buttons that cause React hydration errors. Fix: use <span role="button"> with keyboard handlers when the outer element is a button. Native <button> is preserved when outer is a div. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 6 new pages (18 total) to the Chart Catalog seed dashboard: - Page 12: Graph — force/circular/hierarchical, node sizes, labels, physics - Page 13: Parameter Widgets — select (searchable/not), text, date, date-range, relative-date - Page 14: Form Widget — default form, custom button + no-reset - Page 15: Behavior — showRefreshButton, manualRun, cacheMode (forever/TTL) - Page 16: Axis & Grid — axis labels, grid lines off, bar width/gap, legend off, sorted slices, no percentages - Page 17: Advanced — table pagination/pageSize, gauge min/max/progress/detail, radar legend, sankey nodeWidth/gap, sunburst sort/highlight, treemap breadcrumb/saturation, JSON fontSize/theme/copyButton Every chart option from chart-options-schema.ts now has at least one widget demonstrating it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ces) Add FILMED_IN and BORN_IN relationships to Neo4j seed connecting Movies and Persons to City nodes (which already had lat/lng coordinates). New Map Chart page in Chart Catalog (6 widgets): - Cities by population (OSM) - Filming locations (Carto Light) - Birthplaces (Carto Dark) - Clustered markers - Custom zoom / no popup - Large markers with click action All map options covered: tileLayer (osm/carto-light/carto-dark), autoFitBounds, markerSize, showPopup, clusterMarkers, zoom, minZoom, maxZoom. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Markdown: use actual \n newlines instead of escaped \\n literals - iFrame: replace echarts.apache.org (blocks framing via X-Frame-Options) with Wikipedia which allows embedding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add GFM table parsing to markdown-widget.tsx on this branch (was only on feat/issue-143-markdown-tables branch) - Reduce graph widgets from 6 to 4, use smaller queries (LIMIT 10) to prevent NVL physics engine overload with simultaneous renders - Fix graphSmall query: bind relationship variable properly - Type annotations for parseCells in markdown parser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gate NVL canvas visibility with a layoutReady state that flips true only when onLayoutDone fires. Reset is synchronous during render (not useEffect) to avoid a race where the effect runs after onLayoutDone on the main thread. Replace the arbitrary 100ms autoFit timer with a deterministic layoutReady guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add shared formatNumber() and buildTooltipFormatter() to chart-utils.ts: - Decimal places config (0-6, -1 for automatic) - Comma/compact/percent formatting - Prefix/suffix support - Consistent tooltip formatting across all ECharts chart types Add decimalPlaces option to single-value, bar, line, and pie chart schemas. Update SingleValueChart to use the shared formatter. Closes #138 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Apply userAria spread before ariaDescription so the explicit description takes precedence over user-provided aria options - Add ariaDescription to useEffect dependency array to re-apply when the description changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The -1 sentinel (automatic mode) was passed directly to ECharts as rotate: -1 which is invalid. Now normalized to undefined so ECharts uses its default auto-rotation. Added test for -1 sentinel case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ions - Fix TypeScript compilation error (unused NumberFormatConfig import) - Conditionally include seriesName in tooltip to avoid "undefined:" display - Remove inert decimalPlaces option from bar/line/pie chart schemas (only used in single-value where it's actually wired up) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Conditionally include seriesName in tooltip formatter to prevent "undefined:" display when series name is not provided - Remove decimalPlaces from bar/line/pie chart option registries where it was registered but never wired up Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wire up referenceLines prop in LineChart following the same pattern as BarChart (buildMarkLineFromRefs → markLine option) - Add unit tests for buildMarkLineFromRefs covering empty input, defaults, custom colors, and label text - Increases new code coverage toward 80% target Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Radar chart: only store explicit max when finite and > 0, so invalid/null/NaN values fall back to globalMax instead of being treated as explicit 100. - Seed: move colorPalette to settings.colorPalette (matches chart-renderer.tsx), fix navigateToPageId -> targetPageId. - Markdown: replace ReDoS-vulnerable table alignment regex with linear split-and-check function (isTableAlignmentRow). - Add tests for radar invalid max fallback and GFM table rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The typeInEditor fixture was retrying until timeout when CM6's internal view.state.readOnly was true. Now falls through to the keyboard fallback strategy instead of throwing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The typeInEditor fixture was retrying until timeout when CM6's internal view.state.readOnly was true. Now falls through to the keyboard fallback strategy instead of throwing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lease/chart-improvements # Conflicts: # component/src/charts/bar-chart.tsx # component/src/charts/chart-utils.ts # component/src/charts/line-chart.tsx # component/src/components/composed/chart-options-schema.ts
…els, markLine, pie donut) Merges PRs #169, #170, #171, #173, #174 into a single release branch. Resolves merge conflicts in chart-utils.ts, bar-chart.tsx, line-chart.tsx, and chart-options-schema.ts. Includes: - Number formatting for single-value and tooltips (#169) - DataZoom support for bar and line charts (#170) - Auto-rotate and truncate axis labels (#171) - Reference lines (markLine) for bar and line charts (#173) - Donut center text and Top-N grouping for pie chart (#174) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughAdds chart utilities (formatting, axis-label heuristics, reference-line parsing, pie Top‑N), wires them into bar/line/pie/single-value/base charts, revises graph auto-fit and radar scaling, enriches chart click payloads, updates tests/mocks, and adjusts E2E CodeMirror dispatch/fallback behavior for temporary readonly initialization. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant CR as ChartRenderer
participant CC as ChartComponent
participant CU as chart-utils
participant E as ECharts
User->>E: click/chart interaction
E->>CR: eCharts event payload
CR->>CR: merge data row (if available) into payload
CR->>CC: forward enriched click handler
CC->>CR: (optionally) request markLine/formatter via CU
CC->>CU: parseReferenceLines / buildTooltipFormatter
CU-->>CC: markLine object / formatter
CC->>E: setOption (includes markLine, tooltip.formatter)
CR-->>User: onChartClick callback with enriched payload
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
… into release/a11y-and-fixes
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
app/e2e/fixtures.ts (1)
151-164: Keyboard fallback for"readonly"may silently fail to insert text.When CM6's internal
readOnlystate is true, keyboard input can be silently dropped (as noted in the docstring on lines 56-59). The fallback attemptsCtrl+A→Backspace→insertText, then verifies insertion on line 160.Two concerns:
Clearing may also be dropped: If the editor is readonly, the
Backspacemight not clear existing content. If old content happens to contain the first 20 chars of the new query, the check passes incorrectly (edge case but possible).Masking stability issues: If
"readonly"is returned after the 600ms stability window passes, it suggests the pre-dispatch checks (lines 82-106) aren't sufficient. Falling back to keyboard rather than improving the stability detection could lead to flaky tests.Consider either:
- Adding
await page.keyboard.press("Backspace")verification (check content is empty before insert), or- Logging/tracking when
"readonly"fallback is taken to catch recurring issues.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/e2e/fixtures.ts` around lines 151 - 164, The readonly keyboard-fallback can silently fail because Backspace may be ignored; update the block handling dispatched === "readonly" in fixtures.ts to explicitly verify the clear step before inserting: after page.keyboard.press("Backspace") check cm.textContent() (or await expect(cm).toHaveText("")/empty) and if it’s not empty throw or retry once, then proceed to page.keyboard.insertText(query); additionally emit a test-side log/metric when the readonly fallback path (dispatched === "readonly") is taken so we can detect recurring flaky usage of this fallback (reference symbols: dispatched, cm, page.keyboard.press, query).component/src/charts/__tests__/base-chart.test.tsx (1)
30-40: Centralize theecharts/componentsmock.
component/vitest.setup.tsalready owns this mock shape. Keeping a second copy here is what letMarkLineComponentandGraphicComponentdrift out of sync. Prefer one shared mock source.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@component/src/charts/__tests__/base-chart.test.tsx` around lines 30 - 40, Remove the duplicate vi.mock("echarts/components", ...) block from the test and let the centralized mock in vitest.setup.ts provide the component stubs; if tests fail after removal, update the central mock in vitest.setup.ts to include any missing symbols (e.g., MarkLineComponent, GraphicComponent) so the shared mock shape matches what tests expect, and avoid redefining the mock in base-chart.test.tsx.component/src/charts/chart-utils.ts (1)
307-307: Move import to top of file.Imports should be grouped at the top for consistency and readability.
Suggested fix
import type { EChartsOption } from "echarts"; import type { ColorThreshold } from "./color-threshold"; import { resolveThresholdColor } from "./color-threshold"; import type { StylingRule } from "./styling-rule"; import { resolveStylingRuleColor } from "./styling-rule"; +import type { PieChartDataPoint } from "./types";Then remove line 307.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@component/src/charts/chart-utils.ts` at line 307, The import "import type { PieChartDataPoint } from \"./types\";" is placed mid-file; move this import statement up into the existing import group at the top of chart-utils.ts so all imports are grouped together, then remove the duplicate import line currently at line 307; ensure any references to the PieChartDataPoint type in functions (e.g., functions that use PieChartDataPoint within this file) continue to compile after relocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@component/src/charts/__tests__/pie-utils.test.ts`:
- Around line 5-44: The test-suite misses an unsorted input case and groupTopN
assumes input is pre-sorted; add a test that passes an unsorted data array to
groupTopN to catch this, and fix groupTopN to select the top N by value
regardless of input order (e.g., inside groupTopN make a shallow copy and sort
by value DESC to pick topN, then aggregate the rest into "Other"); reference
groupTopN in tests and pie-chart.tsx's sortSlices flag to ensure behavior is
consistent when sortSlices=false.
In `@component/src/charts/bar-chart.tsx`:
- Around line 92-109: The mark line builder currently assumes reference values
go on the yAxis; update the buildMarkLineFromRefs function to accept an axis
parameter ('xAxis' | 'yAxis') defaulting to 'yAxis' and use that key for each
entry, then update callers (e.g., bar-chart.tsx where markLine is created from
parseReferenceLines(referenceLinesJson)) to pass axis: use "xAxis" when
orientation === "horizontal" and "yAxis" otherwise; also add a unit test for
orientation="horizontal" to assert the markLine places values on xAxis to
prevent regressions.
In `@component/src/charts/base-chart.tsx`:
- Around line 155-165: The current merge in merged (in BaseChartProps rendering)
unconditionally overwrites caller-supplied options.dataZoom when enableDataZoom
is true; change it to preserve and merge with any existing options.dataZoom: if
options.dataZoom is undefined, set the default inside x/y entries as now,
otherwise merge/concat only missing inside sliders (or leave caller array
intact), e.g., compute a finalDataZoom from options.dataZoom and the default
[{type:"inside", xAxisIndex:0},{type:"inside", yAxisIndex:0}] and assign
dataZoom: finalDataZoom in the merged object so enableDataZoom augments rather
than replaces user-provided dataZoom.
In `@component/src/charts/pie-chart.tsx`:
- Around line 29-30: The component currently uses the nullish coalescing
operator when deciding the donut center label, so an explicit empty string
(donutCenterText === "") is treated as a provided value and renders blank;
update the logic where the center text is chosen (references: prop
donutCenterText in pie-chart.tsx and the render/compute function that currently
uses "??") to treat empty string as "use auto-total" (e.g., choose
donutCenterText if it's non-empty, otherwise compute and use the total). Apply
the same change to the other occurrences mentioned so that "" falls back to the
computed total rather than rendering blank.
In `@component/src/charts/single-value-chart.tsx`:
- Around line 37-38: The decimalPlaces prop is documented as 0–6 but currently
only checked with >= 0, so values >6 (or <0 besides -1) leak through; before
formatting (where you currently test decimalPlaces >= 0) clamp/normalize the
prop to the allowed range by: treat undefined or -1 as automatic mode, otherwise
constrain numeric values to Math.max(0, Math.min(6, decimalPlaces)) (or a clamp
helper) and use that clamped value in the formatting logic; update both the prop
handling site (decimalPlaces?) and the formatter call (the location that
currently checks ">= 0") so out-of-range inputs fall back to automatic or the
clamped precision.
In `@component/src/components/composed/chart-options-schema.ts`:
- Around line 82-86: Bar/line charts don't expose the shared number-formatting
options so buildTooltipFormatter() is always called with defaults; update the
composed schema and components to add those missing formatter keys. Add the keys
numberFormat, decimalPlaces, prefix, and suffix to the barOptions and
lineOptions arrays in chart-options-schema.ts (mirror how singleValueOptions
defines them), and then add corresponding props/propTypes/defaults to the
BarChart and LineChart components so they pass these props into their tooltip
config where buildTooltipFormatter(...) is invoked. Ensure the prop names match
the schema keys and are forwarded into the tooltip formatter call (e.g.,
buildTooltipFormatter({ numberFormat, decimalPlaces, prefix, suffix })) so
tooltips honor configured formatting.
---
Nitpick comments:
In `@app/e2e/fixtures.ts`:
- Around line 151-164: The readonly keyboard-fallback can silently fail because
Backspace may be ignored; update the block handling dispatched === "readonly" in
fixtures.ts to explicitly verify the clear step before inserting: after
page.keyboard.press("Backspace") check cm.textContent() (or await
expect(cm).toHaveText("")/empty) and if it’s not empty throw or retry once, then
proceed to page.keyboard.insertText(query); additionally emit a test-side
log/metric when the readonly fallback path (dispatched === "readonly") is taken
so we can detect recurring flaky usage of this fallback (reference symbols:
dispatched, cm, page.keyboard.press, query).
In `@component/src/charts/__tests__/base-chart.test.tsx`:
- Around line 30-40: Remove the duplicate vi.mock("echarts/components", ...)
block from the test and let the centralized mock in vitest.setup.ts provide the
component stubs; if tests fail after removal, update the central mock in
vitest.setup.ts to include any missing symbols (e.g., MarkLineComponent,
GraphicComponent) so the shared mock shape matches what tests expect, and avoid
redefining the mock in base-chart.test.tsx.
In `@component/src/charts/chart-utils.ts`:
- Line 307: The import "import type { PieChartDataPoint } from \"./types\";" is
placed mid-file; move this import statement up into the existing import group at
the top of chart-utils.ts so all imports are grouped together, then remove the
duplicate import line currently at line 307; ensure any references to the
PieChartDataPoint type in functions (e.g., functions that use PieChartDataPoint
within this file) continue to compile after relocation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dd633b35-4d62-4806-95b6-ce33e98c49ea
📒 Files selected for processing (18)
app/e2e/fixtures.tscomponent/src/charts/__tests__/axis-label-utils.test.tscomponent/src/charts/__tests__/base-chart.test.tsxcomponent/src/charts/__tests__/format-number.test.tscomponent/src/charts/__tests__/line-chart.test.tsxcomponent/src/charts/__tests__/pie-utils.test.tscomponent/src/charts/__tests__/reference-line.test.tscomponent/src/charts/__tests__/single-value-chart.test.tsxcomponent/src/charts/__tests__/tooltip-formatter.test.tscomponent/src/charts/bar-chart.tsxcomponent/src/charts/base-chart.tsxcomponent/src/charts/chart-utils.tscomponent/src/charts/line-chart.tsxcomponent/src/charts/pie-chart.tsxcomponent/src/charts/single-value-chart.tsxcomponent/src/charts/types.tscomponent/src/components/composed/chart-options-schema.tscomponent/vitest.setup.ts
| describe("groupTopN", () => { | ||
| const data: PieChartDataPoint[] = [ | ||
| { name: "A", value: 100 }, | ||
| { name: "B", value: 80 }, | ||
| { name: "C", value: 60 }, | ||
| { name: "D", value: 40 }, | ||
| { name: "E", value: 20 }, | ||
| ]; | ||
|
|
||
| it("returns all data when topN is 0 (disabled)", () => { | ||
| expect(groupTopN(data, 0)).toEqual(data); | ||
| }); | ||
|
|
||
| it("returns all data when topN >= data length", () => { | ||
| expect(groupTopN(data, 5)).toEqual(data); | ||
| expect(groupTopN(data, 10)).toEqual(data); | ||
| }); | ||
|
|
||
| it("groups remaining items into Other when topN < data length", () => { | ||
| const result = groupTopN(data, 3); | ||
| expect(result).toHaveLength(4); | ||
| expect(result[0].name).toBe("A"); | ||
| expect(result[1].name).toBe("B"); | ||
| expect(result[2].name).toBe("C"); | ||
| expect(result[3].name).toBe("Other"); | ||
| expect(result[3].value).toBe(60); // 40 + 20 | ||
| }); | ||
|
|
||
| it("handles topN of 1", () => { | ||
| const result = groupTopN(data, 1); | ||
| expect(result).toHaveLength(2); | ||
| expect(result[0].name).toBe("A"); | ||
| expect(result[1].name).toBe("Other"); | ||
| expect(result[1].value).toBe(200); // 80+60+40+20 | ||
| }); | ||
|
|
||
| it("returns empty array for empty input", () => { | ||
| expect(groupTopN([], 5)).toEqual([]); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add an unsorted topN case.
groupTopN() assumes descending input, but component/src/charts/pie-chart.tsx:75-78 passes raw data when sortSlices is false. With the default sortSlices=false, topN keeps the first N rows, not the largest N slices. This suite only uses pre-sorted fixtures, so it will not catch the bug.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/charts/__tests__/pie-utils.test.ts` around lines 5 - 44, The
test-suite misses an unsorted input case and groupTopN assumes input is
pre-sorted; add a test that passes an unsorted data array to groupTopN to catch
this, and fix groupTopN to select the top N by value regardless of input order
(e.g., inside groupTopN make a shallow copy and sort by value DESC to pick topN,
then aggregate the rest into "Other"); reference groupTopN in tests and
pie-chart.tsx's sortSlices flag to ensure behavior is consistent when
sortSlices=false.
| const refLines = parseReferenceLines(referenceLinesJson); | ||
| const markLine = buildMarkLineFromRefs(refLines); | ||
|
|
||
| const categoryLabels = data.map((d) => d.label); | ||
| const axisLabelConfig = buildCategoryAxisLabel(categoryLabels.length, { | ||
| compact, | ||
| rotateOverride: axisLabelRotation, | ||
| }); | ||
|
|
||
| const categoryAxis = { | ||
| type: "category" as const, | ||
| data: data.map((d) => d.label), | ||
| axisLabel: { show: !compact }, | ||
| data: categoryLabels, | ||
| axisLabel: axisLabelConfig, | ||
| axisPointer: { type: "shadow" as const }, | ||
| name: compact ? undefined : (isHorizontal ? yAxisLabel : xAxisLabel), | ||
| nameLocation: "middle" as const, | ||
| nameGap: 30, | ||
| nameGap: axisLabelConfig.rotate > 0 ? 50 : 30, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== buildMarkLineFromRefs ==="
rg -n -A30 -B2 'export function buildMarkLineFromRefs\(' component/src/charts/chart-utils.ts
echo
echo "=== BarChart orientation + markLine wiring ==="
sed -n '88,146p' component/src/charts/bar-chart.tsx
echo
echo "=== Existing reference-line tests ==="
sed -n '1,220p' component/src/charts/__tests__/reference-line.test.tsRepository: alfredo1996/neoboard
Length of output: 7449
Pass axis parameter to handle horizontal bar orientation.
buildMarkLineFromRefs() hardcodes yAxis for reference line coordinates, but when orientation === "horizontal" the yAxis becomes the category axis. Reference lines must target the value axis instead: use xAxis for horizontal bars and yAxis for vertical.
Suggested fix
- const markLine = buildMarkLineFromRefs(refLines);
+ const markLine = buildMarkLineFromRefs(refLines, isHorizontal ? "xAxis" : "yAxis");// component/src/charts/chart-utils.ts
export function buildMarkLineFromRefs(
lines: ReferenceLine[],
axis: "xAxis" | "yAxis" = "yAxis",
) {
if (!lines.length) return undefined;
return {
silent: true,
symbol: "none",
data: lines.map((line) => ({
[axis]: line.value,
label: {
formatter: line.label ?? String(line.value),
position: "insideEndTop" as const,
},
lineStyle: {
color: line.color ?? "#888",
type: "dashed" as const,
},
})),
};
}Add a test case for orientation="horizontal" to prevent regression.
Also applies to: 123–145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/charts/bar-chart.tsx` around lines 92 - 109, The mark line
builder currently assumes reference values go on the yAxis; update the
buildMarkLineFromRefs function to accept an axis parameter ('xAxis' | 'yAxis')
defaulting to 'yAxis' and use that key for each entry, then update callers
(e.g., bar-chart.tsx where markLine is created from
parseReferenceLines(referenceLinesJson)) to pass axis: use "xAxis" when
orientation === "horizontal" and "yAxis" otherwise; also add a unit test for
orientation="horizontal" to assert the markLine places values on xAxis to
prevent regressions.
| const merged: EChartsOption = { | ||
| color: resolvedColors, | ||
| ...options, | ||
| ...(enableDataZoom | ||
| ? { | ||
| dataZoom: [ | ||
| { type: "inside", xAxisIndex: 0 }, | ||
| { type: "inside", yAxisIndex: 0 }, | ||
| ], | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
Don’t overwrite caller-supplied dataZoom.
BaseChartProps.options is documented as raw ECharts config, but this branch replaces any options.dataZoom whenever enableDataZoom is true. That breaks advanced callers that already provide custom slider/inside zoom config.
Possible fix
- ...(enableDataZoom
+ ...(enableDataZoom && options?.dataZoom == null
? {
dataZoom: [
{ type: "inside", xAxisIndex: 0 },
{ type: "inside", yAxisIndex: 0 },
],
}
: {}),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/charts/base-chart.tsx` around lines 155 - 165, The current
merge in merged (in BaseChartProps rendering) unconditionally overwrites
caller-supplied options.dataZoom when enableDataZoom is true; change it to
preserve and merge with any existing options.dataZoom: if options.dataZoom is
undefined, set the default inside x/y entries as now, otherwise merge/concat
only missing inside sliders (or leave caller array intact), e.g., compute a
finalDataZoom from options.dataZoom and the default [{type:"inside",
xAxisIndex:0},{type:"inside", yAxisIndex:0}] and assign dataZoom: finalDataZoom
in the merged object so enableDataZoom augments rather than replaces
user-provided dataZoom.
| /** Text shown in the center of a donut chart (e.g. total value). Empty = auto-total. */ | ||
| donutCenterText?: string; |
There was a problem hiding this comment.
Honor the “empty = auto-total” contract for donutCenterText.
Line 130 uses ??, so donutCenterText="" renders a blank center label instead of falling back to the computed total.
🔧 Possible fix
- text: donutCenterText ?? String(sortedData.reduce((s, d) => s + d.value, 0)),
+ text: donutCenterText && donutCenterText.trim().length > 0
+ ? donutCenterText
+ : String(sortedData.reduce((s, d) => s + d.value, 0)),Also applies to: 57-57, 123-136
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/charts/pie-chart.tsx` around lines 29 - 30, The component
currently uses the nullish coalescing operator when deciding the donut center
label, so an explicit empty string (donutCenterText === "") is treated as a
provided value and renders blank; update the logic where the center text is
chosen (references: prop donutCenterText in pie-chart.tsx and the render/compute
function that currently uses "??") to treat empty string as "use auto-total"
(e.g., choose donutCenterText if it's non-empty, otherwise compute and use the
total). Apply the same change to the other occurrences mentioned so that ""
falls back to the computed total rather than rendering blank.
| /** Fixed decimal places (0-6). Set to -1 or omit for automatic. */ | ||
| decimalPlaces?: number; |
There was a problem hiding this comment.
Clamp decimalPlaces to the documented 0–6 range before formatting.
The prop contract says 0–6, but Line 91 only checks >= 0. Out-of-range values currently leak through the public API instead of falling back to automatic mode.
🔧 Possible fix
- const dp = decimalPlaces !== undefined && decimalPlaces >= 0 ? decimalPlaces : undefined;
+ const dp =
+ Number.isInteger(decimalPlaces) && decimalPlaces !== undefined && decimalPlaces >= 0
+ ? Math.min(decimalPlaces, 6)
+ : undefined;Also applies to: 66-66, 91-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/charts/single-value-chart.tsx` around lines 37 - 38, The
decimalPlaces prop is documented as 0–6 but currently only checked with >= 0, so
values >6 (or <0 besides -1) leak through; before formatting (where you
currently test decimalPlaces >= 0) clamp/normalize the prop to the allowed range
by: treat undefined or -1 as automatic mode, otherwise constrain numeric values
to Math.max(0, Math.min(6, decimalPlaces)) (or a clamp helper) and use that
clamped value in the formatting logic; update both the prop handling site
(decimalPlaces?) and the formatter call (the location that currently checks ">=
0") so out-of-range inputs fall back to automatic or the clamped precision.
| const singleValueOptions: ChartOptionDef[] = [ | ||
| { key: "title", label: "Title", type: "text", default: "", category: "Display", description: "Custom heading shown above the value. Leave blank to hide." }, | ||
| { key: "prefix", label: "Prefix", type: "text", default: "", category: "Display", description: "Text prepended to the value (e.g. '$', '€')." }, | ||
| { key: "suffix", label: "Suffix", type: "text", default: "", category: "Display", description: "Text appended to the value (e.g. '%', ' items')." }, | ||
| { key: "decimalPlaces", label: "Decimal Places", type: "number", default: -1, category: "Display", description: "Fixed number of decimal places (0-6). Set to -1 for automatic." }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Formatter-related props used in chart components ==="
rg -n --type=ts --type=tsx '\b(numberFormat|decimalPlaces|prefix|suffix)\b' component/src/charts
echo
echo "=== Formatter-related keys exposed by chart-options-schema.ts ==="
rg -n --type=ts '\b(numberFormat|decimalPlaces|prefix|suffix)\b' component/src/components/composed/chart-options-schema.ts
echo
echo "=== How chart-options-panel consumes schema keys ==="
rg -n --type=tsx 'getChartOptions|opt\.key' component/src/components/composed/chart-options-panel.tsxRepository: alfredo1996/neoboard
Length of output: 165
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Formatter-related props used in chart components ==="
find component/src/charts -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs rg -n '\b(numberFormat|decimalPlaces|prefix|suffix)\b' 2>/dev/null || echo "(no matches or error)"
echo
echo "=== Formatter-related keys in chart-options-schema.ts ==="
rg -n '\b(numberFormat|decimalPlaces|prefix|suffix)\b' component/src/components/composed/chart-options-schema.ts 2>/dev/null || echo "(no matches)"
echo
echo "=== Relevant sections from chart-options-schema.ts ==="
cat -n component/src/components/composed/chart-options-schema.ts | head -100
echo
echo "=== Chart types and their option definitions ==="
rg -n 'const.*Options.*ChartOptionDef\[\]|case.*:' component/src/components/composed/chart-options-schema.ts | head -30Repository: alfredo1996/neoboard
Length of output: 18382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Bar Chart Props Interface ==="
rg -A 20 'interface.*BarChartProps|type.*BarChartProps' component/src/charts/ | head -40
echo
echo "=== Line Chart Props Interface ==="
rg -A 20 'interface.*LineChartProps|type.*LineChartProps' component/src/charts/ | head -40
echo
echo "=== Pie Chart Props Interface ==="
rg -A 20 'interface.*PieChartProps|type.*PieChartProps' component/src/charts/ | head -40
echo
echo "=== Check if bar/line/pie use tooltip formatters ==="
rg -n 'buildTooltipFormatter|tooltip.*format|formatNumber' component/src/charts/bar-chart.tsx component/src/charts/line-chart.tsx component/src/charts/pie-chart.tsx 2>/dev/null || echo "(no matches)"Repository: alfredo1996/neoboard
Length of output: 9303
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Bar chart tooltip formatter call ==="
rg -B 2 -A 2 'buildTooltipFormatter\(\)' component/src/charts/bar-chart.tsx
echo
echo "=== Line chart tooltip formatter call ==="
rg -B 2 -A 2 'buildTooltipFormatter\(\)' component/src/charts/line-chart.tsx
echo
echo "=== Pie chart tooltip formatter call ==="
rg -B 2 -A 2 'buildTooltipFormatter\(\)' component/src/charts/pie-chart.tsx 2>/dev/null || echo "(no tooltip formatter found)"
echo
echo "=== buildTooltipFormatter signature ==="
rg -B 2 -A 8 'export.*function buildTooltipFormatter|export const buildTooltipFormatter' component/src/charts/chart-utils.tsRepository: alfredo1996/neoboard
Length of output: 1254
Formatter options missing from bar/line chart schema—blocks shared number formatting feature.
Bar and line charts call buildTooltipFormatter() in their tooltip configuration, but with no arguments (defaults only). Neither component exposes numberFormat, decimalPlaces, prefix, or suffix props, and the schema doesn't expose these keys for bar/line either. This prevents users from configuring number formatting in tooltips via the composed options panel, leaving the PR objective of "shared number formatting across all charts" incomplete. Add formatter options to barOptions and lineOptions schema arrays and corresponding props to the chart components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/components/composed/chart-options-schema.ts` around lines 82 -
86, Bar/line charts don't expose the shared number-formatting options so
buildTooltipFormatter() is always called with defaults; update the composed
schema and components to add those missing formatter keys. Add the keys
numberFormat, decimalPlaces, prefix, and suffix to the barOptions and
lineOptions arrays in chart-options-schema.ts (mirror how singleValueOptions
defines them), and then add corresponding props/propTypes/defaults to the
BarChart and LineChart components so they pass these props into their tooltip
config where buildTooltipFormatter(...) is invoked. Ensure the prop names match
the schema keys and are forwarded into the tooltip formatter call (e.g.,
buildTooltipFormatter({ numberFormat, decimalPlaces, prefix, suffix })) so
tooltips honor configured formatting.
…ease/chart-improvements # Conflicts: # component/src/charts/__tests__/format-number.test.ts # component/src/charts/base-chart.tsx # component/src/charts/chart-utils.ts # component/src/charts/types.ts # component/src/components/composed/chart-options-schema.ts
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
scripts/seed-demo.mjs (1)
2402-2403: Page comment numbering is off by one.This says "Page 13" but follows the Graph Chart page which is already "Page 13" (line 2380). Should be "Page 14". Subsequent pages have the same off-by-one error.
Fix page numbering
- // ── Page 13: Parameter Widgets ───────────────────────────────── + // ── Page 14: Parameter Widgets ─────────────────────────────────And similarly for subsequent pages (Form Widget → 15, Behavior Options → 16, etc.).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/seed-demo.mjs` around lines 2402 - 2403, Update the off-by-one page numbering in the seed demo headings: change the comment "// ── Page 13: Parameter Widgets ─────────────────────────────────" to "// ── Page 14: Parameter Widgets ─────────────────────────────────" and increment the page numbers for all subsequent page header comments (e.g., "Form Widget", "Behavior Options", etc.) so they follow the correct sequence (Form Widget → 15, Behavior Options → 16, etc.); locate these headers in scripts/seed-demo.mjs by searching for the page header comment pattern "// ── Page" and adjust each numeric label accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/lib/chart-registry.ts`:
- Around line 496-500: The current logic that sets indicatorExplicitMax in the
block inside chart-registry.ts only keeps the first valid explicitMax per
indName, causing later larger maxima to be ignored; change the behavior in the
code that reads maxKey/explicitMax (the block that calls Number(r[maxKey]) and
indicatorExplicitMax.set(indName, explicitMax)) to compute and store the highest
valid positive finite explicitMax per indName (e.g., if an entry already exists
use Math.max(existing, explicitMax)); preserve the validation (Number.isFinite
&& > 0) and do not overwrite with invalid values, and add a regression test that
feeds two rows with the same indicator and different valid max values to assert
the stored explicit max is the larger one (or alternately fail on disagreement
if that policy is preferred).
In `@component/src/components/composed/__tests__/markdown-widget.test.tsx`:
- Around line 439-441: The test in markdown-widget.test.tsx uses
container.innerHTML.indexOf("</ul>") and indexOf("<table") then compares
ulCloseIndex < tableIndex, which can be a false positive if either index is -1;
update the test (in the same test block that defines ulCloseIndex and
tableIndex) to first assert both markers exist (e.g.,
expect(ulCloseIndex).toBeGreaterThanOrEqual(0) and
expect(tableIndex).toBeGreaterThanOrEqual(0) or similar) and only then assert
the ordering (expect(ulCloseIndex).toBeLessThan(tableIndex)), so that missing
markers fail explicitly instead of passing the order check.
In `@component/src/components/composed/cross-filter-tag.tsx`:
- Around line 31-56: The span used for removeControl when onClick is present
lacks visible focus styling for keyboard users; update the span (the element
created in the onClick ? branch of removeControl) to include accessible focus
styles (e.g., add focus/focus-visible classes or an outline/ring style) so it
shows a clear focus indicator when keyboard-focused, while preserving the
existing role="button", tabIndex, onClick and onKeyDown behavior; ensure the
styling matches the project design tokens (e.g., ring or outline classes) and
does not remove the sr-only label or existing stopPropagation logic.
In `@scripts/seed-demo.mjs`:
- Around line 2027-2034: The clickNavPage helper is defined but never used;
remove the dead closure or wire it to a widget so a user can navigate to
detailPageId. Either (A) delete the clickNavPage function and any unused
imports, or (B) attach a "navigate-to-page" rule to a widget (e.g., the Bar
Chart page/widget) by creating a rule using clickNavPage(triggerColumn,
detailPageId) (type "navigate-to-page" with triggerColumn and targetPageId set
to detailPageId) so the widget triggers navigation to the Detail View; ensure
you update the widget's rules array where rules are defined.
---
Nitpick comments:
In `@scripts/seed-demo.mjs`:
- Around line 2402-2403: Update the off-by-one page numbering in the seed demo
headings: change the comment "// ── Page 13: Parameter Widgets
─────────────────────────────────" to "// ── Page 14: Parameter Widgets
─────────────────────────────────" and increment the page numbers for all
subsequent page header comments (e.g., "Form Widget", "Behavior Options", etc.)
so they follow the correct sequence (Form Widget → 15, Behavior Options → 16,
etc.); locate these headers in scripts/seed-demo.mjs by searching for the page
header comment pattern "// ── Page" and adjust each numeric label accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cadc1765-5f21-4e47-b8c5-383bb68df292
⛔ Files ignored due to path filters (2)
app/package-lock.jsonis excluded by!**/package-lock.jsondocker/neo4j/init.cypheris excluded by!docker/**
📒 Files selected for processing (16)
app/src/lib/__tests__/chart-registry.test.tsapp/src/lib/chart-registry.tscomponent/src/charts/__tests__/graph-chart.test.tsxcomponent/src/charts/base-chart.tsxcomponent/src/charts/chart-utils.tscomponent/src/charts/graph-chart.tsxcomponent/src/charts/types.tscomponent/src/components/composed/__tests__/markdown-widget.test.tsxcomponent/src/components/composed/__tests__/query-editor.test.tsxcomponent/src/components/composed/chart-options-schema.tscomponent/src/components/composed/code-preview.tsxcomponent/src/components/composed/cross-filter-tag.tsxcomponent/src/components/composed/json-viewer.tsxcomponent/src/components/composed/markdown-widget.tsxcomponent/src/components/composed/query-editor.tsxscripts/seed-demo.mjs
✅ Files skipped from review due to trivial changes (4)
- component/src/components/composed/tests/query-editor.test.tsx
- component/src/charts/types.ts
- component/src/components/composed/code-preview.tsx
- component/src/components/composed/markdown-widget.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- component/src/components/composed/chart-options-schema.ts
- component/src/charts/base-chart.tsx
- component/src/charts/chart-utils.ts
| if (maxKey) { | ||
| const explicitMax = Number(r[maxKey]) || 100; | ||
| if (!indicatorExplicitMax.has(indName)) indicatorExplicitMax.set(indName, explicitMax); | ||
| const explicitMax = Number(r[maxKey]); | ||
| if (Number.isFinite(explicitMax) && explicitMax > 0 && !indicatorExplicitMax.has(indName)) { | ||
| indicatorExplicitMax.set(indName, explicitMax); | ||
| } |
There was a problem hiding this comment.
Use the highest valid max per indicator, not the first one.
Line 498 makes the chosen explicit max depend on row order. In long-format radar data the same indicator is repeated across rows, so a later row can carry a larger valid max. Keeping the first one can mis-scale that axis for the later series. Please aggregate valid maxima per indicator (or fail when they disagree), and add a regression test for two rows with the same indicator but different valid max values.
Suggested fix
if (maxKey) {
const explicitMax = Number(r[maxKey]);
- if (Number.isFinite(explicitMax) && explicitMax > 0 && !indicatorExplicitMax.has(indName)) {
- indicatorExplicitMax.set(indName, explicitMax);
+ if (Number.isFinite(explicitMax) && explicitMax > 0) {
+ indicatorExplicitMax.set(
+ indName,
+ Math.max(indicatorExplicitMax.get(indName) ?? 0, explicitMax),
+ );
}
}📝 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.
| if (maxKey) { | |
| const explicitMax = Number(r[maxKey]) || 100; | |
| if (!indicatorExplicitMax.has(indName)) indicatorExplicitMax.set(indName, explicitMax); | |
| const explicitMax = Number(r[maxKey]); | |
| if (Number.isFinite(explicitMax) && explicitMax > 0 && !indicatorExplicitMax.has(indName)) { | |
| indicatorExplicitMax.set(indName, explicitMax); | |
| } | |
| if (maxKey) { | |
| const explicitMax = Number(r[maxKey]); | |
| if (Number.isFinite(explicitMax) && explicitMax > 0) { | |
| indicatorExplicitMax.set( | |
| indName, | |
| Math.max(indicatorExplicitMax.get(indName) ?? 0, explicitMax), | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/lib/chart-registry.ts` around lines 496 - 500, The current logic that
sets indicatorExplicitMax in the block inside chart-registry.ts only keeps the
first valid explicitMax per indName, causing later larger maxima to be ignored;
change the behavior in the code that reads maxKey/explicitMax (the block that
calls Number(r[maxKey]) and indicatorExplicitMax.set(indName, explicitMax)) to
compute and store the highest valid positive finite explicitMax per indName
(e.g., if an entry already exists use Math.max(existing, explicitMax)); preserve
the validation (Number.isFinite && > 0) and do not overwrite with invalid
values, and add a regression test that feeds two rows with the same indicator
and different valid max values to assert the stored explicit max is the larger
one (or alternately fail on disagreement if that policy is preferred).
| const ulCloseIndex = container.innerHTML.indexOf("</ul>"); | ||
| const tableIndex = container.innerHTML.indexOf("<table"); | ||
| expect(ulCloseIndex).toBeLessThan(tableIndex); |
There was a problem hiding this comment.
Guard against false-positive ordering assertion.
At Line 439, indexOf("</ul>") can be -1; then -1 < tableIndex still passes even when list-closing is missing. Assert both markers exist before comparing order.
Suggested fix
const ulCloseIndex = container.innerHTML.indexOf("</ul>");
const tableIndex = container.innerHTML.indexOf("<table");
+ expect(ulCloseIndex).toBeGreaterThanOrEqual(0);
+ expect(tableIndex).toBeGreaterThanOrEqual(0);
expect(ulCloseIndex).toBeLessThan(tableIndex);📝 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.
| const ulCloseIndex = container.innerHTML.indexOf("</ul>"); | |
| const tableIndex = container.innerHTML.indexOf("<table"); | |
| expect(ulCloseIndex).toBeLessThan(tableIndex); | |
| const ulCloseIndex = container.innerHTML.indexOf("</ul>"); | |
| const tableIndex = container.innerHTML.indexOf("<table"); | |
| expect(ulCloseIndex).toBeGreaterThanOrEqual(0); | |
| expect(tableIndex).toBeGreaterThanOrEqual(0); | |
| expect(ulCloseIndex).toBeLessThan(tableIndex); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/components/composed/__tests__/markdown-widget.test.tsx` around
lines 439 - 441, The test in markdown-widget.test.tsx uses
container.innerHTML.indexOf("</ul>") and indexOf("<table") then compares
ulCloseIndex < tableIndex, which can be a false positive if either index is -1;
update the test (in the same test block that defines ulCloseIndex and
tableIndex) to first assert both markers exist (e.g.,
expect(ulCloseIndex).toBeGreaterThanOrEqual(0) and
expect(tableIndex).toBeGreaterThanOrEqual(0) or similar) and only then assert
the ordering (expect(ulCloseIndex).toBeLessThan(tableIndex)), so that missing
markers fail explicitly instead of passing the order check.
| // When onClick is set the outer element is a <button>, so the remove | ||
| // control must NOT be a <button> (nested buttons are invalid HTML and | ||
| // cause React hydration errors). Use a <span role="button"> instead. | ||
| const removeControl = onRemove && ( | ||
| onClick ? ( | ||
| <span | ||
| role="button" | ||
| tabIndex={0} | ||
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | ||
| onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.stopPropagation(); e.preventDefault(); onRemove(); } }} | ||
| className="ml-1 rounded-full p-0.5 hover:bg-muted cursor-pointer" | ||
| > | ||
| <X className="h-3 w-3" /> | ||
| <span className="sr-only">Remove cross-filter</span> | ||
| </span> | ||
| ) : ( | ||
| <button | ||
| type="button" | ||
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | ||
| className="ml-1 rounded-full p-0.5 hover:bg-muted" | ||
| > | ||
| <X className="h-3 w-3" /> | ||
| <span className="sr-only">Remove cross-filter</span> | ||
| </button> | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Nested-button fix is correct; add visible focus styling for keyboard users.
The <span role="button"> approach is the right solution for avoiding invalid nested <button> elements. However, line 41 is missing focus styling — unlike native <button> elements, <span> has no browser-default focus ring, so keyboard users won't see a visible indicator.
🛡️ Proposed fix to add focus styling
<span
role="button"
tabIndex={0}
onClick={(e) => { e.stopPropagation(); onRemove(); }}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.stopPropagation(); e.preventDefault(); onRemove(); } }}
- className="ml-1 rounded-full p-0.5 hover:bg-muted cursor-pointer"
+ className="ml-1 rounded-full p-0.5 hover:bg-muted cursor-pointer focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1"
>📝 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.
| // When onClick is set the outer element is a <button>, so the remove | |
| // control must NOT be a <button> (nested buttons are invalid HTML and | |
| // cause React hydration errors). Use a <span role="button"> instead. | |
| const removeControl = onRemove && ( | |
| onClick ? ( | |
| <span | |
| role="button" | |
| tabIndex={0} | |
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | |
| onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.stopPropagation(); e.preventDefault(); onRemove(); } }} | |
| className="ml-1 rounded-full p-0.5 hover:bg-muted cursor-pointer" | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Remove cross-filter</span> | |
| </span> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | |
| className="ml-1 rounded-full p-0.5 hover:bg-muted" | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Remove cross-filter</span> | |
| </button> | |
| ) | |
| ); | |
| // When onClick is set the outer element is a <button>, so the remove | |
| // control must NOT be a <button> (nested buttons are invalid HTML and | |
| // cause React hydration errors). Use a <span role="button"> instead. | |
| const removeControl = onRemove && ( | |
| onClick ? ( | |
| <span | |
| role="button" | |
| tabIndex={0} | |
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | |
| onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.stopPropagation(); e.preventDefault(); onRemove(); } }} | |
| className="ml-1 rounded-full p-0.5 hover:bg-muted cursor-pointer focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1" | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Remove cross-filter</span> | |
| </span> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={(e) => { e.stopPropagation(); onRemove(); }} | |
| className="ml-1 rounded-full p-0.5 hover:bg-muted" | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Remove cross-filter</span> | |
| </button> | |
| ) | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@component/src/components/composed/cross-filter-tag.tsx` around lines 31 - 56,
The span used for removeControl when onClick is present lacks visible focus
styling for keyboard users; update the span (the element created in the onClick
? branch of removeControl) to include accessible focus styles (e.g., add
focus/focus-visible classes or an outline/ring style) so it shows a clear focus
indicator when keyboard-focused, while preserving the existing role="button",
tabIndex, onClick and onKeyDown behavior; ensure the styling matches the project
design tokens (e.g., ring or outline classes) and does not remove the sr-only
label or existing stopPropagation logic.
| const clickNavPage = (triggerCol, pageId) => ({ | ||
| type: "navigate-to-page", | ||
| rules: [{ | ||
| id: uuid(), type: "navigate-to-page", | ||
| triggerColumn: triggerCol, | ||
| targetPageId: pageId, | ||
| }], | ||
| }); |
There was a problem hiding this comment.
clickNavPage helper is defined but never used.
This closure is dead code. Either wire it up to navigate to detailPageId from a widget, or remove it.
Note: The Detail View page (line 2580) uses detailPageId but no widget actually navigates there.
Suggested fix: wire navigation or remove dead code
Option A — Remove unused helper:
- // Click action: navigate to page
- const clickNavPage = (triggerCol, pageId) => ({
- type: "navigate-to-page",
- rules: [{
- id: uuid(), type: "navigate-to-page",
- triggerColumn: triggerCol,
- targetPageId: pageId,
- }],
- });Option B — Wire a widget to navigate to the detail page (e.g., on the Bar Chart page):
// Bar with click action
{ id: uuid(), chartType: "bar", connectionId: neo4jId, query: Q.barData,
- settings: { title: "Click → Set Parameter", clickAction: clickSetParam("label", "bar_decade") } },
+ settings: { title: "Click → Navigate to Detail", clickAction: clickNavPage("label", detailPageId) } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/seed-demo.mjs` around lines 2027 - 2034, The clickNavPage helper is
defined but never used; remove the dead closure or wire it to a widget so a user
can navigate to detailPageId. Either (A) delete the clickNavPage function and
any unused imports, or (B) attach a "navigate-to-page" rule to a widget (e.g.,
the Bar Chart page/widget) by creating a rule using clickNavPage(triggerColumn,
detailPageId) (type "navigate-to-page" with triggerColumn and targetPageId set
to detailPageId) so the widget triggers navigation to the Detail View; ensure
you update the widget's rules array where rules are defined.
When a user configures a click action with a source field matching a
query column name (e.g. "revenue"), the click point from ECharts only
contains {name, value, seriesName, dataIndex} — the column name lookup
returned undefined and the action silently failed.
Now the original data row is spread into the click point using
dataIndex, so column-name source fields resolve correctly alongside
the built-in ECharts fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…provements seed dashboard - Wire enableDataZoom, referenceLines, axisLabelRotation to BarChart - Wire enableDataZoom, referenceLines to LineChart - Wire topN, donutCenterText to PieChart - Wire decimalPlaces to SingleValueChart - Fix typeInEditor CM6 view lookup: fallback to .cm-editor tile (mirrors EditorView.findFromDOM) - Fix code-completion tests: use CM6 dispatch to bypass closeBrackets corruption - Add buildChartImprovements seed dashboard (6 pages covering all new features) - Add grouped bar example to Chart Catalog - Fix markdown seed: use real newlines instead of escaped \\n - Fix click enrichment seed: use table widget for non-numeric column demo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
feat: chart improvements + accessibility + fixes



Summary
Consolidated batch of 7 PRs (charts + accessibility + fixes):
Chart Improvements
Accessibility and Fixes
Test plan
Closes #138, #134, #137, #136, #139, #101, #103
Summary by CodeRabbit
New Features
Improvements
Accessibility
Tests