create-yss-spec 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.agents/skills/high-fidelity-html-prototype/SKILL.md +102 -0
- package/template/.codex/skills/data-analytics/.app.json +84 -0
- package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +66 -0
- package/template/.codex/skills/data-analytics/.mcp.json +30 -0
- package/template/.codex/skills/data-analytics/AGENTS.md +20 -0
- package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +27 -0
- package/template/.codex/skills/data-analytics/README.md +60 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-small.svg +5 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
- package/template/.codex/skills/data-analytics/assets/datascience.svg +10 -0
- package/template/.codex/skills/data-analytics/mcp/server.cjs +2963 -0
- package/template/.codex/skills/data-analytics/package-lock.json +3048 -0
- package/template/.codex/skills/data-analytics/package.json +43 -0
- package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +75 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +160 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +148 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +18 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +24 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +71 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +85 -0
- package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +207 -0
- package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +70 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +66 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +118 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +1 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +57 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +61 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +362 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +829 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +30 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +129 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +374 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +613 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +54 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +16 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +9 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +78 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +2379 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +88 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +97 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +59 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +75 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +103 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +68 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/index/SKILL.md +251 -0
- package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +131 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +32 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +106 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +130 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +178 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +9 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +31 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +466 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +99 -0
- package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +195 -0
- package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +26 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +51 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +66 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +69 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +204 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +87 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +497 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +74 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +65 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +160 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +75 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +96 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +263 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +1460 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +128 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +101 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +499 -0
- package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +978 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +190 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +157 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +774 -0
- package/template/.codex/skills/data-analytics/src/DESIGN.md +222 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +3666 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +136 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +54 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +96 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +1647 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +245 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +462 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +107 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +192 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +203 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +619 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +402 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +370 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +536 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +681 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +21 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +472 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +75 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +377 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +3468 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +44 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +639 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +327 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +225 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +204 -0
- package/template/.codex/skills/data-analytics/src/analytics-app-core.md +85 -0
- package/template/.codex/skills/data-analytics/src/codex-style-contract.md +30 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +12 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +1008 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +2117 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +122 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +3588 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +483 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +32 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +333 -0
- package/template/.codex/skills/data-analytics/src/mcp-host.js +153 -0
- package/template/.codex/skills/data-analytics/src/recharts-config.js +315 -0
- package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +294 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.css +33 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.js +157 -0
- package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +121 -0
- package/template/.codex/skills/data-analytics/src/table-renderer.jsx +28 -0
- package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +87 -0
- package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +105 -0
- package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +233 -0
- package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +1500 -0
- package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +219 -0
- package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +144 -0
- package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +11 -0
- package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +54 -0
- package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +321 -0
- package/template/.codex/skills/data-analytics/tsconfig.json +19 -0
- package/template/.codex/skills/data-analytics/vite.config.ts +46 -0
- package/template/.codex/skills/high-fidelity-html-prototype/SKILL.md +102 -0
- package/template/.codex/skills/high-fidelity-html-prototype/agents/openai.yaml +5 -0
- package/template/.codex/skills/product-design/.app.json +7 -0
- package/template/.codex/skills/product-design/.codex-plugin/plugin.json +50 -0
- package/template/.codex/skills/product-design/README.md +50 -0
- package/template/.codex/skills/product-design/agents/openai.yaml +4 -0
- package/template/.codex/skills/product-design/assets/composerIcon.svg +9 -0
- package/template/.codex/skills/product-design/assets/logo.png +0 -0
- package/template/.codex/skills/product-design/package.json +11 -0
- package/template/.codex/skills/product-design/references/browser-order.md +9 -0
- package/template/.codex/skills/product-design/references/communication-protocol.md +45 -0
- package/template/.codex/skills/product-design/references/critical-overrides.md +58 -0
- package/template/.codex/skills/product-design/references/local-prototype-preflight.md +16 -0
- package/template/.codex/skills/product-design/scripts/bootstrap-prototype.mjs +147 -0
- package/template/.codex/skills/product-design/skills/audit/SKILL.md +159 -0
- package/template/.codex/skills/product-design/skills/audit/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/audit/references/design-audit-framework.md +73 -0
- package/template/.codex/skills/product-design/skills/design-qa/SKILL.md +133 -0
- package/template/.codex/skills/product-design/skills/design-qa/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/design-qa/references/qa-rubric.md +61 -0
- package/template/.codex/skills/product-design/skills/get-context/SKILL.md +81 -0
- package/template/.codex/skills/product-design/skills/get-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/ideate/SKILL.md +179 -0
- package/template/.codex/skills/product-design/skills/ideate/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/image-to-code/SKILL.md +105 -0
- package/template/.codex/skills/product-design/skills/image-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/index/SKILL.md +126 -0
- package/template/.codex/skills/product-design/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/SKILL.md +128 -0
- package/template/.codex/skills/product-design/skills/prototype/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/references/existing-codebase-edits.md +18 -0
- package/template/.codex/skills/product-design/skills/research/SKILL.md +92 -0
- package/template/.codex/skills/product-design/skills/research/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/share/SKILL.md +41 -0
- package/template/.codex/skills/product-design/skills/share/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/url-to-code/SKILL.md +124 -0
- package/template/.codex/skills/product-design/skills/url-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/SKILL.md +147 -0
- package/template/.codex/skills/product-design/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/plugin-author-config/user-context-template.md +63 -0
- package/template/.codex/skills/product-design/skills/user-context/references/onboarding.md +66 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/init_user_context.py +86 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/user_context_preflight.py +204 -0
- package/template/.codex/skills/product-design/templates/prototype/AGENTS.md +7 -0
- package/template/.codex/skills/product-design/templates/prototype/index.html +12 -0
- package/template/.codex/skills/product-design/templates/prototype/package.json +18 -0
- package/template/.codex/skills/product-design/templates/prototype/src/App.jsx +5 -0
- package/template/.codex/skills/product-design/templates/prototype/src/main.jsx +10 -0
- package/template/.codex/skills/product-design/templates/prototype/src/styles.css +13 -0
- package/template/.codex/skills/product-design/templates/prototype/vite.config.mjs +14 -0
- package/template/.codex/skills/product-design-prototype/SKILL.md +2 -4
- package/template/.codex/skills/prototype-review/SKILL.md +7 -7
- package/template/.codex/skills/yss-design-system/SKILL.md +4 -2
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +10 -7
- package/template/.codex/skills/yss-product-lifecycle/references/artifact-checklist.md +1 -0
- package/template/.codex/skills/yss-product-lifecycle/references/stage-routing.md +1 -1
- package/template/.codex/skills/yss-ui/SKILL.md +4 -10
- package/template/.codex/skills/yss-ui/references/quick-recipes.md +1 -1
- package/template/AGENTS.md +9 -3
- package/template/CONTEXT.md +1 -0
- package/template/docs/adr/adr-product-design-prototype-entrypoint.md +54 -0
- package/template/docs/api/templates/openapi-draft-review-checklist.md +5 -1
- package/template/docs/design/README.md +13 -6
- package/template/docs/design/prototypes/.gitkeep +1 -0
- package/template/docs/design/templates/interaction-spec-template.md +8 -3
- package/template/docs/design/templates/product-overview-design-template.md +34 -5
- package/template/docs/design/templates/prototype-confirmation-template.md +57 -0
- package/template/docs/design/templates/prototype-review-checklist.md +7 -3
- package/template/docs/design/templates/state-matrix-template.md +1 -1
- package/template/docs/process/harness-executive-blueprint.md +6 -1
- package/template/docs/process/harness-process-tailoring.md +3 -3
- package/template/docs/process/harness-work-unit-map.md +1 -1
- package/template/docs/process/lifecycle-artifact-map.md +7 -5
- package/template/docs/process/product-prototype-workflow.md +82 -0
- package/template/docs/templates/requirement-freeze-template.md +4 -3
- package/template/docs/user-guide/product-lifecycle-workflow.md +2 -2
- package/template/docs/user-guide/product-rd-lifecycle-best-practices.md +54 -24
- package/template/.codex/skills/component-story-prototype/SKILL.md +0 -55
- package/template/.codex/skills/component-story-prototype/agents/openai.yaml +0 -4
- package/template/.codex/skills/mock-api-prototype/SKILL.md +0 -48
- package/template/.codex/skills/mock-api-prototype/agents/openai.yaml +0 -4
|
@@ -0,0 +1,1647 @@
|
|
|
1
|
+
import { Fragment, type CSSProperties, type ReactNode, type SVGProps } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Area,
|
|
4
|
+
AreaChart,
|
|
5
|
+
Bar,
|
|
6
|
+
BarChart,
|
|
7
|
+
CartesianGrid,
|
|
8
|
+
Cell,
|
|
9
|
+
Funnel,
|
|
10
|
+
FunnelChart,
|
|
11
|
+
LabelList,
|
|
12
|
+
Line,
|
|
13
|
+
LineChart,
|
|
14
|
+
Pie,
|
|
15
|
+
PieChart,
|
|
16
|
+
ReferenceLine,
|
|
17
|
+
ResponsiveContainer,
|
|
18
|
+
Scatter,
|
|
19
|
+
ScatterChart,
|
|
20
|
+
Tooltip,
|
|
21
|
+
XAxis,
|
|
22
|
+
YAxis,
|
|
23
|
+
ZAxis,
|
|
24
|
+
} from "recharts";
|
|
25
|
+
|
|
26
|
+
import type { ChartSeriesSpec, ChartSpec, ChartSurface, ChartValueLabelMode } from "./chart-contract";
|
|
27
|
+
import { ChartFrame } from "./ChartFrame";
|
|
28
|
+
import { BoxPlotTooltip, ChartTooltip, type ChartTooltipPayloadItem, WaterfallTooltip } from "./ChartTooltip";
|
|
29
|
+
import {
|
|
30
|
+
asFiniteNumber,
|
|
31
|
+
asNumber,
|
|
32
|
+
buildBoxPlotRows,
|
|
33
|
+
buildFunnelRows,
|
|
34
|
+
buildPieRows,
|
|
35
|
+
buildWaterfallRows,
|
|
36
|
+
chartEmptyMessage,
|
|
37
|
+
clamp,
|
|
38
|
+
formatDateAxisLabel,
|
|
39
|
+
formatValue,
|
|
40
|
+
getBoxPlotXAxisScale,
|
|
41
|
+
getDateAxisTicks,
|
|
42
|
+
getYAxisScaleForSeries,
|
|
43
|
+
type ChartDataRow,
|
|
44
|
+
type WaterfallDataRow,
|
|
45
|
+
} from "./chart-transforms";
|
|
46
|
+
import {
|
|
47
|
+
colorForSeries,
|
|
48
|
+
FUNNEL_STAGE_COLORS,
|
|
49
|
+
HEATMAP_BLUE_SCALE,
|
|
50
|
+
isHorizontalChartType,
|
|
51
|
+
isNormalizedStackedChartType,
|
|
52
|
+
isStackedChartType,
|
|
53
|
+
} from "./chart-theme";
|
|
54
|
+
import { chartEncodingField, rechartsChartFromEncodedSpec } from "./chart-app-helpers";
|
|
55
|
+
|
|
56
|
+
const AXIS_LABEL_GAP = 8;
|
|
57
|
+
const RECHARTS_AXIS_TICK_SIZE = 6;
|
|
58
|
+
const RECHARTS_AXIS_TICK_MARGIN = AXIS_LABEL_GAP - RECHARTS_AXIS_TICK_SIZE;
|
|
59
|
+
const Y_AXIS_TICK_LABEL_OVERHANG = 8;
|
|
60
|
+
const CHART_COMPACT_HEIGHT = 240;
|
|
61
|
+
const CHART_CARD_DEFAULT_HEIGHT = 320;
|
|
62
|
+
const CHART_CARD_ROTATED_CATEGORY_HEIGHT = 400;
|
|
63
|
+
const CHART_FULLSCREEN_HEIGHT = 520;
|
|
64
|
+
const CHART_STATIC_WIDTH = 760;
|
|
65
|
+
const CHART_MIN_PLOT_HEIGHT = 180;
|
|
66
|
+
const CHART_LEGEND_RESERVED_HEIGHT = 28;
|
|
67
|
+
const CHART_HORIZONTAL_BAR_ROW_HEIGHT = 28;
|
|
68
|
+
const COMPLEX_CHART_GRID_GAP = 4;
|
|
69
|
+
const COMPLEX_CHART_MIN_ROW_HEIGHT = 28;
|
|
70
|
+
const HEATMAP_HEADER_ROW_HEIGHT = 24;
|
|
71
|
+
const FUNNEL_CORNER_RADIUS = 7;
|
|
72
|
+
const FUNNEL_LABEL_GAP = 6;
|
|
73
|
+
const FUNNEL_SEGMENT_GAP = 4;
|
|
74
|
+
const SCATTER_AUTO_LABEL_LIMIT = 20;
|
|
75
|
+
const CATEGORY_X_AXIS_LABEL_ROTATION_DEGREES = -40;
|
|
76
|
+
const CATEGORY_X_AXIS_LONG_LABEL_LENGTH = 16;
|
|
77
|
+
const CATEGORY_X_AXIS_LONG_AVERAGE_LABEL_LENGTH = 11;
|
|
78
|
+
const CATEGORY_X_AXIS_ROTATED_HEIGHT = 82;
|
|
79
|
+
const CATEGORY_X_AXIS_ROTATED_HEIGHT_WITH_TITLE = 96;
|
|
80
|
+
const CATEGORY_X_AXIS_ROTATED_TICK_MARGIN = 10;
|
|
81
|
+
const CATEGORY_X_AXIS_WRAPPED_HEIGHT = 38;
|
|
82
|
+
const CATEGORY_X_AXIS_WRAPPED_HEIGHT_WITH_TITLE = 48;
|
|
83
|
+
const CATEGORY_X_AXIS_WRAPPED_TICK_MARGIN = 6;
|
|
84
|
+
const CATEGORY_X_AXIS_MIN_LINE_CHARACTERS = 8;
|
|
85
|
+
const CATEGORY_X_AXIS_ESTIMATED_CHAR_WIDTH = 7;
|
|
86
|
+
const CATEGORY_X_AXIS_LABEL_SLOT_GAP = 16;
|
|
87
|
+
const BAR_AUTO_VALUE_LABEL_LIMIT = 8;
|
|
88
|
+
const BAR_VALUE_LABEL_OFFSET = 8;
|
|
89
|
+
const BAR_NEGATIVE_VALUE_LABEL_OFFSET = 18;
|
|
90
|
+
const BAR_VALUE_LABEL_HORIZONTAL_GUTTER = 56;
|
|
91
|
+
const BAR_VALUE_LABEL_VERTICAL_GUTTER = 34;
|
|
92
|
+
const BAR_NEGATIVE_VALUE_LABEL_AXIS_GUTTER = 40;
|
|
93
|
+
|
|
94
|
+
type AxisTickCoordinate = number | string;
|
|
95
|
+
|
|
96
|
+
type AxisTickProps = {
|
|
97
|
+
payload?: { value?: unknown };
|
|
98
|
+
textAnchor?: "end" | "inherit" | "middle" | "start";
|
|
99
|
+
x?: AxisTickCoordinate;
|
|
100
|
+
y?: AxisTickCoordinate;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type WaterfallBarShapeProps = {
|
|
104
|
+
height?: number | string;
|
|
105
|
+
isActive?: boolean;
|
|
106
|
+
parentViewBox?: { width?: number };
|
|
107
|
+
payload?: WaterfallDataRow;
|
|
108
|
+
width?: number | string;
|
|
109
|
+
x?: number | string;
|
|
110
|
+
y?: number | string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type SvgPoint = {
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type FunnelShapeProps = {
|
|
119
|
+
className?: string;
|
|
120
|
+
fill?: string;
|
|
121
|
+
height?: number | string;
|
|
122
|
+
labelViewBox?: FunnelViewBox;
|
|
123
|
+
lowerWidth?: number | string;
|
|
124
|
+
name?: unknown;
|
|
125
|
+
option?: unknown;
|
|
126
|
+
parentViewBox?: unknown;
|
|
127
|
+
payload?: ChartDataRow;
|
|
128
|
+
shapeType?: unknown;
|
|
129
|
+
stroke?: string;
|
|
130
|
+
strokeWidth?: number | string;
|
|
131
|
+
tooltipPayload?: unknown;
|
|
132
|
+
tooltipPosition?: unknown;
|
|
133
|
+
upperWidth?: number | string;
|
|
134
|
+
val?: unknown;
|
|
135
|
+
value?: unknown;
|
|
136
|
+
width?: number | string;
|
|
137
|
+
x?: number | string;
|
|
138
|
+
y?: number | string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type FunnelViewBox = {
|
|
142
|
+
height?: number | string;
|
|
143
|
+
lowerWidth?: number | string;
|
|
144
|
+
upperWidth?: number | string;
|
|
145
|
+
width?: number | string;
|
|
146
|
+
x?: number | string;
|
|
147
|
+
y?: number | string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type FunnelLabelProps = {
|
|
151
|
+
height?: number | string;
|
|
152
|
+
payload?: ChartDataRow;
|
|
153
|
+
value?: unknown;
|
|
154
|
+
viewBox?: {
|
|
155
|
+
height?: number | string;
|
|
156
|
+
width?: number | string;
|
|
157
|
+
x?: number | string;
|
|
158
|
+
y?: number | string;
|
|
159
|
+
};
|
|
160
|
+
width?: number | string;
|
|
161
|
+
x?: number | string;
|
|
162
|
+
y?: number | string;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
type BarValueLabelProps = {
|
|
166
|
+
height?: number | string;
|
|
167
|
+
value?: unknown;
|
|
168
|
+
width?: number | string;
|
|
169
|
+
x?: number | string;
|
|
170
|
+
y?: number | string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type BarValueLabelSides = {
|
|
174
|
+
hasNegative: boolean;
|
|
175
|
+
hasNonNegative: boolean;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type BarSignState = {
|
|
179
|
+
hasNegative: boolean;
|
|
180
|
+
hasPositive: boolean;
|
|
181
|
+
hasZero: boolean;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function chartHeightForSurface(surface: ChartSurface, height?: number | string): number | string | undefined {
|
|
185
|
+
if (height != null) return height;
|
|
186
|
+
if (surface === "explorer") return CHART_FULLSCREEN_HEIGHT;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function chartCardHeightForContent({
|
|
191
|
+
hasLegend,
|
|
192
|
+
horizontal,
|
|
193
|
+
rotateCategoryXAxisLabels,
|
|
194
|
+
rowCount,
|
|
195
|
+
}: {
|
|
196
|
+
hasLegend: boolean;
|
|
197
|
+
horizontal: boolean;
|
|
198
|
+
rotateCategoryXAxisLabels: boolean;
|
|
199
|
+
rowCount: number;
|
|
200
|
+
}): number {
|
|
201
|
+
const legendReserve = hasLegend ? CHART_LEGEND_RESERVED_HEIGHT : 0;
|
|
202
|
+
|
|
203
|
+
if (horizontal) {
|
|
204
|
+
return Math.max(
|
|
205
|
+
CHART_CARD_DEFAULT_HEIGHT,
|
|
206
|
+
Math.max(1, rowCount) * CHART_HORIZONTAL_BAR_ROW_HEIGHT + legendReserve + 96,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (rotateCategoryXAxisLabels) {
|
|
211
|
+
return CHART_CARD_ROTATED_CATEGORY_HEIGHT + legendReserve;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return CHART_CARD_DEFAULT_HEIGHT + legendReserve;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function chartFrameHeightForContent({
|
|
218
|
+
hasLegend,
|
|
219
|
+
height,
|
|
220
|
+
horizontal,
|
|
221
|
+
rotateCategoryXAxisLabels,
|
|
222
|
+
rowCount,
|
|
223
|
+
surface,
|
|
224
|
+
}: {
|
|
225
|
+
hasLegend: boolean;
|
|
226
|
+
height?: number | string;
|
|
227
|
+
horizontal: boolean;
|
|
228
|
+
rotateCategoryXAxisLabels: boolean;
|
|
229
|
+
rowCount: number;
|
|
230
|
+
surface: ChartSurface;
|
|
231
|
+
}): number | string | undefined {
|
|
232
|
+
const requestedHeight = chartHeightForSurface(surface, height);
|
|
233
|
+
if (requestedHeight != null) return requestedHeight;
|
|
234
|
+
if (surface !== "card") return requestedHeight;
|
|
235
|
+
|
|
236
|
+
return chartCardHeightForContent({
|
|
237
|
+
hasLegend,
|
|
238
|
+
horizontal,
|
|
239
|
+
rotateCategoryXAxisLabels,
|
|
240
|
+
rowCount,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function numericChartDimension(value: number | string | undefined, fallback: number): number {
|
|
245
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
246
|
+
if (typeof value === "string") {
|
|
247
|
+
const parsed = Number(value);
|
|
248
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
249
|
+
}
|
|
250
|
+
return fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function complexChartFrameHeight({
|
|
254
|
+
height,
|
|
255
|
+
rowCount,
|
|
256
|
+
surface,
|
|
257
|
+
headerRows = 0,
|
|
258
|
+
}: {
|
|
259
|
+
height?: number | string;
|
|
260
|
+
rowCount: number;
|
|
261
|
+
surface: ChartSurface;
|
|
262
|
+
headerRows?: number;
|
|
263
|
+
}): number | string | undefined {
|
|
264
|
+
const requestedHeight = chartHeightForSurface(surface, height);
|
|
265
|
+
if (typeof requestedHeight === "string" && Number.isNaN(Number(requestedHeight))) {
|
|
266
|
+
return requestedHeight;
|
|
267
|
+
}
|
|
268
|
+
const defaultHeight = surface === "explorer" ? CHART_FULLSCREEN_HEIGHT : CHART_COMPACT_HEIGHT;
|
|
269
|
+
const baseHeight = numericChartDimension(requestedHeight, defaultHeight);
|
|
270
|
+
const visibleRows = Math.max(0, rowCount);
|
|
271
|
+
const visibleHeaderRows = Math.max(0, headerRows);
|
|
272
|
+
const rowSlots = visibleRows + visibleHeaderRows;
|
|
273
|
+
const minimumContentHeight =
|
|
274
|
+
visibleHeaderRows * HEATMAP_HEADER_ROW_HEIGHT +
|
|
275
|
+
visibleRows * COMPLEX_CHART_MIN_ROW_HEIGHT +
|
|
276
|
+
Math.max(0, rowSlots - 1) * COMPLEX_CHART_GRID_GAP;
|
|
277
|
+
|
|
278
|
+
return Math.max(baseHeight, minimumContentHeight);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function NumericYAxisTick({
|
|
282
|
+
payload,
|
|
283
|
+
textAnchor = "end",
|
|
284
|
+
valueFormat = "compact",
|
|
285
|
+
x = 0,
|
|
286
|
+
y = 0,
|
|
287
|
+
}: AxisTickProps & { valueFormat?: ChartSpec["valueFormat"] }) {
|
|
288
|
+
const numericX = typeof x === "number" ? x : Number(x) || 0;
|
|
289
|
+
const numericY = typeof y === "number" ? y : Number(y) || 0;
|
|
290
|
+
return (
|
|
291
|
+
<text
|
|
292
|
+
className="recharts-cartesian-axis-tick-value chart-axis-number"
|
|
293
|
+
dominantBaseline="middle"
|
|
294
|
+
textAnchor={textAnchor}
|
|
295
|
+
x={numericX}
|
|
296
|
+
y={numericY}
|
|
297
|
+
>
|
|
298
|
+
{formatValue(payload?.value, valueFormat)}
|
|
299
|
+
</text>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function XAxisEndpointTick({
|
|
304
|
+
firstTick,
|
|
305
|
+
lastTick,
|
|
306
|
+
payload,
|
|
307
|
+
x = 0,
|
|
308
|
+
y = 0,
|
|
309
|
+
}: AxisTickProps & { firstTick?: string; lastTick?: string }) {
|
|
310
|
+
const value = String(payload?.value ?? "");
|
|
311
|
+
const numericX = typeof x === "number" ? x : Number(x) || 0;
|
|
312
|
+
const textAnchor = value === firstTick ? "start" : value === lastTick ? "end" : "middle";
|
|
313
|
+
return (
|
|
314
|
+
<text
|
|
315
|
+
className="recharts-cartesian-axis-tick-value"
|
|
316
|
+
dominantBaseline="hanging"
|
|
317
|
+
textAnchor={textAnchor}
|
|
318
|
+
x={numericX}
|
|
319
|
+
y={y}
|
|
320
|
+
>
|
|
321
|
+
{formatDateAxisLabel(value)}
|
|
322
|
+
</text>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function truncateCategoryAxisLabel(value: string, maxLength: number): string {
|
|
327
|
+
if (value.length <= maxLength) return value;
|
|
328
|
+
const contentLength = Math.max(1, maxLength - 3);
|
|
329
|
+
return `${value.slice(0, contentLength).trimEnd()}...`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function wrapCategoryAxisLabel(value: string, maxLength: number): string[] {
|
|
333
|
+
const normalized = value.trim();
|
|
334
|
+
if (!normalized || normalized.length <= maxLength) return [normalized];
|
|
335
|
+
const words = normalized.split(/\s+/);
|
|
336
|
+
if (words.length === 1) {
|
|
337
|
+
return [
|
|
338
|
+
normalized.slice(0, maxLength),
|
|
339
|
+
truncateCategoryAxisLabel(normalized.slice(maxLength), maxLength),
|
|
340
|
+
].filter(Boolean);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let firstLine = words.shift() ?? "";
|
|
344
|
+
while (words.length && `${firstLine} ${words[0]}`.length <= maxLength) {
|
|
345
|
+
firstLine = `${firstLine} ${words.shift()}`;
|
|
346
|
+
}
|
|
347
|
+
return [
|
|
348
|
+
truncateCategoryAxisLabel(firstLine, maxLength),
|
|
349
|
+
truncateCategoryAxisLabel(words.join(" "), maxLength),
|
|
350
|
+
].filter(Boolean);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function WrappedCategoryXAxisTick({
|
|
354
|
+
maxLineLength,
|
|
355
|
+
payload,
|
|
356
|
+
x = 0,
|
|
357
|
+
y = 0,
|
|
358
|
+
}: AxisTickProps & { maxLineLength: number }) {
|
|
359
|
+
const numericX = typeof x === "number" ? x : Number(x) || 0;
|
|
360
|
+
const lines = wrapCategoryAxisLabel(String(payload?.value ?? ""), maxLineLength);
|
|
361
|
+
return (
|
|
362
|
+
<text
|
|
363
|
+
className="recharts-cartesian-axis-tick-value"
|
|
364
|
+
dominantBaseline="hanging"
|
|
365
|
+
textAnchor="middle"
|
|
366
|
+
x={numericX}
|
|
367
|
+
y={y}
|
|
368
|
+
>
|
|
369
|
+
{lines.map((line, index) => (
|
|
370
|
+
<tspan dy={index === 0 ? 0 : "1.2em"} key={`${line}-${index}`} x={numericX}>
|
|
371
|
+
{line}
|
|
372
|
+
</tspan>
|
|
373
|
+
))}
|
|
374
|
+
</text>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function categoryXAxisLabels(chart: ChartSpec, rows: ChartDataRow[]): string[] {
|
|
379
|
+
return [
|
|
380
|
+
...new Set(
|
|
381
|
+
rows
|
|
382
|
+
.map((row) => row[chart.xField])
|
|
383
|
+
.filter((value) => value != null)
|
|
384
|
+
.map((value) => String(value)),
|
|
385
|
+
),
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function categoryXAxisLabelSlotWidth(labels: string[], availableWidth: number): number {
|
|
390
|
+
return Math.max(
|
|
391
|
+
0,
|
|
392
|
+
availableWidth / Math.max(1, labels.length) - CATEGORY_X_AXIS_LABEL_SLOT_GAP,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function shouldRotateCategoryXAxisLabels({
|
|
397
|
+
chart,
|
|
398
|
+
horizontal,
|
|
399
|
+
xAxisTicks,
|
|
400
|
+
}: {
|
|
401
|
+
chart: ChartSpec;
|
|
402
|
+
horizontal: boolean;
|
|
403
|
+
xAxisTicks?: string[];
|
|
404
|
+
}): boolean {
|
|
405
|
+
if (horizontal || xAxisTicks?.length) return false;
|
|
406
|
+
return chart.settings?.categoryLabelPolicy === "rotate";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function shouldWrapCategoryXAxisLabels({
|
|
410
|
+
availableWidth,
|
|
411
|
+
chart,
|
|
412
|
+
horizontal,
|
|
413
|
+
rows,
|
|
414
|
+
xAxisTicks,
|
|
415
|
+
}: {
|
|
416
|
+
availableWidth: number;
|
|
417
|
+
chart: ChartSpec;
|
|
418
|
+
horizontal: boolean;
|
|
419
|
+
rows: ChartDataRow[];
|
|
420
|
+
xAxisTicks?: string[];
|
|
421
|
+
}): boolean {
|
|
422
|
+
if (horizontal || xAxisTicks?.length) return false;
|
|
423
|
+
const labelPolicy = chart.settings?.categoryLabelPolicy;
|
|
424
|
+
if (labelPolicy === "wrap") return true;
|
|
425
|
+
if (labelPolicy === "rotate" || labelPolicy === "truncate") return false;
|
|
426
|
+
|
|
427
|
+
const labels = categoryXAxisLabels(chart, rows);
|
|
428
|
+
if (labels.length < 2) return false;
|
|
429
|
+
|
|
430
|
+
const maxLength = Math.max(...labels.map((label) => label.length));
|
|
431
|
+
const longestLabelWidth = maxLength * CATEGORY_X_AXIS_ESTIMATED_CHAR_WIDTH;
|
|
432
|
+
const labelSlotWidth = categoryXAxisLabelSlotWidth(labels, availableWidth);
|
|
433
|
+
if (longestLabelWidth <= labelSlotWidth) return false;
|
|
434
|
+
|
|
435
|
+
if (maxLength >= CATEGORY_X_AXIS_LONG_LABEL_LENGTH) return true;
|
|
436
|
+
|
|
437
|
+
const averageLength =
|
|
438
|
+
labels.reduce((total, label) => total + label.length, 0) / labels.length;
|
|
439
|
+
return labels.length >= 4 && averageLength >= CATEGORY_X_AXIS_LONG_AVERAGE_LABEL_LENGTH;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function categoryXAxisWrappedLineLength(chart: ChartSpec, rows: ChartDataRow[], availableWidth: number): number {
|
|
443
|
+
const slotWidth = categoryXAxisLabelSlotWidth(categoryXAxisLabels(chart, rows), availableWidth);
|
|
444
|
+
return Math.max(CATEGORY_X_AXIS_MIN_LINE_CHARACTERS, Math.floor(slotWidth / CATEGORY_X_AXIS_ESTIMATED_CHAR_WIDTH));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getFunnelStageColor(index: number, rowCount: number): string {
|
|
448
|
+
const colorIndex = Math.round(
|
|
449
|
+
(index * (FUNNEL_STAGE_COLORS.length - 1)) / Math.max(1, rowCount - 1),
|
|
450
|
+
);
|
|
451
|
+
return FUNNEL_STAGE_COLORS[clamp(colorIndex, 0, FUNNEL_STAGE_COLORS.length - 1)];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function getHeatmapFill(intensity: number): string {
|
|
455
|
+
const index = clamp(
|
|
456
|
+
Math.floor(intensity * HEATMAP_BLUE_SCALE.length),
|
|
457
|
+
0,
|
|
458
|
+
HEATMAP_BLUE_SCALE.length - 1,
|
|
459
|
+
);
|
|
460
|
+
return HEATMAP_BLUE_SCALE[index] ?? HEATMAP_BLUE_SCALE[0];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatSvgNumber(value: number): string {
|
|
464
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/\.?0+$/, "");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function getPointDistance(from: SvgPoint, to: SvgPoint): number {
|
|
468
|
+
return Math.hypot(to.x - from.x, to.y - from.y);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function movePointToward(from: SvgPoint, to: SvgPoint, distance: number): SvgPoint {
|
|
472
|
+
const totalDistance = getPointDistance(from, to);
|
|
473
|
+
if (!Number.isFinite(totalDistance) || totalDistance <= 0 || distance <= 0) return from;
|
|
474
|
+
const ratio = Math.min(1, distance / totalDistance);
|
|
475
|
+
return {
|
|
476
|
+
x: from.x + (to.x - from.x) * ratio,
|
|
477
|
+
y: from.y + (to.y - from.y) * ratio,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function interpolatePoint(from: SvgPoint, to: SvgPoint, ratio: number): SvgPoint {
|
|
482
|
+
return {
|
|
483
|
+
x: from.x + (to.x - from.x) * ratio,
|
|
484
|
+
y: from.y + (to.y - from.y) * ratio,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getRoundedPolygonPath(points: SvgPoint[], radius: number): string {
|
|
489
|
+
if (points.length < 3) return "";
|
|
490
|
+
const cornerRadii = points.map((point, index) => {
|
|
491
|
+
const previous = points[(index - 1 + points.length) % points.length] ?? point;
|
|
492
|
+
const next = points[(index + 1) % points.length] ?? point;
|
|
493
|
+
return Math.min(radius, getPointDistance(point, previous) / 2, getPointDistance(point, next) / 2);
|
|
494
|
+
});
|
|
495
|
+
const cornerStarts = points.map((point, index) => {
|
|
496
|
+
const previous = points[(index - 1 + points.length) % points.length] ?? point;
|
|
497
|
+
return movePointToward(point, previous, cornerRadii[index] ?? 0);
|
|
498
|
+
});
|
|
499
|
+
const cornerEnds = points.map((point, index) => {
|
|
500
|
+
const next = points[(index + 1) % points.length] ?? point;
|
|
501
|
+
return movePointToward(point, next, cornerRadii[index] ?? 0);
|
|
502
|
+
});
|
|
503
|
+
const firstStart = cornerStarts[0];
|
|
504
|
+
if (!firstStart) return "";
|
|
505
|
+
const commands = [`M ${formatSvgNumber(firstStart.x)} ${formatSvgNumber(firstStart.y)}`];
|
|
506
|
+
points.forEach((point, index) => {
|
|
507
|
+
const cornerEnd = cornerEnds[index];
|
|
508
|
+
if (!cornerEnd) return;
|
|
509
|
+
commands.push(
|
|
510
|
+
`Q ${formatSvgNumber(point.x)} ${formatSvgNumber(point.y)} ${formatSvgNumber(cornerEnd.x)} ${formatSvgNumber(cornerEnd.y)}`,
|
|
511
|
+
);
|
|
512
|
+
const nextStart = cornerStarts[index + 1];
|
|
513
|
+
if (nextStart) {
|
|
514
|
+
commands.push(`L ${formatSvgNumber(nextStart.x)} ${formatSvgNumber(nextStart.y)}`);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
commands.push("Z");
|
|
518
|
+
return commands.join(" ");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const PLANNING_SERIES_ROLES = new Set(["baseline", "target", "forecast", "plan"]);
|
|
522
|
+
|
|
523
|
+
function isTrendChartType(type: ChartSpec["type"]): boolean {
|
|
524
|
+
return type === "area" || type === "line" || type === "sparkline" || type === "stackedArea";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isPlanningSeries(series: ChartSeriesSpec): boolean {
|
|
528
|
+
return PLANNING_SERIES_ROLES.has(series.semanticRole ?? series.role ?? "");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function getSeriesColor(chart: ChartSpec, series: ChartSeriesSpec, index: number): string {
|
|
532
|
+
const color = isPlanningSeries(series) && !series.color
|
|
533
|
+
? "neutral"
|
|
534
|
+
: series.color;
|
|
535
|
+
return colorForSeries(color, index, {
|
|
536
|
+
singleSeriesTrend: (chart.type === "line" || chart.type === "area") && chart.series.length === 1,
|
|
537
|
+
stacked: isStackedChartType(chart.type, chart.settings?.groupMode),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getSeriesStrokeDasharray(chart: ChartSpec, series: ChartSeriesSpec): string | undefined {
|
|
542
|
+
if (!isTrendChartType(chart.type)) return undefined;
|
|
543
|
+
if (series.lineStyle === "solid") return undefined;
|
|
544
|
+
if (series.lineStyle === "dotted") return "2 4";
|
|
545
|
+
if (series.lineStyle === "dashed" || isPlanningSeries(series)) return "5 5";
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function shouldColorBarsByCategory(): boolean {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function renderCategoryBarCells(chart: ChartSpec, series: ChartSeriesSpec, rows: ChartDataRow[], colorBarsBySign = false) {
|
|
554
|
+
if (colorBarsBySign) {
|
|
555
|
+
return rows.map((row, index) => (
|
|
556
|
+
<Cell
|
|
557
|
+
fill={barSignColor(asNumber(row[series.field]))}
|
|
558
|
+
key={`${chart.id}-${series.field}-${String(row[chart.xField] ?? index)}-${index}`}
|
|
559
|
+
/>
|
|
560
|
+
));
|
|
561
|
+
}
|
|
562
|
+
if (!shouldColorBarsByCategory() || series.color) return null;
|
|
563
|
+
return rows.map((row, index) => (
|
|
564
|
+
<Cell
|
|
565
|
+
fill={colorForSeries(undefined, index)}
|
|
566
|
+
key={`${chart.id}-${series.field}-${String(row[chart.xField] ?? index)}-${index}`}
|
|
567
|
+
/>
|
|
568
|
+
));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function waterfallBarPath({
|
|
572
|
+
height,
|
|
573
|
+
isPositive,
|
|
574
|
+
radius,
|
|
575
|
+
width,
|
|
576
|
+
x,
|
|
577
|
+
y,
|
|
578
|
+
}: {
|
|
579
|
+
height: number;
|
|
580
|
+
isPositive: boolean;
|
|
581
|
+
radius: number;
|
|
582
|
+
width: number;
|
|
583
|
+
x: number;
|
|
584
|
+
y: number;
|
|
585
|
+
}) {
|
|
586
|
+
const right = x + width;
|
|
587
|
+
const bottom = y + height;
|
|
588
|
+
|
|
589
|
+
if (radius <= 0) {
|
|
590
|
+
return `M${x},${y}H${right}V${bottom}H${x}Z`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (isPositive) {
|
|
594
|
+
return [
|
|
595
|
+
`M${x},${bottom}`,
|
|
596
|
+
`V${y + radius}`,
|
|
597
|
+
`Q${x},${y} ${x + radius},${y}`,
|
|
598
|
+
`H${right - radius}`,
|
|
599
|
+
`Q${right},${y} ${right},${y + radius}`,
|
|
600
|
+
`V${bottom}`,
|
|
601
|
+
"Z",
|
|
602
|
+
].join("");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return [
|
|
606
|
+
`M${x},${y}`,
|
|
607
|
+
`H${right}`,
|
|
608
|
+
`V${bottom - radius}`,
|
|
609
|
+
`Q${right},${bottom} ${right - radius},${bottom}`,
|
|
610
|
+
`H${x + radius}`,
|
|
611
|
+
`Q${x},${bottom} ${x},${bottom - radius}`,
|
|
612
|
+
"Z",
|
|
613
|
+
].join("");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function renderWaterfallBarShape(props: WaterfallBarShapeProps) {
|
|
617
|
+
const payload = props.payload;
|
|
618
|
+
const x = asFiniteNumber(props.x);
|
|
619
|
+
const y = asFiniteNumber(props.y);
|
|
620
|
+
const width = Math.max(2, asFiniteNumber(props.width));
|
|
621
|
+
const height = Math.max(2, asFiniteNumber(props.height));
|
|
622
|
+
const isPositive = Boolean(payload?.__waterfallIsPositive);
|
|
623
|
+
const isLast = Boolean(payload?.__waterfallIsLast);
|
|
624
|
+
const value = asFiniteNumber(payload?.__waterfallValue);
|
|
625
|
+
const rowCount = Math.max(1, asFiniteNumber(payload?.__waterfallRowCount, 1));
|
|
626
|
+
const slotWidth = props.parentViewBox?.width ? props.parentViewBox.width / rowCount : width;
|
|
627
|
+
const connectorY = isPositive ? y : y + height;
|
|
628
|
+
const connectorEndX = x + slotWidth;
|
|
629
|
+
const radius = value === 0 ? 0 : Math.min(6, width / 2, height / 2);
|
|
630
|
+
const fill = isPositive ? "var(--ds-chart-series-green)" : "var(--ds-chart-series-red)";
|
|
631
|
+
|
|
632
|
+
return (
|
|
633
|
+
<g className={`waterfall-bar-shape ${props.isActive ? "active" : ""}`}>
|
|
634
|
+
{!isLast ? (
|
|
635
|
+
<line
|
|
636
|
+
className="waterfall-connector"
|
|
637
|
+
stroke="var(--ds-chart-reference-line)"
|
|
638
|
+
strokeLinecap="round"
|
|
639
|
+
strokeWidth={1}
|
|
640
|
+
vectorEffect="non-scaling-stroke"
|
|
641
|
+
x1={x + width + 3}
|
|
642
|
+
x2={Math.max(x + width + 3, connectorEndX - 3)}
|
|
643
|
+
y1={connectorY}
|
|
644
|
+
y2={connectorY}
|
|
645
|
+
/>
|
|
646
|
+
) : null}
|
|
647
|
+
<path
|
|
648
|
+
d={waterfallBarPath({ height, isPositive, radius, width, x, y })}
|
|
649
|
+
fill={fill}
|
|
650
|
+
vectorEffect="non-scaling-stroke"
|
|
651
|
+
/>
|
|
652
|
+
</g>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function renderRoundedFunnelShape(props: FunnelShapeProps) {
|
|
657
|
+
const {
|
|
658
|
+
className,
|
|
659
|
+
fill,
|
|
660
|
+
height,
|
|
661
|
+
labelViewBox,
|
|
662
|
+
lowerWidth,
|
|
663
|
+
name: _name,
|
|
664
|
+
option: _option,
|
|
665
|
+
parentViewBox: _parentViewBox,
|
|
666
|
+
payload,
|
|
667
|
+
shapeType: _shapeType,
|
|
668
|
+
stroke: _stroke,
|
|
669
|
+
strokeWidth: _strokeWidth,
|
|
670
|
+
tooltipPayload: _tooltipPayload,
|
|
671
|
+
tooltipPosition: _tooltipPosition,
|
|
672
|
+
upperWidth,
|
|
673
|
+
val: _val,
|
|
674
|
+
value: _value,
|
|
675
|
+
width: _width,
|
|
676
|
+
x,
|
|
677
|
+
y,
|
|
678
|
+
...svgProps
|
|
679
|
+
} = props;
|
|
680
|
+
const topX = asFiniteNumber(labelViewBox?.x ?? x);
|
|
681
|
+
const topY = asFiniteNumber(labelViewBox?.y ?? y);
|
|
682
|
+
const topWidth = asFiniteNumber(labelViewBox?.upperWidth ?? upperWidth);
|
|
683
|
+
const bottomWidth = asFiniteNumber(labelViewBox?.lowerWidth ?? lowerWidth);
|
|
684
|
+
const segmentHeight = asFiniteNumber(labelViewBox?.height ?? height);
|
|
685
|
+
if (topWidth <= 0 || bottomWidth <= 0 || segmentHeight <= 0) return null;
|
|
686
|
+
|
|
687
|
+
const segmentIndex = asFiniteNumber(payload?.__funnelIndex);
|
|
688
|
+
const segmentCount = asFiniteNumber(payload?.__funnelRowCount, 1);
|
|
689
|
+
const topInset = segmentIndex > 0 ? FUNNEL_SEGMENT_GAP / 2 : 0;
|
|
690
|
+
const bottomInset = segmentIndex < segmentCount - 1 ? FUNNEL_SEGMENT_GAP / 2 : 0;
|
|
691
|
+
const topInsetRatio = topInset / segmentHeight;
|
|
692
|
+
const bottomInsetRatio = bottomInset / segmentHeight;
|
|
693
|
+
const widthGap = topWidth - bottomWidth;
|
|
694
|
+
const bottomRightX = topX + topWidth - widthGap / 2;
|
|
695
|
+
const bottomLeftX = bottomRightX - bottomWidth;
|
|
696
|
+
const topLeft = { x: topX, y: topY };
|
|
697
|
+
const topRight = { x: topX + topWidth, y: topY };
|
|
698
|
+
const bottomRight = { x: bottomRightX, y: topY + segmentHeight };
|
|
699
|
+
const bottomLeft = { x: bottomLeftX, y: topY + segmentHeight };
|
|
700
|
+
const segmentPath = getRoundedPolygonPath(
|
|
701
|
+
[
|
|
702
|
+
interpolatePoint(topLeft, bottomLeft, topInsetRatio),
|
|
703
|
+
interpolatePoint(topRight, bottomRight, topInsetRatio),
|
|
704
|
+
interpolatePoint(bottomRight, topRight, bottomInsetRatio),
|
|
705
|
+
interpolatePoint(bottomLeft, topLeft, bottomInsetRatio),
|
|
706
|
+
],
|
|
707
|
+
FUNNEL_CORNER_RADIUS,
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<path
|
|
712
|
+
{...(svgProps as SVGProps<SVGPathElement>)}
|
|
713
|
+
className={`funnel-segment ${className ?? ""}`.trim()}
|
|
714
|
+
d={segmentPath}
|
|
715
|
+
fill={fill}
|
|
716
|
+
shapeRendering="geometricPrecision"
|
|
717
|
+
stroke="none"
|
|
718
|
+
/>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function renderFunnelCenterLabel(props: FunnelLabelProps) {
|
|
723
|
+
const viewBox = props.viewBox;
|
|
724
|
+
const x = asFiniteNumber(viewBox?.x ?? props.x, Number.NaN);
|
|
725
|
+
const y = asFiniteNumber(viewBox?.y ?? props.y, Number.NaN);
|
|
726
|
+
const width = asFiniteNumber(viewBox?.width ?? props.width, Number.NaN);
|
|
727
|
+
const height = asFiniteNumber(viewBox?.height ?? props.height, Number.NaN);
|
|
728
|
+
if (![x, y, width, height].every(Number.isFinite)) return null;
|
|
729
|
+
|
|
730
|
+
const [fallbackLabel = "", fallbackValue = ""] =
|
|
731
|
+
typeof props.value === "string" ? props.value.split("\u0000") : [];
|
|
732
|
+
const label = String(props.payload?.__funnelLabel ?? fallbackLabel);
|
|
733
|
+
const value = String(props.payload?.__funnelValueLabel ?? fallbackValue);
|
|
734
|
+
if (!label && !value) return null;
|
|
735
|
+
|
|
736
|
+
return (
|
|
737
|
+
<text className="funnel-label" dominantBaseline="middle" textAnchor="middle" x={x + width / 2} y={y + height / 2}>
|
|
738
|
+
<tspan className="funnel-label-text">{label}</tspan>
|
|
739
|
+
<tspan className="funnel-label-value" dx={FUNNEL_LABEL_GAP}>
|
|
740
|
+
{value}
|
|
741
|
+
</tspan>
|
|
742
|
+
</text>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function renderScatterPointLabel(props: {
|
|
747
|
+
value?: unknown;
|
|
748
|
+
x?: number | string;
|
|
749
|
+
y?: number | string;
|
|
750
|
+
}) {
|
|
751
|
+
const label = props.value == null ? "" : String(props.value).trim();
|
|
752
|
+
const x = asFiniteNumber(props.x, Number.NaN);
|
|
753
|
+
const y = asFiniteNumber(props.y, Number.NaN);
|
|
754
|
+
if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
755
|
+
const shortLabel = label.length > 28 ? `${label.slice(0, 25)}...` : label;
|
|
756
|
+
return (
|
|
757
|
+
<text className="scatter-point-label" dominantBaseline="auto" textAnchor="middle" x={x} y={y - 8}>
|
|
758
|
+
{shortLabel}
|
|
759
|
+
</text>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function valueLabelMode(chart: ChartSpec): ChartValueLabelMode {
|
|
764
|
+
const explicitMode = chart.labels?.values;
|
|
765
|
+
if (explicitMode) return explicitMode;
|
|
766
|
+
if (chart.settings?.showValues === true) return "all";
|
|
767
|
+
if (chart.settings?.showValues === false) return "none";
|
|
768
|
+
return "auto";
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function visibleBarValueCount(rows: ChartDataRow[], series: ChartSeriesSpec[]): number {
|
|
772
|
+
return rows.reduce(
|
|
773
|
+
(count, row) => count + series.filter((candidate) => asNumber(row[candidate.field]) != null).length,
|
|
774
|
+
0,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function visibleBarValueLabelSides(rows: ChartDataRow[], series: ChartSeriesSpec[]): BarValueLabelSides {
|
|
779
|
+
return rows.reduce<BarValueLabelSides>(
|
|
780
|
+
(sides, row) => {
|
|
781
|
+
for (const candidate of series) {
|
|
782
|
+
const value = asNumber(row[candidate.field]);
|
|
783
|
+
if (value == null) continue;
|
|
784
|
+
if (value < 0) {
|
|
785
|
+
sides.hasNegative = true;
|
|
786
|
+
} else {
|
|
787
|
+
sides.hasNonNegative = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return sides;
|
|
791
|
+
},
|
|
792
|
+
{ hasNegative: false, hasNonNegative: false },
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function barSignState(rows: ChartDataRow[], series: ChartSeriesSpec[]): BarSignState {
|
|
797
|
+
return rows.reduce<BarSignState>(
|
|
798
|
+
(state, row) => {
|
|
799
|
+
for (const candidate of series) {
|
|
800
|
+
const value = asNumber(row[candidate.field]);
|
|
801
|
+
if (value == null) continue;
|
|
802
|
+
if (value > 0) state.hasPositive = true;
|
|
803
|
+
else if (value < 0) state.hasNegative = true;
|
|
804
|
+
else state.hasZero = true;
|
|
805
|
+
}
|
|
806
|
+
return state;
|
|
807
|
+
},
|
|
808
|
+
{ hasNegative: false, hasPositive: false, hasZero: false },
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function shouldColorBarsBySign({
|
|
813
|
+
activeSeries,
|
|
814
|
+
chart,
|
|
815
|
+
signState,
|
|
816
|
+
stacked,
|
|
817
|
+
}: {
|
|
818
|
+
activeSeries: ChartSeriesSpec[];
|
|
819
|
+
chart: ChartSpec;
|
|
820
|
+
signState: BarSignState;
|
|
821
|
+
stacked: boolean;
|
|
822
|
+
}): boolean {
|
|
823
|
+
if (stacked || activeSeries.length !== 1) return false;
|
|
824
|
+
if (chart.type !== "bar" && chart.type !== "horizontalBar") return false;
|
|
825
|
+
return signState.hasPositive && signState.hasNegative;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function barSignColor(value: number | null): string {
|
|
829
|
+
if (value == null || value === 0) return "var(--ds-chart-series-neutral)";
|
|
830
|
+
return value > 0 ? "var(--ds-chart-series-green)" : "var(--ds-chart-series-red)";
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function formatSignedValue(value: number, chart: ChartSpec, showPositiveSign: boolean): string {
|
|
834
|
+
const label = formatValue(value, chart.valueFormat, chart.unit);
|
|
835
|
+
return showPositiveSign && value > 0 ? `+${label}` : label;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function shouldShowBarValueLabels({
|
|
839
|
+
activeSeries,
|
|
840
|
+
chart,
|
|
841
|
+
rows,
|
|
842
|
+
stacked,
|
|
843
|
+
}: {
|
|
844
|
+
activeSeries: ChartSeriesSpec[];
|
|
845
|
+
chart: ChartSpec;
|
|
846
|
+
rows: ChartDataRow[];
|
|
847
|
+
stacked: boolean;
|
|
848
|
+
}): boolean {
|
|
849
|
+
if (chart.type !== "bar" && chart.type !== "horizontalBar" && chart.type !== "histogram") {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (stacked) return false;
|
|
853
|
+
const labelMode = valueLabelMode(chart);
|
|
854
|
+
if (labelMode === "none" || labelMode === "endpoints") return false;
|
|
855
|
+
if (labelMode === "all") return true;
|
|
856
|
+
|
|
857
|
+
const valueCount = visibleBarValueCount(rows, activeSeries);
|
|
858
|
+
return valueCount > 0 && valueCount <= BAR_AUTO_VALUE_LABEL_LIMIT;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function renderBarValueLabel({
|
|
862
|
+
chart,
|
|
863
|
+
height,
|
|
864
|
+
horizontal,
|
|
865
|
+
showPositiveSign = false,
|
|
866
|
+
value,
|
|
867
|
+
width,
|
|
868
|
+
x,
|
|
869
|
+
y,
|
|
870
|
+
}: BarValueLabelProps & { chart: ChartSpec; horizontal: boolean; showPositiveSign?: boolean }) {
|
|
871
|
+
const numericValue = asNumber(value);
|
|
872
|
+
const numericX = asFiniteNumber(x, Number.NaN);
|
|
873
|
+
const numericY = asFiniteNumber(y, Number.NaN);
|
|
874
|
+
const numericWidth = asFiniteNumber(width, Number.NaN);
|
|
875
|
+
const numericHeight = asFiniteNumber(height, Number.NaN);
|
|
876
|
+
if (
|
|
877
|
+
numericValue == null ||
|
|
878
|
+
![numericX, numericY, numericWidth, numericHeight].every(Number.isFinite)
|
|
879
|
+
) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const label = formatSignedValue(numericValue, chart, showPositiveSign);
|
|
884
|
+
const isNegative = numericValue < 0;
|
|
885
|
+
if (horizontal) {
|
|
886
|
+
const left = Math.min(numericX, numericX + numericWidth);
|
|
887
|
+
const right = Math.max(numericX, numericX + numericWidth);
|
|
888
|
+
return (
|
|
889
|
+
<text
|
|
890
|
+
className="chart-bar-value-label"
|
|
891
|
+
dominantBaseline="middle"
|
|
892
|
+
textAnchor={isNegative ? "end" : "start"}
|
|
893
|
+
x={isNegative ? left - BAR_VALUE_LABEL_OFFSET : right + BAR_VALUE_LABEL_OFFSET}
|
|
894
|
+
y={numericY + numericHeight / 2}
|
|
895
|
+
>
|
|
896
|
+
{label}
|
|
897
|
+
</text>
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const top = Math.min(numericY, numericY + numericHeight);
|
|
902
|
+
const bottom = Math.max(numericY, numericY + numericHeight);
|
|
903
|
+
return (
|
|
904
|
+
<text
|
|
905
|
+
className="chart-bar-value-label"
|
|
906
|
+
dominantBaseline={isNegative ? "hanging" : "auto"}
|
|
907
|
+
textAnchor="middle"
|
|
908
|
+
x={numericX + numericWidth / 2}
|
|
909
|
+
y={isNegative ? bottom + BAR_NEGATIVE_VALUE_LABEL_OFFSET : top - BAR_VALUE_LABEL_OFFSET}
|
|
910
|
+
>
|
|
911
|
+
{label}
|
|
912
|
+
</text>
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function categoryBarTooltipColor(chart: ChartSpec, rows: ChartDataRow[], item: ChartTooltipPayloadItem, colorBarsBySign = false): string | undefined {
|
|
917
|
+
if (colorBarsBySign) return barSignColor(asNumber(item.value));
|
|
918
|
+
if (!shouldColorBarsByCategory()) return undefined;
|
|
919
|
+
if (chart.series[0]?.color) return undefined;
|
|
920
|
+
const payload = item.payload && typeof item.payload === "object"
|
|
921
|
+
? item.payload as ChartDataRow
|
|
922
|
+
: null;
|
|
923
|
+
const category = payload ? payload[chart.xField] : null;
|
|
924
|
+
const rowIndex = rows.findIndex((row) => String(row[chart.xField] ?? "") === String(category ?? ""));
|
|
925
|
+
return colorForSeries(undefined, rowIndex < 0 ? 0 : rowIndex);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function getAreaGradientId(chart: ChartSpec, series: ChartSeriesSpec): string {
|
|
929
|
+
return `${chart.id}-${series.field}`.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function referenceLines(chart: ChartSpec, horizontal = false) {
|
|
933
|
+
return (chart.referenceLines ?? []).map((line, index) => {
|
|
934
|
+
const axis = line.axis ?? (horizontal ? "x" : "y");
|
|
935
|
+
const value = typeof line.value === "number" ? line.value : Number(line.value);
|
|
936
|
+
if (!Number.isFinite(value)) return null;
|
|
937
|
+
const props = {
|
|
938
|
+
stroke: "var(--ds-chart-reference-line)",
|
|
939
|
+
strokeDasharray: value === 0 ? undefined : "4 4",
|
|
940
|
+
};
|
|
941
|
+
const key = `${axis}-${value}-${index}`;
|
|
942
|
+
return axis === "x" ? <ReferenceLine key={key} {...props} x={value} /> : <ReferenceLine key={key} {...props} y={value} />;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function chartLegendItems(chart: ChartSpec, visibleSeries: ChartSeriesSpec[], signState?: BarSignState | null, rows: ChartDataRow[] = []) {
|
|
947
|
+
if (signState) {
|
|
948
|
+
const items = [];
|
|
949
|
+
if (signState.hasPositive) items.push({ color: barSignColor(1), id: "__positive", value: "positive" });
|
|
950
|
+
if (signState.hasNegative) items.push({ color: barSignColor(-1), id: "__negative", value: "negative" });
|
|
951
|
+
if (signState.hasZero) items.push({ color: barSignColor(0), id: "__zero", value: "neutral" });
|
|
952
|
+
return items;
|
|
953
|
+
}
|
|
954
|
+
if (chart.type === "pie") {
|
|
955
|
+
return buildPieRows(chart, rows).map((row) => ({
|
|
956
|
+
color: getSeriesColor(chart, chart.series[0], asFiniteNumber(row.__pieIndex)),
|
|
957
|
+
id: String(row.__pieField ?? row.__pieName ?? row.__pieIndex),
|
|
958
|
+
value: String(row.__pieName ?? row.__pieField ?? ""),
|
|
959
|
+
}));
|
|
960
|
+
}
|
|
961
|
+
if (chart.series.length <= 1) return [];
|
|
962
|
+
if (
|
|
963
|
+
chart.type !== "area" &&
|
|
964
|
+
chart.type !== "bar" &&
|
|
965
|
+
chart.type !== "horizontalBar" &&
|
|
966
|
+
chart.type !== "line" &&
|
|
967
|
+
chart.type !== "scatter" &&
|
|
968
|
+
chart.type !== "stackedArea" &&
|
|
969
|
+
chart.type !== "stackedBar" &&
|
|
970
|
+
chart.type !== "stackedBar100" &&
|
|
971
|
+
chart.type !== "horizontalStackedBar" &&
|
|
972
|
+
chart.type !== "horizontalStackedBar100" &&
|
|
973
|
+
chart.type !== "pie"
|
|
974
|
+
) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
return chart.series.map((series, index) => ({
|
|
978
|
+
color: getSeriesColor(chart, series, index),
|
|
979
|
+
id: series.field,
|
|
980
|
+
lineStyle: series.lineStyle ?? (isTrendChartType(chart.type) && isPlanningSeries(series) ? "dashed" : undefined),
|
|
981
|
+
value: series.label ?? series.field,
|
|
982
|
+
visible: visibleSeries.some((visible) => visible.field === series.field),
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function renderAreaGradientDefs(chart: ChartSpec) {
|
|
987
|
+
const shouldRenderAreaFill =
|
|
988
|
+
chart.type === "area" || chart.type === "stackedArea" || chart.type === "sparkline";
|
|
989
|
+
if (!shouldRenderAreaFill) return null;
|
|
990
|
+
return (
|
|
991
|
+
<defs>
|
|
992
|
+
{chart.series.map((series, index) => {
|
|
993
|
+
const color = getSeriesColor(chart, series, index);
|
|
994
|
+
const gradientId = getAreaGradientId(chart, series);
|
|
995
|
+
return (
|
|
996
|
+
<linearGradient id={gradientId} key={gradientId} x1="0" x2="0" y1="0" y2="1">
|
|
997
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.12} />
|
|
998
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
999
|
+
</linearGradient>
|
|
1000
|
+
);
|
|
1001
|
+
})}
|
|
1002
|
+
</defs>
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function LeaderboardPanel({ chart, rows }: { chart: ChartSpec; rows: ChartDataRow[] }) {
|
|
1007
|
+
const series = chart.series[0];
|
|
1008
|
+
const requestedRows = typeof chart.maxRows === "number" && Number.isFinite(chart.maxRows) ? chart.maxRows : 6;
|
|
1009
|
+
const rowLimit = Math.max(1, Math.min(8, Math.floor(requestedRows)));
|
|
1010
|
+
const rankedRows = [...rows].sort(
|
|
1011
|
+
(a, b) => Math.abs(asNumber(b[series?.field ?? ""]) ?? 0) - Math.abs(asNumber(a[series?.field ?? ""]) ?? 0),
|
|
1012
|
+
);
|
|
1013
|
+
const visibleRows = rankedRows.slice(0, rowLimit);
|
|
1014
|
+
const max = Math.max(...visibleRows.map((row) => Math.abs(asNumber(row[series?.field ?? ""]) ?? 0)), 1);
|
|
1015
|
+
const style = { "--leaderboard-row-count": visibleRows.length } as CSSProperties;
|
|
1016
|
+
return (
|
|
1017
|
+
<div className="leaderboard" style={style}>
|
|
1018
|
+
{visibleRows.map((row, index) => {
|
|
1019
|
+
const value = asNumber(row[series?.field ?? ""]) ?? 0;
|
|
1020
|
+
const width = `${Math.max(4, (Math.abs(value) / max) * 100)}%`;
|
|
1021
|
+
const rowStyle = {
|
|
1022
|
+
"--bar-color": getSeriesColor(chart, series, 0),
|
|
1023
|
+
"--bar-width": width,
|
|
1024
|
+
} as CSSProperties;
|
|
1025
|
+
return (
|
|
1026
|
+
<div
|
|
1027
|
+
className={`leaderboard-row ${value >= 0 ? "positive" : "negative"}`}
|
|
1028
|
+
key={`${String(row[chart.xField])}-${index}`}
|
|
1029
|
+
style={rowStyle}
|
|
1030
|
+
>
|
|
1031
|
+
<span className="leaderboard-label">{String(row[chart.xField] ?? "")}</span>
|
|
1032
|
+
<span className="leaderboard-value">{formatValue(value, chart.valueFormat, chart.unit)}</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
);
|
|
1035
|
+
})}
|
|
1036
|
+
</div>
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function HeatmapPanel({ chart, rows }: { chart: ChartSpec; rows: ChartDataRow[] }) {
|
|
1041
|
+
const values = rows.flatMap((row) =>
|
|
1042
|
+
chart.series
|
|
1043
|
+
.map((series) => asNumber(row[series.field]))
|
|
1044
|
+
.filter((value): value is number => value != null),
|
|
1045
|
+
);
|
|
1046
|
+
if (!values.length) return <div className="empty-state">No numeric heatmap values.</div>;
|
|
1047
|
+
const min = Math.min(...values);
|
|
1048
|
+
const max = Math.max(...values);
|
|
1049
|
+
const range = max - min || 1;
|
|
1050
|
+
const heatmapStyle = {
|
|
1051
|
+
"--heatmap-row-min-height": `${COMPLEX_CHART_MIN_ROW_HEIGHT}px`,
|
|
1052
|
+
"--heatmap-header-row-height": `${HEATMAP_HEADER_ROW_HEIGHT}px`,
|
|
1053
|
+
gridTemplateColumns: `minmax(88px, max-content) repeat(${chart.series.length}, minmax(40px, 1fr))`,
|
|
1054
|
+
gridTemplateRows: `repeat(${rows.length}, minmax(var(--heatmap-row-min-height), 1fr)) minmax(var(--heatmap-header-row-height), auto)`,
|
|
1055
|
+
} as CSSProperties;
|
|
1056
|
+
|
|
1057
|
+
return (
|
|
1058
|
+
<div className="heatmap-grid-panel">
|
|
1059
|
+
<div className="heatmap-grid" style={heatmapStyle}>
|
|
1060
|
+
{rows.map((row, rowIndex) => {
|
|
1061
|
+
const rowLabel = String(row[chart.xField] ?? "");
|
|
1062
|
+
return (
|
|
1063
|
+
<Fragment key={`${chart.id}-heatmap-row-${rowIndex}`}>
|
|
1064
|
+
<strong key={`${rowIndex}-label`}>{rowLabel}</strong>
|
|
1065
|
+
{chart.series.map((series) => {
|
|
1066
|
+
const seriesLabel = series.label ?? series.field;
|
|
1067
|
+
const value = asNumber(row[series.field]) ?? min;
|
|
1068
|
+
const intensity = clamp((value - min) / range, 0, 1);
|
|
1069
|
+
const valueLabel = formatValue(value, chart.valueFormat, chart.unit);
|
|
1070
|
+
const label = `${rowLabel} ${seriesLabel}: ${valueLabel}`;
|
|
1071
|
+
return (
|
|
1072
|
+
<span
|
|
1073
|
+
aria-label={label}
|
|
1074
|
+
className="heatmap-cell"
|
|
1075
|
+
key={`${rowIndex}-${series.field}`}
|
|
1076
|
+
role="img"
|
|
1077
|
+
style={{ background: getHeatmapFill(intensity) }}
|
|
1078
|
+
tabIndex={0}
|
|
1079
|
+
>
|
|
1080
|
+
<span className="chart-tooltip heatmap-tooltip" role="presentation">
|
|
1081
|
+
<span className="chart-tooltip-label">{rowLabel}</span>
|
|
1082
|
+
<span className="chart-tooltip-items">
|
|
1083
|
+
<span className="chart-tooltip-item">
|
|
1084
|
+
<span
|
|
1085
|
+
aria-hidden="true"
|
|
1086
|
+
className="chart-tooltip-marker"
|
|
1087
|
+
style={{ background: getHeatmapFill(intensity) }}
|
|
1088
|
+
/>
|
|
1089
|
+
<span className="chart-tooltip-name">{seriesLabel}</span>
|
|
1090
|
+
<span className="chart-tooltip-value">{valueLabel}</span>
|
|
1091
|
+
</span>
|
|
1092
|
+
</span>
|
|
1093
|
+
</span>
|
|
1094
|
+
</span>
|
|
1095
|
+
);
|
|
1096
|
+
})}
|
|
1097
|
+
</Fragment>
|
|
1098
|
+
);
|
|
1099
|
+
})}
|
|
1100
|
+
<span className="heatmap-axis-corner" />
|
|
1101
|
+
{chart.series.map((series) => (
|
|
1102
|
+
<b key={series.field}>{series.label ?? series.field}</b>
|
|
1103
|
+
))}
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function CustomPanelFrame({
|
|
1110
|
+
chart,
|
|
1111
|
+
children,
|
|
1112
|
+
height,
|
|
1113
|
+
surface,
|
|
1114
|
+
}: {
|
|
1115
|
+
chart: ChartSpec;
|
|
1116
|
+
children: ReactNode;
|
|
1117
|
+
height?: number | string;
|
|
1118
|
+
surface: ChartSurface;
|
|
1119
|
+
}) {
|
|
1120
|
+
return (
|
|
1121
|
+
<ChartFrame
|
|
1122
|
+
className={`chart-frame--custom chart-frame--${chart.type}`}
|
|
1123
|
+
height={chartHeightForSurface(surface, height)}
|
|
1124
|
+
surface={surface}
|
|
1125
|
+
>
|
|
1126
|
+
{children}
|
|
1127
|
+
</ChartFrame>
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function ChartRenderer({
|
|
1132
|
+
chart: chartSpec,
|
|
1133
|
+
height,
|
|
1134
|
+
onVisibleSeriesChange,
|
|
1135
|
+
responsive = true,
|
|
1136
|
+
rows: rawRows,
|
|
1137
|
+
showLegend = true,
|
|
1138
|
+
surface,
|
|
1139
|
+
visibleSeries,
|
|
1140
|
+
width,
|
|
1141
|
+
}: {
|
|
1142
|
+
chart: ChartSpec;
|
|
1143
|
+
height?: number | string;
|
|
1144
|
+
onVisibleSeriesChange?: (visible: Set<string>) => void;
|
|
1145
|
+
responsive?: boolean;
|
|
1146
|
+
rows: ChartDataRow[];
|
|
1147
|
+
showLegend?: boolean;
|
|
1148
|
+
surface?: ChartSurface;
|
|
1149
|
+
visibleSeries?: Set<string>;
|
|
1150
|
+
width?: number | string;
|
|
1151
|
+
}) {
|
|
1152
|
+
const { chart, rows } = rechartsChartFromEncodedSpec(chartSpec, rawRows);
|
|
1153
|
+
const resolvedSurface = surface ?? chart.surface?.surface ?? "card";
|
|
1154
|
+
const activeSeries = chart.series.filter((series) => !visibleSeries || !visibleSeries.size || visibleSeries.has(series.field));
|
|
1155
|
+
const firstSeries = chart.series[0];
|
|
1156
|
+
if (!firstSeries) return <div className="empty-state">Chart needs at least one series.</div>;
|
|
1157
|
+
|
|
1158
|
+
if (chart.type === "leaderboard") {
|
|
1159
|
+
return <CustomPanelFrame chart={chart} height={height} surface={resolvedSurface}><LeaderboardPanel chart={chart} rows={rows} /></CustomPanelFrame>;
|
|
1160
|
+
}
|
|
1161
|
+
if (chart.type === "heatmap") {
|
|
1162
|
+
return (
|
|
1163
|
+
<CustomPanelFrame
|
|
1164
|
+
chart={chart}
|
|
1165
|
+
height={complexChartFrameHeight({ headerRows: 1, height, rowCount: rows.length, surface: resolvedSurface })}
|
|
1166
|
+
surface={resolvedSurface}
|
|
1167
|
+
>
|
|
1168
|
+
<HeatmapPanel chart={chart} rows={rows} />
|
|
1169
|
+
</CustomPanelFrame>
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const fixedChartWidth = numericChartDimension(width, CHART_STATIC_WIDTH);
|
|
1174
|
+
const xAxisTicks = getDateAxisTicks(rows, chart.xField, fixedChartWidth);
|
|
1175
|
+
const barGroupMode = chart.settings?.groupMode;
|
|
1176
|
+
const barOrientation = chart.settings?.orientation;
|
|
1177
|
+
const horizontal = isHorizontalChartType(chart.type, barOrientation);
|
|
1178
|
+
const rotateCategoryXAxisLabels = shouldRotateCategoryXAxisLabels({
|
|
1179
|
+
chart,
|
|
1180
|
+
horizontal,
|
|
1181
|
+
xAxisTicks,
|
|
1182
|
+
});
|
|
1183
|
+
const wrapCategoryXAxisLabels = shouldWrapCategoryXAxisLabels({
|
|
1184
|
+
availableWidth: fixedChartWidth,
|
|
1185
|
+
chart,
|
|
1186
|
+
horizontal,
|
|
1187
|
+
rows,
|
|
1188
|
+
xAxisTicks,
|
|
1189
|
+
});
|
|
1190
|
+
const wrappedCategoryXAxisLineLength = categoryXAxisWrappedLineLength(chart, rows, fixedChartWidth);
|
|
1191
|
+
const stacked = isStackedChartType(chart.type, barGroupMode);
|
|
1192
|
+
const normalized = isNormalizedStackedChartType(chart.type, barGroupMode);
|
|
1193
|
+
const currentBarSignState = barSignState(rows, activeSeries);
|
|
1194
|
+
const colorBarsBySign = shouldColorBarsBySign({
|
|
1195
|
+
activeSeries,
|
|
1196
|
+
chart,
|
|
1197
|
+
signState: currentBarSignState,
|
|
1198
|
+
stacked,
|
|
1199
|
+
});
|
|
1200
|
+
const showBarValueLabels = shouldShowBarValueLabels({
|
|
1201
|
+
activeSeries,
|
|
1202
|
+
chart,
|
|
1203
|
+
rows,
|
|
1204
|
+
stacked,
|
|
1205
|
+
});
|
|
1206
|
+
const barValueLabelSides = showBarValueLabels
|
|
1207
|
+
? visibleBarValueLabelSides(rows, activeSeries)
|
|
1208
|
+
: { hasNegative: false, hasNonNegative: false };
|
|
1209
|
+
const legendItems = showLegend ? chartLegendItems(chart, activeSeries, colorBarsBySign ? currentBarSignState : null, rows) : [];
|
|
1210
|
+
const tooltipChart = chart;
|
|
1211
|
+
const yAxisScale = getYAxisScaleForSeries(chart, rows, activeSeries);
|
|
1212
|
+
const frameHeight = chartFrameHeightForContent({
|
|
1213
|
+
hasLegend: legendItems.length > 0,
|
|
1214
|
+
height,
|
|
1215
|
+
horizontal,
|
|
1216
|
+
rotateCategoryXAxisLabels,
|
|
1217
|
+
rowCount: rows.length,
|
|
1218
|
+
surface: resolvedSurface,
|
|
1219
|
+
});
|
|
1220
|
+
const commonProps = { data: rows };
|
|
1221
|
+
const tooltipWrapperStyle: CSSProperties = { pointerEvents: "none", zIndex: 40 };
|
|
1222
|
+
const fastTooltipProps = {
|
|
1223
|
+
animationDuration: 0,
|
|
1224
|
+
animationEasing: "linear",
|
|
1225
|
+
isAnimationActive: false,
|
|
1226
|
+
offset: 12,
|
|
1227
|
+
wrapperStyle: tooltipWrapperStyle,
|
|
1228
|
+
};
|
|
1229
|
+
const subtleTooltipCursor = {
|
|
1230
|
+
fill: "var(--ds-chart-hover-bg)",
|
|
1231
|
+
stroke: "transparent",
|
|
1232
|
+
strokeWidth: 0,
|
|
1233
|
+
};
|
|
1234
|
+
const visibleIds = new Set(activeSeries.map((series) => series.field));
|
|
1235
|
+
const interactiveLegend = Boolean(onVisibleSeriesChange) && !colorBarsBySign;
|
|
1236
|
+
const fixedFrameHeight = numericChartDimension(
|
|
1237
|
+
frameHeight,
|
|
1238
|
+
resolvedSurface === "explorer" ? CHART_FULLSCREEN_HEIGHT : CHART_COMPACT_HEIGHT,
|
|
1239
|
+
);
|
|
1240
|
+
const fixedLegendReserve = legendItems.length ? CHART_LEGEND_RESERVED_HEIGHT : 0;
|
|
1241
|
+
const fixedChartHeight = Math.max(CHART_MIN_PLOT_HEIGHT, fixedFrameHeight - fixedLegendReserve);
|
|
1242
|
+
function bottomAxisLabel(value: string) {
|
|
1243
|
+
return {
|
|
1244
|
+
offset: -4,
|
|
1245
|
+
position: "insideBottom",
|
|
1246
|
+
style: { fill: "var(--ds-text-primary)", fontSize: 12, fontWeight: 500, textAnchor: "middle" },
|
|
1247
|
+
value,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function leftAxisLabel(value: string) {
|
|
1252
|
+
return {
|
|
1253
|
+
angle: -90,
|
|
1254
|
+
offset: 0,
|
|
1255
|
+
position: "insideLeft",
|
|
1256
|
+
style: { fill: "var(--ds-text-primary)", fontSize: 12, fontWeight: 500, textAnchor: "middle" },
|
|
1257
|
+
value,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const xAxisLabel = chart.xAxisTitle ? bottomAxisLabel(chart.xAxisTitle) : undefined;
|
|
1262
|
+
const yAxisLabel = chart.yAxisTitle ? leftAxisLabel(chart.yAxisTitle) : undefined;
|
|
1263
|
+
const horizontalValueAxisLabel = chart.yAxisTitle ? bottomAxisLabel(chart.yAxisTitle) : undefined;
|
|
1264
|
+
// Horizontal bars already show category names as row labels; an extra axis title competes for left-side space.
|
|
1265
|
+
const horizontalCategoryAxisLabel = undefined;
|
|
1266
|
+
const bottomMarginAxisLabel = horizontal ? horizontalValueAxisLabel : xAxisLabel;
|
|
1267
|
+
const leftMarginAxisLabel = horizontal ? horizontalCategoryAxisLabel : yAxisLabel;
|
|
1268
|
+
const baseBottomMargin = !horizontal && rotateCategoryXAxisLabels ? (bottomMarginAxisLabel ? 16 : 8) : bottomMarginAxisLabel ? 8 : 0;
|
|
1269
|
+
const baseLeftMargin = leftMarginAxisLabel ? 4 : 0;
|
|
1270
|
+
const cartesianMargin = {
|
|
1271
|
+
bottom:
|
|
1272
|
+
!horizontal && barValueLabelSides.hasNegative
|
|
1273
|
+
? Math.max(baseBottomMargin, BAR_NEGATIVE_VALUE_LABEL_AXIS_GUTTER)
|
|
1274
|
+
: baseBottomMargin,
|
|
1275
|
+
left:
|
|
1276
|
+
horizontal && barValueLabelSides.hasNegative
|
|
1277
|
+
? Math.max(baseLeftMargin, BAR_VALUE_LABEL_HORIZONTAL_GUTTER)
|
|
1278
|
+
: baseLeftMargin,
|
|
1279
|
+
right: horizontal && barValueLabelSides.hasNonNegative ? BAR_VALUE_LABEL_HORIZONTAL_GUTTER : 0,
|
|
1280
|
+
top:
|
|
1281
|
+
!horizontal && barValueLabelSides.hasNonNegative
|
|
1282
|
+
? Math.max(Y_AXIS_TICK_LABEL_OVERHANG, BAR_VALUE_LABEL_VERTICAL_GUTTER)
|
|
1283
|
+
: Y_AXIS_TICK_LABEL_OVERHANG,
|
|
1284
|
+
};
|
|
1285
|
+
const resolveTooltipColor = (item: ChartTooltipPayloadItem) => categoryBarTooltipColor(chart, rows, item, colorBarsBySign);
|
|
1286
|
+
const responsiveChartProps = responsive
|
|
1287
|
+
? {
|
|
1288
|
+
margin: cartesianMargin,
|
|
1289
|
+
}
|
|
1290
|
+
: {
|
|
1291
|
+
height: fixedChartHeight,
|
|
1292
|
+
margin: cartesianMargin,
|
|
1293
|
+
width: fixedChartWidth,
|
|
1294
|
+
};
|
|
1295
|
+
const chartProps = { ...responsiveChartProps, ...commonProps };
|
|
1296
|
+
const emptyMessage = chartEmptyMessage(chart, rows, visibleSeries);
|
|
1297
|
+
if (emptyMessage) {
|
|
1298
|
+
return (
|
|
1299
|
+
<ChartFrame
|
|
1300
|
+
height={frameHeight}
|
|
1301
|
+
interactiveLegend={interactiveLegend}
|
|
1302
|
+
legendItems={legendItems}
|
|
1303
|
+
onVisibleChange={onVisibleSeriesChange}
|
|
1304
|
+
surface={resolvedSurface}
|
|
1305
|
+
visibleIds={visibleIds}
|
|
1306
|
+
>
|
|
1307
|
+
<div className="empty-state">{emptyMessage}</div>
|
|
1308
|
+
</ChartFrame>
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function renderYAxis(dataKey?: string) {
|
|
1313
|
+
const yAxisValueFormat = normalized ? "percent" : chart.valueFormat ?? "compact";
|
|
1314
|
+
return (
|
|
1315
|
+
<YAxis
|
|
1316
|
+
axisLine={false}
|
|
1317
|
+
dataKey={dataKey}
|
|
1318
|
+
domain={normalized ? [0, 1] : undefined}
|
|
1319
|
+
interval={0}
|
|
1320
|
+
label={yAxisLabel}
|
|
1321
|
+
tick={(props: AxisTickProps) => (
|
|
1322
|
+
<NumericYAxisTick {...props} valueFormat={yAxisValueFormat} />
|
|
1323
|
+
)}
|
|
1324
|
+
tickFormatter={(value) => formatValue(value, yAxisValueFormat)}
|
|
1325
|
+
tickLine={false}
|
|
1326
|
+
tickMargin={RECHARTS_AXIS_TICK_MARGIN}
|
|
1327
|
+
tickSize={RECHARTS_AXIS_TICK_SIZE}
|
|
1328
|
+
ticks={yAxisScale.ticks}
|
|
1329
|
+
width="auto"
|
|
1330
|
+
/>
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function shouldShowSeriesPoints(series: ChartSeriesSpec): boolean {
|
|
1335
|
+
const showPoints = chart.settings?.showPoints;
|
|
1336
|
+
if (showPoints === "always") return true;
|
|
1337
|
+
if (showPoints === "never") return false;
|
|
1338
|
+
let definedPointCount = 0;
|
|
1339
|
+
for (const row of rows) {
|
|
1340
|
+
if (asNumber(row[series.field]) == null) continue;
|
|
1341
|
+
definedPointCount += 1;
|
|
1342
|
+
if (definedPointCount > 1) return false;
|
|
1343
|
+
}
|
|
1344
|
+
return definedPointCount === 1;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function renderSeries() {
|
|
1348
|
+
return activeSeries.map((series, index) => {
|
|
1349
|
+
const originalIndex = chart.series.findIndex((item) => item.field === series.field);
|
|
1350
|
+
const color = getSeriesColor(chart, series, originalIndex < 0 ? index : originalIndex);
|
|
1351
|
+
const key = `${chart.id}-${series.field}`;
|
|
1352
|
+
if (
|
|
1353
|
+
chart.type === "bar" ||
|
|
1354
|
+
chart.type === "horizontalBar" ||
|
|
1355
|
+
chart.type === "histogram" ||
|
|
1356
|
+
chart.type === "stackedBar" ||
|
|
1357
|
+
chart.type === "stackedBar100" ||
|
|
1358
|
+
chart.type === "horizontalStackedBar" ||
|
|
1359
|
+
chart.type === "horizontalStackedBar100"
|
|
1360
|
+
) {
|
|
1361
|
+
const isStacked = stacked;
|
|
1362
|
+
const isHorizontal = horizontal;
|
|
1363
|
+
const isHorizontalStacked = horizontal && stacked;
|
|
1364
|
+
const isTopStackSegment = isStacked && index === activeSeries.length - 1;
|
|
1365
|
+
return (
|
|
1366
|
+
<Bar
|
|
1367
|
+
dataKey={series.field}
|
|
1368
|
+
fill={color}
|
|
1369
|
+
isAnimationActive={false}
|
|
1370
|
+
key={key}
|
|
1371
|
+
minPointSize={!isStacked && showBarValueLabels ? 2 : undefined}
|
|
1372
|
+
name={series.label ?? series.field}
|
|
1373
|
+
radius={isStacked ? (isTopStackSegment ? (isHorizontalStacked ? [0, 6, 6, 0] : [6, 6, 0, 0]) : 0) : (isHorizontal ? [0, 6, 6, 0] : [6, 6, 0, 0])}
|
|
1374
|
+
stackId={isStacked ? "stack" : undefined}
|
|
1375
|
+
>
|
|
1376
|
+
{!isStacked ? renderCategoryBarCells(chart, series, rows, colorBarsBySign) : null}
|
|
1377
|
+
{showBarValueLabels ? (
|
|
1378
|
+
<LabelList
|
|
1379
|
+
content={(props) => renderBarValueLabel({ ...props, chart, horizontal: isHorizontal, showPositiveSign: colorBarsBySign })}
|
|
1380
|
+
dataKey={series.field}
|
|
1381
|
+
/>
|
|
1382
|
+
) : null}
|
|
1383
|
+
</Bar>
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (chart.type === "area" || chart.type === "stackedArea" || chart.type === "sparkline") {
|
|
1387
|
+
const strokeDasharray = getSeriesStrokeDasharray(chart, series);
|
|
1388
|
+
return (
|
|
1389
|
+
<Area
|
|
1390
|
+
dataKey={series.field}
|
|
1391
|
+
dot={shouldShowSeriesPoints(series)}
|
|
1392
|
+
fill={`url(#${getAreaGradientId(chart, series)})`}
|
|
1393
|
+
fillOpacity={1}
|
|
1394
|
+
isAnimationActive={false}
|
|
1395
|
+
key={key}
|
|
1396
|
+
name={series.label ?? series.field}
|
|
1397
|
+
stackId={chart.type === "stackedArea" ? "stack" : undefined}
|
|
1398
|
+
stroke={color}
|
|
1399
|
+
strokeDasharray={strokeDasharray}
|
|
1400
|
+
strokeWidth={2}
|
|
1401
|
+
type="monotone"
|
|
1402
|
+
/>
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
const strokeDasharray = getSeriesStrokeDasharray(chart, series);
|
|
1406
|
+
return (
|
|
1407
|
+
<Line
|
|
1408
|
+
dataKey={series.field}
|
|
1409
|
+
dot={shouldShowSeriesPoints(series)}
|
|
1410
|
+
isAnimationActive={false}
|
|
1411
|
+
key={key}
|
|
1412
|
+
name={series.label ?? series.field}
|
|
1413
|
+
stroke={color}
|
|
1414
|
+
strokeDasharray={strokeDasharray}
|
|
1415
|
+
strokeWidth={2}
|
|
1416
|
+
type="monotone"
|
|
1417
|
+
/>
|
|
1418
|
+
);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const xAxisProps = {
|
|
1423
|
+
angle: rotateCategoryXAxisLabels ? CATEGORY_X_AXIS_LABEL_ROTATION_DEGREES : undefined,
|
|
1424
|
+
axisLine: false,
|
|
1425
|
+
dataKey: chart.xField,
|
|
1426
|
+
height: rotateCategoryXAxisLabels
|
|
1427
|
+
? xAxisLabel
|
|
1428
|
+
? CATEGORY_X_AXIS_ROTATED_HEIGHT_WITH_TITLE
|
|
1429
|
+
: CATEGORY_X_AXIS_ROTATED_HEIGHT
|
|
1430
|
+
: wrapCategoryXAxisLabels
|
|
1431
|
+
? xAxisLabel
|
|
1432
|
+
? CATEGORY_X_AXIS_WRAPPED_HEIGHT_WITH_TITLE
|
|
1433
|
+
: CATEGORY_X_AXIS_WRAPPED_HEIGHT
|
|
1434
|
+
: undefined,
|
|
1435
|
+
interval: rotateCategoryXAxisLabels || wrapCategoryXAxisLabels || xAxisTicks ? 0 : undefined,
|
|
1436
|
+
label: xAxisLabel,
|
|
1437
|
+
tick: xAxisTicks
|
|
1438
|
+
? ((props: AxisTickProps) => (
|
|
1439
|
+
<XAxisEndpointTick
|
|
1440
|
+
{...props}
|
|
1441
|
+
firstTick={xAxisTicks[0]}
|
|
1442
|
+
lastTick={xAxisTicks[xAxisTicks.length - 1]}
|
|
1443
|
+
/>
|
|
1444
|
+
))
|
|
1445
|
+
: wrapCategoryXAxisLabels
|
|
1446
|
+
? ((props: AxisTickProps) => (
|
|
1447
|
+
<WrappedCategoryXAxisTick
|
|
1448
|
+
{...props}
|
|
1449
|
+
maxLineLength={wrappedCategoryXAxisLineLength}
|
|
1450
|
+
/>
|
|
1451
|
+
))
|
|
1452
|
+
: undefined,
|
|
1453
|
+
tickLine: false,
|
|
1454
|
+
tickMargin: rotateCategoryXAxisLabels
|
|
1455
|
+
? CATEGORY_X_AXIS_ROTATED_TICK_MARGIN
|
|
1456
|
+
: wrapCategoryXAxisLabels
|
|
1457
|
+
? CATEGORY_X_AXIS_WRAPPED_TICK_MARGIN
|
|
1458
|
+
: RECHARTS_AXIS_TICK_MARGIN,
|
|
1459
|
+
tickSize: RECHARTS_AXIS_TICK_SIZE,
|
|
1460
|
+
ticks: xAxisTicks,
|
|
1461
|
+
textAnchor: rotateCategoryXAxisLabels ? "end" : undefined,
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
const boxPlotRows = chart.type === "boxPlot" ? buildBoxPlotRows(chart, rows) : [];
|
|
1465
|
+
const boxPlotXAxisScale = chart.type === "boxPlot" ? getBoxPlotXAxisScale(boxPlotRows) : null;
|
|
1466
|
+
const waterfallRows = chart.type === "waterfall" ? buildWaterfallRows(chart, rows) : [];
|
|
1467
|
+
const funnelRows = chart.type === "funnel" ? buildFunnelRows(chart, rows, getFunnelStageColor) : [];
|
|
1468
|
+
const pieRows = chart.type === "pie"
|
|
1469
|
+
? buildPieRows(chart, rows).filter((row) => chart.series.length <= 1 || visibleIds.has(String(row.__pieField ?? "")))
|
|
1470
|
+
: [];
|
|
1471
|
+
const scatterYField = chart.type === "scatter" ? chartEncodingField(chart, "y") : undefined;
|
|
1472
|
+
const scatterLabelField = chart.type === "scatter" ? chartEncodingField(chart, "label") : undefined;
|
|
1473
|
+
const scatterSizeField = chart.type === "scatter" ? chartEncodingField(chart, "size") : undefined;
|
|
1474
|
+
const scatterLabelCount = scatterLabelField
|
|
1475
|
+
? rows.filter((row) => row[scatterLabelField] != null && String(row[scatterLabelField]).trim()).length
|
|
1476
|
+
: 0;
|
|
1477
|
+
const showScatterPointLabels = Boolean(
|
|
1478
|
+
scatterLabelField &&
|
|
1479
|
+
chart.labels?.values !== "none" &&
|
|
1480
|
+
(chart.labels?.values === "all" || scatterLabelCount <= SCATTER_AUTO_LABEL_LIMIT),
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
const chartElement =
|
|
1484
|
+
chart.type === "boxPlot" ? (
|
|
1485
|
+
<BarChart {...chartProps} data={boxPlotRows} layout="vertical" margin={{ ...cartesianMargin, top: 0 }}>
|
|
1486
|
+
<CartesianGrid horizontal={false} />
|
|
1487
|
+
<XAxis axisLine={false} domain={boxPlotXAxisScale?.domain} label={horizontalValueAxisLabel} tickFormatter={(value) => formatValue(value, chart.valueFormat)} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} ticks={boxPlotXAxisScale?.ticks} type="number" />
|
|
1488
|
+
<YAxis axisLine={false} dataKey={chart.xField} interval={0} label={horizontalCategoryAxisLabel} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} type="category" width="auto" />
|
|
1489
|
+
<Tooltip {...fastTooltipProps} content={<BoxPlotTooltip chart={tooltipChart} />} cursor={false} />
|
|
1490
|
+
<Bar barSize={22} dataKey="__boxRange" fill="var(--ds-chart-series-blue)" isAnimationActive={false} name="Quartile range" radius={[6, 6, 6, 6]} />
|
|
1491
|
+
</BarChart>
|
|
1492
|
+
) : chart.type === "waterfall" ? (
|
|
1493
|
+
<BarChart {...chartProps} data={waterfallRows} margin={cartesianMargin}>
|
|
1494
|
+
<CartesianGrid vertical={false} />
|
|
1495
|
+
<XAxis {...xAxisProps} />
|
|
1496
|
+
<YAxis axisLine={false} interval={0} label={yAxisLabel} tick={(props: AxisTickProps) => (<NumericYAxisTick {...props} valueFormat={chart.valueFormat ?? "compact"} />)} tickFormatter={(value) => formatValue(value, chart.valueFormat)} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} width="auto" />
|
|
1497
|
+
<ReferenceLine y={0} stroke="var(--ds-chart-reference-line)" strokeWidth={1} />
|
|
1498
|
+
<Tooltip {...fastTooltipProps} content={<WaterfallTooltip chart={tooltipChart} />} cursor={subtleTooltipCursor} />
|
|
1499
|
+
<Bar
|
|
1500
|
+
background={{ fill: "transparent" }}
|
|
1501
|
+
dataKey="__waterfallRange"
|
|
1502
|
+
isAnimationActive={false}
|
|
1503
|
+
name={firstSeries.label ?? firstSeries.field}
|
|
1504
|
+
shape={renderWaterfallBarShape}
|
|
1505
|
+
/>
|
|
1506
|
+
</BarChart>
|
|
1507
|
+
) : horizontal && !stacked && (chart.type === "bar" || chart.type === "horizontalBar") ? (
|
|
1508
|
+
<BarChart {...chartProps} layout="vertical" margin={{ ...cartesianMargin, top: 0 }}>
|
|
1509
|
+
<CartesianGrid horizontal={false} />
|
|
1510
|
+
<XAxis axisLine={false} label={horizontalValueAxisLabel} tickFormatter={(value) => formatValue(value, chart.valueFormat)} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} ticks={yAxisScale.ticks} type="number" />
|
|
1511
|
+
<YAxis axisLine={false} dataKey={chart.xField} interval={0} label={horizontalCategoryAxisLabel} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} type="category" width="auto" />
|
|
1512
|
+
{referenceLines(chart, true)}
|
|
1513
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1514
|
+
{renderSeries()}
|
|
1515
|
+
</BarChart>
|
|
1516
|
+
) : horizontal && stacked && (
|
|
1517
|
+
chart.type === "bar" ||
|
|
1518
|
+
chart.type === "horizontalStackedBar" ||
|
|
1519
|
+
chart.type === "horizontalStackedBar100"
|
|
1520
|
+
) ? (
|
|
1521
|
+
<BarChart {...chartProps} layout="vertical" margin={cartesianMargin} stackOffset={normalized ? "expand" : undefined}>
|
|
1522
|
+
<CartesianGrid horizontal={false} />
|
|
1523
|
+
<XAxis axisLine={false} domain={normalized ? [0, 1] : undefined} label={horizontalValueAxisLabel} tickFormatter={(value) => formatValue(value, normalized ? "percent" : chart.valueFormat)} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} type="number" />
|
|
1524
|
+
<YAxis axisLine={false} dataKey={chart.xField} interval={0} label={horizontalCategoryAxisLabel} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} type="category" width="auto" />
|
|
1525
|
+
{referenceLines(chart, true)}
|
|
1526
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1527
|
+
{renderSeries()}
|
|
1528
|
+
</BarChart>
|
|
1529
|
+
) : chart.type === "funnel" ? (
|
|
1530
|
+
<FunnelChart {...chartProps} data={funnelRows} margin={{ bottom: 0, left: 24, right: 24, top: 0 }}>
|
|
1531
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1532
|
+
<Funnel
|
|
1533
|
+
data={funnelRows}
|
|
1534
|
+
dataKey={firstSeries.field}
|
|
1535
|
+
fill={FUNNEL_STAGE_COLORS[0]}
|
|
1536
|
+
isAnimationActive={false}
|
|
1537
|
+
nameKey={chart.xField}
|
|
1538
|
+
shape={renderRoundedFunnelShape}
|
|
1539
|
+
stroke="none"
|
|
1540
|
+
>
|
|
1541
|
+
{funnelRows.map((row, index) => (
|
|
1542
|
+
<Cell key={`${chart.id}-funnel-stage-${index}`} fill={String(row.__funnelStageColor)} />
|
|
1543
|
+
))}
|
|
1544
|
+
<LabelList content={renderFunnelCenterLabel} dataKey="__funnelPairLabel" position="center" />
|
|
1545
|
+
</Funnel>
|
|
1546
|
+
</FunnelChart>
|
|
1547
|
+
) : chart.type === "pie" ? (
|
|
1548
|
+
<PieChart {...chartProps} data={pieRows} margin={{ bottom: 0, left: 0, right: 0, top: 0 }}>
|
|
1549
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1550
|
+
<Pie data={pieRows} dataKey="__pieValue" innerRadius="42%" isAnimationActive={false} nameKey="__pieName" outerRadius="78%">
|
|
1551
|
+
{pieRows.map((row) => (
|
|
1552
|
+
<Cell fill={getSeriesColor(chart, firstSeries, asFiniteNumber(row.__pieIndex))} key={`${chart.id}-pie-${row.__pieIndex}`} />
|
|
1553
|
+
))}
|
|
1554
|
+
</Pie>
|
|
1555
|
+
</PieChart>
|
|
1556
|
+
) : chart.type === "sparkline" ? (
|
|
1557
|
+
<AreaChart {...chartProps} margin={{ bottom: 20, left: 8, right: 8, top: 18 }}>
|
|
1558
|
+
{renderAreaGradientDefs(chart)}
|
|
1559
|
+
<XAxis dataKey={chart.xField} hide />
|
|
1560
|
+
<YAxis hide />
|
|
1561
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1562
|
+
<Area dataKey={firstSeries.field} dot={false} fill={`url(#${getAreaGradientId(chart, firstSeries)})`} fillOpacity={1} isAnimationActive={false} name={firstSeries.label ?? firstSeries.field} stroke={getSeriesColor(chart, firstSeries, 0)} strokeDasharray={getSeriesStrokeDasharray(chart, firstSeries)} strokeWidth={2} type="monotone" />
|
|
1563
|
+
</AreaChart>
|
|
1564
|
+
) : chart.type === "histogram" || (!horizontal && (
|
|
1565
|
+
chart.type === "bar" ||
|
|
1566
|
+
chart.type === "stackedBar" ||
|
|
1567
|
+
chart.type === "stackedBar100"
|
|
1568
|
+
)) ? (
|
|
1569
|
+
<BarChart {...chartProps} barCategoryGap={chart.type === "histogram" ? 1 : undefined} stackOffset={normalized ? "expand" : undefined}>
|
|
1570
|
+
{renderAreaGradientDefs(chart)}
|
|
1571
|
+
<CartesianGrid vertical={false} />
|
|
1572
|
+
<XAxis {...xAxisProps} />
|
|
1573
|
+
{renderYAxis()}
|
|
1574
|
+
{referenceLines(chart)}
|
|
1575
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1576
|
+
{renderSeries()}
|
|
1577
|
+
</BarChart>
|
|
1578
|
+
) : chart.type === "scatter" ? (
|
|
1579
|
+
<ScatterChart {...chartProps}>
|
|
1580
|
+
<CartesianGrid vertical={false} />
|
|
1581
|
+
<XAxis axisLine={false} dataKey={chart.xField} label={xAxisLabel} name={chart.xAxisTitle ?? chart.xField} tickLine={false} tickMargin={RECHARTS_AXIS_TICK_MARGIN} tickSize={RECHARTS_AXIS_TICK_SIZE} type="number" />
|
|
1582
|
+
{renderYAxis(scatterYField ?? firstSeries.field)}
|
|
1583
|
+
{scatterSizeField ? <ZAxis dataKey={scatterSizeField} range={[72, 420]} type="number" /> : null}
|
|
1584
|
+
{referenceLines(chart)}
|
|
1585
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1586
|
+
{activeSeries.map((series, index) => {
|
|
1587
|
+
const originalIndex = chart.series.findIndex((item) => item.field === series.field);
|
|
1588
|
+
const seriesRows = rows.filter((row) => asNumber(row[series.field]) != null);
|
|
1589
|
+
const scatterDataKey = scatterYField && seriesRows.some((row) => asNumber(row[scatterYField]) != null)
|
|
1590
|
+
? scatterYField
|
|
1591
|
+
: series.field;
|
|
1592
|
+
return (
|
|
1593
|
+
<Scatter
|
|
1594
|
+
data={seriesRows}
|
|
1595
|
+
dataKey={scatterDataKey}
|
|
1596
|
+
fill={getSeriesColor(chart, series, originalIndex < 0 ? index : originalIndex)}
|
|
1597
|
+
isAnimationActive={false}
|
|
1598
|
+
key={`${chart.id}-${series.field}`}
|
|
1599
|
+
name={series.label ?? series.field}
|
|
1600
|
+
>
|
|
1601
|
+
{showScatterPointLabels && scatterLabelField ? (
|
|
1602
|
+
<LabelList content={renderScatterPointLabel} dataKey={scatterLabelField} />
|
|
1603
|
+
) : null}
|
|
1604
|
+
</Scatter>
|
|
1605
|
+
);
|
|
1606
|
+
})}
|
|
1607
|
+
</ScatterChart>
|
|
1608
|
+
) : chart.type === "area" || chart.type === "stackedArea" ? (
|
|
1609
|
+
<AreaChart {...chartProps}>
|
|
1610
|
+
{renderAreaGradientDefs(chart)}
|
|
1611
|
+
<CartesianGrid vertical={false} />
|
|
1612
|
+
<XAxis {...xAxisProps} />
|
|
1613
|
+
{renderYAxis()}
|
|
1614
|
+
{referenceLines(chart)}
|
|
1615
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1616
|
+
{renderSeries()}
|
|
1617
|
+
</AreaChart>
|
|
1618
|
+
) : (
|
|
1619
|
+
<LineChart {...chartProps}>
|
|
1620
|
+
<CartesianGrid vertical={false} />
|
|
1621
|
+
<XAxis {...xAxisProps} />
|
|
1622
|
+
{renderYAxis()}
|
|
1623
|
+
{referenceLines(chart)}
|
|
1624
|
+
<Tooltip {...fastTooltipProps} content={<ChartTooltip chart={tooltipChart} getItemColor={resolveTooltipColor} />} cursor={subtleTooltipCursor} />
|
|
1625
|
+
{renderSeries()}
|
|
1626
|
+
</LineChart>
|
|
1627
|
+
);
|
|
1628
|
+
|
|
1629
|
+
return (
|
|
1630
|
+
<ChartFrame
|
|
1631
|
+
height={frameHeight}
|
|
1632
|
+
interactiveLegend={interactiveLegend}
|
|
1633
|
+
legendItems={legendItems}
|
|
1634
|
+
onVisibleChange={onVisibleSeriesChange}
|
|
1635
|
+
surface={resolvedSurface}
|
|
1636
|
+
visibleIds={visibleIds}
|
|
1637
|
+
>
|
|
1638
|
+
{responsive ? (
|
|
1639
|
+
<ResponsiveContainer height="100%" initialDimension={{ height: 1, width: 1 }} width="100%">
|
|
1640
|
+
{chartElement}
|
|
1641
|
+
</ResponsiveContainer>
|
|
1642
|
+
) : (
|
|
1643
|
+
chartElement
|
|
1644
|
+
)}
|
|
1645
|
+
</ChartFrame>
|
|
1646
|
+
);
|
|
1647
|
+
}
|