Skip to content

chore: chart polish, E2E speedup, and implementation guide - #599

Merged
alfredo1996 merged 10 commits into
release/2.0from
chore/chart-polish-gauge-sunburst
Apr 24, 2026
Merged

chore: chart polish, E2E speedup, and implementation guide#599
alfredo1996 merged 10 commits into
release/2.0from
chore/chart-polish-gauge-sunburst

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Gauge chart: redesigned to minimal style — thin arc with rounded progress fill, no pointer, no ticks, clean centered value display
  • Sunburst chart: added maxLabelDepth prop for smart label management — auto-hides labels on crowded rings, reveals full ancestor path on hover
  • E2E performance: replaced next dev --webpack with next build + next start in global-setup — cuts local E2E runtime from 30+ min to ~18 min
  • Documentation: added comprehensive APP_IMPLEMENTATION_GUIDE.md covering all 24 sections of the app/ package architecture

Test plan

  • Component tests pass (1240/1240)
  • App unit tests pass (2180/2180)
  • E2E tests pass (248 passed, 5 skipped, 0 failures)
  • Gauge chart verified in Storybook (Default, WithLabel, HighValue, CustomRange, ThresholdZones)
  • Sunburst chart verified in Storybook (Default, DeepHierarchy, ManyFirstLevel)
  • E2E runs with production build locally

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Gantt chart type with data transformation, styling options, progress overlay, and import support.
    • Sunburst charts gain a "Label Depth" setting (default: 2).
  • Style

    • Gauge visuals redesigned: pointer removed, ticks/labels hidden, progress arc and typography standardized.
  • Chores

    • E2E setup now uses production build/start flow; Playwright worker defaults adjusted.
  • Documentation / Stories

    • New Storybook stories and a demo Gantt page; gauge story updated to show threshold zones.
  • Tests

    • Test suites and E2E fixtures updated to include Gantt and adjusted expectations.

Gauge — minimal redesign:
- Thin arc with rounded progress fill, no ticks/labels
- Remove pointer entirely (showPointer prop, setting, chart option)
- Clean centered value with system font

Sunburst — smart label management:
- Add maxLabelDepth prop to control label visibility by ring depth
- Auto-adapt rotation and font size based on segment count per level
- Use transparent color (not show:false) for hidden labels so emphasis
  reveals the full ancestor path on hover
- Add DeepHierarchy story (5-level, ~80 cities) and ManyFirstLevel story

E2E — use production build locally:
- Replace `next dev --webpack` with `next build` + `next start` in
  global-setup.ts — matches CI behavior, cuts local E2E from 30+ min
  to ~18 min
- Add E2E_SKIP_BUILD env var to skip rebuild when iterating

Docs:
- Add comprehensive APP_IMPLEMENTATION_GUIDE.md (24 sections)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a77f3e03-894e-4a9c-a2d9-a4b98cd35241

📥 Commits

Reviewing files that changed from the base of the PR and between 3a5db1d and 7fd4404.

📒 Files selected for processing (1)
  • component/src/charts/gantt-chart.tsx
✅ Files skipped from review due to trivial changes (1)
  • component/src/charts/gantt-chart.tsx

Walkthrough

Adds a Gantt chart feature (component, plugin, transform, settings, stories, tests, and exports), removes the gauge showPointer option and refactors gauge visuals, adds maxLabelDepth for Sunburst (schema, prop, option), unifies E2E global setup to always build/start production, and updates tests/configs to include and validate the new chart type.

Changes

Cohort / File(s) Summary
E2E Setup
app/e2e/global-setup.ts
Always build/start using npx next build / npx next start (unless E2E_SKIP_BUILD), use shared serverEnv, spawn detached server.
Gauge plugin & charts
app/src/plugins/gauge/settings.ts, app/src/plugins/gauge/component.tsx, app/src/plugins/settings/__tests__/settings-schemas.test.ts, component/src/charts/gauge-chart.tsx, component/src/charts/__tests__/gauge-chart.test.tsx, component/src/components/composed/chart-options/gauge.ts, component/stories/charts/gauge-chart.stories.tsx, scripts/demo/chart-gallery.json
Removed showPointer from schema/options/props; gauge now hides pointer/ticks/labels/anchor, emphasizes progress arc/threshold zones; tests, stories, and demo updated.
Sunburst plugin & charts
app/src/plugins/sunburst/settings.ts, app/src/plugins/sunburst/component.tsx, component/src/charts/sunburst-chart.tsx, component/src/components/composed/chart-options/sunburst.ts, component/stories/charts/sunburst-chart.stories.tsx
Added maxLabelDepth (schema, prop, option); chart computes per-level levels and adjusts label visibility and hover emphasis; stories added.
Gantt plugin (new)
app/src/plugins/gantt/component.tsx, app/src/plugins/gantt/index.ts, app/src/plugins/gantt/settings.ts, app/src/plugins/gantt/transform.ts, app/src/plugins/index.ts, app/src/plugins/transforms/index.ts
New Gantt plugin export/registration, settings schema, transformToGanttData transform and re-export; plugin wired into built-ins and exposes capabilities/options.
Gantt chart component & tests
component/src/charts/gantt-chart.tsx, component/src/charts/__tests__/gantt-chart.test.tsx, component/src/charts/index.ts, component/src/components/composed/chart-options/gantt.ts, component/src/components/composed/chart-options/index.ts, component/stories/charts/gantt-chart.stories.tsx
New GanttChart, types, tests, composed chart options, barrel exports, and multiple Storybook stories.
Playwright / Tests / Tooling
app/playwright.config.ts, app/src/components/widget-editor/transform-editor.tsx, app/src/components/widget-editor/__tests__/transform-editor.test.tsx, app/src/lib/__tests__/plugin/chart-helpers.test.ts, app/src/plugins/__tests__/plugin-options.test.ts, component/vitest.setup.ts, app/e2e/*.spec.ts, app/e2e/fixtures/*.json
Increased local Playwright workers (4→6), migrated TransformEditor options to Radix primitives with test mocks, updated chart-type registry/tests for gantt, added ECharts CustomChart mock, updated E2E fixtures/specs to include/validate Gantt and other import changes.

Sequence Diagram(s)

sequenceDiagram
  participant Data as Data Source
  participant Transform as transformToGanttData
  participant Plugin as Gantt Plugin
  participant Chart as GanttChart
  participant ECharts as ECharts runtime

  rect rgba(200,200,255,0.5)
  Data->>Transform: raw query result
  Transform-->>Plugin: normalized Gantt data
  end

  rect rgba(200,255,200,0.5)
  Plugin->>Chart: props (data, settings, stylingRules)
  Chart->>ECharts: setOption(options with custom renderItem)
  ECharts-->>Chart: render/update visual
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, pkg:app, pkg:component, area:charts, testing

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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 relates to real parts of the changeset (gauge redesign, sunburst label depth, E2E performance, and implementation guide), but is quite broad and uses a generic 'polish' term that doesn't convey specific details about the main changes.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/chart-polish-gauge-sunburst

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 and usage tips.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
component/src/charts/gauge-chart.tsx (1)

1-17: ⚠️ Potential issue | 🔴 Critical

Wrap GaugeChart with next/dynamic(..., { ssr: false }) in the component library.

The chart is exported directly from component/src/charts/gauge-chart.tsx and component/src/charts/index.ts without a client-only boundary. ECharts initialization (echarts.use() at line 17) executes at module scope and will evaluate during SSR. While the app layer wraps it at consumption time (e.g., app/src/plugins/gauge/component.tsx), the component library itself must enforce the client-only boundary. Move the next/dynamic wrapper to the library's export in component/src/charts/gauge-chart.tsx or component/src/charts/index.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gauge-chart.tsx` around lines 1 - 17, The GaugeChart
module initializes echarts at module scope which runs during SSR; wrap the
exported GaugeChart component with next/dynamic(..., { ssr: false }) in this
library so the echarts.use(...) and GaugeChart rendering only run on the client.
Replace/export the GaugeChart (the component defined in gauge-chart.tsx and
re-exported from charts/index.ts) as a dynamically imported component via
next/dynamic with ssr: false, or move the echarts.use(...) calls into the
client-only wrapper so that BaseChart/GaugeChart are only imported/executed on
the client.
🤖 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/e2e/global-setup.ts`:
- Around line 267-279: The comment and behavior mismatch: current code only
checks E2E_SKIP_BUILD and will skip building even when .next/BUILD_ID is
missing, causing next start to fail; update the build block in global-setup.ts
to check both that process.env.E2E_SKIP_BUILD is set AND that the BUILD_ID file
exists before skipping (i.e., only skip when BUILD_ID is present), and if
E2E_SKIP_BUILD is set but BUILD_ID is missing, either run the build or throw a
clear error; also remove the unnecessary dynamic await
import("node:child_process") and use the existing static import (replace
execSync import with the already-imported execSync used alongside spawn) so
execSync is available without dynamic import.

In `@app/src/plugins/sunburst/settings.ts`:
- Line 9: The maxLabelDepth schema uses z.coerce.number() which silently turns
blank inputs into 0 and bypasses the default; update the maxLabelDepth schema to
preprocess blank/null/empty-string into undefined (using z.preprocess) before
coercion, then validate with z.number().int().min(1).max(10).default(2) (or
another appropriate max) so empty inputs fall back to the default and values are
constrained; modify the maxLabelDepth line to use
z.preprocess(...)->z.number().int().min(1).max(10).default(2).

In `@component/src/charts/gauge-chart.tsx`:
- Around line 90-102: The progressColor currently ignores thresholdZones; change
progressColor computation so it first uses resolvedColor (from
resolveItemColor(point.value, stylingRules, paramValues)), and if that is
null/undefined then if hasCustomZones is true derive the active progress color
from thresholdZones using the current value (point.value) or the same selection
logic used for trackColor's zones, otherwise fall back to the default "#5470c6";
update references to progressColor, hasCustomZones, thresholdZones and
resolvedColor to implement this selection.

In `@component/src/charts/sunburst-chart.tsx`:
- Around line 26-27: The prop maxLabelDepth currently treats 0 as a real value
due to using nullish coalescing (maxLabelDepth ?? 2), which makes 0 hide labels;
change the normalization to treat 0 as the "auto" sentinel so the component
falls back to the default (e.g., 2) when maxLabelDepth is 0 or undefined. Locate
where maxLabelDepth is normalized (references: the prop name maxLabelDepth and
any internal variable like maxLabelDepthUsed / normalizedMaxLabelDepth or the
logic around lines 26 and 61-65) and update it to: if maxLabelDepth === 0 then
use the auto/default value, otherwise use the provided positive number (or the
existing ?? default). Ensure the comparison logic that decides whether to render
a label uses the normalized value so depth <= normalized behaves correctly.
- Around line 143-157: The series-level label configuration in the Sunburst
chart is ignoring the showLabels prop: update the top-level series label.show
(currently using only !compact) to respect showLabels (i.e., label.show =
showLabels && !compact) and also change the emphasis.block's label.show
(currently hardcoded true) to use showLabels (e.g., emphasis.label.show =
showLabels) so hover/emphasis respects the prop; adjust in the series object
where label, minAngle, and emphasis are defined to be consistent with the
existing levels check that uses showLabels && !compact.

---

Outside diff comments:
In `@component/src/charts/gauge-chart.tsx`:
- Around line 1-17: The GaugeChart module initializes echarts at module scope
which runs during SSR; wrap the exported GaugeChart component with
next/dynamic(..., { ssr: false }) in this library so the echarts.use(...) and
GaugeChart rendering only run on the client. Replace/export the GaugeChart (the
component defined in gauge-chart.tsx and re-exported from charts/index.ts) as a
dynamically imported component via next/dynamic with ssr: false, or move the
echarts.use(...) calls into the client-only wrapper so that BaseChart/GaugeChart
are only imported/executed on the client.
🪄 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: CHILL

Plan: Pro

Run ID: ff9cae30-5ada-40af-b72b-cb75f82514dd

📥 Commits

Reviewing files that changed from the base of the PR and between ee01f34 and b1501c5.

📒 Files selected for processing (15)
  • app/e2e/global-setup.ts
  • app/src/plugins/gauge/component.tsx
  • app/src/plugins/gauge/settings.ts
  • app/src/plugins/settings/__tests__/settings-schemas.test.ts
  • app/src/plugins/sunburst/component.tsx
  • app/src/plugins/sunburst/settings.ts
  • component/src/charts/__tests__/gauge-chart.test.tsx
  • component/src/charts/gauge-chart.tsx
  • component/src/charts/sunburst-chart.tsx
  • component/src/components/composed/chart-options/gauge.ts
  • component/src/components/composed/chart-options/sunburst.ts
  • component/stories/charts/gauge-chart.stories.tsx
  • component/stories/charts/sunburst-chart.stories.tsx
  • docs/APP_IMPLEMENTATION_GUIDE.md
  • scripts/demo/chart-gallery.json
💤 Files with no reviewable changes (4)
  • app/src/plugins/gauge/component.tsx
  • component/src/components/composed/chart-options/gauge.ts
  • app/src/plugins/gauge/settings.ts
  • app/src/plugins/settings/tests/settings-schemas.test.ts

Comment thread app/e2e/global-setup.ts Outdated
Comment thread app/src/plugins/sunburst/settings.ts
Comment thread component/src/charts/gauge-chart.tsx
Comment thread component/src/charts/sunburst-chart.tsx
Comment on lines 143 to +157
label: {
show: showLabels && !compact,
show: !compact,
fontSize: 11,
},
// Hide labels for segments with arc angle below 5 degrees
// Hide labels on very thin slivers regardless of level settings
minAngle: 5,
emphasis: highlightOnHover
? {
focus: "ancestor",
label: { show: showLabels && !compact },
label: {
show: true,
fontSize: 12,
fontWeight: "bold" as const,
color: "inherit",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the sunburst-chart.tsx file
find . -name "sunburst-chart.tsx" -type f

Repository: alfredo1996/neoboard

Length of output: 105


🏁 Script executed:

# Once we have the file, let's examine the full component context
cat -n component/src/charts/sunburst-chart.tsx | head -200

Repository: alfredo1996/neoboard

Length of output: 7775


🏁 Script executed:

# Check for showLabels prop definition and its usage throughout the file
rg "showLabels" component/src/charts/sunburst-chart.tsx -B 2 -A 2

Repository: alfredo1996/neoboard

Length of output: 1053


🏁 Script executed:

# Check imports to ensure ECharts imports follow the coding guideline
rg "import.*echarts" component/src/charts/sunburst-chart.tsx

Repository: alfredo1996/neoboard

Length of output: 339


Respect showLabels={false} in top-level label config and emphasis state.

The levels array correctly checks showLabels && !compact (line 97), but the series-level label.show at line 144 only checks !compact, and emphasis.label.show is hardcoded to true at line 153. When showLabels={false}, labels will still appear on hover, violating the prop's intent.

Suggested fix
           label: {
-            show: !compact,
+            show: showLabels && !compact,
             fontSize: 11,
           },
@@
                 label: {
-                  show: true,
+                  show: showLabels && !compact,
                   fontSize: 12,
                   fontWeight: "bold" as const,
                   color: "inherit",
📝 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
label: {
show: showLabels && !compact,
show: !compact,
fontSize: 11,
},
// Hide labels for segments with arc angle below 5 degrees
// Hide labels on very thin slivers regardless of level settings
minAngle: 5,
emphasis: highlightOnHover
? {
focus: "ancestor",
label: { show: showLabels && !compact },
label: {
show: true,
fontSize: 12,
fontWeight: "bold" as const,
color: "inherit",
},
label: {
show: showLabels && !compact,
fontSize: 11,
},
// Hide labels on very thin slivers regardless of level settings
minAngle: 5,
emphasis: highlightOnHover
? {
focus: "ancestor",
label: {
show: showLabels && !compact,
fontSize: 12,
fontWeight: "bold" as const,
color: "inherit",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/sunburst-chart.tsx` around lines 143 - 157, The
series-level label configuration in the Sunburst chart is ignoring the
showLabels prop: update the top-level series label.show (currently using only
!compact) to respect showLabels (i.e., label.show = showLabels && !compact) and
also change the emphasis.block's label.show (currently hardcoded true) to use
showLabels (e.g., emphasis.label.show = showLabels) so hover/emphasis respects
the prop; adjust in the series object where label, minAngle, and emphasis are
defined to be consistent with the existing levels check that uses showLabels &&
!compact.

alfredorubin96 and others added 3 commits April 23, 2026 19:08
- Increase local Playwright workers from 4 to 6 (10-core machine)
- Auto-detect cached .next/BUILD_ID — print hint about E2E_SKIP_BUILD
- E2E_SKIP_BUILD=1 skips rebuild for instant repeat runs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The transform type selector was rendering both the label and description
in the collapsed trigger because Radix SelectValue mirrors all children
of SelectItem. Fix by using SelectPrimitive.Item with ItemText wrapping
only the label — the description is a sibling outside ItemText, visible
in the dropdown but not in the trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New chart type: timeline visualization with horizontal task bars on a
time axis. Built with ECharts custom series + renderItem.

Features:
- Auto-detect task/start/end columns from query results
- Color by category via rule-based styling
- Progress overlay (optional, 0-1 or 0-100 range)
- Today marker (red dashed vertical line)
- Data zoom slider for large timelines
- Click action support (enrichClickEvent with task, start, end, category)
- Responsive with tooltips showing duration

Component library:
- gantt-chart.tsx — ECharts custom series component
- 12 unit tests, 6 Storybook stories (Default, Categories, Progress,
  LargeDataset, NoTodayLine, EmptyState)
- Chart options: showTodayLine, showProgress, showGridLines, barBorderRadius

App plugin:
- plugins/gantt/ — component, transform, settings, index
- Registered as 18th chart type in chart-types.ts
- Transform heuristically detects columns with date parsing (ISO, Unix, Date)

NeoDash migration:
- gantt → gantt mapping in neodash-converter.ts (was falling back to json)

Closes #601

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

@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: 5

🧹 Nitpick comments (7)
app/src/components/widget-editor/__tests__/transform-editor.test.tsx (1)

59-70: Mock aligns with the component — LGTM, with one optional add.

Partial mock of Item/ItemText/ItemIndicator exactly matches what transform-editor.tsx imports via SelectPrimitive.*, so the tests stay green and will fail loudly if someone reaches for another primitive later. Good minimalism.

Optional: none of the existing assertions actually lock in the bug fix (i.e., that the collapsed trigger shows only the label, not label + description). A single targeted test — e.g., asserting the selected trigger's accessible text equals "Filter" and not "Filter Remove rows matching a condition" — would prevent a regression if someone swaps back to a wrapped SelectItem. Feel free to skip if you'd rather keep the suite lean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/widget-editor/__tests__/transform-editor.test.tsx` around
lines 59 - 70, Tests currently mock `@radix-ui/react-select` primitives (Item,
ItemText, ItemIndicator) and the suite misses an assertion ensuring the
collapsed Select trigger only shows the label (not label + description); add a
focused test in transform-editor.test.tsx that renders the transform-editor,
selects the relevant item via the mocked SelectPrimitive.Item, and asserts the
selected trigger's accessible text equals "Filter" (and does not contain the
description like "Remove rows matching a condition") so regressions reverting to
wrapped SelectItem (label+description) are caught.
app/src/components/widget-editor/transform-editor.tsx (1)

13-13: Fix is correct — trigger now displays only t.label.

Dropping to SelectPrimitive.Item so only t.label is wrapped in ItemText is the right move: @neoboard's SelectItem presumably wraps all children in ItemText, which is why both label and description were bleeding into the collapsed trigger. Description stays visible in the listbox (as a sibling span inside Item) and is still part of the option's accessible name. 👍

One optional follow-up to reduce the primitive reach-in: consider adding a description/subtitle prop to @neoboard/components's SelectItem so this "label + subtitle" pattern can be reused by other editors (styling-rules-editor, action-rules-editor, field-selector-input) without each of them importing @radix-ui/react-select directly. Not a blocker for this PR.

Also applies to: 526-540

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/widget-editor/transform-editor.tsx` at line 13, The
trigger was showing both label and description because `@neoboard/components`'
SelectItem wraps all children in ItemText; switch those option usages to use
SelectPrimitive.Item so only t.label is wrapped in ItemText for the trigger.
Update occurrences in transform-editor.tsx (the SelectItem imports/usages) to
render options with SelectPrimitive.Item (and keep the description as a sibling
span inside the Item) so the collapsed trigger shows only t.label; consider
mirroring this change for the same pattern at the other occurrences mentioned
(lines ~526-540) to keep behavior consistent.
component/src/charts/__tests__/gantt-chart.test.tsx (1)

119-134: stylingRules assertion only checks length.

The test is titled "applies styling rule color to matching tasks" but only asserts seriesData.toHaveLength(3). It won't catch regressions in the rule→color resolution path (e.g., a broken column/operator match). Consider inspecting renderItem output or the per-item color metadata attached to series[0].data[i].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/__tests__/gantt-chart.test.tsx` around lines 119 - 134,
The test currently only checks seriesData length and should verify that
stylingRules are applied to matching tasks; update the test in
gantt-chart.test.tsx to locate the data entries in optionsCall.series[0].data
that correspond to tasks with column value "Phase 2" (use the same sampleData
field name used by GanttChart) and assert those entries carry the expected color
metadata (e.g., an itemStyle.color or color property equal to "#ff0000");
reference GanttChart, stylingRules, mockSetOption, seriesData and renderItem
when locating the code paths to validate the per-item color metadata rather than
only length.
app/src/plugins/gantt/transform.ts (1)

13-18: Return type could be narrowed to GanttDataItem[].

Returning unknown pushes a cast onto every caller (see component.tsx line 32: data as GanttDataItem[]). If the plugin transform signature allows it, typing this as GanttDataItem[] (or at least Array<Record<string, unknown>>) would give the component.tsx cast something to lean on and keep TS strict mode honest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/plugins/gantt/transform.ts` around lines 13 - 18, Change the
transformToGanttData signature and its returns to a concrete array type instead
of unknown: replace the return type unknown with GanttDataItem[] (or
Array<Record<string, unknown>> if GanttDataItem isn't available), import or
define the GanttDataItem type, and ensure all early returns (e.g., when records
are empty or keys.length < 3) return an empty array of that type; update callers
(such as component.tsx) to remove unnecessary casts now that
transformToGanttData produces the proper typed array.
app/src/plugins/__tests__/plugin-options.test.ts (1)

6-24: Add gantt to the mock tables for consistency.

Two small gaps line up with the new chart type:

  1. OPTION_COUNTS (lines 6–24) has no "gantt" entry, so fakeGetChartOptions("gantt") returns []. The "every registered chart type has options array" test still passes (empty array is an array), but the gantt plugin ends up with zero options at registration time, which weakens the coverage intent of this file.
  2. The @neoboard/components mock (lines 44–62) doesn't export a GanttChart stub. No current assertion renders it (the plugin's dynamic() import is lazy), but it's inconsistent with the other chart stubs.
♻️ Proposed patch
     treemap: 7,
+    gantt: 8,
   };
   TreemapChart: Stub,
+  GanttChart: Stub,
   EmptyState: Stub,

Also applies to: 44-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/plugins/__tests__/plugin-options.test.ts` around lines 6 - 24, The
mock OPTION_COUNTS object used by fakeGetChartOptions is missing a "gantt" entry
and the `@neoboard/components` test mock doesn't export a GanttChart stub; update
OPTION_COUNTS (refer to the constant named OPTION_COUNTS) to include a "gantt":
<number> entry (choose a realistic count like 4–8) so
fakeGetChartOptions("gantt") returns the expected options array, and add a
GanttChart export to the `@neoboard/components` mock (the same module that exports
BarChart/LineChart/etc.) so the gantt plugin’s dynamic import has a matching
stub (keep the stub shape consistent with other chart stubs).
component/stories/charts/gantt-chart.stories.tsx (1)

167-172: Math.random() makes LargeDataset non-deterministic.

Every Storybook render (and snapshot / Chromatic run) will produce a different end date, which causes visual-regression flakes. A small pseudo-random seeded function or a modulo-based pattern fixes this without losing the "varied durations" feel.

♻️ Deterministic variation
     data: Array.from({ length: 30 }, (_, i) => ({
       task: `Task ${i + 1}`,
       start: day(i * 2),
-      end: day(i * 2 + Math.floor(Math.random() * 8) + 3),
+      end: day(i * 2 + ((i * 7) % 8) + 3),
       category: ["Backend", "Frontend", "DevOps", "QA"][i % 4],
     })),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/stories/charts/gantt-chart.stories.tsx` around lines 167 - 172, The
story's data array uses Math.random() to compute task end dates which makes the
LargeDataset non-deterministic; replace the Math.random() call used in the end
calculation (the Array.from(... => ({ task: `Task ${i + 1}`, start: day(...),
end: day(...), category: ... }))) with a deterministic variation—either
implement a small seeded PRNG and call it for each index or use a deterministic
formula like (i * prime) % N to produce the variable duration—so the end value
uses day(i * 2 + deterministicOffset + 3) instead of Math.random() to ensure
stable Storybook/snapshot outputs while keeping varied durations.
component/src/charts/gantt-chart.tsx (1)

253-262: Use ECharts' CustomSeriesRenderItem type instead of as never.

renderItem: renderItem as never bypasses type-checking on the handler. Replace it by importing the proper type from echarts:

import type { CustomSeriesRenderItem } from 'echarts';

Then type renderItem directly at lines 104–115 instead of casting at line 256. This matches your hand-typed signature and will catch any drift from ECharts' API on upgrades.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gantt-chart.tsx` around lines 253 - 262, The current
series config sidesteps type-checking by using "renderItem: renderItem as
never"; instead import and use ECharts' CustomSeriesRenderItem type and type the
renderItem function accordingly (the function currently defined around lines
104–115), then remove the "as never" cast in the series object (where renderItem
is assigned) so the series entry uses the properly typed renderItem; ensure you
add "import type { CustomSeriesRenderItem } from 'echarts'" and annotate the
renderItem signature with that type to catch API drift.
🤖 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/plugins/gantt/settings.ts`:
- Line 11: The barBorderRadius schema currently uses
z.coerce.number().default(2) which allows negative numbers; update the
validation for the barBorderRadius symbol to enforce non-negative values by
adding .min(0) (e.g. z.coerce.number().min(0).default(2)); optionally consider
adding an upper bound with .max(...) if you want to cap extreme values to avoid
ECharts rendering issues.

In `@app/src/plugins/gantt/transform.ts`:
- Around line 89-104: parseTime currently uses a fixed 1e12 threshold to decide
seconds vs milliseconds which misclassifies pre-2001 millisecond timestamps;
update parseTime to accept an optional unit hint (e.g., unitHint?: "seconds" |
"milliseconds") and use that when provided, otherwise keep the heuristic but add
a clear comment about its limitations; update the function signature
parseTime(value: unknown, unitHint?: "seconds" | "milliseconds") and apply
unitHint to choose whether to multiply by 1000 (if "seconds") or treat as ms (if
"milliseconds"), leaving the existing fallback heuristic only when unitHint is
absent.
- Around line 76-82: Normalize the progress value as you already do (using
progressKey and local p from row[progressKey]) but then clamp it into the 0..1
range before assigning to item.progress; i.e., after converting percentages (p >
1 ? p/100 : p) apply Math.max(0, Math.min(1, normalized)) so negatives become 0
and overly large percentages (e.g., 150) are capped at 1.

In `@component/src/charts/gantt-chart.tsx`:
- Around line 153-169: The progress overlay uses the same corner radius for all
corners (shape.r = barBorderRadius) which causes the overlay's trailing edge to
appear rounded mid-bar; update the overlay rectangle creation in the render code
that builds the overlay (the block that checks showProgress && progress > 0) to
use a corner-radius array that only rounds the leading/left corners when
progress < 1 (e.g., left-top and left-bottom = barBorderRadius, right corners =
0), and keep full rounding when progress >= 1; additionally clamp progress to
the [0,1] range once when constructing seriesData (the place that currently
feeds Math.round(Number(v[5]) * 100) in the tooltip) so both rendering and
tooltip percent calculations agree.

---

Nitpick comments:
In `@app/src/components/widget-editor/__tests__/transform-editor.test.tsx`:
- Around line 59-70: Tests currently mock `@radix-ui/react-select` primitives
(Item, ItemText, ItemIndicator) and the suite misses an assertion ensuring the
collapsed Select trigger only shows the label (not label + description); add a
focused test in transform-editor.test.tsx that renders the transform-editor,
selects the relevant item via the mocked SelectPrimitive.Item, and asserts the
selected trigger's accessible text equals "Filter" (and does not contain the
description like "Remove rows matching a condition") so regressions reverting to
wrapped SelectItem (label+description) are caught.

In `@app/src/components/widget-editor/transform-editor.tsx`:
- Line 13: The trigger was showing both label and description because
`@neoboard/components`' SelectItem wraps all children in ItemText; switch those
option usages to use SelectPrimitive.Item so only t.label is wrapped in ItemText
for the trigger. Update occurrences in transform-editor.tsx (the SelectItem
imports/usages) to render options with SelectPrimitive.Item (and keep the
description as a sibling span inside the Item) so the collapsed trigger shows
only t.label; consider mirroring this change for the same pattern at the other
occurrences mentioned (lines ~526-540) to keep behavior consistent.

In `@app/src/plugins/__tests__/plugin-options.test.ts`:
- Around line 6-24: The mock OPTION_COUNTS object used by fakeGetChartOptions is
missing a "gantt" entry and the `@neoboard/components` test mock doesn't export a
GanttChart stub; update OPTION_COUNTS (refer to the constant named
OPTION_COUNTS) to include a "gantt": <number> entry (choose a realistic count
like 4–8) so fakeGetChartOptions("gantt") returns the expected options array,
and add a GanttChart export to the `@neoboard/components` mock (the same module
that exports BarChart/LineChart/etc.) so the gantt plugin’s dynamic import has a
matching stub (keep the stub shape consistent with other chart stubs).

In `@app/src/plugins/gantt/transform.ts`:
- Around line 13-18: Change the transformToGanttData signature and its returns
to a concrete array type instead of unknown: replace the return type unknown
with GanttDataItem[] (or Array<Record<string, unknown>> if GanttDataItem isn't
available), import or define the GanttDataItem type, and ensure all early
returns (e.g., when records are empty or keys.length < 3) return an empty array
of that type; update callers (such as component.tsx) to remove unnecessary casts
now that transformToGanttData produces the proper typed array.

In `@component/src/charts/__tests__/gantt-chart.test.tsx`:
- Around line 119-134: The test currently only checks seriesData length and
should verify that stylingRules are applied to matching tasks; update the test
in gantt-chart.test.tsx to locate the data entries in optionsCall.series[0].data
that correspond to tasks with column value "Phase 2" (use the same sampleData
field name used by GanttChart) and assert those entries carry the expected color
metadata (e.g., an itemStyle.color or color property equal to "#ff0000");
reference GanttChart, stylingRules, mockSetOption, seriesData and renderItem
when locating the code paths to validate the per-item color metadata rather than
only length.

In `@component/src/charts/gantt-chart.tsx`:
- Around line 253-262: The current series config sidesteps type-checking by
using "renderItem: renderItem as never"; instead import and use ECharts'
CustomSeriesRenderItem type and type the renderItem function accordingly (the
function currently defined around lines 104–115), then remove the "as never"
cast in the series object (where renderItem is assigned) so the series entry
uses the properly typed renderItem; ensure you add "import type {
CustomSeriesRenderItem } from 'echarts'" and annotate the renderItem signature
with that type to catch API drift.

In `@component/stories/charts/gantt-chart.stories.tsx`:
- Around line 167-172: The story's data array uses Math.random() to compute task
end dates which makes the LargeDataset non-deterministic; replace the
Math.random() call used in the end calculation (the Array.from(... => ({ task:
`Task ${i + 1}`, start: day(...), end: day(...), category: ... }))) with a
deterministic variation—either implement a small seeded PRNG and call it for
each index or use a deterministic formula like (i * prime) % N to produce the
variable duration—so the end value uses day(i * 2 + deterministicOffset + 3)
instead of Math.random() to ensure stable Storybook/snapshot outputs while
keeping varied durations.
🪄 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: CHILL

Plan: Pro

Run ID: c681f0ed-0942-4698-aad3-e01d347b2fac

📥 Commits

Reviewing files that changed from the base of the PR and between b1501c5 and 1fe1df8.

📒 Files selected for processing (21)
  • app/e2e/global-setup.ts
  • app/playwright.config.ts
  • app/src/components/widget-editor/__tests__/transform-editor.test.tsx
  • app/src/components/widget-editor/transform-editor.tsx
  • app/src/lib/__tests__/plugin/chart-helpers.test.ts
  • app/src/lib/dashboard/neodash-converter.ts
  • app/src/plugins/__tests__/plugin-options.test.ts
  • app/src/plugins/chart-types.ts
  • app/src/plugins/gantt/component.tsx
  • app/src/plugins/gantt/index.ts
  • app/src/plugins/gantt/settings.ts
  • app/src/plugins/gantt/transform.ts
  • app/src/plugins/index.ts
  • app/src/plugins/transforms/index.ts
  • component/src/charts/__tests__/gantt-chart.test.tsx
  • component/src/charts/gantt-chart.tsx
  • component/src/charts/index.ts
  • component/src/components/composed/chart-options/gantt.ts
  • component/src/components/composed/chart-options/index.ts
  • component/stories/charts/gantt-chart.stories.tsx
  • component/vitest.setup.ts
✅ Files skipped from review due to trivial changes (8)
  • app/src/lib/tests/plugin/chart-helpers.test.ts
  • app/src/lib/dashboard/neodash-converter.ts
  • app/src/plugins/chart-types.ts
  • component/src/charts/index.ts
  • app/src/plugins/gantt/index.ts
  • app/src/plugins/transforms/index.ts
  • component/vitest.setup.ts
  • component/src/components/composed/chart-options/gantt.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/e2e/global-setup.ts

showTodayLine: z.boolean().default(true),
showProgress: z.boolean().default(false),
showGridLines: z.boolean().default(true),
barBorderRadius: z.coerce.number().default(2),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Zod 4 z.coerce.number() accept NaN by default?

💡 Result:

No, Zod 4's z.coerce.number does not accept NaN by default. It coerces the input using JavaScript's Number function, which preserves NaN (Number(NaN) === NaN). The resulting value then passes through z.number validation, which rejects NaN with an "Expected number, received nan" error. To accept NaN, use z.nan or chain .or(z.nan).

Citations:


Correct the borderRadius validation approach.

z.coerce.number().default(2) will accept any valid number including negatives and extreme values. However, Zod 4's z.coerce.number() actually rejects NaN by default—it's not a concern. That said, reasonable bounds prevent unexpected ECharts rendering issues. Consider adding .min(0) to disallow negative radius:

♻️ Proposed tightening
-    barBorderRadius: z.coerce.number().default(2),
+    barBorderRadius: z.coerce.number().min(0).default(2),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/plugins/gantt/settings.ts` at line 11, The barBorderRadius schema
currently uses z.coerce.number().default(2) which allows negative numbers;
update the validation for the barBorderRadius symbol to enforce non-negative
values by adding .min(0) (e.g. z.coerce.number().min(0).default(2)); optionally
consider adding an upper bound with .max(...) if you want to cap extreme values
to avoid ECharts rendering issues.

Comment thread app/src/plugins/gantt/transform.ts
Comment on lines +89 to +104
function parseTime(value: unknown): number | null {
if (value == null) return null;
if (value instanceof Date) return value.getTime();
if (typeof value === "number") {
// Heuristic: if < 1e12, assume seconds; otherwise milliseconds
return value < 1e12 ? value * 1000 : value;
}
if (typeof value === "string") {
const parsed = Date.parse(value);
if (!Number.isNaN(parsed)) return parsed;
// Try as numeric string
const num = Number(value);
if (!Number.isNaN(num)) return num < 1e12 ? num * 1000 : num;
}
return 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.

⚠️ Potential issue | 🟡 Minor

parseTime seconds-vs-ms heuristic will misclassify pre-2001 ms timestamps.

The < 1e12 threshold (Sep 2001) means any millisecond timestamp older than that gets multiplied by 1000 and lands in the far future. Almost certainly irrelevant for Gantt data in practice, but worth a comment or a different signal (e.g., column metadata / explicit unit) if you expect historical schedules. Leaving as-is is acceptable; just want it on the record.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/plugins/gantt/transform.ts` around lines 89 - 104, parseTime
currently uses a fixed 1e12 threshold to decide seconds vs milliseconds which
misclassifies pre-2001 millisecond timestamps; update parseTime to accept an
optional unit hint (e.g., unitHint?: "seconds" | "milliseconds") and use that
when provided, otherwise keep the heuristic but add a clear comment about its
limitations; update the function signature parseTime(value: unknown, unitHint?:
"seconds" | "milliseconds") and apply unitHint to choose whether to multiply by
1000 (if "seconds") or treat as ms (if "milliseconds"), leaving the existing
fallback heuristic only when unitHint is absent.

Comment on lines +153 to +169
// Progress overlay
if (showProgress && progress > 0) {
const progressWidth = width * Math.min(progress, 1);
(group.children as unknown[]).push({
type: "rect",
shape: {
x,
y,
width: progressWidth,
height: barHeight,
r: barBorderRadius,
},
style: {
fill: "rgba(255, 255, 255, 0.3)",
},
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Progress overlay rounds its trailing edge mid-bar.

The overlay rect reuses r: barBorderRadius on all four corners. When progress < 1, the overlay is narrower than the full bar, so its right-hand corners are rounded in the middle of a still-straight bar edge — the overlay visually "pulls away" from the underlying bar.

♻️ Flatten the trailing corners (or only round when progress ≈ 1)
-        (group.children as unknown[]).push({
-          type: "rect",
-          shape: {
-            x,
-            y,
-            width: progressWidth,
-            height: barHeight,
-            r: barBorderRadius,
-          },
+        const full = progress >= 1;
+        (group.children as unknown[]).push({
+          type: "rect",
+          shape: {
+            x,
+            y,
+            width: progressWidth,
+            height: barHeight,
+            // Only round the right side when the overlay spans the whole bar.
+            r: full
+              ? barBorderRadius
+              : [barBorderRadius, 0, 0, barBorderRadius],
+          },

Also worth noting: Math.round(Number(v[5]) * 100) in the tooltip (Line 209) will happily print 150% if a caller passes progress > 1. Consider clamping progress to [0, 1] once when building seriesData so both the renderer and the tooltip agree.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gantt-chart.tsx` around lines 153 - 169, The progress
overlay uses the same corner radius for all corners (shape.r = barBorderRadius)
which causes the overlay's trailing edge to appear rounded mid-bar; update the
overlay rectangle creation in the render code that builds the overlay (the block
that checks showProgress && progress > 0) to use a corner-radius array that only
rounds the leading/left corners when progress < 1 (e.g., left-top and
left-bottom = barBorderRadius, right corners = 0), and keep full rounding when
progress >= 1; additionally clamp progress to the [0,1] range once when
constructing seriesData (the place that currently feeds Math.round(Number(v[5])
* 100) in the tooltip) so both rendering and tooltip percent calculations agree.

Comment thread component/src/charts/gantt-chart.tsx
alfredorubin96 and others added 5 commits April 24, 2026 13:55
- Add page 18 (Gantt) to chart-gallery.json demo showcase
- Add E2E creation flow test for Gantt in new-charts.spec.ts
- Add vertical Y-axis dataZoom for task lists with 15+ items —
  shows a scrollbar slider on the right edge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rules with a `column` field now match against that exact data property
instead of only checking category/task. Rules without `column` fall back
to category then task name for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converter improvements:
- Map graph3d/3d-graph → graph (was falling back to json)
- Map circle_packing → sunburst (same hierarchical data)
- Map choropleth/areamap → map (best-effort, point markers)
- Preserve report.title → widget settings.title (was dropped)

Test coverage (21 → 50 tests):
- All new type mappings (gantt, graph3d, circle_packing, choropleth, areamap)
- Widget title preservation (present, multiple, empty)
- Multiple parameter conversion in single query
- Non-neodash parameters left unchanged
- Degraded type conversions documented in test names

E2E fixture:
- Added gantt and graph3d widgets to neodash-sample.json (4 → 6 widgets)
- Updated portability spec to verify 6 widgets render after import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
global-setup.ts:
- Guard E2E_SKIP_BUILD: throw if set without .next/BUILD_ID
- Use static execSync import instead of dynamic await import()

gauge-chart.tsx:
- Use threshold zone color for progress arc when custom zones are set

sunburst-chart.tsx:
- Treat maxLabelDepth=0 as auto (default 2) instead of "show nothing"

gantt/transform.ts:
- Clamp progress to [0, 1] after normalization
- Document parseTime seconds-vs-ms heuristic limitation

gantt-chart.tsx:
- Fix progress overlay corner radius: only round right side when full
- Use "inherit" for today-line label color (adapts to dark mode)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ChartType union includes "gantt" but the chartTypeIcons map was
missing the entry, causing a TypeScript error on build. Added the
GanttChart icon from lucide-react.

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

@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: 3

♻️ Duplicate comments (1)
component/src/charts/gantt-chart.tsx (1)

113-123: ⚠️ Potential issue | 🟡 Minor

Clamp progress when building seriesData, not only at render time.

item.progress ?? 0 is stored raw, while only the overlay width clamps via Math.min(progress, 1) (Line 177). The tooltip at Line 235 reuses v[5] verbatim, so a caller passing progress = 1.5 will render correctly-sized bars but a cheerful Progress: 150% in the tooltip. Clamp once here and both sites stay in sync.

♻️ Proposed fix
     const seriesData = data.map((item, i) => ({
       value: [
         i,
         item.start,
         item.end,
         item.end - item.start,
         item.category ?? "",
-        item.progress ?? 0,
+        Math.max(0, Math.min(item.progress ?? 0, 1)),
       ],
       itemStyle: resolvedColors[i] ? { color: resolvedColors[i] } : undefined,
     }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gantt-chart.tsx` around lines 113 - 123, When building
seriesData in the data.map callback, clamp the progress value before storing it
(replace item.progress ?? 0 with a clamped value like Math.max(0,
Math.min(item.progress ?? 0, 1))) so the stored v[5] used by both the rendered
overlay and the tooltip stays consistent; update the seriesData construction
(the map creating value: [..., item.progress ?? 0, ...]) to use the clamped
progress and leave resolvedColors/itemStyle unchanged.
🧹 Nitpick comments (2)
component/src/charts/gauge-chart.tsx (1)

109-111: Prefer a typed ECharts color over as never.

The double as never cast bypasses the type system instead of satisfying it. ECharts' lineStyle.color accepts string | [number, string][], so a narrower type (or a shared alias) is cleaner and safer against future refactors.

♻️ Suggested tweak
-    const trackColor = hasCustomZones
-      ? (thresholdZones as never)
-      : ([[1, "rgba(140, 140, 140, 0.15)"]] as never);
+    const trackColor: [number, string][] = hasCustomZones
+      ? thresholdZones
+      : [[1, "rgba(140, 140, 140, 0.15)"]];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gauge-chart.tsx` around lines 109 - 111, The code uses
"as never" to silence the type system for trackColor; instead declare/use a
proper ECharts color type (e.g., type EChartsColor = string | [number,
string][]) and apply it to trackColor and/or thresholdZones so the union
expression matches ECharts' lineStyle.color; update the expression in
gauge-chart.tsx that sets trackColor (referencing trackColor, hasCustomZones,
thresholdZones) to use this EChartsColor type or cast to EChartsColor rather
than using "as never", or annotate the original thresholdZones declaration with
EChartsColor so no unsafe cast is needed.
app/src/plugins/gantt/transform.ts (1)

13-13: Return type unknown leaks heuristics to every caller.

transformToGanttData has well-defined shape ({ task, start, end, category?, progress? }[]). Returning unknown forces consumers (component.tsx, transforms barrel) to re-assert/cast. Consider exposing a GanttDatum type so the Gantt component gets compile-time safety on start/end/progress.

♻️ Proposed tweak
+export interface GanttDatum {
+  task: string;
+  start: number;
+  end: number;
+  category?: string;
+  progress?: number;
+}
+
-export function transformToGanttData(data: unknown): unknown {
+export function transformToGanttData(data: unknown): GanttDatum[] {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/plugins/gantt/transform.ts` at line 13, The function
transformToGanttData currently returns unknown and should instead return a
well-typed array; define a GanttDatum type/interface with required fields {
task: string; start: Date | string; end: Date | string; category?: string;
progress?: number } and change transformToGanttData signature to return
GanttDatum[]; update any callers (component.tsx and the transforms barrel) to
use GanttDatum instead of casting, and ensure any internal parsing/conversion
within transformToGanttData produces values matching those types (e.g., convert
start/end to Date or a consistent string format).
🤖 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/e2e/global-setup.ts`:
- Around line 272-297: The code currently treats process.env.E2E_SKIP_BUILD as a
boolean which makes values like "0" or "false" still count as truthy and skip
the build; update the checks around process.env.E2E_SKIP_BUILD (the block that
decides whether to skip or run execSync("npx next build", { cwd: appDir, stdio:
"inherit", env: serverEnv })) to explicitly parse/normalize the env var (e.g.
treat "0", "false", "no" as falsy and "1", "true", "yes" as truthy) before
branching; use the normalized boolean in both the initial hasCachedBuild guard
and the later if/else that logs skipping vs building so a user setting
E2E_SKIP_BUILD=0 will not skip the rebuild.

In `@component/src/charts/gantt-chart.tsx`:
- Around line 224-237: The tooltip formatter can call echarts.format.encodeHTML
with an undefined name when v[0] is out of range; in the formatter function
(formatter: (params: unknown) => { ... }) validate that params.value is an array
and that taskIndex = Number(v[0]) is an integer inside the bounds of taskNames,
then derive name = taskNames[taskIndex] ?? 'Unknown task' (or empty string)
before calling echarts.format.encodeHTML; also guard accesses to v[1..5] (coerce
to numbers or provide safe defaults) so formatDuration(v[3]) and progress
calculation never receive undefined.

In `@component/src/charts/gauge-chart.tsx`:
- Around line 102-105: The lookup for thresholdColor assumes thresholdZones are
ordered but parseGaugeThresholdZones preserves user order; to fix, ensure the
zones used for color selection are sorted ascending by their stop value before
performing the find — e.g., sort the thresholdZones array (the one used in
gauge-chart.tsx where thresholdColor is computed) by the numeric stop ([stop])
so thresholdZones.find(([stop]) => normalizedValue <= stop) yields the correct
zone; alternatively, enforce sorting inside parseGaugeThresholdZones so any
consumer (including thresholdColor computation) receives zones in ascending
order.

---

Duplicate comments:
In `@component/src/charts/gantt-chart.tsx`:
- Around line 113-123: When building seriesData in the data.map callback, clamp
the progress value before storing it (replace item.progress ?? 0 with a clamped
value like Math.max(0, Math.min(item.progress ?? 0, 1))) so the stored v[5] used
by both the rendered overlay and the tooltip stays consistent; update the
seriesData construction (the map creating value: [..., item.progress ?? 0, ...])
to use the clamped progress and leave resolvedColors/itemStyle unchanged.

---

Nitpick comments:
In `@app/src/plugins/gantt/transform.ts`:
- Line 13: The function transformToGanttData currently returns unknown and
should instead return a well-typed array; define a GanttDatum type/interface
with required fields { task: string; start: Date | string; end: Date | string;
category?: string; progress?: number } and change transformToGanttData signature
to return GanttDatum[]; update any callers (component.tsx and the transforms
barrel) to use GanttDatum instead of casting, and ensure any internal
parsing/conversion within transformToGanttData produces values matching those
types (e.g., convert start/end to Date or a consistent string format).

In `@component/src/charts/gauge-chart.tsx`:
- Around line 109-111: The code uses "as never" to silence the type system for
trackColor; instead declare/use a proper ECharts color type (e.g., type
EChartsColor = string | [number, string][]) and apply it to trackColor and/or
thresholdZones so the union expression matches ECharts' lineStyle.color; update
the expression in gauge-chart.tsx that sets trackColor (referencing trackColor,
hasCustomZones, thresholdZones) to use this EChartsColor type or cast to
EChartsColor rather than using "as never", or annotate the original
thresholdZones declaration with EChartsColor so no unsafe cast is needed.
🪄 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: CHILL

Plan: Pro

Run ID: bf0e6cc0-f63a-4e5a-8614-a84f991b5093

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe1df8 and 3a5db1d.

📒 Files selected for processing (12)
  • app/e2e/dashboard-portability.spec.ts
  • app/e2e/fixtures/imports/neodash-sample.json
  • app/e2e/global-setup.ts
  • app/e2e/new-charts.spec.ts
  • app/src/components/widget-editor/chart-type-selector.tsx
  • app/src/lib/__tests__/dashboard/neodash-converter.test.ts
  • app/src/lib/dashboard/neodash-converter.ts
  • app/src/plugins/gantt/transform.ts
  • component/src/charts/gantt-chart.tsx
  • component/src/charts/gauge-chart.tsx
  • component/src/charts/sunburst-chart.tsx
  • scripts/demo/chart-gallery.json
✅ Files skipped from review due to trivial changes (1)
  • app/src/components/widget-editor/chart-type-selector.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/lib/dashboard/neodash-converter.ts
  • scripts/demo/chart-gallery.json

Comment thread app/e2e/global-setup.ts
Comment on lines +272 to +297
if (process.env.E2E_SKIP_BUILD && !hasCachedBuild) {
throw new Error(
"E2E_SKIP_BUILD is set but no prior build found at .next/BUILD_ID. " +
"Run `npx next build` once or unset E2E_SKIP_BUILD.",
);
}

if (process.env.E2E_SKIP_BUILD) {
console.log(
"⏩ Skipping build (E2E_SKIP_BUILD set, reusing existing .next)",
);
} else {
if (hasCachedBuild) {
console.log(
"⏳ Rebuilding Next.js (production)... (set E2E_SKIP_BUILD=1 to reuse previous build)",
);
} else {
console.log("⏳ Building Next.js (production)...");
}
execSync("npx next build", {
cwd: appDir,
stdio: "inherit",
env: serverEnv,
});
console.log("✅ Next.js build complete");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

E2E_SKIP_BUILD=0 is truthy and will still skip the build.

process.env.E2E_SKIP_BUILD is a string, so E2E_SKIP_BUILD=0 / false / "" (unset via export then overridden) all evaluate truthy (except ""). A contributor who flips this to 0 expecting "off" will unintentionally skip the rebuild and quietly run stale bundles in E2E. Consider parsing it explicitly.

♻️ Proposed tweak
-  if (process.env.E2E_SKIP_BUILD && !hasCachedBuild) {
+  const skipBuild = /^(1|true|yes)$/i.test(process.env.E2E_SKIP_BUILD ?? "");
+
+  if (skipBuild && !hasCachedBuild) {
     throw new Error(
       "E2E_SKIP_BUILD is set but no prior build found at .next/BUILD_ID. " +
         "Run `npx next build` once or unset E2E_SKIP_BUILD.",
     );
   }
 
-  if (process.env.E2E_SKIP_BUILD) {
+  if (skipBuild) {
     console.log(
       "⏩ Skipping build (E2E_SKIP_BUILD set, reusing existing .next)",
     );
   } else {
📝 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
if (process.env.E2E_SKIP_BUILD && !hasCachedBuild) {
throw new Error(
"E2E_SKIP_BUILD is set but no prior build found at .next/BUILD_ID. " +
"Run `npx next build` once or unset E2E_SKIP_BUILD.",
);
}
if (process.env.E2E_SKIP_BUILD) {
console.log(
"⏩ Skipping build (E2E_SKIP_BUILD set, reusing existing .next)",
);
} else {
if (hasCachedBuild) {
console.log(
"⏳ Rebuilding Next.js (production)... (set E2E_SKIP_BUILD=1 to reuse previous build)",
);
} else {
console.log("⏳ Building Next.js (production)...");
}
execSync("npx next build", {
cwd: appDir,
stdio: "inherit",
env: serverEnv,
});
console.log("✅ Next.js build complete");
}
const skipBuild = /^(1|true|yes)$/i.test(process.env.E2E_SKIP_BUILD ?? "");
if (skipBuild && !hasCachedBuild) {
throw new Error(
"E2E_SKIP_BUILD is set but no prior build found at .next/BUILD_ID. " +
"Run `npx next build` once or unset E2E_SKIP_BUILD.",
);
}
if (skipBuild) {
console.log(
"⏩ Skipping build (E2E_SKIP_BUILD set, reusing existing .next)",
);
} else {
if (hasCachedBuild) {
console.log(
"⏳ Rebuilding Next.js (production)... (set E2E_SKIP_BUILD=1 to reuse previous build)",
);
} else {
console.log("⏳ Building Next.js (production)...");
}
execSync("npx next build", {
cwd: appDir,
stdio: "inherit",
env: serverEnv,
});
console.log("✅ Next.js build complete");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/e2e/global-setup.ts` around lines 272 - 297, The code currently treats
process.env.E2E_SKIP_BUILD as a boolean which makes values like "0" or "false"
still count as truthy and skip the build; update the checks around
process.env.E2E_SKIP_BUILD (the block that decides whether to skip or run
execSync("npx next build", { cwd: appDir, stdio: "inherit", env: serverEnv }))
to explicitly parse/normalize the env var (e.g. treat "0", "false", "no" as
falsy and "1", "true", "yes" as truthy) before branching; use the normalized
boolean in both the initial hasCachedBuild guard and the later if/else that logs
skipping vs building so a user setting E2E_SKIP_BUILD=0 will not skip the
rebuild.

Comment on lines +224 to +237
formatter: (params: unknown) => {
const p = params as { value: number[] };
const v = p.value;
const name = taskNames[v[0]];
const start = new Date(v[1]).toLocaleDateString();
const end = new Date(v[2]).toLocaleDateString();
const duration = formatDuration(v[3]);
const category = v[4]
? `<br/>Category: ${echarts.format.encodeHTML(String(v[4]))}`
: "";
const progress =
v[5] > 0 ? `<br/>Progress: ${Math.round(Number(v[5]) * 100)}%` : "";
return `<strong>${echarts.format.encodeHTML(name)}</strong><br/>${start} → ${end} (${duration})${category}${progress}`;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against out-of-range taskIndex in the tooltip.

If v[0] ever falls outside taskNames (e.g., stale data frame), name is undefined and echarts.format.encodeHTML(name) is called on a non-string. Cheap fallback:

♻️ Proposed fix
-          const name = taskNames[v[0]];
+          const name = taskNames[v[0]] ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gantt-chart.tsx` around lines 224 - 237, The tooltip
formatter can call echarts.format.encodeHTML with an undefined name when v[0] is
out of range; in the formatter function (formatter: (params: unknown) => { ...
}) validate that params.value is an array and that taskIndex = Number(v[0]) is
an integer inside the bounds of taskNames, then derive name =
taskNames[taskIndex] ?? 'Unknown task' (or empty string) before calling
echarts.format.encodeHTML; also guard accesses to v[1..5] (coerce to numbers or
provide safe defaults) so formatDuration(v[3]) and progress calculation never
receive undefined.

Comment on lines +102 to +105
const thresholdColor =
hasCustomZones && normalizedValue !== undefined
? thresholdZones.find(([stop]) => normalizedValue <= stop)?.[1]
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Threshold-zone lookup silently assumes ascending order.

parseGaugeThresholdZones returns zones in the same order the user authored them (no sort), so thresholdZones.find(([stop]) => normalizedValue <= stop) will pick the wrong color if a user configures zones out of order (e.g. [{value:90,color:"red"},{value:50,color:"yellow"}] → everything ≤ 90 resolves to red). ECharts' own axisLine rendering has the same sensitivity, but there the mis-color is at least consistent with the progress arc; here the two can disagree visually only in edge cases — worth either sorting once, or documenting the contract.

🛠️ Suggested hardening
     const thresholdZones = parseGaugeThresholdZones(
       thresholdZonesJson,
       min,
       max,
-    ) as [number, string][];
+    ).slice().sort((a, b) => a[0] - b[0]) as [number, string][];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@component/src/charts/gauge-chart.tsx` around lines 102 - 105, The lookup for
thresholdColor assumes thresholdZones are ordered but parseGaugeThresholdZones
preserves user order; to fix, ensure the zones used for color selection are
sorted ascending by their stop value before performing the find — e.g., sort the
thresholdZones array (the one used in gauge-chart.tsx where thresholdColor is
computed) by the numeric stop ([stop]) so thresholdZones.find(([stop]) =>
normalizedValue <= stop) yields the correct zone; alternatively, enforce sorting
inside parseGaugeThresholdZones so any consumer (including thresholdColor
computation) receives zones in ascending order.

Fixes TS6133 "'params' is declared but its value is never read" in CI
type-check step. The params argument is required by ECharts renderItem
signature but unused in our implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alfredo1996
alfredo1996 merged commit 848a19d into release/2.0 Apr 24, 2026
9 of 11 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
42.3% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@alfredo1996
alfredo1996 deleted the chore/chart-polish-gauge-sunburst branch May 16, 2026 17:47
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.

2 participants