drizzle-cube 0.8.1 → 0.8.2
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/dist/adapters/ActivityGridChart.config-Bc7mPqcf.cjs +1 -0
- package/dist/adapters/ActivityGridChart.config-Dd2YDeaM.js +2 -0
- package/dist/adapters/AreaChart.config-BqNusnpg.js +2 -0
- package/dist/adapters/AreaChart.config-dI84V34B.cjs +1 -0
- package/dist/adapters/BarChart.config-B9MQjgLJ.cjs +1 -0
- package/dist/adapters/BarChart.config-Cq_qcxBA.js +2 -0
- package/dist/adapters/BoxPlotChart.config-XmtRN2xq.cjs +1 -0
- package/dist/adapters/BoxPlotChart.config-Zc-JGV-W.js +2 -0
- package/dist/adapters/BubbleChart.config-CU_18uxb.js +2 -0
- package/dist/adapters/BubbleChart.config-CbhOzA6-.cjs +1 -0
- package/dist/adapters/CandlestickChart.config-Bwo3uaa-.js +2 -0
- package/dist/adapters/CandlestickChart.config-CmRBlWFv.cjs +1 -0
- package/dist/adapters/DataTable.config-7JNSj6qQ.js +2 -0
- package/dist/adapters/DataTable.config-DNxm1sYk.cjs +1 -0
- package/dist/adapters/DotStripChart.config-B7Dl-T1h.js +2 -0
- package/dist/adapters/DotStripChart.config-Co4g1fpZ.cjs +1 -0
- package/dist/adapters/FunnelChart.config-D1TpuGtL.cjs +1 -0
- package/dist/adapters/FunnelChart.config-vuZScOHQ.js +2 -0
- package/dist/adapters/GaugeChart.config-BMgUHKhC.cjs +1 -0
- package/dist/adapters/GaugeChart.config-zdpFydo6.js +2 -0
- package/dist/adapters/HeatMapChart.config-4XMab1zq.js +2 -0
- package/dist/adapters/HeatMapChart.config-C-Ns6QBq.cjs +1 -0
- package/dist/adapters/KpiDelta.config-BGdqsHcc.js +2 -0
- package/dist/adapters/KpiDelta.config-DuNxrGIi.cjs +1 -0
- package/dist/adapters/KpiNumber.config-CkPWpcSz.js +2 -0
- package/dist/adapters/KpiNumber.config-DMGmuA3i.cjs +1 -0
- package/dist/adapters/KpiText.config-Bvtuv8vE.js +2 -0
- package/dist/adapters/KpiText.config-DOMLO16U.cjs +1 -0
- package/dist/adapters/LineChart.config-CJmTSmZ1.js +2 -0
- package/dist/adapters/LineChart.config-CrkGu0aa.cjs +1 -0
- package/dist/adapters/MarkdownChart.config-BgY5Xc97.cjs +1 -0
- package/dist/adapters/MarkdownChart.config-Cm6G4Yuh.js +2 -0
- package/dist/adapters/MeasureProfileChart.config-BYwDo_ox.cjs +1 -0
- package/dist/adapters/MeasureProfileChart.config-Dt7npubZ.js +2 -0
- package/dist/adapters/PieChart.config-BRbPctBN.cjs +1 -0
- package/dist/adapters/PieChart.config-HZofeody.js +2 -0
- package/dist/adapters/ProportionBarChart.config-B_rTtGZr.cjs +1 -0
- package/dist/adapters/ProportionBarChart.config-ChCSoM0L.js +2 -0
- package/dist/adapters/RadarChart.config-B9OdMEib.js +2 -0
- package/dist/adapters/RadarChart.config-BOgPD2Kz.cjs +1 -0
- package/dist/adapters/RadialBarChart.config-CeP7jEbq.js +2 -0
- package/dist/adapters/RadialBarChart.config-sivA1Jmo.cjs +1 -0
- package/dist/adapters/RecordsTable.config-BInSgo-G.js +2 -0
- package/dist/adapters/RecordsTable.config-CVmxLdjz.cjs +1 -0
- package/dist/adapters/RetentionCombinedChart.config-8ry7gh6X.cjs +1 -0
- package/dist/adapters/RetentionCombinedChart.config-Bxtxnlcf.js +2 -0
- package/dist/adapters/RetentionHeatmap.config-DCtOD8dO.js +2 -0
- package/dist/adapters/RetentionHeatmap.config-i-LgmmIT.cjs +1 -0
- package/dist/adapters/SankeyChart.config-DfpeHfNT.cjs +1 -0
- package/dist/adapters/SankeyChart.config-hfM0ACm1.js +2 -0
- package/dist/adapters/ScatterChart.config-BD2vfOZ6.js +2 -0
- package/dist/adapters/ScatterChart.config-CdpvRueF.cjs +1 -0
- package/dist/adapters/SunburstChart.config-Boyt88L_.cjs +1 -0
- package/dist/adapters/SunburstChart.config-BvrofzMv.js +2 -0
- package/dist/adapters/TreeMapChart.config-DBrqwtqT.js +2 -0
- package/dist/adapters/TreeMapChart.config-KcRLlZAG.cjs +1 -0
- package/dist/adapters/WaterfallChart.config-CuCGaIl5.cjs +1 -0
- package/dist/adapters/{TreeMapChart.config-BJ8HnZU0.cjs → WaterfallChart.config-DZ8H0V2L.js} +2 -1
- package/dist/adapters/{compiler-CBGD_4Et.js → compiler-BFLAXV7W.js} +1 -1
- package/dist/adapters/{compiler-BxZHh0UD.cjs → compiler-DG6dLFQU.cjs} +1 -1
- package/dist/adapters/core/index.cjs +1 -1
- package/dist/adapters/core/index.js +1 -1
- package/dist/adapters/{core-CIGuwGuI.cjs → core-CrJu6vJY.cjs} +1 -1
- package/dist/adapters/{core-DAAnqPyN.js → core-l3KMTu9u.js} +2 -2
- package/dist/adapters/express/index.cjs +1 -1
- package/dist/adapters/express/index.js +5 -5
- package/dist/adapters/fastify/index.cjs +1 -1
- package/dist/adapters/fastify/index.js +5 -5
- package/dist/adapters/{handler-BcYTw6zB.cjs → handler-CRGZ8gd2.cjs} +9 -9
- package/dist/adapters/{handler-eA0VJDHj.js → handler-DESp4wPg.js} +215 -95
- package/dist/adapters/hono/index.cjs +1 -1
- package/dist/adapters/hono/index.js +4 -4
- package/dist/adapters/mcp-tools.cjs +1 -1
- package/dist/adapters/mcp-tools.js +2 -2
- package/dist/adapters/mcp-transport-BqdBHmXL.js +730 -0
- package/dist/adapters/mcp-transport-D8xZJLPD.cjs +42 -0
- package/dist/adapters/nextjs/index.cjs +1 -1
- package/dist/adapters/nextjs/index.js +5 -5
- package/dist/adapters/{openai-CYYjUG_3.js → openai-BJyU4kvE.js} +1 -1
- package/dist/adapters/{openai-Bw1j-OiP.cjs → openai-DH9MtcLA.cjs} +1 -1
- package/dist/adapters/{openai-BZ48ioYp.js → openai-DK_KWiI7.js} +1 -1
- package/dist/adapters/{openai-CLjyL5_J.cjs → openai-GO3M7Hwi.cjs} +1 -1
- package/dist/adapters/{utils-BagURNRb.js → utils-BhlkVCzZ.js} +38 -0
- package/dist/adapters/utils-C1JpSDOX.cjs +128 -0
- package/dist/adapters/utils.cjs +1 -1
- package/dist/adapters/utils.js +1 -1
- package/dist/cjs/client/components/charts/DotStripChart.config.d.ts +15 -0
- package/dist/cjs/client/components/charts/DotStripChart.d.ts +4 -0
- package/dist/cjs/client/components/charts/DotStripChart.helpers.d.ts +91 -0
- package/dist/cjs/client/icons/types.d.ts +1 -0
- package/dist/cjs/client/types.d.ts +6 -1
- package/dist/cli/index.cjs +4 -0
- package/dist/client/charts.js +12 -12
- package/dist/client/chunks/{DashboardEditModal-_ZDMwaGX.js → DashboardEditModal-BliiIHcI.js} +12 -12
- package/dist/client/chunks/DashboardEditModal-BliiIHcI.js.map +1 -0
- package/dist/client/chunks/DotStripChart-ChwsVeRU.js +408 -0
- package/dist/client/chunks/DotStripChart-ChwsVeRU.js.map +1 -0
- package/dist/client/chunks/{ProportionBarChart-qfpAWhIg.js → ProportionBarChart-D-Qt9nkF.js} +3 -3
- package/dist/client/chunks/{ProportionBarChart-qfpAWhIg.js.map → ProportionBarChart-D-Qt9nkF.js.map} +1 -1
- package/dist/client/chunks/{RecordsTable-KmJy94xN.js → RecordsTable-aG-QYXOH.js} +4 -4
- package/dist/client/chunks/{RecordsTable-KmJy94xN.js.map → RecordsTable-aG-QYXOH.js.map} +1 -1
- package/dist/client/chunks/{RetentionCombinedChart-C7X6aj97.js → RetentionCombinedChart-BRZ9w7ah.js} +3 -3
- package/dist/client/chunks/{RetentionCombinedChart-C7X6aj97.js.map → RetentionCombinedChart-BRZ9w7ah.js.map} +1 -1
- package/dist/client/chunks/{RetentionHeatmap-PpRpiqR_.js → RetentionHeatmap-LkTt89pK.js} +2 -2
- package/dist/client/chunks/{RetentionHeatmap-PpRpiqR_.js.map → RetentionHeatmap-LkTt89pK.js.map} +1 -1
- package/dist/client/chunks/{analysis-builder-DaqyOjLX.js → analysis-builder-C0gJ3kDD.js} +9 -9
- package/dist/client/chunks/{analysis-builder-DaqyOjLX.js.map → analysis-builder-C0gJ3kDD.js.map} +1 -1
- package/dist/client/chunks/{analysis-builder-shared-ow9KAHDP.js → analysis-builder-shared-dqcpl_WL.js} +9 -9
- package/dist/client/chunks/{analysis-builder-shared-ow9KAHDP.js.map → analysis-builder-shared-dqcpl_WL.js.map} +1 -1
- package/dist/client/chunks/{chart-activity-grid-DO82rrbW.js → chart-activity-grid-CJzxC18G.js} +40 -2
- package/dist/client/chunks/chart-activity-grid-CJzxC18G.js.map +1 -0
- package/dist/client/chunks/{chart-area-9SBeG0GM.js → chart-area-C66dgy84.js} +2 -2
- package/dist/client/chunks/{chart-area-9SBeG0GM.js.map → chart-area-C66dgy84.js.map} +1 -1
- package/dist/client/chunks/{chart-bar-tfgmLK2D.js → chart-bar-D9kiSOCT.js} +3 -3
- package/dist/client/chunks/{chart-bar-tfgmLK2D.js.map → chart-bar-D9kiSOCT.js.map} +1 -1
- package/dist/client/chunks/{chart-box-plot-BuYW6j6g.js → chart-box-plot-FsLIMisc.js} +2 -2
- package/dist/client/chunks/{chart-box-plot-BuYW6j6g.js.map → chart-box-plot-FsLIMisc.js.map} +1 -1
- package/dist/client/chunks/{chart-bubble-BNmonWqW.js → chart-bubble-BzyvrqKs.js} +2 -2
- package/dist/client/chunks/{chart-bubble-BNmonWqW.js.map → chart-bubble-BzyvrqKs.js.map} +1 -1
- package/dist/client/chunks/{chart-candlestick-BYeWNkKl.js → chart-candlestick-6lD35bLw.js} +2 -2
- package/dist/client/chunks/{chart-candlestick-BYeWNkKl.js.map → chart-candlestick-6lD35bLw.js.map} +1 -1
- package/dist/client/chunks/{chart-data-table-C_oHiK6N.js → chart-data-table-Cou9qOab.js} +248 -120
- package/dist/client/chunks/chart-data-table-Cou9qOab.js.map +1 -0
- package/dist/client/chunks/{chart-funnel-B_CWC3cL.js → chart-funnel-B0775aaT.js} +2 -2
- package/dist/client/chunks/{chart-funnel-B_CWC3cL.js.map → chart-funnel-B0775aaT.js.map} +1 -1
- package/dist/client/chunks/{chart-gauge-D7VxwPnx.js → chart-gauge-pf-2E8w9.js} +3 -3
- package/dist/client/chunks/{chart-gauge-D7VxwPnx.js.map → chart-gauge-pf-2E8w9.js.map} +1 -1
- package/dist/client/chunks/{chart-heat-map-q7Vf_dog.js → chart-heat-map-EzVGe6fl.js} +3 -3
- package/dist/client/chunks/{chart-heat-map-q7Vf_dog.js.map → chart-heat-map-EzVGe6fl.js.map} +1 -1
- package/dist/client/chunks/{chart-kpi-delta-PEuA3iGs.js → chart-kpi-delta-lfskDdx4.js} +3 -3
- package/dist/client/chunks/{chart-kpi-delta-PEuA3iGs.js.map → chart-kpi-delta-lfskDdx4.js.map} +1 -1
- package/dist/client/chunks/{chart-kpi-number-DlxrO6Pq.js → chart-kpi-number-BnsofbTf.js} +5 -5
- package/dist/client/chunks/{chart-kpi-number-DlxrO6Pq.js.map → chart-kpi-number-BnsofbTf.js.map} +1 -1
- package/dist/client/chunks/{chart-kpi-text-Cx6cJu2s.js → chart-kpi-text-U5EdmdkV.js} +3 -3
- package/dist/client/chunks/{chart-kpi-text-Cx6cJu2s.js.map → chart-kpi-text-U5EdmdkV.js.map} +1 -1
- package/dist/client/chunks/{chart-line-pDe25JzB.js → chart-line-IwXqPY80.js} +3 -3
- package/dist/client/chunks/{chart-line-pDe25JzB.js.map → chart-line-IwXqPY80.js.map} +1 -1
- package/dist/client/chunks/{chart-markdown-CRY15yp0.js → chart-markdown-yxN_xBP9.js} +2 -2
- package/dist/client/chunks/{chart-markdown-CRY15yp0.js.map → chart-markdown-yxN_xBP9.js.map} +1 -1
- package/dist/client/chunks/{chart-measure-profile-DyQ-vvuC.js → chart-measure-profile-Bs4vhBJQ.js} +3 -3
- package/dist/client/chunks/{chart-measure-profile-DyQ-vvuC.js.map → chart-measure-profile-Bs4vhBJQ.js.map} +1 -1
- package/dist/client/chunks/{chart-pie-ZrnBycy1.js → chart-pie-LDJfDXfX.js} +3 -3
- package/dist/client/chunks/{chart-pie-ZrnBycy1.js.map → chart-pie-LDJfDXfX.js.map} +1 -1
- package/dist/client/chunks/{chart-radar-BXbnYMUJ.js → chart-radar-BZY6TP9m.js} +3 -3
- package/dist/client/chunks/{chart-radar-BXbnYMUJ.js.map → chart-radar-BZY6TP9m.js.map} +1 -1
- package/dist/client/chunks/{chart-radial-bar-CYxH3ryh.js → chart-radial-bar-aga05X7h.js} +3 -3
- package/dist/client/chunks/{chart-radial-bar-CYxH3ryh.js.map → chart-radial-bar-aga05X7h.js.map} +1 -1
- package/dist/client/chunks/{chart-sankey-C9R_TDDh.js → chart-sankey-MRCoWFd4.js} +2 -2
- package/dist/client/chunks/{chart-sankey-C9R_TDDh.js.map → chart-sankey-MRCoWFd4.js.map} +1 -1
- package/dist/client/chunks/{chart-scatter-DrwRkzzl.js → chart-scatter-DP-nn9hH.js} +3 -3
- package/dist/client/chunks/{chart-scatter-DrwRkzzl.js.map → chart-scatter-DP-nn9hH.js.map} +1 -1
- package/dist/client/chunks/{chart-sunburst-Bs1yqIPp.js → chart-sunburst-DE4EX7zi.js} +3 -3
- package/dist/client/chunks/{chart-sunburst-Bs1yqIPp.js.map → chart-sunburst-DE4EX7zi.js.map} +1 -1
- package/dist/client/chunks/{chart-tree-map-BNjHhhVq.js → chart-tree-map-C3O6uQcP.js} +3 -3
- package/dist/client/chunks/{chart-tree-map-BNjHhhVq.js.map → chart-tree-map-C3O6uQcP.js.map} +1 -1
- package/dist/client/chunks/{chart-waterfall-CyoC0uLT.js → chart-waterfall-DU2Fg_7P.js} +3 -3
- package/dist/client/chunks/{chart-waterfall-CyoC0uLT.js.map → chart-waterfall-DU2Fg_7P.js.map} +1 -1
- package/dist/client/chunks/{charts-core-DCQ7od_L.js → charts-core-DnirPG1F.js} +2 -2
- package/dist/client/chunks/{charts-core-DCQ7od_L.js.map → charts-core-DnirPG1F.js.map} +1 -1
- package/dist/client/chunks/charts-loader-tCSbqT7H.js +87 -0
- package/dist/client/chunks/charts-loader-tCSbqT7H.js.map +1 -0
- package/dist/client/chunks/{nl-NL-CEdoCz6Q.js → nl-NL-B9VnaVAm.js} +39 -1
- package/dist/client/chunks/nl-NL-B9VnaVAm.js.map +1 -0
- package/dist/client/chunks/{schema-visualization-DfXxFBIY.js → schema-visualization-o1zzuMQy.js} +3 -3
- package/dist/client/chunks/{schema-visualization-DfXxFBIY.js.map → schema-visualization-o1zzuMQy.js.map} +1 -1
- package/dist/client/chunks/{useDirtyStateTracking-BFJHiiVK.js → useDirtyStateTracking-DTYbmRBn.js} +4 -4
- package/dist/client/chunks/{useDirtyStateTracking-BFJHiiVK.js.map → useDirtyStateTracking-DTYbmRBn.js.map} +1 -1
- package/dist/client/chunks/{useExplainAI-_C4WQE5I.js → useExplainAI-MuC9jBn-.js} +4 -4
- package/dist/client/chunks/{useExplainAI-_C4WQE5I.js.map → useExplainAI-MuC9jBn-.js.map} +1 -1
- package/dist/client/chunks/{vendor-Gb3lH0cc.js → vendor-XQT9APIq.js} +2 -2
- package/dist/client/chunks/{vendor-Gb3lH0cc.js.map → vendor-XQT9APIq.js.map} +1 -1
- package/dist/client/components/charts/DotStripChart.config.d.ts +15 -0
- package/dist/client/components/charts/DotStripChart.d.ts +4 -0
- package/dist/client/components/charts/DotStripChart.helpers.d.ts +91 -0
- package/dist/client/components.js +1 -1
- package/dist/client/hooks.js +3 -3
- package/dist/client/icons/types.d.ts +1 -0
- package/dist/client/icons.js +1 -1
- package/dist/client/index.js +13 -13
- package/dist/client/providers.js +1 -1
- package/dist/client/schema.js +1 -1
- package/dist/client/styles.css +1 -1
- package/dist/client/types.d.ts +6 -1
- package/dist/client/utils.js +4 -4
- package/dist/client-bundle-stats.html +1 -1
- package/dist/mcp-app/mcp-app.html +99 -99
- package/dist/server/index.cjs +29 -29
- package/dist/server/index.js +312 -154
- package/package.json +1 -1
- package/dist/adapters/ActivityGridChart.config-DXUUgx0N.cjs +0 -1
- package/dist/adapters/ActivityGridChart.config-Dyozu3VW.js +0 -2
- package/dist/adapters/AreaChart.config-Di5odBXp.cjs +0 -1
- package/dist/adapters/AreaChart.config-KEaXP_4K.js +0 -2
- package/dist/adapters/BarChart.config-B-hBHykR.cjs +0 -1
- package/dist/adapters/BarChart.config-C5sqbn0A.js +0 -2
- package/dist/adapters/BoxPlotChart.config-B1R4Br7L.js +0 -2
- package/dist/adapters/BoxPlotChart.config-C82Fhd3c.cjs +0 -1
- package/dist/adapters/BubbleChart.config-CoBxmmfp.js +0 -2
- package/dist/adapters/BubbleChart.config-DfOnNjL-.cjs +0 -1
- package/dist/adapters/CandlestickChart.config-C5ggV_3j.cjs +0 -1
- package/dist/adapters/CandlestickChart.config-ZaJpWVuL.js +0 -2
- package/dist/adapters/DataTable.config-Cx2_ACdY.cjs +0 -1
- package/dist/adapters/DataTable.config-DkAgD-iz.js +0 -2
- package/dist/adapters/FunnelChart.config-CvzcECUB.js +0 -2
- package/dist/adapters/FunnelChart.config-DVH7IFnb.cjs +0 -1
- package/dist/adapters/GaugeChart.config-Csx8rt8o.cjs +0 -1
- package/dist/adapters/GaugeChart.config-DEmrNgMa.js +0 -2
- package/dist/adapters/HeatMapChart.config-BfD_dpP7.js +0 -2
- package/dist/adapters/HeatMapChart.config-D_jlyPJL.cjs +0 -1
- package/dist/adapters/KpiDelta.config-CVBdUL_n.cjs +0 -1
- package/dist/adapters/KpiDelta.config-D4dCXOxl.js +0 -2
- package/dist/adapters/KpiNumber.config-BK5pgIKz.cjs +0 -1
- package/dist/adapters/KpiNumber.config-D6XqjZrm.js +0 -2
- package/dist/adapters/KpiText.config-ChphMwG5.js +0 -2
- package/dist/adapters/KpiText.config-fO1wc6FH.cjs +0 -1
- package/dist/adapters/LineChart.config-CXJrgz1s.js +0 -2
- package/dist/adapters/LineChart.config-Cg_h-SiJ.cjs +0 -1
- package/dist/adapters/MarkdownChart.config-ByPjX-4O.cjs +0 -1
- package/dist/adapters/MarkdownChart.config-Df2yY9AT.js +0 -2
- package/dist/adapters/MeasureProfileChart.config-BBWGIteF.cjs +0 -1
- package/dist/adapters/MeasureProfileChart.config-C9OFbN9Z.js +0 -2
- package/dist/adapters/PieChart.config-C6jJj6xm.js +0 -2
- package/dist/adapters/PieChart.config-CmCw_8eN.cjs +0 -1
- package/dist/adapters/ProportionBarChart.config-B9NEyFEe.js +0 -2
- package/dist/adapters/ProportionBarChart.config-C3GAVPWn.cjs +0 -1
- package/dist/adapters/RadarChart.config-5AMwEqUJ.cjs +0 -1
- package/dist/adapters/RadarChart.config-Ct_b7IM5.js +0 -2
- package/dist/adapters/RadialBarChart.config-Bp-Sl0cm.js +0 -2
- package/dist/adapters/RadialBarChart.config-D14yoexI.cjs +0 -1
- package/dist/adapters/RecordsTable.config-D2-qmG06.cjs +0 -1
- package/dist/adapters/RecordsTable.config-DIwoqZCU.js +0 -2
- package/dist/adapters/RetentionCombinedChart.config-BWHipi5M.cjs +0 -1
- package/dist/adapters/RetentionCombinedChart.config-D40UgTcl.js +0 -2
- package/dist/adapters/RetentionHeatmap.config-GlnSLrjn.js +0 -2
- package/dist/adapters/RetentionHeatmap.config-MlhZf26c.cjs +0 -1
- package/dist/adapters/SankeyChart.config-Bf1_NHiv.js +0 -2
- package/dist/adapters/SankeyChart.config-BoYf6BFE.cjs +0 -1
- package/dist/adapters/ScatterChart.config-CV2rdGJp.js +0 -2
- package/dist/adapters/ScatterChart.config-DywduGF3.cjs +0 -1
- package/dist/adapters/SunburstChart.config-CrqiE1UZ.js +0 -2
- package/dist/adapters/SunburstChart.config-D5tbF_GJ.cjs +0 -1
- package/dist/adapters/TreeMapChart.config-DxgIqIIj.js +0 -2
- package/dist/adapters/WaterfallChart.config-Cns8fLFu.js +0 -2
- package/dist/adapters/WaterfallChart.config-D_kdL9SN.cjs +0 -1
- package/dist/adapters/mcp-transport-BpG44Fvn.js +0 -730
- package/dist/adapters/mcp-transport-CDxZXpc-.cjs +0 -42
- package/dist/adapters/utils-BiKIg2tk.cjs +0 -128
- package/dist/client/chunks/DashboardEditModal-_ZDMwaGX.js.map +0 -1
- package/dist/client/chunks/chart-activity-grid-DO82rrbW.js.map +0 -1
- package/dist/client/chunks/chart-data-table-C_oHiK6N.js.map +0 -1
- package/dist/client/chunks/charts-loader-Bya_zDIA.js +0 -86
- package/dist/client/chunks/charts-loader-Bya_zDIA.js.map +0 -1
- package/dist/client/chunks/nl-NL-CEdoCz6Q.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordsTable-KmJy94xN.js","names":[],"sources":["../../../src/client/utils/rowLinkUtils.ts","../../../src/client/utils/recordsTableUtils.ts","../../../src/client/components/charts/recordsTableHooks.ts","../../../src/client/components/charts/RecordsTable.tsx"],"sourcesContent":["/**\n * Row click-through URLs for the records table.\n *\n * A dashboard author writes a template such as `/employees/{Employees.id}`,\n * and each row fills its own tokens. The template is author-controlled but the\n * *values* are data, so they are percent-encoded before substitution — an\n * encoded value cannot introduce a scheme, a path segment or a query separator.\n *\n * The resulting URL is then checked rather than trusted: only same-origin\n * relative paths and absolute http(s) URLs are allowed through.\n */\n\nconst TOKEN_PATTERN = /\\{([A-Za-z0-9_]+\\.[A-Za-z0-9_]+)\\}/g\n\n/** Schemes that must never survive validation, even spelled oddly. */\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:'])\n\n/**\n * Resolve a row link template against one row.\n *\n * Returns `null` — meaning \"render no link\" — when the template references a\n * field the row does not carry, or when the result is not a URL we are willing\n * to navigate to. Never throws: a malformed template must not take the table\n * down with it.\n *\n * @param template - e.g. `/employees/{Employees.id}?tab=profile`\n * @param row - the row's values, including hidden columns\n * @param origin - the origin relative URLs are resolved against\n */\nexport function buildRowUrl(\n template: string | undefined,\n row: Record<string, unknown>,\n origin: string = defaultOrigin()\n): string | null {\n if (!template) return null\n\n let unresolved = false\n const substituted = template.replace(TOKEN_PATTERN, (_match, field: string) => {\n const value = row[field]\n if (value === null || value === undefined) {\n unresolved = true\n return ''\n }\n return encodeURIComponent(String(value))\n })\n\n if (unresolved) return null\n\n return isSafeUrl(substituted, origin) ? substituted : null\n}\n\n/**\n * Whether a URL is one we will navigate to: an absolute `http(s)` URL, or a\n * path that stays on this origin.\n *\n * Rejected, deliberately: `javascript:`, `data:`, `vbscript:` and every other\n * scheme; protocol-relative `//evil.example`, which inherits the current scheme\n * and silently leaves the origin; and backslash forms such as `\\\\evil.example`\n * or `/\\evil.example`, which several browsers normalise into that same\n * protocol-relative shape.\n */\nexport function isSafeUrl(url: string, origin: string = defaultOrigin()): boolean {\n const trimmed = url.trim()\n if (trimmed === '') return false\n\n // Control characters, including the tab/newline a browser strips out of a\n // scheme — which turns `java<TAB>script:` back into `javascript:`.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001F\\u007F]/.test(trimmed)) return false\n\n if (trimmed.includes('\\\\')) return false\n if (trimmed.startsWith('//')) return false\n\n try {\n const parsed = new URL(trimmed, origin)\n if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) return false\n\n // A relative path is only allowed while it stays here. An absolute http(s)\n // URL to another host is fine — that is the \"external link\" case — so the\n // origin check applies to inputs that did not declare a scheme.\n const declaresScheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.test(trimmed)\n if (!declaresScheme && parsed.origin !== new URL(origin).origin) return false\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * The origin to resolve relative links against. Falls back to a placeholder\n * outside a browser (SSR, tests) so validation still runs rather than throwing.\n */\nfunction defaultOrigin(): string {\n return typeof window !== 'undefined' && window.location?.origin\n ? window.location.origin\n : 'http://localhost'\n}\n","/**\n * Cell formatting and column-width persistence for the records table.\n *\n * Kept out of the component so each piece is unit-testable without a DOM, and\n * so the format switch stays a pure value → display transform.\n */\n\nimport type { ColumnFormatConfig, ColumnFormatKind } from '../types.js'\nimport { formatAxisValue, formatTimeValue } from './chartUtils.js'\n\n/** Fallback when an author has not sized a column. */\nexport const DEFAULT_COLUMN_WIDTH = 160\n/** Narrower than this and the resize handle overlaps the header text. */\nexport const MIN_COLUMN_WIDTH = 60\n\nconst COLUMN_WIDTHS_STORAGE_KEY = 'dc-records-table-column-widths'\nconst COLUMN_ORDER_STORAGE_KEY = 'dc-records-table-column-order'\n\n/**\n * Identity under which a viewer's column widths are remembered.\n *\n * A chart component has no portlet id, so widths key on the columns themselves:\n * two records tables showing the same fields share widths, which is the\n * behaviour an author would expect from two views of the same records.\n */\nexport function columnWidthStorageKey(columns: string[]): string {\n return [...columns].sort().join('|')\n}\n\nexport function loadColumnWidths(key: string): Record<string, number> {\n try {\n const raw = localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY)\n if (!raw) return {}\n return JSON.parse(raw)[key] ?? {}\n } catch {\n return {}\n }\n}\n\nexport function saveColumnWidths(key: string, widths: Record<string, number>): void {\n try {\n const raw = localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY)\n const all = raw ? JSON.parse(raw) : {}\n all[key] = widths\n localStorage.setItem(COLUMN_WIDTHS_STORAGE_KEY, JSON.stringify(all))\n } catch {\n // localStorage unavailable (private mode, SSR) — widths stay session-local\n }\n}\n\n/**\n * A viewer's own column order, remembered under the same identity as widths.\n *\n * Stored as a list rather than a map so it survives the author adding or\n * removing a column: {@link applyColumnOrder} reconciles it against the\n * columns that actually exist.\n */\nexport function loadColumnOrder(key: string): string[] {\n try {\n const raw = localStorage.getItem(COLUMN_ORDER_STORAGE_KEY)\n if (!raw) return []\n const stored = JSON.parse(raw)[key]\n return Array.isArray(stored) ? stored : []\n } catch {\n return []\n }\n}\n\nexport function saveColumnOrder(key: string, order: string[]): void {\n try {\n const raw = localStorage.getItem(COLUMN_ORDER_STORAGE_KEY)\n const all = raw ? JSON.parse(raw) : {}\n all[key] = order\n localStorage.setItem(COLUMN_ORDER_STORAGE_KEY, JSON.stringify(all))\n } catch {\n // localStorage unavailable — the order stays session-local\n }\n}\n\n/**\n * Lay a remembered order over the columns the chart actually has.\n *\n * Columns the order does not mention keep their authored position relative to\n * the end, so an author adding a column still sees it rather than having it\n * silently swallowed by a stale local order.\n */\nexport function applyColumnOrder(columns: string[], order: string[]): string[] {\n if (order.length === 0) return columns\n const present = new Set(columns)\n const ordered = order.filter(column => present.has(column))\n const remaining = columns.filter(column => !ordered.includes(column))\n return [...ordered, ...remaining]\n}\n\n/** Move one column to another's position, returning a new order. */\nexport function moveColumn(columns: string[], from: string, to: string): string[] {\n const fromIndex = columns.indexOf(from)\n const toIndex = columns.indexOf(to)\n if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return columns\n\n const next = [...columns]\n next.splice(fromIndex, 1)\n next.splice(toIndex, 0, from)\n return next\n}\n\n/**\n * A cell resolved to something renderable. `progress` carries its own shape\n * because the component draws a bar or a ring rather than a string; every\n * other kind reduces to text plus, for badges, a colour.\n *\n * The component only ever sees a `RenderedCell`, so the progress `style` is\n * resolved here rather than being read back out of the column's config.\n */\nexport type RenderedCell =\n | { kind: 'text'; text: string }\n | { kind: 'badge'; text: string; colorIndex?: number }\n | { kind: 'progress'; text: string; fraction: number; style: 'bar' | 'circle' }\n\nconst EMPTY: RenderedCell = { kind: 'text', text: '' }\n\n/**\n * Turn a raw cell value into its display form under the column's format.\n *\n * Every kind degrades to text rather than erroring: a non-numeric value under\n * `number`/`progress` renders as-is (EAV columns legitimately contain 'n/a'),\n * and a badge value with no colour mapping renders neutral rather than being\n * given a guessed colour.\n */\nexport function renderCellValue(\n value: unknown,\n format: ColumnFormatConfig | undefined\n): RenderedCell {\n if (isEmpty(value)) return EMPTY\n const render = RENDERERS[format?.kind ?? 'text'] ?? renderText\n return render(value, format)\n}\n\ntype CellRenderer = (value: unknown, format: ColumnFormatConfig | undefined) => RenderedCell\n\nconst RENDERERS: Record<ColumnFormatKind, CellRenderer> = {\n text: renderText,\n number: renderNumber,\n date: renderDate,\n badge: renderBadge,\n progress: renderProgress\n}\n\nfunction renderText(value: unknown): RenderedCell {\n return { kind: 'text', text: stringify(value) }\n}\n\nfunction renderNumber(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const num = toNumber(value)\n if (num === null) return { kind: 'text', text: String(value) }\n return { kind: 'text', text: formatAxisValue(num, format?.numberFormat ?? {}) }\n}\n\nfunction renderDate(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n return { kind: 'text', text: formatTimeValue(value, format?.dateGranularity) }\n}\n\n/**\n * Colour mappings that are actually usable.\n *\n * A `columnFormats` entry can arrive from an agent tool call or a hand-edited\n * portlet, where `badgeColors` may not be the array of `{ value, colorIndex }`\n * the type promises — an object map and colour names instead of palette indices\n * are the common shapes. Anything unusable is discarded so the badge renders\n * neutral, which is what an unmapped value does anyway.\n */\nfunction usableBadgeColors(format: ColumnFormatConfig | undefined): ColumnFormatConfig['badgeColors'] {\n if (!Array.isArray(format?.badgeColors)) return []\n return format.badgeColors.filter(entry => entry && typeof entry.colorIndex === 'number')\n}\n\nfunction renderBadge(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const text = stringify(value)\n return { kind: 'badge', text, colorIndex: usableBadgeColors(format)?.find(e => e.value === text)?.colorIndex }\n}\n\nconst PROGRESS_NUMBER_FORMAT = { unit: 'number', abbreviate: false, decimals: 0 } as const\n\nfunction renderProgress(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const num = toNumber(value)\n if (num === null) return { kind: 'text', text: String(value) }\n\n return {\n kind: 'progress',\n fraction: progressFraction(num, format),\n style: format?.progressStyle ?? 'bar',\n text: formatAxisValue(num, format?.numberFormat ?? PROGRESS_NUMBER_FORMAT)\n }\n}\n\n/** Where a value sits within the column's bounds, as 0-1. */\nfunction progressFraction(value: number, format: ColumnFormatConfig | undefined): number {\n const min = format?.progressMin ?? 0\n const max = format?.progressMax ?? 100\n const span = max - min\n // A zero-width range would divide by zero; treat it as \"full\".\n return span === 0 ? 1 : clamp((value - min) / span, 0, 1)\n}\n\nfunction stringify(value: unknown): string {\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') return value.toLocaleString()\n if (value instanceof Date) return value.toISOString()\n return String(value)\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/**\n * Sort rows by one column, client-side.\n *\n * Only used when the host provides no server-side pagination — with paging on,\n * sorting the loaded page alone would put the wrong rows on page 1.\n */\nexport function sortRows(\n rows: Record<string, unknown>[],\n column: string,\n direction: 'asc' | 'desc'\n): Record<string, unknown>[] {\n const factor = direction === 'asc' ? 1 : -1\n return [...rows].sort((a, b) => {\n // Empty values sort last in *both* directions — reversing the sort should\n // not push a column's blanks to the top of the table.\n const aEmpty = isEmpty(a[column])\n const bEmpty = isEmpty(b[column])\n if (aEmpty || bEmpty) return aEmpty && bEmpty ? 0 : aEmpty ? 1 : -1\n return factor * compareValues(a[column], b[column])\n })\n}\n\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === ''\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n const aNum = toNumber(a)\n const bNum = toNumber(b)\n if (aNum !== null && bNum !== null) return aNum - bNum\n\n return String(a).localeCompare(String(b))\n}\n","/**\n * State for the records table, split out of the component so each concern —\n * which columns and in what order, how wide they are, which page and sort —\n * reads on its own.\n *\n * All three are viewer-side. The *author's* choices (assigned columns, page\n * size, widths) arrive through `chartConfig`/`displayConfig`; these hooks layer\n * the viewer's own adjustments over them.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { getOrderedColumnsFromQuery } from '../../utils/pivotUtils.js'\nimport {\n DEFAULT_COLUMN_WIDTH,\n MIN_COLUMN_WIDTH,\n applyColumnOrder,\n columnWidthStorageKey,\n loadColumnOrder,\n loadColumnWidths,\n moveColumn,\n saveColumnOrder,\n saveColumnWidths,\n sortRows\n} from '../../utils/recordsTableUtils.js'\nimport type { ChartAxisConfig, ChartPagination, CubeQuery } from '../../types.js'\n\nexport const DEFAULT_PAGE_SIZE = 25\n\ntype SortState = { column: string; direction: 'asc' | 'desc' }\n\n/**\n * Which columns to render, in which order.\n *\n * The author's arrangement comes from the Columns drop zone; a viewer can drag\n * a header on top of it, and that order is remembered per column set. It is\n * *reconciled* rather than substituted, so a column the author adds later still\n * appears instead of being swallowed by a stale local order.\n */\nexport function useRecordsColumns(params: {\n rows: Record<string, unknown>[]\n chartConfig?: ChartAxisConfig\n queryObject?: CubeQuery\n}) {\n const { rows, chartConfig, queryObject } = params\n\n const baseColumns = useMemo(\n () => resolveColumns(rows, chartConfig, queryObject),\n [rows, chartConfig, queryObject]\n )\n\n const storageKey = useMemo(() => columnWidthStorageKey(baseColumns), [baseColumns])\n const [columnOrder, setColumnOrder] = useState<string[]>([])\n\n useEffect(() => {\n setColumnOrder(loadColumnOrder(storageKey))\n }, [storageKey])\n\n const columns = useMemo(\n () => applyColumnOrder(baseColumns, columnOrder),\n [baseColumns, columnOrder]\n )\n\n const [draggedColumn, setDraggedColumn] = useState<string | null>(null)\n // A drag ending on the header it started from can still produce a click;\n // without this, reordering would also toggle the sort.\n const didDragRef = useRef(false)\n\n const startColumnDrag = useCallback((column: string) => {\n didDragRef.current = true\n setDraggedColumn(column)\n }, [])\n\n const endColumnDrag = useCallback(() => {\n setDraggedColumn(null)\n requestAnimationFrame(() => { didDragRef.current = false })\n }, [])\n\n const dropColumn = useCallback((target: string) => {\n const source = draggedColumn\n endColumnDrag()\n if (!source || source === target) return\n const next = moveColumn(columns, source, target)\n setColumnOrder(next)\n saveColumnOrder(storageKey, next)\n }, [columns, draggedColumn, endColumnDrag, storageKey])\n\n return { columns, storageKey, draggedColumn, didDragRef, startColumnDrag, endColumnDrag, dropColumn }\n}\n\nfunction resolveColumns(\n rows: Record<string, unknown>[],\n chartConfig: ChartAxisConfig | undefined,\n queryObject: CubeQuery | undefined\n): string[] {\n const available = Object.keys(rows[0] ?? {})\n const hidden = new Set(chartConfig?.hiddenColumns ?? [])\n\n const configured = chartConfig?.columns?.filter(column => available.includes(column))\n if (configured && configured.length > 0) {\n return configured.filter(column => !hidden.has(column))\n }\n\n // Nothing assigned: follow the order the query asked for, then anything else\n // the rows happen to carry.\n const fromQuery = getOrderedColumnsFromQuery(queryObject).filter(column => available.includes(column))\n const ordered = fromQuery.length > 0\n ? [...fromQuery, ...available.filter(column => !fromQuery.includes(column))]\n : available\n\n return ordered.filter(column => !hidden.has(column))\n}\n\n/**\n * Drag-resizable column widths, remembered per column set.\n *\n * A chart has no portlet id and no write path back to `displayConfig`, so an\n * authored `columnWidths` acts as the default a viewer's own drags layer over —\n * the same arrangement the data browser uses.\n */\nexport function useColumnWidths(params: {\n columns: string[]\n storageKey: string\n authored?: Record<string, number>\n}) {\n const { columns, storageKey, authored } = params\n\n const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})\n const tableRef = useRef<HTMLTableElement>(null)\n const didResizeRef = useRef(false)\n\n useEffect(() => {\n setColumnWidths({ ...(authored ?? {}), ...loadColumnWidths(storageKey) })\n }, [storageKey, authored])\n\n const startResize = useCallback((event: React.MouseEvent, column: string) => {\n event.preventDefault()\n event.stopPropagation()\n\n const startX = event.clientX\n didResizeRef.current = false\n\n // Freeze every column at its rendered width first, so the ones either side\n // of the handle do not jump when this one changes.\n const snapshot: Record<string, number> = {}\n tableRef.current?.querySelectorAll('thead th').forEach((th, index) => {\n const name = columns[index]\n if (name) snapshot[name] = th.getBoundingClientRect().width\n })\n\n const startWidth = snapshot[column] ?? DEFAULT_COLUMN_WIDTH\n let latest = snapshot\n\n const onMove = (moveEvent: MouseEvent) => {\n const diff = moveEvent.clientX - startX\n if (Math.abs(diff) > 2) didResizeRef.current = true\n latest = { ...latest, [column]: Math.max(MIN_COLUMN_WIDTH, startWidth + diff) }\n setColumnWidths(latest)\n }\n\n const onUp = () => {\n document.removeEventListener('mousemove', onMove)\n document.removeEventListener('mouseup', onUp)\n document.body.style.cursor = ''\n document.body.style.userSelect = ''\n saveColumnWidths(storageKey, latest)\n // Held one frame so the header's click handler can see it and not sort.\n requestAnimationFrame(() => { didResizeRef.current = false })\n }\n\n document.addEventListener('mousemove', onMove)\n document.addEventListener('mouseup', onUp)\n document.body.style.cursor = 'col-resize'\n document.body.style.userSelect = 'none'\n }, [columns, storageKey])\n\n const totalWidth = Object.keys(columnWidths).length > 0\n ? columns.reduce((sum, column) => sum + (columnWidths[column] ?? DEFAULT_COLUMN_WIDTH), 0)\n : undefined\n\n return { columnWidths, totalWidth, tableRef, didResizeRef, startResize }\n}\n\n/**\n * Sorting and paging, server-side when the host offers it.\n *\n * With a `pagination` prop the host re-queries, so both must go through it:\n * sorting only the loaded page would put the wrong rows on page 1. Without one\n * — the AnalysisBuilder preview, the notebook, plugin hosts — the table sorts\n * and pages over the rows it already has.\n */\nexport function useRecordsPaging(params: {\n rows: Record<string, unknown>[]\n pagination?: ChartPagination\n authoredPageSize?: number\n}) {\n const { rows, pagination, authoredPageSize } = params\n\n const [localSort, setLocalSort] = useState<SortState | null>(null)\n const [localPage, setLocalPage] = useState(0)\n\n const isServerPaged = Boolean(pagination)\n const sort = pagination ? pagination.sort ?? null : localSort\n\n const toggleSort = useCallback((column: string) => {\n if (pagination) {\n pagination.toggleSort(column)\n return\n }\n setLocalSort(current => {\n if (current?.column !== column) return { column, direction: 'asc' }\n return current.direction === 'asc' ? { column, direction: 'desc' } : null\n })\n }, [pagination])\n\n const sortedRows = useMemo(\n () => (!isServerPaged && localSort ? sortRows(rows, localSort.column, localSort.direction) : rows),\n [rows, isServerPaged, localSort]\n )\n\n const pageSize = pagination?.pageSize ?? authoredPageSize ?? DEFAULT_PAGE_SIZE\n const page = pagination?.page ?? localPage\n // Server-side the count is whatever the server reported; falling back to the\n // loaded page keeps the pager honest before the first total arrives.\n const rowCount = isServerPaged\n ? pagination?.total ?? page * pageSize + sortedRows.length\n : sortedRows.length\n const pageCount = Math.max(1, Math.ceil(rowCount / pageSize))\n\n const goToPage = useCallback((next: number) => {\n if (pagination) pagination.setPage(next)\n else setLocalPage(next)\n }, [pagination])\n\n // A shrinking result set can leave the viewer past the last page.\n useEffect(() => {\n if (isServerPaged) return\n setLocalPage(current => Math.min(current, pageCount - 1))\n }, [pageCount, isServerPaged])\n\n const visibleRows = useMemo(\n () => (isServerPaged ? sortedRows : sortedRows.slice(page * pageSize, page * pageSize + pageSize)),\n [sortedRows, isServerPaged, page, pageSize]\n )\n\n return {\n sort,\n toggleSort,\n visibleRows,\n page,\n pageSize,\n pageCount,\n rowCount,\n goToPage,\n showPager: rowCount > pageSize\n }\n}\n","/**\n * Records Table\n *\n * A record-grain listing (as opposed to DataTable's aggregate view): ordered\n * columns with per-column formats, hidden columns fetched for row context,\n * click-to-sort headers, drag-resizable widths and pagination.\n *\n * Hand-rolled rather than built on a table library: the sort/resize/<colgroup>\n * patterns already exist in `DataBrowser/DataBrowserTable.tsx` (copied, not\n * imported — that component reads the data-browser store), and badge/progress\n * cells are bespoke either way.\n */\n\nimport React, { useCallback, useMemo } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport { useCubeMeta } from '../../providers/CubeProvider.js'\nimport { ChartEmptyState } from './ChartStates.js'\nimport { getIcon } from '../../icons/index.js'\nimport { buildRowUrl } from '../../utils/rowLinkUtils.js'\nimport { DEFAULT_COLUMN_WIDTH, renderCellValue, type RenderedCell } from '../../utils/recordsTableUtils.js'\nimport { useColumnWidths, useRecordsColumns, useRecordsPaging } from './recordsTableHooks.js'\nimport type { ChartProps, ColumnFormatConfig } from '../../types.js'\n\nconst SortAscIcon = getIcon('chevronUp')\nconst SortDescIcon = getIcon('chevronDown')\n\nconst RecordsTable = React.memo(function RecordsTable({\n data,\n chartConfig,\n displayConfig = {},\n queryObject,\n colorPalette,\n height = 300,\n onDataPointClick,\n drillEnabled,\n pagination\n}: ChartProps) {\n const { t } = useTranslation()\n const { getFieldLabel } = useCubeMeta()\n\n const rows = useMemo(() => (Array.isArray(data) ? (data as Record<string, unknown>[]) : []), [data])\n const columnFormats = displayConfig.columnFormats ?? {}\n\n const {\n columns,\n storageKey,\n draggedColumn,\n didDragRef,\n startColumnDrag,\n endColumnDrag,\n dropColumn\n } = useRecordsColumns({ rows, chartConfig, queryObject })\n\n const { columnWidths, totalWidth, tableRef, didResizeRef, startResize } = useColumnWidths({\n columns,\n storageKey,\n authored: displayConfig.columnWidths\n })\n\n const {\n sort,\n toggleSort,\n visibleRows,\n page,\n pageSize,\n pageCount,\n rowCount,\n goToPage,\n showPager\n } = useRecordsPaging({ rows, pagination, authoredPageSize: displayConfig.pageSize })\n\n const handleHeaderClick = useCallback((column: string) => {\n // Suppressed after a resize or reorder drag, which can still emit a click.\n if (didResizeRef.current || didDragRef.current) return\n toggleSort(column)\n }, [didDragRef, didResizeRef, toggleSort])\n\n // A configured row link wins over drill: the row becomes a real anchor, so\n // modifier-clicks and \"open in new tab\" behave as they do anywhere else.\n const rowLink = displayConfig.rowLink\n const rowClickable = Boolean(!rowLink && drillEnabled && onDataPointClick)\n\n const handleRowClick = useCallback((row: Record<string, unknown>, column: string, event: React.MouseEvent) => {\n if (!rowClickable) return\n onDataPointClick?.({\n dataPoint: row,\n clickedField: column,\n xValue: row[column],\n position: { x: event.clientX, y: event.clientY },\n nativeEvent: event\n })\n }, [onDataPointClick, rowClickable])\n\n if (rows.length === 0) {\n return <ChartEmptyState height={height} hint={t('chart.runtime.noDataHint.table')} />\n }\n\n if (columns.length === 0) {\n return (\n <ChartEmptyState\n height={height}\n titleKey=\"chart.runtime.recordsTable.noColumns\"\n hint={t('chart.runtime.table.invalidStructure')}\n />\n )\n }\n\n const firstRow = page * pageSize + 1\n const lastRow = Math.min(rowCount, page * pageSize + visibleRows.length)\n\n return (\n <div className=\"dc:flex dc:flex-col dc:w-full\" style={{ height }}>\n <div className=\"dc:flex-1 dc:overflow-auto\">\n <table\n ref={tableRef}\n className=\"dc:border-collapse\"\n style={{ tableLayout: 'fixed', width: totalWidth, minWidth: '100%' }}\n >\n <colgroup>\n {columns.map(column => (\n <col key={column} style={{ width: columnWidths[column] ?? DEFAULT_COLUMN_WIDTH }} />\n ))}\n </colgroup>\n <thead className=\"bg-dc-surface-secondary dc:sticky dc:top-0 dc:z-20\">\n <tr>\n {columns.map(column => (\n <HeaderCell\n key={column}\n column={column}\n label={columnFormats[column]?.label || getFieldLabel(column)}\n align={columnFormats[column]?.align ?? defaultAlign(columnFormats[column])}\n sortDirection={sort?.column === column ? sort.direction : undefined}\n isDropTarget={draggedColumn !== null && draggedColumn !== column}\n onClick={handleHeaderClick}\n onResizeStart={startResize}\n onDragStart={startColumnDrag}\n onDragEnd={endColumnDrag}\n onDrop={dropColumn}\n />\n ))}\n </tr>\n </thead>\n <tbody className=\"bg-dc-surface\">\n {visibleRows.map((row, index) => (\n <Row\n key={index}\n row={row}\n columns={columns}\n columnFormats={columnFormats}\n colorPalette={colorPalette}\n href={rowLink ? buildRowUrl(rowLink.urlTemplate, row) : null}\n target={rowLink?.target}\n clickable={rowClickable}\n onCellClick={handleRowClick}\n />\n ))}\n </tbody>\n </table>\n </div>\n\n {showPager && (\n <div className=\"dc:flex dc:items-center dc:justify-between dc:gap-2 dc:px-3 dc:py-1.5 dc:border-t border-dc-border dc:text-xs text-dc-text-secondary\">\n <span>{t('chart.runtime.recordsTable.rowRange', { from: firstRow, to: lastRow, total: rowCount })}</span>\n <div className=\"dc:flex dc:items-center dc:gap-1\">\n <PagerButton\n label={t('chart.runtime.recordsTable.previousPage')}\n disabled={page === 0}\n onClick={() => goToPage(Math.max(0, page - 1))}\n >\n ‹\n </PagerButton>\n <span>{t('chart.runtime.recordsTable.pageOf', { page: page + 1, pages: pageCount })}</span>\n <PagerButton\n label={t('chart.runtime.recordsTable.nextPage')}\n disabled={page >= pageCount - 1}\n onClick={() => goToPage(Math.min(pageCount - 1, page + 1))}\n >\n ›\n </PagerButton>\n </div>\n </div>\n )}\n </div>\n )\n})\n\nexport default RecordsTable\n\n/** Numeric-ish formats read better right-aligned unless the author says otherwise. */\nfunction defaultAlign(format: ColumnFormatConfig | undefined): 'left' | 'right' {\n return format?.kind === 'number' ? 'right' : 'left'\n}\n\ninterface HeaderCellProps {\n column: string\n label: string\n align: 'left' | 'right'\n sortDirection?: 'asc' | 'desc'\n isDropTarget: boolean\n onClick: (column: string) => void\n onResizeStart: (event: React.MouseEvent, column: string) => void\n onDragStart: (column: string) => void\n onDragEnd: () => void\n onDrop: (column: string) => void\n}\n\nfunction HeaderCell({\n column,\n label,\n align,\n sortDirection,\n isDropTarget,\n onClick,\n onResizeStart,\n onDragStart,\n onDragEnd,\n onDrop\n}: HeaderCellProps) {\n return (\n <th\n draggable\n onDragStart={(event) => {\n // Required for Firefox to start the drag at all.\n event.dataTransfer.setData('text/plain', column)\n event.dataTransfer.effectAllowed = 'move'\n onDragStart(column)\n }}\n onDragOver={(event) => { if (isDropTarget) event.preventDefault() }}\n onDrop={(event) => { event.preventDefault(); onDrop(column) }}\n onDragEnd={onDragEnd}\n onClick={() => onClick(column)}\n className={`dc:relative dc:px-3 dc:py-2 dc:text-xs dc:font-medium text-dc-text-muted dc:uppercase dc:tracking-wider dc:cursor-pointer dc:select-none dc:border-b border-dc-border ${\n align === 'right' ? 'dc:text-right' : 'dc:text-left'\n }${isDropTarget ? ' dc:border-l-2 border-dc-accent' : ''}`}\n >\n <div className={`dc:flex dc:items-center dc:gap-1.5 dc:overflow-hidden${align === 'right' ? ' dc:justify-end' : ''}`}>\n <span className=\"dc:truncate\">{label}</span>\n {sortDirection === 'asc' && <SortAscIcon className=\"dc:w-3 dc:h-3 text-dc-accent dc:shrink-0\" />}\n {sortDirection === 'desc' && <SortDescIcon className=\"dc:w-3 dc:h-3 text-dc-accent dc:shrink-0\" />}\n </div>\n <div\n onMouseDown={(event) => onResizeStart(event, column)}\n className=\"dc:absolute dc:top-0 dc:right-0 dc:w-1.5 dc:h-full dc:cursor-col-resize dc:hover:bg-dc-accent dc:opacity-0 dc:hover:opacity-100 dc:transition-opacity dc:z-30\"\n />\n </th>\n )\n}\n\ninterface RowProps {\n row: Record<string, unknown>\n columns: string[]\n columnFormats: Record<string, ColumnFormatConfig>\n colorPalette?: ChartProps['colorPalette']\n href: string | null\n target?: 'self' | 'blank'\n clickable: boolean\n onCellClick: (row: Record<string, unknown>, column: string, event: React.MouseEvent) => void\n}\n\nfunction Row({ row, columns, columnFormats, colorPalette, href, target, clickable, onCellClick }: RowProps) {\n return (\n <tr className={`dc:border-b border-dc-border hover:bg-dc-surface-secondary${clickable ? ' dc:cursor-pointer' : ''}`}>\n {columns.map(column => {\n const format = columnFormats[column]\n const align = format?.align ?? defaultAlign(format)\n return (\n <td\n key={column}\n onClick={(event) => onCellClick(row, column, event)}\n className={`dc:px-3 dc:py-1.5 dc:text-sm text-dc-text dc:overflow-hidden${\n align === 'right' ? ' dc:text-right dc:tabular-nums' : ''\n }`}\n >\n <CellContent\n cell={renderCellValue(row[column], format)}\n colorPalette={colorPalette}\n href={href}\n target={target}\n />\n </td>\n )\n })}\n </tr>\n )\n}\n\n/**\n * A cell, wrapped in the row's link when one resolves. An unresolvable or\n * unsafe template yields no anchor rather than a dead one.\n */\nfunction CellContent({\n cell,\n colorPalette,\n href,\n target\n}: {\n cell: RenderedCell\n colorPalette?: ChartProps['colorPalette']\n href: string | null\n target?: 'self' | 'blank'\n}) {\n const content = <Cell cell={cell} colorPalette={colorPalette} />\n if (!href) return content\n\n return (\n <a\n href={href}\n target={target === 'blank' ? '_blank' : undefined}\n rel={target === 'blank' ? 'noopener noreferrer' : undefined}\n className=\"dc:block dc:no-underline dc:text-inherit hover:dc:underline\"\n >\n {content}\n </a>\n )\n}\n\nfunction Cell({ cell, colorPalette }: { cell: RenderedCell; colorPalette?: ChartProps['colorPalette'] }) {\n if (cell.kind === 'badge') {\n const color = cell.colorIndex !== undefined ? colorPalette?.colors[cell.colorIndex] : undefined\n return (\n <span\n className=\"dc:inline-block dc:px-2 dc:py-0.5 dc:rounded-full dc:text-xs dc:font-medium dc:truncate dc:max-w-full\"\n style={color\n ? { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }\n : { backgroundColor: 'var(--dc-surface-secondary)', color: 'var(--dc-text-secondary)', border: '1px solid var(--dc-border)' }}\n >\n {cell.text}\n </span>\n )\n }\n\n if (cell.kind === 'progress') {\n return (\n <div className=\"dc:flex dc:items-center dc:gap-2\">\n {cell.style === 'circle'\n ? <ProgressRing fraction={cell.fraction} label={cell.text} />\n : (\n // Outlined track, so the full 0-100% extent is visible and a\n // near-complete bar is distinguishable from a complete one.\n <div className=\"dc:flex-1 dc:h-2 dc:rounded-full dc:overflow-hidden dc:border border-dc-border bg-dc-surface-secondary dc:min-w-[2rem]\">\n <div\n className=\"dc:h-full dc:rounded-full bg-dc-primary\"\n style={{ width: `${cell.fraction * 100}%` }}\n />\n </div>\n )}\n {/*\n Fixed width, not shrink-to-fit: the track is flexible, so a wider\n label (\"£117K\") would shorten its row's bar and two equal values\n would draw different lengths. Right-aligned and tabular so the\n numbers line up as a column.\n */}\n <span className=\"dc:w-16 dc:shrink-0 dc:text-right dc:text-xs dc:tabular-nums dc:truncate\">\n {cell.text}\n </span>\n </div>\n )\n }\n\n return <span className=\"dc:truncate dc:block\">{cell.text}</span>\n}\n\nconst RING_SIZE = 18\nconst RING_STROKE = 3\nconst RING_CENTRE = RING_SIZE / 2\nconst RING_RADIUS = RING_CENTRE - RING_STROKE / 2\n\n/**\n * The compact alternative to the progress bar: a ring that fills 0→100%, so a\n * progress column can be as narrow as its value label.\n *\n * `pathLength` normalises the circumference to 100, which lets the dash offset\n * be the percentage remaining without any 2πr arithmetic. The ring alone\n * carries the value, hence the `img` role and label.\n */\nfunction ProgressRing({ fraction, label }: { fraction: number; label: string }) {\n return (\n <svg\n role=\"img\"\n aria-label={label}\n width={RING_SIZE}\n height={RING_SIZE}\n viewBox={`0 0 ${RING_SIZE} ${RING_SIZE}`}\n className=\"dc:shrink-0\"\n >\n <circle\n cx={RING_CENTRE}\n cy={RING_CENTRE}\n r={RING_RADIUS}\n fill=\"none\"\n stroke=\"var(--dc-surface-secondary)\"\n strokeWidth={RING_STROKE}\n />\n {fraction > 0 && (\n <circle\n cx={RING_CENTRE}\n cy={RING_CENTRE}\n r={RING_RADIUS}\n fill=\"none\"\n stroke=\"var(--dc-primary)\"\n strokeWidth={RING_STROKE}\n strokeLinecap=\"round\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={100 - fraction * 100}\n transform={`rotate(-90 ${RING_CENTRE} ${RING_CENTRE})`}\n />\n )}\n </svg>\n )\n}\n\nfunction PagerButton({\n label,\n disabled,\n onClick,\n children\n}: {\n label: string\n disabled: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n className=\"dc:px-2 dc:py-0.5 dc:border border-dc-border dc:rounded-sm bg-dc-surface text-dc-text hover:bg-dc-surface-secondary dc:disabled:opacity-40 dc:disabled:cursor-not-allowed\"\n >\n {children}\n </button>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RecordsTable-aG-QYXOH.js","names":[],"sources":["../../../src/client/utils/rowLinkUtils.ts","../../../src/client/utils/recordsTableUtils.ts","../../../src/client/components/charts/recordsTableHooks.ts","../../../src/client/components/charts/RecordsTable.tsx"],"sourcesContent":["/**\n * Row click-through URLs for the records table.\n *\n * A dashboard author writes a template such as `/employees/{Employees.id}`,\n * and each row fills its own tokens. The template is author-controlled but the\n * *values* are data, so they are percent-encoded before substitution — an\n * encoded value cannot introduce a scheme, a path segment or a query separator.\n *\n * The resulting URL is then checked rather than trusted: only same-origin\n * relative paths and absolute http(s) URLs are allowed through.\n */\n\nconst TOKEN_PATTERN = /\\{([A-Za-z0-9_]+\\.[A-Za-z0-9_]+)\\}/g\n\n/** Schemes that must never survive validation, even spelled oddly. */\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:'])\n\n/**\n * Resolve a row link template against one row.\n *\n * Returns `null` — meaning \"render no link\" — when the template references a\n * field the row does not carry, or when the result is not a URL we are willing\n * to navigate to. Never throws: a malformed template must not take the table\n * down with it.\n *\n * @param template - e.g. `/employees/{Employees.id}?tab=profile`\n * @param row - the row's values, including hidden columns\n * @param origin - the origin relative URLs are resolved against\n */\nexport function buildRowUrl(\n template: string | undefined,\n row: Record<string, unknown>,\n origin: string = defaultOrigin()\n): string | null {\n if (!template) return null\n\n let unresolved = false\n const substituted = template.replace(TOKEN_PATTERN, (_match, field: string) => {\n const value = row[field]\n if (value === null || value === undefined) {\n unresolved = true\n return ''\n }\n return encodeURIComponent(String(value))\n })\n\n if (unresolved) return null\n\n return isSafeUrl(substituted, origin) ? substituted : null\n}\n\n/**\n * Whether a URL is one we will navigate to: an absolute `http(s)` URL, or a\n * path that stays on this origin.\n *\n * Rejected, deliberately: `javascript:`, `data:`, `vbscript:` and every other\n * scheme; protocol-relative `//evil.example`, which inherits the current scheme\n * and silently leaves the origin; and backslash forms such as `\\\\evil.example`\n * or `/\\evil.example`, which several browsers normalise into that same\n * protocol-relative shape.\n */\nexport function isSafeUrl(url: string, origin: string = defaultOrigin()): boolean {\n const trimmed = url.trim()\n if (trimmed === '') return false\n\n // Control characters, including the tab/newline a browser strips out of a\n // scheme — which turns `java<TAB>script:` back into `javascript:`.\n // eslint-disable-next-line no-control-regex\n if (/[\\u0000-\\u001F\\u007F]/.test(trimmed)) return false\n\n if (trimmed.includes('\\\\')) return false\n if (trimmed.startsWith('//')) return false\n\n try {\n const parsed = new URL(trimmed, origin)\n if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) return false\n\n // A relative path is only allowed while it stays here. An absolute http(s)\n // URL to another host is fine — that is the \"external link\" case — so the\n // origin check applies to inputs that did not declare a scheme.\n const declaresScheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.test(trimmed)\n if (!declaresScheme && parsed.origin !== new URL(origin).origin) return false\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * The origin to resolve relative links against. Falls back to a placeholder\n * outside a browser (SSR, tests) so validation still runs rather than throwing.\n */\nfunction defaultOrigin(): string {\n return typeof window !== 'undefined' && window.location?.origin\n ? window.location.origin\n : 'http://localhost'\n}\n","/**\n * Cell formatting and column-width persistence for the records table.\n *\n * Kept out of the component so each piece is unit-testable without a DOM, and\n * so the format switch stays a pure value → display transform.\n */\n\nimport type { ColumnFormatConfig, ColumnFormatKind } from '../types.js'\nimport { formatAxisValue, formatTimeValue } from './chartUtils.js'\n\n/** Fallback when an author has not sized a column. */\nexport const DEFAULT_COLUMN_WIDTH = 160\n/** Narrower than this and the resize handle overlaps the header text. */\nexport const MIN_COLUMN_WIDTH = 60\n\nconst COLUMN_WIDTHS_STORAGE_KEY = 'dc-records-table-column-widths'\nconst COLUMN_ORDER_STORAGE_KEY = 'dc-records-table-column-order'\n\n/**\n * Identity under which a viewer's column widths are remembered.\n *\n * A chart component has no portlet id, so widths key on the columns themselves:\n * two records tables showing the same fields share widths, which is the\n * behaviour an author would expect from two views of the same records.\n */\nexport function columnWidthStorageKey(columns: string[]): string {\n return [...columns].sort().join('|')\n}\n\nexport function loadColumnWidths(key: string): Record<string, number> {\n try {\n const raw = localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY)\n if (!raw) return {}\n return JSON.parse(raw)[key] ?? {}\n } catch {\n return {}\n }\n}\n\nexport function saveColumnWidths(key: string, widths: Record<string, number>): void {\n try {\n const raw = localStorage.getItem(COLUMN_WIDTHS_STORAGE_KEY)\n const all = raw ? JSON.parse(raw) : {}\n all[key] = widths\n localStorage.setItem(COLUMN_WIDTHS_STORAGE_KEY, JSON.stringify(all))\n } catch {\n // localStorage unavailable (private mode, SSR) — widths stay session-local\n }\n}\n\n/**\n * A viewer's own column order, remembered under the same identity as widths.\n *\n * Stored as a list rather than a map so it survives the author adding or\n * removing a column: {@link applyColumnOrder} reconciles it against the\n * columns that actually exist.\n */\nexport function loadColumnOrder(key: string): string[] {\n try {\n const raw = localStorage.getItem(COLUMN_ORDER_STORAGE_KEY)\n if (!raw) return []\n const stored = JSON.parse(raw)[key]\n return Array.isArray(stored) ? stored : []\n } catch {\n return []\n }\n}\n\nexport function saveColumnOrder(key: string, order: string[]): void {\n try {\n const raw = localStorage.getItem(COLUMN_ORDER_STORAGE_KEY)\n const all = raw ? JSON.parse(raw) : {}\n all[key] = order\n localStorage.setItem(COLUMN_ORDER_STORAGE_KEY, JSON.stringify(all))\n } catch {\n // localStorage unavailable — the order stays session-local\n }\n}\n\n/**\n * Lay a remembered order over the columns the chart actually has.\n *\n * Columns the order does not mention keep their authored position relative to\n * the end, so an author adding a column still sees it rather than having it\n * silently swallowed by a stale local order.\n */\nexport function applyColumnOrder(columns: string[], order: string[]): string[] {\n if (order.length === 0) return columns\n const present = new Set(columns)\n const ordered = order.filter(column => present.has(column))\n const remaining = columns.filter(column => !ordered.includes(column))\n return [...ordered, ...remaining]\n}\n\n/** Move one column to another's position, returning a new order. */\nexport function moveColumn(columns: string[], from: string, to: string): string[] {\n const fromIndex = columns.indexOf(from)\n const toIndex = columns.indexOf(to)\n if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return columns\n\n const next = [...columns]\n next.splice(fromIndex, 1)\n next.splice(toIndex, 0, from)\n return next\n}\n\n/**\n * A cell resolved to something renderable. `progress` carries its own shape\n * because the component draws a bar or a ring rather than a string; every\n * other kind reduces to text plus, for badges, a colour.\n *\n * The component only ever sees a `RenderedCell`, so the progress `style` is\n * resolved here rather than being read back out of the column's config.\n */\nexport type RenderedCell =\n | { kind: 'text'; text: string }\n | { kind: 'badge'; text: string; colorIndex?: number }\n | { kind: 'progress'; text: string; fraction: number; style: 'bar' | 'circle' }\n\nconst EMPTY: RenderedCell = { kind: 'text', text: '' }\n\n/**\n * Turn a raw cell value into its display form under the column's format.\n *\n * Every kind degrades to text rather than erroring: a non-numeric value under\n * `number`/`progress` renders as-is (EAV columns legitimately contain 'n/a'),\n * and a badge value with no colour mapping renders neutral rather than being\n * given a guessed colour.\n */\nexport function renderCellValue(\n value: unknown,\n format: ColumnFormatConfig | undefined\n): RenderedCell {\n if (isEmpty(value)) return EMPTY\n const render = RENDERERS[format?.kind ?? 'text'] ?? renderText\n return render(value, format)\n}\n\ntype CellRenderer = (value: unknown, format: ColumnFormatConfig | undefined) => RenderedCell\n\nconst RENDERERS: Record<ColumnFormatKind, CellRenderer> = {\n text: renderText,\n number: renderNumber,\n date: renderDate,\n badge: renderBadge,\n progress: renderProgress\n}\n\nfunction renderText(value: unknown): RenderedCell {\n return { kind: 'text', text: stringify(value) }\n}\n\nfunction renderNumber(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const num = toNumber(value)\n if (num === null) return { kind: 'text', text: String(value) }\n return { kind: 'text', text: formatAxisValue(num, format?.numberFormat ?? {}) }\n}\n\nfunction renderDate(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n return { kind: 'text', text: formatTimeValue(value, format?.dateGranularity) }\n}\n\n/**\n * Colour mappings that are actually usable.\n *\n * A `columnFormats` entry can arrive from an agent tool call or a hand-edited\n * portlet, where `badgeColors` may not be the array of `{ value, colorIndex }`\n * the type promises — an object map and colour names instead of palette indices\n * are the common shapes. Anything unusable is discarded so the badge renders\n * neutral, which is what an unmapped value does anyway.\n */\nfunction usableBadgeColors(format: ColumnFormatConfig | undefined): ColumnFormatConfig['badgeColors'] {\n if (!Array.isArray(format?.badgeColors)) return []\n return format.badgeColors.filter(entry => entry && typeof entry.colorIndex === 'number')\n}\n\nfunction renderBadge(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const text = stringify(value)\n return { kind: 'badge', text, colorIndex: usableBadgeColors(format)?.find(e => e.value === text)?.colorIndex }\n}\n\nconst PROGRESS_NUMBER_FORMAT = { unit: 'number', abbreviate: false, decimals: 0 } as const\n\nfunction renderProgress(value: unknown, format: ColumnFormatConfig | undefined): RenderedCell {\n const num = toNumber(value)\n if (num === null) return { kind: 'text', text: String(value) }\n\n return {\n kind: 'progress',\n fraction: progressFraction(num, format),\n style: format?.progressStyle ?? 'bar',\n text: formatAxisValue(num, format?.numberFormat ?? PROGRESS_NUMBER_FORMAT)\n }\n}\n\n/** Where a value sits within the column's bounds, as 0-1. */\nfunction progressFraction(value: number, format: ColumnFormatConfig | undefined): number {\n const min = format?.progressMin ?? 0\n const max = format?.progressMax ?? 100\n const span = max - min\n // A zero-width range would divide by zero; treat it as \"full\".\n return span === 0 ? 1 : clamp((value - min) / span, 0, 1)\n}\n\nfunction stringify(value: unknown): string {\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') return value.toLocaleString()\n if (value instanceof Date) return value.toISOString()\n return String(value)\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/**\n * Sort rows by one column, client-side.\n *\n * Only used when the host provides no server-side pagination — with paging on,\n * sorting the loaded page alone would put the wrong rows on page 1.\n */\nexport function sortRows(\n rows: Record<string, unknown>[],\n column: string,\n direction: 'asc' | 'desc'\n): Record<string, unknown>[] {\n const factor = direction === 'asc' ? 1 : -1\n return [...rows].sort((a, b) => {\n // Empty values sort last in *both* directions — reversing the sort should\n // not push a column's blanks to the top of the table.\n const aEmpty = isEmpty(a[column])\n const bEmpty = isEmpty(b[column])\n if (aEmpty || bEmpty) return aEmpty && bEmpty ? 0 : aEmpty ? 1 : -1\n return factor * compareValues(a[column], b[column])\n })\n}\n\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === ''\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n const aNum = toNumber(a)\n const bNum = toNumber(b)\n if (aNum !== null && bNum !== null) return aNum - bNum\n\n return String(a).localeCompare(String(b))\n}\n","/**\n * State for the records table, split out of the component so each concern —\n * which columns and in what order, how wide they are, which page and sort —\n * reads on its own.\n *\n * All three are viewer-side. The *author's* choices (assigned columns, page\n * size, widths) arrive through `chartConfig`/`displayConfig`; these hooks layer\n * the viewer's own adjustments over them.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { getOrderedColumnsFromQuery } from '../../utils/pivotUtils.js'\nimport {\n DEFAULT_COLUMN_WIDTH,\n MIN_COLUMN_WIDTH,\n applyColumnOrder,\n columnWidthStorageKey,\n loadColumnOrder,\n loadColumnWidths,\n moveColumn,\n saveColumnOrder,\n saveColumnWidths,\n sortRows\n} from '../../utils/recordsTableUtils.js'\nimport type { ChartAxisConfig, ChartPagination, CubeQuery } from '../../types.js'\n\nexport const DEFAULT_PAGE_SIZE = 25\n\ntype SortState = { column: string; direction: 'asc' | 'desc' }\n\n/**\n * Which columns to render, in which order.\n *\n * The author's arrangement comes from the Columns drop zone; a viewer can drag\n * a header on top of it, and that order is remembered per column set. It is\n * *reconciled* rather than substituted, so a column the author adds later still\n * appears instead of being swallowed by a stale local order.\n */\nexport function useRecordsColumns(params: {\n rows: Record<string, unknown>[]\n chartConfig?: ChartAxisConfig\n queryObject?: CubeQuery\n}) {\n const { rows, chartConfig, queryObject } = params\n\n const baseColumns = useMemo(\n () => resolveColumns(rows, chartConfig, queryObject),\n [rows, chartConfig, queryObject]\n )\n\n const storageKey = useMemo(() => columnWidthStorageKey(baseColumns), [baseColumns])\n const [columnOrder, setColumnOrder] = useState<string[]>([])\n\n useEffect(() => {\n setColumnOrder(loadColumnOrder(storageKey))\n }, [storageKey])\n\n const columns = useMemo(\n () => applyColumnOrder(baseColumns, columnOrder),\n [baseColumns, columnOrder]\n )\n\n const [draggedColumn, setDraggedColumn] = useState<string | null>(null)\n // A drag ending on the header it started from can still produce a click;\n // without this, reordering would also toggle the sort.\n const didDragRef = useRef(false)\n\n const startColumnDrag = useCallback((column: string) => {\n didDragRef.current = true\n setDraggedColumn(column)\n }, [])\n\n const endColumnDrag = useCallback(() => {\n setDraggedColumn(null)\n requestAnimationFrame(() => { didDragRef.current = false })\n }, [])\n\n const dropColumn = useCallback((target: string) => {\n const source = draggedColumn\n endColumnDrag()\n if (!source || source === target) return\n const next = moveColumn(columns, source, target)\n setColumnOrder(next)\n saveColumnOrder(storageKey, next)\n }, [columns, draggedColumn, endColumnDrag, storageKey])\n\n return { columns, storageKey, draggedColumn, didDragRef, startColumnDrag, endColumnDrag, dropColumn }\n}\n\nfunction resolveColumns(\n rows: Record<string, unknown>[],\n chartConfig: ChartAxisConfig | undefined,\n queryObject: CubeQuery | undefined\n): string[] {\n const available = Object.keys(rows[0] ?? {})\n const hidden = new Set(chartConfig?.hiddenColumns ?? [])\n\n const configured = chartConfig?.columns?.filter(column => available.includes(column))\n if (configured && configured.length > 0) {\n return configured.filter(column => !hidden.has(column))\n }\n\n // Nothing assigned: follow the order the query asked for, then anything else\n // the rows happen to carry.\n const fromQuery = getOrderedColumnsFromQuery(queryObject).filter(column => available.includes(column))\n const ordered = fromQuery.length > 0\n ? [...fromQuery, ...available.filter(column => !fromQuery.includes(column))]\n : available\n\n return ordered.filter(column => !hidden.has(column))\n}\n\n/**\n * Drag-resizable column widths, remembered per column set.\n *\n * A chart has no portlet id and no write path back to `displayConfig`, so an\n * authored `columnWidths` acts as the default a viewer's own drags layer over —\n * the same arrangement the data browser uses.\n */\nexport function useColumnWidths(params: {\n columns: string[]\n storageKey: string\n authored?: Record<string, number>\n}) {\n const { columns, storageKey, authored } = params\n\n const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})\n const tableRef = useRef<HTMLTableElement>(null)\n const didResizeRef = useRef(false)\n\n useEffect(() => {\n setColumnWidths({ ...(authored ?? {}), ...loadColumnWidths(storageKey) })\n }, [storageKey, authored])\n\n const startResize = useCallback((event: React.MouseEvent, column: string) => {\n event.preventDefault()\n event.stopPropagation()\n\n const startX = event.clientX\n didResizeRef.current = false\n\n // Freeze every column at its rendered width first, so the ones either side\n // of the handle do not jump when this one changes.\n const snapshot: Record<string, number> = {}\n tableRef.current?.querySelectorAll('thead th').forEach((th, index) => {\n const name = columns[index]\n if (name) snapshot[name] = th.getBoundingClientRect().width\n })\n\n const startWidth = snapshot[column] ?? DEFAULT_COLUMN_WIDTH\n let latest = snapshot\n\n const onMove = (moveEvent: MouseEvent) => {\n const diff = moveEvent.clientX - startX\n if (Math.abs(diff) > 2) didResizeRef.current = true\n latest = { ...latest, [column]: Math.max(MIN_COLUMN_WIDTH, startWidth + diff) }\n setColumnWidths(latest)\n }\n\n const onUp = () => {\n document.removeEventListener('mousemove', onMove)\n document.removeEventListener('mouseup', onUp)\n document.body.style.cursor = ''\n document.body.style.userSelect = ''\n saveColumnWidths(storageKey, latest)\n // Held one frame so the header's click handler can see it and not sort.\n requestAnimationFrame(() => { didResizeRef.current = false })\n }\n\n document.addEventListener('mousemove', onMove)\n document.addEventListener('mouseup', onUp)\n document.body.style.cursor = 'col-resize'\n document.body.style.userSelect = 'none'\n }, [columns, storageKey])\n\n const totalWidth = Object.keys(columnWidths).length > 0\n ? columns.reduce((sum, column) => sum + (columnWidths[column] ?? DEFAULT_COLUMN_WIDTH), 0)\n : undefined\n\n return { columnWidths, totalWidth, tableRef, didResizeRef, startResize }\n}\n\n/**\n * Sorting and paging, server-side when the host offers it.\n *\n * With a `pagination` prop the host re-queries, so both must go through it:\n * sorting only the loaded page would put the wrong rows on page 1. Without one\n * — the AnalysisBuilder preview, the notebook, plugin hosts — the table sorts\n * and pages over the rows it already has.\n */\nexport function useRecordsPaging(params: {\n rows: Record<string, unknown>[]\n pagination?: ChartPagination\n authoredPageSize?: number\n}) {\n const { rows, pagination, authoredPageSize } = params\n\n const [localSort, setLocalSort] = useState<SortState | null>(null)\n const [localPage, setLocalPage] = useState(0)\n\n const isServerPaged = Boolean(pagination)\n const sort = pagination ? pagination.sort ?? null : localSort\n\n const toggleSort = useCallback((column: string) => {\n if (pagination) {\n pagination.toggleSort(column)\n return\n }\n setLocalSort(current => {\n if (current?.column !== column) return { column, direction: 'asc' }\n return current.direction === 'asc' ? { column, direction: 'desc' } : null\n })\n }, [pagination])\n\n const sortedRows = useMemo(\n () => (!isServerPaged && localSort ? sortRows(rows, localSort.column, localSort.direction) : rows),\n [rows, isServerPaged, localSort]\n )\n\n const pageSize = pagination?.pageSize ?? authoredPageSize ?? DEFAULT_PAGE_SIZE\n const page = pagination?.page ?? localPage\n // Server-side the count is whatever the server reported; falling back to the\n // loaded page keeps the pager honest before the first total arrives.\n const rowCount = isServerPaged\n ? pagination?.total ?? page * pageSize + sortedRows.length\n : sortedRows.length\n const pageCount = Math.max(1, Math.ceil(rowCount / pageSize))\n\n const goToPage = useCallback((next: number) => {\n if (pagination) pagination.setPage(next)\n else setLocalPage(next)\n }, [pagination])\n\n // A shrinking result set can leave the viewer past the last page.\n useEffect(() => {\n if (isServerPaged) return\n setLocalPage(current => Math.min(current, pageCount - 1))\n }, [pageCount, isServerPaged])\n\n const visibleRows = useMemo(\n () => (isServerPaged ? sortedRows : sortedRows.slice(page * pageSize, page * pageSize + pageSize)),\n [sortedRows, isServerPaged, page, pageSize]\n )\n\n return {\n sort,\n toggleSort,\n visibleRows,\n page,\n pageSize,\n pageCount,\n rowCount,\n goToPage,\n showPager: rowCount > pageSize\n }\n}\n","/**\n * Records Table\n *\n * A record-grain listing (as opposed to DataTable's aggregate view): ordered\n * columns with per-column formats, hidden columns fetched for row context,\n * click-to-sort headers, drag-resizable widths and pagination.\n *\n * Hand-rolled rather than built on a table library: the sort/resize/<colgroup>\n * patterns already exist in `DataBrowser/DataBrowserTable.tsx` (copied, not\n * imported — that component reads the data-browser store), and badge/progress\n * cells are bespoke either way.\n */\n\nimport React, { useCallback, useMemo } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport { useCubeMeta } from '../../providers/CubeProvider.js'\nimport { ChartEmptyState } from './ChartStates.js'\nimport { getIcon } from '../../icons/index.js'\nimport { buildRowUrl } from '../../utils/rowLinkUtils.js'\nimport { DEFAULT_COLUMN_WIDTH, renderCellValue, type RenderedCell } from '../../utils/recordsTableUtils.js'\nimport { useColumnWidths, useRecordsColumns, useRecordsPaging } from './recordsTableHooks.js'\nimport type { ChartProps, ColumnFormatConfig } from '../../types.js'\n\nconst SortAscIcon = getIcon('chevronUp')\nconst SortDescIcon = getIcon('chevronDown')\n\nconst RecordsTable = React.memo(function RecordsTable({\n data,\n chartConfig,\n displayConfig = {},\n queryObject,\n colorPalette,\n height = 300,\n onDataPointClick,\n drillEnabled,\n pagination\n}: ChartProps) {\n const { t } = useTranslation()\n const { getFieldLabel } = useCubeMeta()\n\n const rows = useMemo(() => (Array.isArray(data) ? (data as Record<string, unknown>[]) : []), [data])\n const columnFormats = displayConfig.columnFormats ?? {}\n\n const {\n columns,\n storageKey,\n draggedColumn,\n didDragRef,\n startColumnDrag,\n endColumnDrag,\n dropColumn\n } = useRecordsColumns({ rows, chartConfig, queryObject })\n\n const { columnWidths, totalWidth, tableRef, didResizeRef, startResize } = useColumnWidths({\n columns,\n storageKey,\n authored: displayConfig.columnWidths\n })\n\n const {\n sort,\n toggleSort,\n visibleRows,\n page,\n pageSize,\n pageCount,\n rowCount,\n goToPage,\n showPager\n } = useRecordsPaging({ rows, pagination, authoredPageSize: displayConfig.pageSize })\n\n const handleHeaderClick = useCallback((column: string) => {\n // Suppressed after a resize or reorder drag, which can still emit a click.\n if (didResizeRef.current || didDragRef.current) return\n toggleSort(column)\n }, [didDragRef, didResizeRef, toggleSort])\n\n // A configured row link wins over drill: the row becomes a real anchor, so\n // modifier-clicks and \"open in new tab\" behave as they do anywhere else.\n const rowLink = displayConfig.rowLink\n const rowClickable = Boolean(!rowLink && drillEnabled && onDataPointClick)\n\n const handleRowClick = useCallback((row: Record<string, unknown>, column: string, event: React.MouseEvent) => {\n if (!rowClickable) return\n onDataPointClick?.({\n dataPoint: row,\n clickedField: column,\n xValue: row[column],\n position: { x: event.clientX, y: event.clientY },\n nativeEvent: event\n })\n }, [onDataPointClick, rowClickable])\n\n if (rows.length === 0) {\n return <ChartEmptyState height={height} hint={t('chart.runtime.noDataHint.table')} />\n }\n\n if (columns.length === 0) {\n return (\n <ChartEmptyState\n height={height}\n titleKey=\"chart.runtime.recordsTable.noColumns\"\n hint={t('chart.runtime.table.invalidStructure')}\n />\n )\n }\n\n const firstRow = page * pageSize + 1\n const lastRow = Math.min(rowCount, page * pageSize + visibleRows.length)\n\n return (\n <div className=\"dc:flex dc:flex-col dc:w-full\" style={{ height }}>\n <div className=\"dc:flex-1 dc:overflow-auto\">\n <table\n ref={tableRef}\n className=\"dc:border-collapse\"\n style={{ tableLayout: 'fixed', width: totalWidth, minWidth: '100%' }}\n >\n <colgroup>\n {columns.map(column => (\n <col key={column} style={{ width: columnWidths[column] ?? DEFAULT_COLUMN_WIDTH }} />\n ))}\n </colgroup>\n <thead className=\"bg-dc-surface-secondary dc:sticky dc:top-0 dc:z-20\">\n <tr>\n {columns.map(column => (\n <HeaderCell\n key={column}\n column={column}\n label={columnFormats[column]?.label || getFieldLabel(column)}\n align={columnFormats[column]?.align ?? defaultAlign(columnFormats[column])}\n sortDirection={sort?.column === column ? sort.direction : undefined}\n isDropTarget={draggedColumn !== null && draggedColumn !== column}\n onClick={handleHeaderClick}\n onResizeStart={startResize}\n onDragStart={startColumnDrag}\n onDragEnd={endColumnDrag}\n onDrop={dropColumn}\n />\n ))}\n </tr>\n </thead>\n <tbody className=\"bg-dc-surface\">\n {visibleRows.map((row, index) => (\n <Row\n key={index}\n row={row}\n columns={columns}\n columnFormats={columnFormats}\n colorPalette={colorPalette}\n href={rowLink ? buildRowUrl(rowLink.urlTemplate, row) : null}\n target={rowLink?.target}\n clickable={rowClickable}\n onCellClick={handleRowClick}\n />\n ))}\n </tbody>\n </table>\n </div>\n\n {showPager && (\n <div className=\"dc:flex dc:items-center dc:justify-between dc:gap-2 dc:px-3 dc:py-1.5 dc:border-t border-dc-border dc:text-xs text-dc-text-secondary\">\n <span>{t('chart.runtime.recordsTable.rowRange', { from: firstRow, to: lastRow, total: rowCount })}</span>\n <div className=\"dc:flex dc:items-center dc:gap-1\">\n <PagerButton\n label={t('chart.runtime.recordsTable.previousPage')}\n disabled={page === 0}\n onClick={() => goToPage(Math.max(0, page - 1))}\n >\n ‹\n </PagerButton>\n <span>{t('chart.runtime.recordsTable.pageOf', { page: page + 1, pages: pageCount })}</span>\n <PagerButton\n label={t('chart.runtime.recordsTable.nextPage')}\n disabled={page >= pageCount - 1}\n onClick={() => goToPage(Math.min(pageCount - 1, page + 1))}\n >\n ›\n </PagerButton>\n </div>\n </div>\n )}\n </div>\n )\n})\n\nexport default RecordsTable\n\n/** Numeric-ish formats read better right-aligned unless the author says otherwise. */\nfunction defaultAlign(format: ColumnFormatConfig | undefined): 'left' | 'right' {\n return format?.kind === 'number' ? 'right' : 'left'\n}\n\ninterface HeaderCellProps {\n column: string\n label: string\n align: 'left' | 'right'\n sortDirection?: 'asc' | 'desc'\n isDropTarget: boolean\n onClick: (column: string) => void\n onResizeStart: (event: React.MouseEvent, column: string) => void\n onDragStart: (column: string) => void\n onDragEnd: () => void\n onDrop: (column: string) => void\n}\n\nfunction HeaderCell({\n column,\n label,\n align,\n sortDirection,\n isDropTarget,\n onClick,\n onResizeStart,\n onDragStart,\n onDragEnd,\n onDrop\n}: HeaderCellProps) {\n return (\n <th\n draggable\n onDragStart={(event) => {\n // Required for Firefox to start the drag at all.\n event.dataTransfer.setData('text/plain', column)\n event.dataTransfer.effectAllowed = 'move'\n onDragStart(column)\n }}\n onDragOver={(event) => { if (isDropTarget) event.preventDefault() }}\n onDrop={(event) => { event.preventDefault(); onDrop(column) }}\n onDragEnd={onDragEnd}\n onClick={() => onClick(column)}\n className={`dc:relative dc:px-3 dc:py-2 dc:text-xs dc:font-medium text-dc-text-muted dc:uppercase dc:tracking-wider dc:cursor-pointer dc:select-none dc:border-b border-dc-border ${\n align === 'right' ? 'dc:text-right' : 'dc:text-left'\n }${isDropTarget ? ' dc:border-l-2 border-dc-accent' : ''}`}\n >\n <div className={`dc:flex dc:items-center dc:gap-1.5 dc:overflow-hidden${align === 'right' ? ' dc:justify-end' : ''}`}>\n <span className=\"dc:truncate\">{label}</span>\n {sortDirection === 'asc' && <SortAscIcon className=\"dc:w-3 dc:h-3 text-dc-accent dc:shrink-0\" />}\n {sortDirection === 'desc' && <SortDescIcon className=\"dc:w-3 dc:h-3 text-dc-accent dc:shrink-0\" />}\n </div>\n <div\n onMouseDown={(event) => onResizeStart(event, column)}\n className=\"dc:absolute dc:top-0 dc:right-0 dc:w-1.5 dc:h-full dc:cursor-col-resize dc:hover:bg-dc-accent dc:opacity-0 dc:hover:opacity-100 dc:transition-opacity dc:z-30\"\n />\n </th>\n )\n}\n\ninterface RowProps {\n row: Record<string, unknown>\n columns: string[]\n columnFormats: Record<string, ColumnFormatConfig>\n colorPalette?: ChartProps['colorPalette']\n href: string | null\n target?: 'self' | 'blank'\n clickable: boolean\n onCellClick: (row: Record<string, unknown>, column: string, event: React.MouseEvent) => void\n}\n\nfunction Row({ row, columns, columnFormats, colorPalette, href, target, clickable, onCellClick }: RowProps) {\n return (\n <tr className={`dc:border-b border-dc-border hover:bg-dc-surface-secondary${clickable ? ' dc:cursor-pointer' : ''}`}>\n {columns.map(column => {\n const format = columnFormats[column]\n const align = format?.align ?? defaultAlign(format)\n return (\n <td\n key={column}\n onClick={(event) => onCellClick(row, column, event)}\n className={`dc:px-3 dc:py-1.5 dc:text-sm text-dc-text dc:overflow-hidden${\n align === 'right' ? ' dc:text-right dc:tabular-nums' : ''\n }`}\n >\n <CellContent\n cell={renderCellValue(row[column], format)}\n colorPalette={colorPalette}\n href={href}\n target={target}\n />\n </td>\n )\n })}\n </tr>\n )\n}\n\n/**\n * A cell, wrapped in the row's link when one resolves. An unresolvable or\n * unsafe template yields no anchor rather than a dead one.\n */\nfunction CellContent({\n cell,\n colorPalette,\n href,\n target\n}: {\n cell: RenderedCell\n colorPalette?: ChartProps['colorPalette']\n href: string | null\n target?: 'self' | 'blank'\n}) {\n const content = <Cell cell={cell} colorPalette={colorPalette} />\n if (!href) return content\n\n return (\n <a\n href={href}\n target={target === 'blank' ? '_blank' : undefined}\n rel={target === 'blank' ? 'noopener noreferrer' : undefined}\n className=\"dc:block dc:no-underline dc:text-inherit hover:dc:underline\"\n >\n {content}\n </a>\n )\n}\n\nfunction Cell({ cell, colorPalette }: { cell: RenderedCell; colorPalette?: ChartProps['colorPalette'] }) {\n if (cell.kind === 'badge') {\n const color = cell.colorIndex !== undefined ? colorPalette?.colors[cell.colorIndex] : undefined\n return (\n <span\n className=\"dc:inline-block dc:px-2 dc:py-0.5 dc:rounded-full dc:text-xs dc:font-medium dc:truncate dc:max-w-full\"\n style={color\n ? { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }\n : { backgroundColor: 'var(--dc-surface-secondary)', color: 'var(--dc-text-secondary)', border: '1px solid var(--dc-border)' }}\n >\n {cell.text}\n </span>\n )\n }\n\n if (cell.kind === 'progress') {\n return (\n <div className=\"dc:flex dc:items-center dc:gap-2\">\n {cell.style === 'circle'\n ? <ProgressRing fraction={cell.fraction} label={cell.text} />\n : (\n // Outlined track, so the full 0-100% extent is visible and a\n // near-complete bar is distinguishable from a complete one.\n <div className=\"dc:flex-1 dc:h-2 dc:rounded-full dc:overflow-hidden dc:border border-dc-border bg-dc-surface-secondary dc:min-w-[2rem]\">\n <div\n className=\"dc:h-full dc:rounded-full bg-dc-primary\"\n style={{ width: `${cell.fraction * 100}%` }}\n />\n </div>\n )}\n {/*\n Fixed width, not shrink-to-fit: the track is flexible, so a wider\n label (\"£117K\") would shorten its row's bar and two equal values\n would draw different lengths. Right-aligned and tabular so the\n numbers line up as a column.\n */}\n <span className=\"dc:w-16 dc:shrink-0 dc:text-right dc:text-xs dc:tabular-nums dc:truncate\">\n {cell.text}\n </span>\n </div>\n )\n }\n\n return <span className=\"dc:truncate dc:block\">{cell.text}</span>\n}\n\nconst RING_SIZE = 18\nconst RING_STROKE = 3\nconst RING_CENTRE = RING_SIZE / 2\nconst RING_RADIUS = RING_CENTRE - RING_STROKE / 2\n\n/**\n * The compact alternative to the progress bar: a ring that fills 0→100%, so a\n * progress column can be as narrow as its value label.\n *\n * `pathLength` normalises the circumference to 100, which lets the dash offset\n * be the percentage remaining without any 2πr arithmetic. The ring alone\n * carries the value, hence the `img` role and label.\n */\nfunction ProgressRing({ fraction, label }: { fraction: number; label: string }) {\n return (\n <svg\n role=\"img\"\n aria-label={label}\n width={RING_SIZE}\n height={RING_SIZE}\n viewBox={`0 0 ${RING_SIZE} ${RING_SIZE}`}\n className=\"dc:shrink-0\"\n >\n <circle\n cx={RING_CENTRE}\n cy={RING_CENTRE}\n r={RING_RADIUS}\n fill=\"none\"\n stroke=\"var(--dc-surface-secondary)\"\n strokeWidth={RING_STROKE}\n />\n {fraction > 0 && (\n <circle\n cx={RING_CENTRE}\n cy={RING_CENTRE}\n r={RING_RADIUS}\n fill=\"none\"\n stroke=\"var(--dc-primary)\"\n strokeWidth={RING_STROKE}\n strokeLinecap=\"round\"\n pathLength={100}\n strokeDasharray={100}\n strokeDashoffset={100 - fraction * 100}\n transform={`rotate(-90 ${RING_CENTRE} ${RING_CENTRE})`}\n />\n )}\n </svg>\n )\n}\n\nfunction PagerButton({\n label,\n disabled,\n onClick,\n children\n}: {\n label: string\n disabled: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n className=\"dc:px-2 dc:py-0.5 dc:border border-dc-border dc:rounded-sm bg-dc-surface text-dc-text hover:bg-dc-surface-secondary dc:disabled:opacity-40 dc:disabled:cursor-not-allowed\"\n >\n {children}\n </button>\n )\n}\n"],"mappings":";;;;;;AAYA,IAAM,IAAgB,uCAGhB,oBAAoB,IAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;AAcrD,SAAgB,EACd,GACA,GACA,IAAiB,EAAc,GAChB;CACf,IAAI,CAAC,GAAU,OAAO;CAEtB,IAAI,IAAa,IACX,IAAc,EAAS,QAAQ,IAAgB,GAAQ,MAAkB;EAC7E,IAAM,IAAQ,EAAI;EAKlB,OAJI,KAAU,QACZ,IAAa,IACN,MAEF,mBAAmB,OAAO,CAAK,CAAC;CACzC,CAAC;CAID,OAFI,IAAmB,OAEhB,EAAU,GAAa,CAAM,IAAI,IAAc;AACxD;AAYA,SAAgB,EAAU,GAAa,IAAiB,EAAc,GAAY;CAChF,IAAM,IAAU,EAAI,KAAK;CASzB,IARI,MAAY,MAKZ,wBAAwB,KAAK,CAAO,KAEpC,EAAQ,SAAS,IAAI,KACrB,EAAQ,WAAW,IAAI,GAAG,OAAO;CAErC,IAAI;EACF,IAAM,IAAS,IAAI,IAAI,GAAS,CAAM;EAStC,OAFA,EANI,CAAC,EAAkB,IAAI,EAAO,QAAQ,KAMtC,CADmB,4BAA4B,KAAK,CACnD,KAAkB,EAAO,WAAW,IAAI,IAAI,CAAM,CAAC,CAAC;CAG3D,QAAQ;EACN,OAAO;CACT;AACF;AAMA,SAAS,IAAwB;CAC/B,OAAO,OAAO,SAAW,OAAe,OAAO,UAAU,SACrD,OAAO,SAAS,SAChB;AACN;;;AClFA,IAAM,IAA4B,kCAC5B,IAA2B;AASjC,SAAgB,EAAsB,GAA2B;CAC/D,OAAO,CAAC,GAAG,CAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;AACrC;AAEA,SAAgB,EAAiB,GAAqC;CACpE,IAAI;EACF,IAAM,IAAM,aAAa,QAAQ,CAAyB;EAE1D,OADK,IACE,KAAK,MAAM,CAAG,CAAC,CAAC,MAAQ,CAAC,IADf,CAAC;CAEpB,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAgB,EAAiB,GAAa,GAAsC;CAClF,IAAI;EACF,IAAM,IAAM,aAAa,QAAQ,CAAyB,GACpD,IAAM,IAAM,KAAK,MAAM,CAAG,IAAI,CAAC;EAErC,AADA,EAAI,KAAO,GACX,aAAa,QAAQ,GAA2B,KAAK,UAAU,CAAG,CAAC;CACrE,QAAQ,CAER;AACF;AASA,SAAgB,EAAgB,GAAuB;CACrD,IAAI;EACF,IAAM,IAAM,aAAa,QAAQ,CAAwB;EACzD,IAAI,CAAC,GAAK,OAAO,CAAC;EAClB,IAAM,IAAS,KAAK,MAAM,CAAG,CAAC,CAAC;EAC/B,OAAO,MAAM,QAAQ,CAAM,IAAI,IAAS,CAAC;CAC3C,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAgB,EAAgB,GAAa,GAAuB;CAClE,IAAI;EACF,IAAM,IAAM,aAAa,QAAQ,CAAwB,GACnD,IAAM,IAAM,KAAK,MAAM,CAAG,IAAI,CAAC;EAErC,AADA,EAAI,KAAO,GACX,aAAa,QAAQ,GAA0B,KAAK,UAAU,CAAG,CAAC;CACpE,QAAQ,CAER;AACF;AASA,SAAgB,EAAiB,GAAmB,GAA2B;CAC7E,IAAI,EAAM,WAAW,GAAG,OAAO;CAC/B,IAAM,IAAU,IAAI,IAAI,CAAO,GACzB,IAAU,EAAM,QAAO,MAAU,EAAQ,IAAI,CAAM,CAAC,GACpD,IAAY,EAAQ,QAAO,MAAU,CAAC,EAAQ,SAAS,CAAM,CAAC;CACpE,OAAO,CAAC,GAAG,GAAS,GAAG,CAAS;AAClC;AAGA,SAAgB,EAAW,GAAmB,GAAc,GAAsB;CAChF,IAAM,IAAY,EAAQ,QAAQ,CAAI,GAChC,IAAU,EAAQ,QAAQ,CAAE;CAClC,IAAI,MAAc,MAAM,MAAY,MAAM,MAAc,GAAS,OAAO;CAExE,IAAM,IAAO,CAAC,GAAG,CAAO;CAGxB,OAFA,EAAK,OAAO,GAAW,CAAC,GACxB,EAAK,OAAO,GAAS,GAAG,CAAI,GACrB;AACT;AAeA,IAAM,IAAsB;CAAE,MAAM;CAAQ,MAAM;AAAG;AAUrD,SAAgB,EACd,GACA,GACc;CAGd,OAFI,EAAQ,CAAK,IAAU,KACZ,EAAU,GAAQ,QAAQ,WAAW,EAAA,CACtC,GAAO,CAAM;AAC7B;AAIA,IAAM,IAAoD;CACxD,MAAM;CACN,QAAQ;CACR,MAAM;CACN,OAAO;CACP,UAAU;AACZ;AAEA,SAAS,EAAW,GAA8B;CAChD,OAAO;EAAE,MAAM;EAAQ,MAAM,EAAU,CAAK;CAAE;AAChD;AAEA,SAAS,EAAa,GAAgB,GAAsD;CAC1F,IAAM,IAAM,EAAS,CAAK;CAE1B,OADI,MAAQ,OAAa;EAAE,MAAM;EAAQ,MAAM,OAAO,CAAK;CAAE,IACtD;EAAE,MAAM;EAAQ,MAAM,EAAgB,GAAK,GAAQ,gBAAgB,CAAC,CAAC;CAAE;AAChF;AAEA,SAAS,EAAW,GAAgB,GAAsD;CACxF,OAAO;EAAE,MAAM;EAAQ,MAAM,EAAgB,GAAO,GAAQ,eAAe;CAAE;AAC/E;AAWA,SAAS,EAAkB,GAA2E;CAEpG,OADK,MAAM,QAAQ,GAAQ,WAAW,IAC/B,EAAO,YAAY,QAAO,MAAS,KAAS,OAAO,EAAM,cAAe,QAAQ,IADvC,CAAC;AAEnD;AAEA,SAAS,EAAY,GAAgB,GAAsD;CACzF,IAAM,IAAO,EAAU,CAAK;CAC5B,OAAO;EAAE,MAAM;EAAS;EAAM,YAAY,EAAkB,CAAM,CAAC,EAAE,MAAK,MAAK,EAAE,UAAU,CAAI,CAAC,EAAE;CAAW;AAC/G;AAEA,IAAM,IAAyB;CAAE,MAAM;CAAU,YAAY;CAAO,UAAU;AAAE;AAEhF,SAAS,EAAe,GAAgB,GAAsD;CAC5F,IAAM,IAAM,EAAS,CAAK;CAG1B,OAFI,MAAQ,OAAa;EAAE,MAAM;EAAQ,MAAM,OAAO,CAAK;CAAE,IAEtD;EACL,MAAM;EACN,UAAU,EAAiB,GAAK,CAAM;EACtC,OAAO,GAAQ,iBAAiB;EAChC,MAAM,EAAgB,GAAK,GAAQ,gBAAgB,CAAsB;CAC3E;AACF;AAGA,SAAS,EAAiB,GAAe,GAAgD;CACvF,IAAM,IAAM,GAAQ,eAAe,GAE7B,KADM,GAAQ,eAAe,OAChB;CAEnB,OAAO,MAAS,IAAI,IAAI,GAAO,IAAQ,KAAO,GAAM,GAAG,CAAC;AAC1D;AAEA,SAAS,EAAU,GAAwB;CAIzC,OAHI,OAAO,KAAU,YAAkB,IAAQ,SAAS,UACpD,OAAO,KAAU,WAAiB,EAAM,eAAe,IACvD,aAAiB,OAAa,EAAM,YAAY,IAC7C,OAAO,CAAK;AACrB;AAEA,SAAS,EAAS,GAA+B;CAC/C,IAAI,OAAO,KAAU,UAAU,OAAO,OAAO,SAAS,CAAK,IAAI,IAAQ;CACvE,IAAI,OAAO,KAAU,YAAY,EAAM,KAAK,MAAM,IAAI;EACpD,IAAM,IAAS,OAAO,CAAK;EAC3B,OAAO,OAAO,SAAS,CAAM,IAAI,IAAS;CAC5C;CACA,OAAO;AACT;AAEA,SAAS,EAAM,GAAe,GAAa,GAAqB;CAC9D,OAAO,KAAK,IAAI,GAAK,KAAK,IAAI,GAAK,CAAK,CAAC;AAC3C;AAQA,SAAgB,EACd,GACA,GACA,GAC2B;CAC3B,IAAM,IAAS,MAAc,QAAQ,IAAI;CACzC,OAAO,CAAC,GAAG,CAAI,CAAC,CAAC,MAAM,GAAG,MAAM;EAG9B,IAAM,IAAS,EAAQ,EAAE,EAAO,GAC1B,IAAS,EAAQ,EAAE,EAAO;EAEhC,OADI,KAAU,IAAe,KAAU,IAAS,IAAI,IAAS,IAAI,KAC1D,IAAS,EAAc,EAAE,IAAS,EAAE,EAAO;CACpD,CAAC;AACH;AAEA,SAAS,EAAQ,GAAyB;CACxC,OAAO,KAAU,QAA+B,MAAU;AAC5D;AAEA,SAAS,EAAc,GAAY,GAAoB;CACrD,IAAM,IAAO,EAAS,CAAC,GACjB,IAAO,EAAS,CAAC;CAGvB,OAFI,MAAS,QAAQ,MAAS,OAAa,IAAO,IAE3C,OAAO,CAAC,CAAC,CAAC,cAAc,OAAO,CAAC,CAAC;AAC1C;AC1NA,SAAgB,GAAkB,GAI/B;CACD,IAAM,EAAE,SAAM,gBAAa,mBAAgB,GAErC,IAAc,QACZ,EAAe,GAAM,GAAa,CAAW,GACnD;EAAC;EAAM;EAAa;CAAW,CACjC,GAEM,IAAa,QAAc,EAAsB,CAAW,GAAG,CAAC,CAAW,CAAC,GAC5E,CAAC,GAAa,KAAkB,EAAmB,CAAC,CAAC;CAE3D,QAAgB;EACd,EAAe,EAAgB,CAAU,CAAC;CAC5C,GAAG,CAAC,CAAU,CAAC;CAEf,IAAM,IAAU,QACR,EAAiB,GAAa,CAAW,GAC/C,CAAC,GAAa,CAAW,CAC3B,GAEM,CAAC,GAAe,KAAoB,EAAwB,IAAI,GAGhE,IAAa,EAAO,EAAK,GAEzB,IAAkB,GAAa,MAAmB;EAEtD,AADA,EAAW,UAAU,IACrB,EAAiB,CAAM;CACzB,GAAG,CAAC,CAAC,GAEC,IAAgB,QAAkB;EAEtC,AADA,EAAiB,IAAI,GACrB,4BAA4B;GAAE,EAAW,UAAU;EAAM,CAAC;CAC5D,GAAG,CAAC,CAAC;CAWL,OAAO;EAAE;EAAS;EAAY;EAAe;EAAY;EAAiB;EAAe,YATtE,GAAa,MAAmB;GACjD,IAAM,IAAS;GAEf,IADA,EAAc,GACV,CAAC,KAAU,MAAW,GAAQ;GAClC,IAAM,IAAO,EAAW,GAAS,GAAQ,CAAM;GAE/C,AADA,EAAe,CAAI,GACnB,EAAgB,GAAY,CAAI;EAClC,GAAG;GAAC;GAAS;GAAe;GAAe;EAAU,CAEoC;CAAW;AACtG;AAEA,SAAS,EACP,GACA,GACA,GACU;CACV,IAAM,IAAY,OAAO,KAAK,EAAK,MAAM,CAAC,CAAC,GACrC,IAAS,IAAI,IAAI,GAAa,iBAAiB,CAAC,CAAC,GAEjD,IAAa,GAAa,SAAS,QAAO,MAAU,EAAU,SAAS,CAAM,CAAC;CACpF,IAAI,KAAc,EAAW,SAAS,GACpC,OAAO,EAAW,QAAO,MAAU,CAAC,EAAO,IAAI,CAAM,CAAC;CAKxD,IAAM,IAAY,EAA2B,CAAW,CAAC,CAAC,QAAO,MAAU,EAAU,SAAS,CAAM,CAAC;CAKrG,QAJgB,EAAU,SAAS,IAC/B,CAAC,GAAG,GAAW,GAAG,EAAU,QAAO,MAAU,CAAC,EAAU,SAAS,CAAM,CAAC,CAAC,IACzE,EAAA,CAEW,QAAO,MAAU,CAAC,EAAO,IAAI,CAAM,CAAC;AACrD;AASA,SAAgB,GAAgB,GAI7B;CACD,IAAM,EAAE,YAAS,eAAY,gBAAa,GAEpC,CAAC,GAAc,KAAmB,EAAiC,CAAC,CAAC,GACrE,IAAW,EAAyB,IAAI,GACxC,IAAe,EAAO,EAAK;CAEjC,QAAgB;EACd,EAAgB;GAAE,GAAI,KAAY,CAAC;GAAI,GAAG,EAAiB,CAAU;EAAE,CAAC;CAC1E,GAAG,CAAC,GAAY,CAAQ,CAAC;CAEzB,IAAM,IAAc,GAAa,GAAyB,MAAmB;EAE3E,AADA,EAAM,eAAe,GACrB,EAAM,gBAAgB;EAEtB,IAAM,IAAS,EAAM;EACrB,EAAa,UAAU;EAIvB,IAAM,IAAmC,CAAC;EAC1C,EAAS,SAAS,iBAAiB,UAAU,CAAC,CAAC,SAAS,GAAI,MAAU;GACpE,IAAM,IAAO,EAAQ;GACrB,AAAI,MAAM,EAAS,KAAQ,EAAG,sBAAsB,CAAC,CAAC;EACxD,CAAC;EAED,IAAM,IAAa,EAAS,MAAA,KACxB,IAAS,GAEP,KAAU,MAA0B;GACxC,IAAM,IAAO,EAAU,UAAU;GAGjC,AAFI,KAAK,IAAI,CAAI,IAAI,MAAG,EAAa,UAAU,KAC/C,IAAS;IAAE,GAAG;KAAS,IAAS,KAAK,IAAA,IAAsB,IAAa,CAAI;GAAE,GAC9E,EAAgB,CAAM;EACxB,GAEM,UAAa;GAOjB,AANA,SAAS,oBAAoB,aAAa,CAAM,GAChD,SAAS,oBAAoB,WAAW,CAAI,GAC5C,SAAS,KAAK,MAAM,SAAS,IAC7B,SAAS,KAAK,MAAM,aAAa,IACjC,EAAiB,GAAY,CAAM,GAEnC,4BAA4B;IAAE,EAAa,UAAU;GAAM,CAAC;EAC9D;EAKA,AAHA,SAAS,iBAAiB,aAAa,CAAM,GAC7C,SAAS,iBAAiB,WAAW,CAAI,GACzC,SAAS,KAAK,MAAM,SAAS,cAC7B,SAAS,KAAK,MAAM,aAAa;CACnC,GAAG,CAAC,GAAS,CAAU,CAAC;CAMxB,OAAO;EAAE;EAAc,YAJJ,OAAO,KAAK,CAAY,CAAC,CAAC,SAAS,IAClD,EAAQ,QAAQ,GAAK,MAAW,KAAO,EAAa,MAAA,MAAkC,CAAC,IACvF,KAAA;EAE+B;EAAU;EAAc;CAAY;AACzE;AAUA,SAAgB,GAAiB,GAI9B;CACD,IAAM,EAAE,SAAM,eAAY,wBAAqB,GAEzC,CAAC,GAAW,KAAgB,EAA2B,IAAI,GAC3D,CAAC,GAAW,KAAgB,EAAS,CAAC,GAEtC,IAAgB,EAAQ,GACxB,IAAO,IAAa,EAAW,QAAQ,OAAO,GAE9C,IAAa,GAAa,MAAmB;EACjD,IAAI,GAAY;GACd,EAAW,WAAW,CAAM;GAC5B;EACF;EACA,GAAa,MACP,GAAS,WAAW,IACjB,EAAQ,cAAc,QAAQ;GAAE;GAAQ,WAAW;EAAO,IAAI,OAD9B;GAAE;GAAQ,WAAW;EAAM,CAEnE;CACH,GAAG,CAAC,CAAU,CAAC,GAET,IAAa,QACV,CAAC,KAAiB,IAAY,EAAS,GAAM,EAAU,QAAQ,EAAU,SAAS,IAAI,GAC7F;EAAC;EAAM;EAAe;CAAS,CACjC,GAEM,IAAW,GAAY,YAAY,KAAA,IACnC,IAAO,GAAY,QAAQ,GAG3B,IAAW,IACb,GAAY,SAAS,IAAO,IAAW,EAAW,SAClD,EAAW,QACT,IAAY,KAAK,IAAI,GAAG,KAAK,KAAK,IAAW,CAAQ,CAAC,GAEtD,IAAW,GAAa,MAAiB;EAC7C,AAAI,IAAY,EAAW,QAAQ,CAAI,IAClC,EAAa,CAAI;CACxB,GAAG,CAAC,CAAU,CAAC;CAaf,OAVA,QAAgB;EACV,KACJ,GAAa,MAAW,KAAK,IAAI,GAAS,IAAY,CAAC,CAAC;CAC1D,GAAG,CAAC,GAAW,CAAa,CAAC,GAOtB;EACL;EACA;EACA,aARkB,QACX,IAAgB,IAAa,EAAW,MAAM,IAAO,GAAU,IAAO,IAAW,CAAQ,GAChG;GAAC;GAAY;GAAe;GAAM;EAAQ,CAM1C;EACA;EACA;EACA;EACA;EACA;EACA,WAAW,IAAW;CACxB;AACF;;;ACxOA,IAAM,IAAc,EAAQ,WAAW,GACjC,KAAe,EAAQ,aAAa,GAEpC,KAAe,EAAM,KAAK,SAAsB,EACpD,SACA,gBACA,mBAAgB,CAAC,GACjB,gBACA,iBACA,YAAS,KACT,qBACA,iBACA,iBACa;CACb,IAAM,EAAE,SAAM,EAAe,GACvB,EAAE,qBAAkB,EAAY,GAEhC,IAAO,QAAe,MAAM,QAAQ,CAAI,IAAK,IAAqC,CAAC,GAAI,CAAC,CAAI,CAAC,GAC7F,IAAgB,EAAc,iBAAiB,CAAC,GAEhD,EACJ,YACA,eACA,kBACA,eACA,oBACA,kBACA,kBACE,GAAkB;EAAE;EAAM;EAAa;CAAY,CAAC,GAElD,EAAE,iBAAc,eAAY,aAAU,iBAAc,mBAAgB,GAAgB;EACxF;EACA;EACA,UAAU,EAAc;CAC1B,CAAC,GAEK,EACJ,SACA,eACA,gBACA,SACA,aACA,cACA,aACA,aACA,iBACE,GAAiB;EAAE;EAAM;EAAY,kBAAkB,EAAc;CAAS,CAAC,GAE7E,IAAoB,GAAa,MAAmB;EAEpD,EAAa,WAAW,EAAW,WACvC,EAAW,CAAM;CACnB,GAAG;EAAC;EAAY;EAAc;CAAU,CAAC,GAInC,IAAU,EAAc,SACxB,IAAe,GAAQ,CAAC,KAAW,KAAgB,IAEnD,IAAiB,GAAa,GAA8B,GAAgB,MAA4B;EACvG,KACL,IAAmB;GACjB,WAAW;GACX,cAAc;GACd,QAAQ,EAAI;GACZ,UAAU;IAAE,GAAG,EAAM;IAAS,GAAG,EAAM;GAAQ;GAC/C,aAAa;EACf,CAAC;CACH,GAAG,CAAC,GAAkB,CAAY,CAAC;CAEnC,IAAI,EAAK,WAAW,GAClB,OAAO,kBAAC,GAAD;EAAyB;EAAQ,MAAM,EAAE,gCAAgC;CAAI,CAAA;CAGtF,IAAI,EAAQ,WAAW,GACrB,OACE,kBAAC,GAAD;EACU;EACR,UAAS;EACT,MAAM,EAAE,sCAAsC;CAC/C,CAAA;CAIL,IAAM,IAAW,IAAO,IAAW,GAC7B,IAAU,KAAK,IAAI,GAAU,IAAO,IAAW,EAAY,MAAM;CAEvE,OACE,kBAAC,OAAD;EAAK,WAAU;EAAgC,OAAO,EAAE,UAAO;EAA/D,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;GACb,UAAA,kBAAC,SAAD;IACE,KAAK;IACL,WAAU;IACV,OAAO;KAAE,aAAa;KAAS,OAAO;KAAY,UAAU;IAAO;IAHrE,UAAA;KAKE,kBAAC,YAAD,EAAA,UACG,EAAQ,KAAI,MACX,kBAAC,OAAD,EAAkB,OAAO,EAAE,OAAO,EAAa,MAAA,IAAgC,EAAI,GAAzE,CAAyE,CACpF,EACO,CAAA;KACV,kBAAC,SAAD;MAAO,WAAU;MACf,UAAA,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAI,MACX,kBAAC,IAAD;OAEU;OACR,OAAO,EAAc,EAAO,EAAE,SAAS,EAAc,CAAM;OAC3D,OAAO,EAAc,EAAO,EAAE,SAAS,EAAa,EAAc,EAAO;OACzE,eAAe,GAAM,WAAW,IAAS,EAAK,YAAY,KAAA;OAC1D,cAAc,MAAkB,QAAQ,MAAkB;OAC1D,SAAS;OACT,eAAe;OACf,aAAa;OACb,WAAW;OACX,QAAQ;MACT,GAXM,CAWN,CACF,EACC,CAAA;KACC,CAAA;KACP,kBAAC,SAAD;MAAO,WAAU;MACd,UAAA,EAAY,KAAK,GAAK,MACrB,kBAAC,IAAD;OAEO;OACI;OACM;OACD;OACd,MAAM,IAAU,EAAY,EAAQ,aAAa,CAAG,IAAI;OACxD,QAAQ,GAAS;OACjB,WAAW;OACX,aAAa;MACd,GATM,CASN,CACF;KACI,CAAA;IACF;;EACJ,CAAA,GAEJ,KACC,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,QAAD,EAAA,UAAO,EAAE,uCAAuC;IAAE,MAAM;IAAU,IAAI;IAAS,OAAO;GAAS,CAAC,EAAQ,CAAA,GACxG,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,kBAAC,GAAD;MACE,OAAO,EAAE,yCAAyC;MAClD,UAAU,MAAS;MACnB,eAAe,EAAS,KAAK,IAAI,GAAG,IAAO,CAAC,CAAC;MAC9C,UAAA;KAEY,CAAA;KACb,kBAAC,QAAD,EAAA,UAAO,EAAE,qCAAqC;MAAE,MAAM,IAAO;MAAG,OAAO;KAAU,CAAC,EAAQ,CAAA;KAC1F,kBAAC,GAAD;MACE,OAAO,EAAE,qCAAqC;MAC9C,UAAU,KAAQ,IAAY;MAC9B,eAAe,EAAS,KAAK,IAAI,IAAY,GAAG,IAAO,CAAC,CAAC;MAC1D,UAAA;KAEY,CAAA;IACV;GACF,CAAA,CAAA;EAEJ,CAAA,CAAA;;AAET,CAAC;AAKD,SAAS,EAAa,GAA0D;CAC9E,OAAO,GAAQ,SAAS,WAAW,UAAU;AAC/C;AAeA,SAAS,GAAW,EAClB,WACA,UACA,UACA,kBACA,iBACA,YACA,kBACA,gBACA,cACA,aACkB;CAClB,OACE,kBAAC,MAAD;EACE,WAAA;EACA,cAAc,MAAU;GAItB,AAFA,EAAM,aAAa,QAAQ,cAAc,CAAM,GAC/C,EAAM,aAAa,gBAAgB,QACnC,EAAY,CAAM;EACpB;EACA,aAAa,MAAU;GAAE,AAAI,KAAc,EAAM,eAAe;EAAE;EAClE,SAAS,MAAU;GAA0B,AAAxB,EAAM,eAAe,GAAG,EAAO,CAAM;EAAE;EACjD;EACX,eAAe,EAAQ,CAAM;EAC7B,WAAW,yKACT,MAAU,UAAU,kBAAkB,iBACrC,IAAe,oCAAoC;EAdxD,UAAA,CAgBE,kBAAC,OAAD;GAAK,WAAW,wDAAwD,MAAU,UAAU,oBAAoB;GAAhH,UAAA;IACE,kBAAC,QAAD;KAAM,WAAU;KAAe,UAAA;IAAY,CAAA;IAC1C,MAAkB,SAAS,kBAAC,GAAD,EAAa,WAAU,2CAA4C,CAAA;IAC9F,MAAkB,UAAU,kBAAC,IAAD,EAAc,WAAU,2CAA4C,CAAA;GAC9F;EACL,CAAA,GAAA,kBAAC,OAAD;GACE,cAAc,MAAU,EAAc,GAAO,CAAM;GACnD,WAAU;EACX,CAAA,CACC;;AAER;AAaA,SAAS,GAAI,EAAE,QAAK,YAAS,kBAAe,iBAAc,SAAM,WAAQ,cAAW,kBAAyB;CAC1G,OACE,kBAAC,MAAD;EAAI,WAAW,6DAA6D,IAAY,uBAAuB;EAC5G,UAAA,EAAQ,KAAI,MAAU;GACrB,IAAM,IAAS,EAAc,IACvB,IAAQ,GAAQ,SAAS,EAAa,CAAM;GAClD,OACE,kBAAC,MAAD;IAEE,UAAU,MAAU,EAAY,GAAK,GAAQ,CAAK;IAClD,WAAW,+DACT,MAAU,UAAU,mCAAmC;IAGzD,UAAA,kBAAC,IAAD;KACE,MAAM,EAAgB,EAAI,IAAS,CAAM;KAC3B;KACR;KACE;IACT,CAAA;GACC,GAZG,CAYH;EAER,CAAC;CACC,CAAA;AAER;AAMA,SAAS,GAAY,EACnB,SACA,iBACA,SACA,aAMC;CACD,IAAM,IAAU,kBAAC,IAAD;EAAY;EAAoB;CAAe,CAAA;CAG/D,OAFK,IAGH,kBAAC,KAAD;EACQ;EACN,QAAQ,MAAW,UAAU,WAAW,KAAA;EACxC,KAAK,MAAW,UAAU,wBAAwB,KAAA;EAClD,WAAU;EAET,UAAA;CACA,CAAA,IAVa;AAYpB;AAEA,SAAS,GAAK,EAAE,SAAM,mBAAmF;CACvG,IAAI,EAAK,SAAS,SAAS;EACzB,IAAM,IAAQ,EAAK,eAAe,KAAA,IAAoD,KAAA,IAAxC,GAAc,OAAO,EAAK;EACxE,OACE,kBAAC,QAAD;GACE,WAAU;GACV,OAAO,IACH;IAAE,iBAAiB,GAAG,EAAM;IAAK;IAAO,QAAQ,aAAa,EAAM;GAAI,IACvE;IAAE,iBAAiB;IAA+B,OAAO;IAA4B,QAAQ;GAA6B;GAE7H,UAAA,EAAK;EACF,CAAA;CAEV;CA8BA,OA5BI,EAAK,SAAS,aAEd,kBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACG,EAAK,UAAU,WACZ,kBAAC,IAAD;GAAc,UAAU,EAAK;GAAU,OAAO,EAAK;EAAO,CAAA,IAI1D,kBAAC,OAAD;GAAK,WAAU;GACb,UAAA,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,OAAO,GAAG,EAAK,WAAW,IAAI,GAAG;GAC3C,CAAA;EACE,CAAA,GAQT,kBAAC,QAAD;GAAM,WAAU;GACb,UAAA,EAAK;EACF,CAAA,CACH;MAIF,kBAAC,QAAD;EAAM,WAAU;EAAwB,UAAA,EAAK;CAAW,CAAA;AACjE;AAEA,IAAM,IAAY,IACZ,IAAc,GACd,IAAc,IAAY,GAC1B,IAAc,IAAc,IAAc;AAUhD,SAAS,GAAa,EAAE,aAAU,YAA8C;CAC9E,OACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,OAAO;EACP,QAAQ;EACR,SAAS,OAAO,EAAU,GAAG;EAC7B,WAAU;EANZ,UAAA,CAQE,kBAAC,UAAD;GACE,IAAI;GACJ,IAAI;GACJ,GAAG;GACH,MAAK;GACL,QAAO;GACP,aAAa;EACd,CAAA,GACA,IAAW,KACV,kBAAC,UAAD;GACE,IAAI;GACJ,IAAI;GACJ,GAAG;GACH,MAAK;GACL,QAAO;GACP,aAAa;GACb,eAAc;GACd,YAAY;GACZ,iBAAiB;GACjB,kBAAkB,MAAM,IAAW;GACnC,WAAW,cAAc,EAAY,GAAG,EAAY;EACrD,CAAA,CAEA;;AAET;AAEA,SAAS,EAAY,EACnB,UACA,aACA,YACA,eAMC;CACD,OACE,kBAAC,UAAD;EACE,MAAK;EACL,cAAY;EACZ,OAAO;EACG;EACD;EACT,WAAU;EAET;CACK,CAAA;AAEZ"}
|
package/dist/client/chunks/{RetentionCombinedChart-C7X6aj97.js → RetentionCombinedChart-BRZ9w7ah.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as e } from "./retention-ChW9jYdy.js";
|
|
2
|
-
import { F as t, l as n, s as r } from "./chart-activity-grid-
|
|
3
|
-
import { M as i, j as a } from "./chart-area-
|
|
2
|
+
import { F as t, l as n, s as r } from "./chart-activity-grid-CJzxC18G.js";
|
|
3
|
+
import { M as i, j as a } from "./chart-area-C66dgy84.js";
|
|
4
4
|
import o, { useMemo as s, useState as c } from "react";
|
|
5
5
|
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
6
6
|
import { CartesianGrid as d, ComposedChart as f, Legend as p, Line as m, XAxis as h, YAxis as g } from "recharts";
|
|
@@ -267,4 +267,4 @@ var C = o.memo(function({ data: o, height: x = "100%", displayConfig: C, colorPa
|
|
|
267
267
|
//#endregion
|
|
268
268
|
export { C as default };
|
|
269
269
|
|
|
270
|
-
//# sourceMappingURL=RetentionCombinedChart-
|
|
270
|
+
//# sourceMappingURL=RetentionCombinedChart-BRZ9w7ah.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RetentionCombinedChart-C7X6aj97.js","names":[],"sources":["../../../src/client/components/charts/RetentionCombinedChart.tsx"],"sourcesContent":["/**\n * RetentionCombinedChart Component\n *\n * Combined visualization for retention analysis data.\n * Supports multiple display modes: heatmap, line chart, or combined view.\n *\n * Features:\n * - X-axis: Period numbers (P0, P1, P2...)\n * - Y-axis: Retention % (0-100%)\n * - Lines: One per breakdown value (or single if no breakdown)\n * - Display modes: 'heatmap' | 'line' | 'combined'\n * - Heatmap shows color-coded retention matrix\n * - Line chart shows retention curves over periods\n */\n\nimport React, { useMemo, useState } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport {\n ComposedChart,\n Line,\n XAxis,\n YAxis,\n CartesianGrid,\n Legend,\n} from 'recharts'\nimport ChartContainer from './ChartContainer.js'\nimport ChartTooltip from './ChartTooltip.js'\nimport { CHART_COLORS, CHART_MARGINS } from '../../utils/chartConstants.js'\nimport type { ChartProps } from '../../types.js'\nimport type { RetentionChartData, RetentionResultRow, RetentionGranularity } from '../../types/retention.js'\nimport { isRetentionData } from '../../types/retention.js'\n\n/**\n * Retention display mode\n * - 'heatmap': Show retention as color-coded bars\n * - 'line': Show retention as line curves\n * - 'combined': Show both heatmap background and line overlay\n */\nexport type RetentionDisplayMode = 'heatmap' | 'line' | 'combined'\n\n/**\n * Get color with opacity based on retention rate for heatmap cells\n * Uses a green gradient: higher retention = more saturated green\n */\nfunction getRetentionColor(rate: number): string {\n const clampedRate = Math.max(0, Math.min(1, rate))\n const alpha = 0.1 + clampedRate * 0.7\n return `rgba(34, 197, 94, ${alpha})`\n}\n\n/**\n * Format percentage for display\n */\nfunction formatPercentage(rate: number): string {\n return `${Math.round(rate * 100)}%`\n}\n\n/**\n * Format period label based on granularity\n * Period 0 shows \"< 1 Day\" / \"< 1 Week\" etc. to indicate the initial cohort\n * e.g., Period 0 with 'week' granularity → \"< 1 Week\", Period 1 → \"Week 1\"\n */\nfunction formatPeriodLabel(period: number, granularity?: RetentionGranularity): string {\n const prefix = granularity === 'day' ? 'Day'\n : granularity === 'week' ? 'Week'\n : granularity === 'month' ? 'Month'\n : 'P' // Fallback to P0, P1, etc.\n\n // Period 0 is special - shows \"< 1 Day\" / \"< 1 Week\" etc.\n if (period === 0) {\n return granularity ? `< 1 ${prefix}` : 'P0'\n }\n\n return granularity ? `${prefix} ${period}` : `P${period}`\n}\n\n/**\n * Get display label for the cohort total column\n * Shows \"Total\" regardless of binding key - it's the cohort size count\n */\nfunction getCohortLabel(_bindingKeyLabel?: string): string {\n return 'Total'\n}\n\n/**\n * Get default series name based on binding key\n * e.g., \"userId\" → \"userId Retention\", null → \"Retention\"\n */\nfunction getDefaultSeriesName(bindingKeyLabel?: string): string {\n if (!bindingKeyLabel) return 'Retention'\n return `${bindingKeyLabel} Retention`\n}\n\n/**\n * Transform retention data for chart display\n * Groups data by period with breakdown values as series\n */\nfunction transformRetentionData(\n rows: RetentionResultRow[],\n periods: number[],\n breakdownValues?: string[],\n granularity?: RetentionGranularity,\n bindingKeyLabel?: string\n): { chartData: any[]; seriesKeys: string[]; defaultSeriesName: string } {\n const defaultSeriesName = getDefaultSeriesName(bindingKeyLabel)\n\n // If no breakdown, single series\n if (!breakdownValues || breakdownValues.length === 0) {\n const chartData = periods.map((period) => {\n const row = rows.find((r) => r.period === period && !r.breakdownValue)\n return {\n period,\n periodLabel: formatPeriodLabel(period, granularity),\n [defaultSeriesName]: row ? row.retentionRate : null,\n cohortSize: row?.cohortSize ?? 0,\n retainedUsers: row?.retainedUsers ?? 0,\n }\n })\n return { chartData, seriesKeys: [defaultSeriesName], defaultSeriesName }\n }\n\n // With breakdown, create series per breakdown value\n const chartData = periods.map((period) => {\n const dataPoint: any = {\n period,\n periodLabel: formatPeriodLabel(period, granularity),\n }\n\n breakdownValues.forEach((bv) => {\n const row = rows.find((r) => r.period === period && r.breakdownValue === bv)\n dataPoint[bv] = row ? row.retentionRate : null\n dataPoint[`${bv}_cohortSize`] = row?.cohortSize ?? 0\n dataPoint[`${bv}_retainedUsers`] = row?.retainedUsers ?? 0\n })\n\n return dataPoint\n })\n\n return { chartData, seriesKeys: breakdownValues, defaultSeriesName }\n}\n\ninterface TooltipData {\n period: number\n breakdownValue?: string | null\n cohortSize: number\n retainedUsers: number\n retentionRate: number\n x: number\n y: number\n}\n\n/**\n * RetentionCombinedChart Component\n */\nconst RetentionCombinedChart = React.memo(function RetentionCombinedChart({\n data,\n height = '100%',\n displayConfig,\n colorPalette,\n}: ChartProps) {\n const { t } = useTranslation()\n const [hoveredLegend, setHoveredLegend] = useState<string | null>(null)\n const [heatmapTooltip, setHeatmapTooltip] = useState<TooltipData | null>(null)\n\n // Parse retention data\n const retentionData = useMemo<RetentionChartData | null>(() => {\n if (!data) return null\n\n // Check if data is already in RetentionChartData format\n if (isRetentionData(data)) {\n return data\n }\n\n // If data is an array of RetentionResultRow, convert it\n if (Array.isArray(data) && data.length > 0) {\n const rows = data as RetentionResultRow[]\n const periods = [...new Set(rows.map((r) => r.period))].sort((a, b) => a - b)\n const breakdownValues = [\n ...new Set(rows.filter((r) => r.breakdownValue).map((r) => r.breakdownValue!)),\n ]\n\n return {\n rows,\n periods,\n breakdownValues: breakdownValues.length > 0 ? breakdownValues : undefined,\n }\n }\n\n return null\n }, [data])\n\n // Transform data for chart\n const { chartData, seriesKeys, defaultSeriesName } = useMemo(() => {\n if (!retentionData) {\n return { chartData: [], seriesKeys: [], defaultSeriesName: 'Retention' }\n }\n return transformRetentionData(\n retentionData.rows,\n retentionData.periods,\n retentionData.breakdownValues,\n retentionData.granularity,\n retentionData.bindingKeyLabel\n )\n }, [retentionData])\n\n // Get cohort label for heatmap column header\n const cohortLabel = getCohortLabel(retentionData?.bindingKeyLabel)\n\n // Display mode from config\n const displayMode: RetentionDisplayMode =\n (displayConfig as any)?.retentionDisplayMode || 'line'\n\n const showLegend = displayConfig?.showLegend ?? true\n const showGrid = displayConfig?.showGrid ?? true\n const showTooltip = displayConfig?.showTooltip ?? true\n\n // Handle empty/loading states\n if (!data || (Array.isArray(data) && data.length === 0)) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.noData')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.noDataHint.retention')}\n </div>\n </div>\n </div>\n )\n }\n\n if (!chartData || chartData.length === 0) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.unableToRender')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">{t('chart.runtime.dataFormatIncorrect')}</div>\n </div>\n </div>\n )\n }\n\n // Render line chart component (reused in line and combined modes)\n const renderLineChart = (chartHeight: string | number) => {\n const chartMargins = {\n ...CHART_MARGINS,\n left: 50,\n right: 20,\n }\n\n return (\n <ChartContainer height={chartHeight}>\n <ComposedChart data={chartData} margin={chartMargins} accessibilityLayer={false}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" />}\n <XAxis\n dataKey=\"periodLabel\"\n tick={{ fontSize: 12 }}\n axisLine={{ stroke: 'var(--dc-border)' }}\n tickLine={{ stroke: 'var(--dc-border)' }}\n />\n <YAxis\n domain={[0, 1]}\n tickFormatter={(value) => formatPercentage(value)}\n tick={{ fontSize: 12 }}\n axisLine={{ stroke: 'var(--dc-border)' }}\n tickLine={{ stroke: 'var(--dc-border)' }}\n label={{\n value: 'Retention %',\n angle: -90,\n position: 'insideLeft',\n style: { textAnchor: 'middle', fontSize: '12px', fill: 'var(--dc-text-secondary)' },\n }}\n />\n {showTooltip && (\n <ChartTooltip\n formatter={(value: any, name: string) => {\n if (value === null || value === undefined) {\n return ['No data', name]\n }\n return [formatPercentage(value), name]\n }}\n labelFormatter={(label: string) => label}\n />\n )}\n {showLegend && (\n <Legend\n wrapperStyle={{ fontSize: '12px', paddingTop: '10px' }}\n iconType=\"line\"\n iconSize={8}\n layout=\"horizontal\"\n align=\"center\"\n verticalAlign=\"bottom\"\n onMouseEnter={(o) => setHoveredLegend(String(o.dataKey || ''))}\n onMouseLeave={() => setHoveredLegend(null)}\n />\n )}\n\n {/* Render lines */}\n {seriesKeys.map((seriesKey, index) => (\n <Line\n key={seriesKey}\n type=\"monotone\"\n dataKey={seriesKey}\n stroke={\n (colorPalette?.colors && colorPalette.colors[index % colorPalette.colors.length]) ||\n CHART_COLORS[index % CHART_COLORS.length]\n }\n strokeWidth={2}\n dot={{ r: 4, strokeWidth: 2 }}\n activeDot={{ r: 6 }}\n strokeOpacity={hoveredLegend ? (hoveredLegend === seriesKey ? 1 : 0.3) : 1}\n connectNulls={false}\n />\n ))}\n </ComposedChart>\n </ChartContainer>\n )\n }\n\n // Render heatmap table component (reused in heatmap and combined modes)\n const renderHeatmapTable = () => (\n <table className=\"dc:w-full dc:border-collapse dc:text-sm\">\n <thead className=\"dc:sticky dc:top-0 bg-dc-bg dc:z-10\">\n <tr>\n <th className=\"dc:text-left dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[100px] dc:whitespace-nowrap\">\n {retentionData?.breakdownValues?.length ? 'Segment' : 'Cohort'}\n </th>\n <th className=\"dc:text-right dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[60px] dc:whitespace-nowrap\">\n {cohortLabel}\n </th>\n {retentionData?.periods.map((period) => (\n <th\n key={period}\n className=\"dc:text-center dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[70px] dc:whitespace-nowrap\"\n >\n {formatPeriodLabel(period, retentionData?.granularity)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {seriesKeys.map((seriesKey, rowIndex) => {\n const period0Data = chartData.find((d) => d.period === 0)\n const isDefaultSeries = seriesKey === defaultSeriesName\n const cohortSize = isDefaultSeries\n ? period0Data?.cohortSize ?? 0\n : period0Data?.[`${seriesKey}_cohortSize`] ?? 0\n\n return (\n <tr\n key={seriesKey}\n className={rowIndex % 2 === 0 ? 'bg-dc-bg' : 'bg-dc-surface-secondary'}\n >\n <td className=\"dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:whitespace-nowrap\">\n {seriesKey}\n </td>\n <td className=\"dc:p-2 dc:text-right text-dc-text-secondary dc:border-b border-dc-border\">\n {cohortSize.toLocaleString()}\n </td>\n {retentionData?.periods.map((period) => {\n const dataPoint = chartData.find((d) => d.period === period)\n const rate = dataPoint?.[seriesKey] ?? 0\n const bgColor = rate > 0 ? getRetentionColor(rate) : 'transparent'\n const textColor = rate > 0.5 ? '#ffffff' : 'var(--dc-text)'\n\n return (\n <td\n key={period}\n className=\"dc:p-2 dc:text-center dc:border-b border-dc-border dc:cursor-default dc:transition-opacity dc:hover:opacity-80\"\n style={{ backgroundColor: bgColor, color: textColor }}\n onMouseEnter={(e) => {\n const rect = e.currentTarget.getBoundingClientRect()\n const retainedUsers = isDefaultSeries\n ? dataPoint?.retainedUsers ?? 0\n : dataPoint?.[`${seriesKey}_retainedUsers`] ?? 0\n setHeatmapTooltip({\n period,\n breakdownValue: isDefaultSeries ? null : seriesKey,\n cohortSize,\n retainedUsers,\n retentionRate: rate,\n x: rect.left + rect.width / 2,\n y: rect.top,\n })\n }}\n onMouseLeave={() => setHeatmapTooltip(null)}\n >\n {rate > 0 ? formatPercentage(rate) : '-'}\n </td>\n )\n })}\n </tr>\n )\n })}\n </tbody>\n </table>\n )\n\n // Render heatmap tooltip (shared between heatmap and combined modes)\n const renderHeatmapTooltip = () =>\n heatmapTooltip && (\n <div\n className=\"dc:fixed dc:z-50 dc:px-3 dc:py-2 bg-dc-surface dc:border border-dc-border dc:rounded-sm dc:shadow-lg dc:text-sm dc:pointer-events-none\"\n style={{\n left: heatmapTooltip.x,\n top: heatmapTooltip.y - 10,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <div className=\"dc:font-medium text-dc-text dc:mb-1\">\n {heatmapTooltip.breakdownValue\n ? `${heatmapTooltip.breakdownValue} - ${formatPeriodLabel(heatmapTooltip.period, retentionData?.granularity)}`\n : formatPeriodLabel(heatmapTooltip.period, retentionData?.granularity)}\n </div>\n <div className=\"text-dc-text-secondary dc:space-y-0.5\">\n <div>{t('chart.runtime.retention.cohortSize', { count: heatmapTooltip.cohortSize.toLocaleString() })}</div>\n <div>{t('chart.runtime.retention.retained', { count: heatmapTooltip.retainedUsers.toLocaleString() })}</div>\n <div className=\"dc:font-medium text-dc-text\">\n {t('chart.runtime.retention.rate', { rate: formatPercentage(heatmapTooltip.retentionRate) })}\n </div>\n </div>\n </div>\n )\n\n // Render heatmap mode (table-based only)\n if (displayMode === 'heatmap') {\n return (\n <div className=\"dc:relative dc:w-full dc:h-full dc:overflow-auto\" style={{ height }}>\n {renderHeatmapTable()}\n {renderHeatmapTooltip()}\n </div>\n )\n }\n\n // Combined mode: line chart on top, heatmap table below\n if (displayMode === 'combined') {\n return (\n <div className=\"dc:flex dc:flex-col dc:w-full dc:h-full\" style={{ height }}>\n {/* Line chart - takes remaining space after heatmap */}\n <div className=\"dc:flex-1 dc:min-h-[200px]\">\n {renderLineChart('100%')}\n </div>\n {/* Heatmap table - auto-height based on content, scrolls if needed */}\n <div className=\"dc:flex-shrink-0 dc:max-h-[40%] dc:overflow-auto dc:border-t border-dc-border\">\n {renderHeatmapTable()}\n </div>\n {/* Shared heatmap tooltip */}\n {renderHeatmapTooltip()}\n </div>\n )\n }\n\n // Line mode: just the line chart\n return renderLineChart(height)\n})\n\nexport default RetentionCombinedChart\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RetentionCombinedChart-BRZ9w7ah.js","names":[],"sources":["../../../src/client/components/charts/RetentionCombinedChart.tsx"],"sourcesContent":["/**\n * RetentionCombinedChart Component\n *\n * Combined visualization for retention analysis data.\n * Supports multiple display modes: heatmap, line chart, or combined view.\n *\n * Features:\n * - X-axis: Period numbers (P0, P1, P2...)\n * - Y-axis: Retention % (0-100%)\n * - Lines: One per breakdown value (or single if no breakdown)\n * - Display modes: 'heatmap' | 'line' | 'combined'\n * - Heatmap shows color-coded retention matrix\n * - Line chart shows retention curves over periods\n */\n\nimport React, { useMemo, useState } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport {\n ComposedChart,\n Line,\n XAxis,\n YAxis,\n CartesianGrid,\n Legend,\n} from 'recharts'\nimport ChartContainer from './ChartContainer.js'\nimport ChartTooltip from './ChartTooltip.js'\nimport { CHART_COLORS, CHART_MARGINS } from '../../utils/chartConstants.js'\nimport type { ChartProps } from '../../types.js'\nimport type { RetentionChartData, RetentionResultRow, RetentionGranularity } from '../../types/retention.js'\nimport { isRetentionData } from '../../types/retention.js'\n\n/**\n * Retention display mode\n * - 'heatmap': Show retention as color-coded bars\n * - 'line': Show retention as line curves\n * - 'combined': Show both heatmap background and line overlay\n */\nexport type RetentionDisplayMode = 'heatmap' | 'line' | 'combined'\n\n/**\n * Get color with opacity based on retention rate for heatmap cells\n * Uses a green gradient: higher retention = more saturated green\n */\nfunction getRetentionColor(rate: number): string {\n const clampedRate = Math.max(0, Math.min(1, rate))\n const alpha = 0.1 + clampedRate * 0.7\n return `rgba(34, 197, 94, ${alpha})`\n}\n\n/**\n * Format percentage for display\n */\nfunction formatPercentage(rate: number): string {\n return `${Math.round(rate * 100)}%`\n}\n\n/**\n * Format period label based on granularity\n * Period 0 shows \"< 1 Day\" / \"< 1 Week\" etc. to indicate the initial cohort\n * e.g., Period 0 with 'week' granularity → \"< 1 Week\", Period 1 → \"Week 1\"\n */\nfunction formatPeriodLabel(period: number, granularity?: RetentionGranularity): string {\n const prefix = granularity === 'day' ? 'Day'\n : granularity === 'week' ? 'Week'\n : granularity === 'month' ? 'Month'\n : 'P' // Fallback to P0, P1, etc.\n\n // Period 0 is special - shows \"< 1 Day\" / \"< 1 Week\" etc.\n if (period === 0) {\n return granularity ? `< 1 ${prefix}` : 'P0'\n }\n\n return granularity ? `${prefix} ${period}` : `P${period}`\n}\n\n/**\n * Get display label for the cohort total column\n * Shows \"Total\" regardless of binding key - it's the cohort size count\n */\nfunction getCohortLabel(_bindingKeyLabel?: string): string {\n return 'Total'\n}\n\n/**\n * Get default series name based on binding key\n * e.g., \"userId\" → \"userId Retention\", null → \"Retention\"\n */\nfunction getDefaultSeriesName(bindingKeyLabel?: string): string {\n if (!bindingKeyLabel) return 'Retention'\n return `${bindingKeyLabel} Retention`\n}\n\n/**\n * Transform retention data for chart display\n * Groups data by period with breakdown values as series\n */\nfunction transformRetentionData(\n rows: RetentionResultRow[],\n periods: number[],\n breakdownValues?: string[],\n granularity?: RetentionGranularity,\n bindingKeyLabel?: string\n): { chartData: any[]; seriesKeys: string[]; defaultSeriesName: string } {\n const defaultSeriesName = getDefaultSeriesName(bindingKeyLabel)\n\n // If no breakdown, single series\n if (!breakdownValues || breakdownValues.length === 0) {\n const chartData = periods.map((period) => {\n const row = rows.find((r) => r.period === period && !r.breakdownValue)\n return {\n period,\n periodLabel: formatPeriodLabel(period, granularity),\n [defaultSeriesName]: row ? row.retentionRate : null,\n cohortSize: row?.cohortSize ?? 0,\n retainedUsers: row?.retainedUsers ?? 0,\n }\n })\n return { chartData, seriesKeys: [defaultSeriesName], defaultSeriesName }\n }\n\n // With breakdown, create series per breakdown value\n const chartData = periods.map((period) => {\n const dataPoint: any = {\n period,\n periodLabel: formatPeriodLabel(period, granularity),\n }\n\n breakdownValues.forEach((bv) => {\n const row = rows.find((r) => r.period === period && r.breakdownValue === bv)\n dataPoint[bv] = row ? row.retentionRate : null\n dataPoint[`${bv}_cohortSize`] = row?.cohortSize ?? 0\n dataPoint[`${bv}_retainedUsers`] = row?.retainedUsers ?? 0\n })\n\n return dataPoint\n })\n\n return { chartData, seriesKeys: breakdownValues, defaultSeriesName }\n}\n\ninterface TooltipData {\n period: number\n breakdownValue?: string | null\n cohortSize: number\n retainedUsers: number\n retentionRate: number\n x: number\n y: number\n}\n\n/**\n * RetentionCombinedChart Component\n */\nconst RetentionCombinedChart = React.memo(function RetentionCombinedChart({\n data,\n height = '100%',\n displayConfig,\n colorPalette,\n}: ChartProps) {\n const { t } = useTranslation()\n const [hoveredLegend, setHoveredLegend] = useState<string | null>(null)\n const [heatmapTooltip, setHeatmapTooltip] = useState<TooltipData | null>(null)\n\n // Parse retention data\n const retentionData = useMemo<RetentionChartData | null>(() => {\n if (!data) return null\n\n // Check if data is already in RetentionChartData format\n if (isRetentionData(data)) {\n return data\n }\n\n // If data is an array of RetentionResultRow, convert it\n if (Array.isArray(data) && data.length > 0) {\n const rows = data as RetentionResultRow[]\n const periods = [...new Set(rows.map((r) => r.period))].sort((a, b) => a - b)\n const breakdownValues = [\n ...new Set(rows.filter((r) => r.breakdownValue).map((r) => r.breakdownValue!)),\n ]\n\n return {\n rows,\n periods,\n breakdownValues: breakdownValues.length > 0 ? breakdownValues : undefined,\n }\n }\n\n return null\n }, [data])\n\n // Transform data for chart\n const { chartData, seriesKeys, defaultSeriesName } = useMemo(() => {\n if (!retentionData) {\n return { chartData: [], seriesKeys: [], defaultSeriesName: 'Retention' }\n }\n return transformRetentionData(\n retentionData.rows,\n retentionData.periods,\n retentionData.breakdownValues,\n retentionData.granularity,\n retentionData.bindingKeyLabel\n )\n }, [retentionData])\n\n // Get cohort label for heatmap column header\n const cohortLabel = getCohortLabel(retentionData?.bindingKeyLabel)\n\n // Display mode from config\n const displayMode: RetentionDisplayMode =\n (displayConfig as any)?.retentionDisplayMode || 'line'\n\n const showLegend = displayConfig?.showLegend ?? true\n const showGrid = displayConfig?.showGrid ?? true\n const showTooltip = displayConfig?.showTooltip ?? true\n\n // Handle empty/loading states\n if (!data || (Array.isArray(data) && data.length === 0)) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.noData')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.noDataHint.retention')}\n </div>\n </div>\n </div>\n )\n }\n\n if (!chartData || chartData.length === 0) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.unableToRender')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">{t('chart.runtime.dataFormatIncorrect')}</div>\n </div>\n </div>\n )\n }\n\n // Render line chart component (reused in line and combined modes)\n const renderLineChart = (chartHeight: string | number) => {\n const chartMargins = {\n ...CHART_MARGINS,\n left: 50,\n right: 20,\n }\n\n return (\n <ChartContainer height={chartHeight}>\n <ComposedChart data={chartData} margin={chartMargins} accessibilityLayer={false}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" />}\n <XAxis\n dataKey=\"periodLabel\"\n tick={{ fontSize: 12 }}\n axisLine={{ stroke: 'var(--dc-border)' }}\n tickLine={{ stroke: 'var(--dc-border)' }}\n />\n <YAxis\n domain={[0, 1]}\n tickFormatter={(value) => formatPercentage(value)}\n tick={{ fontSize: 12 }}\n axisLine={{ stroke: 'var(--dc-border)' }}\n tickLine={{ stroke: 'var(--dc-border)' }}\n label={{\n value: 'Retention %',\n angle: -90,\n position: 'insideLeft',\n style: { textAnchor: 'middle', fontSize: '12px', fill: 'var(--dc-text-secondary)' },\n }}\n />\n {showTooltip && (\n <ChartTooltip\n formatter={(value: any, name: string) => {\n if (value === null || value === undefined) {\n return ['No data', name]\n }\n return [formatPercentage(value), name]\n }}\n labelFormatter={(label: string) => label}\n />\n )}\n {showLegend && (\n <Legend\n wrapperStyle={{ fontSize: '12px', paddingTop: '10px' }}\n iconType=\"line\"\n iconSize={8}\n layout=\"horizontal\"\n align=\"center\"\n verticalAlign=\"bottom\"\n onMouseEnter={(o) => setHoveredLegend(String(o.dataKey || ''))}\n onMouseLeave={() => setHoveredLegend(null)}\n />\n )}\n\n {/* Render lines */}\n {seriesKeys.map((seriesKey, index) => (\n <Line\n key={seriesKey}\n type=\"monotone\"\n dataKey={seriesKey}\n stroke={\n (colorPalette?.colors && colorPalette.colors[index % colorPalette.colors.length]) ||\n CHART_COLORS[index % CHART_COLORS.length]\n }\n strokeWidth={2}\n dot={{ r: 4, strokeWidth: 2 }}\n activeDot={{ r: 6 }}\n strokeOpacity={hoveredLegend ? (hoveredLegend === seriesKey ? 1 : 0.3) : 1}\n connectNulls={false}\n />\n ))}\n </ComposedChart>\n </ChartContainer>\n )\n }\n\n // Render heatmap table component (reused in heatmap and combined modes)\n const renderHeatmapTable = () => (\n <table className=\"dc:w-full dc:border-collapse dc:text-sm\">\n <thead className=\"dc:sticky dc:top-0 bg-dc-bg dc:z-10\">\n <tr>\n <th className=\"dc:text-left dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[100px] dc:whitespace-nowrap\">\n {retentionData?.breakdownValues?.length ? 'Segment' : 'Cohort'}\n </th>\n <th className=\"dc:text-right dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[60px] dc:whitespace-nowrap\">\n {cohortLabel}\n </th>\n {retentionData?.periods.map((period) => (\n <th\n key={period}\n className=\"dc:text-center dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[70px] dc:whitespace-nowrap\"\n >\n {formatPeriodLabel(period, retentionData?.granularity)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {seriesKeys.map((seriesKey, rowIndex) => {\n const period0Data = chartData.find((d) => d.period === 0)\n const isDefaultSeries = seriesKey === defaultSeriesName\n const cohortSize = isDefaultSeries\n ? period0Data?.cohortSize ?? 0\n : period0Data?.[`${seriesKey}_cohortSize`] ?? 0\n\n return (\n <tr\n key={seriesKey}\n className={rowIndex % 2 === 0 ? 'bg-dc-bg' : 'bg-dc-surface-secondary'}\n >\n <td className=\"dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:whitespace-nowrap\">\n {seriesKey}\n </td>\n <td className=\"dc:p-2 dc:text-right text-dc-text-secondary dc:border-b border-dc-border\">\n {cohortSize.toLocaleString()}\n </td>\n {retentionData?.periods.map((period) => {\n const dataPoint = chartData.find((d) => d.period === period)\n const rate = dataPoint?.[seriesKey] ?? 0\n const bgColor = rate > 0 ? getRetentionColor(rate) : 'transparent'\n const textColor = rate > 0.5 ? '#ffffff' : 'var(--dc-text)'\n\n return (\n <td\n key={period}\n className=\"dc:p-2 dc:text-center dc:border-b border-dc-border dc:cursor-default dc:transition-opacity dc:hover:opacity-80\"\n style={{ backgroundColor: bgColor, color: textColor }}\n onMouseEnter={(e) => {\n const rect = e.currentTarget.getBoundingClientRect()\n const retainedUsers = isDefaultSeries\n ? dataPoint?.retainedUsers ?? 0\n : dataPoint?.[`${seriesKey}_retainedUsers`] ?? 0\n setHeatmapTooltip({\n period,\n breakdownValue: isDefaultSeries ? null : seriesKey,\n cohortSize,\n retainedUsers,\n retentionRate: rate,\n x: rect.left + rect.width / 2,\n y: rect.top,\n })\n }}\n onMouseLeave={() => setHeatmapTooltip(null)}\n >\n {rate > 0 ? formatPercentage(rate) : '-'}\n </td>\n )\n })}\n </tr>\n )\n })}\n </tbody>\n </table>\n )\n\n // Render heatmap tooltip (shared between heatmap and combined modes)\n const renderHeatmapTooltip = () =>\n heatmapTooltip && (\n <div\n className=\"dc:fixed dc:z-50 dc:px-3 dc:py-2 bg-dc-surface dc:border border-dc-border dc:rounded-sm dc:shadow-lg dc:text-sm dc:pointer-events-none\"\n style={{\n left: heatmapTooltip.x,\n top: heatmapTooltip.y - 10,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <div className=\"dc:font-medium text-dc-text dc:mb-1\">\n {heatmapTooltip.breakdownValue\n ? `${heatmapTooltip.breakdownValue} - ${formatPeriodLabel(heatmapTooltip.period, retentionData?.granularity)}`\n : formatPeriodLabel(heatmapTooltip.period, retentionData?.granularity)}\n </div>\n <div className=\"text-dc-text-secondary dc:space-y-0.5\">\n <div>{t('chart.runtime.retention.cohortSize', { count: heatmapTooltip.cohortSize.toLocaleString() })}</div>\n <div>{t('chart.runtime.retention.retained', { count: heatmapTooltip.retainedUsers.toLocaleString() })}</div>\n <div className=\"dc:font-medium text-dc-text\">\n {t('chart.runtime.retention.rate', { rate: formatPercentage(heatmapTooltip.retentionRate) })}\n </div>\n </div>\n </div>\n )\n\n // Render heatmap mode (table-based only)\n if (displayMode === 'heatmap') {\n return (\n <div className=\"dc:relative dc:w-full dc:h-full dc:overflow-auto\" style={{ height }}>\n {renderHeatmapTable()}\n {renderHeatmapTooltip()}\n </div>\n )\n }\n\n // Combined mode: line chart on top, heatmap table below\n if (displayMode === 'combined') {\n return (\n <div className=\"dc:flex dc:flex-col dc:w-full dc:h-full\" style={{ height }}>\n {/* Line chart - takes remaining space after heatmap */}\n <div className=\"dc:flex-1 dc:min-h-[200px]\">\n {renderLineChart('100%')}\n </div>\n {/* Heatmap table - auto-height based on content, scrolls if needed */}\n <div className=\"dc:flex-shrink-0 dc:max-h-[40%] dc:overflow-auto dc:border-t border-dc-border\">\n {renderHeatmapTable()}\n </div>\n {/* Shared heatmap tooltip */}\n {renderHeatmapTooltip()}\n </div>\n )\n }\n\n // Line mode: just the line chart\n return renderLineChart(height)\n})\n\nexport default RetentionCombinedChart\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as e } from "./retention-ChW9jYdy.js";
|
|
2
|
-
import { F as t } from "./chart-activity-grid-
|
|
2
|
+
import { F as t } from "./chart-activity-grid-CJzxC18G.js";
|
|
3
3
|
import n, { useMemo as r, useState as i } from "react";
|
|
4
4
|
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
5
5
|
//#region src/client/components/charts/RetentionHeatmap.tsx
|
|
@@ -190,4 +190,4 @@ var d = n.memo(function({ data: n, height: d = "100%", displayConfig: f }) {
|
|
|
190
190
|
//#endregion
|
|
191
191
|
export { d as default };
|
|
192
192
|
|
|
193
|
-
//# sourceMappingURL=RetentionHeatmap-
|
|
193
|
+
//# sourceMappingURL=RetentionHeatmap-LkTt89pK.js.map
|
package/dist/client/chunks/{RetentionHeatmap-PpRpiqR_.js.map → RetentionHeatmap-LkTt89pK.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RetentionHeatmap-PpRpiqR_.js","names":[],"sources":["../../../src/client/components/charts/RetentionHeatmap.tsx"],"sourcesContent":["/**\n * RetentionHeatmap Component\n *\n * Visualizes retention analysis data as a cohort × period matrix.\n * Displays retention rates with color intensity based on percentage.\n *\n * Features:\n * - Cohort labels in first column (e.g., \"2024-01\", \"2024-02\")\n * - Cohort size in second column\n * - Period columns (P0, P1, P2, ... PN)\n * - Cell background color intensity based on retention rate\n * - Hover tooltip with detailed stats\n */\n\nimport React, { useMemo, useState } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport type { ChartProps } from '../../types.js'\nimport type { RetentionChartData, RetentionResultRow } from '../../types/retention.js'\nimport { isRetentionData } from '../../types/retention.js'\n\n/**\n * Get color with opacity based on retention rate\n * Uses a green gradient: higher retention = more saturated green\n */\nfunction getRetentionColor(rate: number): string {\n // Clamp rate between 0 and 1\n const clampedRate = Math.max(0, Math.min(1, rate))\n\n // Use CSS variable for theming support\n // Fallback to a green color if CSS var not available\n const alpha = 0.1 + clampedRate * 0.7 // Range from 0.1 to 0.8 opacity\n\n // Green color (success color)\n return `rgba(34, 197, 94, ${alpha})`\n}\n\n/**\n * Get text color that contrasts with background\n */\nfunction getTextColor(rate: number): string {\n // Use dark text for lower rates, light text for higher rates\n return rate > 0.5 ? '#ffffff' : 'var(--dc-text)'\n}\n\n/**\n * Format cohort period for display\n * Converts date strings to readable format\n */\nfunction formatCohortPeriod(cohort: string): string {\n // If it's already in YYYY-MM format, return as-is\n if (/^\\d{4}-\\d{2}$/.test(cohort)) {\n return cohort\n }\n\n // Try to parse as date\n const date = new Date(cohort)\n if (!isNaN(date.getTime())) {\n return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })\n }\n\n return cohort\n}\n\n/**\n * Format percentage for display\n */\nfunction formatPercentage(rate: number): string {\n return `${Math.round(rate * 100)}%`\n}\n\ninterface TooltipData {\n cohort: string\n period: number\n cohortSize: number\n retainedUsers: number\n retentionRate: number\n x: number\n y: number\n}\n\n/**\n * RetentionHeatmap Component\n */\nconst RetentionHeatmap = React.memo(function RetentionHeatmap({\n data,\n height = '100%',\n displayConfig,\n}: ChartProps) {\n const { t } = useTranslation()\n const [tooltip, setTooltip] = useState<TooltipData | null>(null)\n\n // Parse retention data\n const retentionData = useMemo<RetentionChartData | null>(() => {\n if (!data) return null\n\n // Check if data is already in RetentionChartData format\n if (isRetentionData(data)) {\n return data\n }\n\n // If data is an array of RetentionResultRow, convert it\n if (Array.isArray(data) && data.length > 0) {\n const rows = data as RetentionResultRow[]\n const breakdownValues = [...new Set(rows.map(r => r.breakdownValue || 'All Users'))].sort()\n const periods = [...new Set(rows.map(r => r.period))].sort((a, b) => a - b)\n\n return {\n rows,\n breakdownValues: breakdownValues.length > 1 || breakdownValues[0] !== 'All Users' ? breakdownValues : undefined,\n periods,\n }\n }\n\n return null\n }, [data])\n\n // Build matrix for display\n // In the new simplified format, rows are grouped by breakdownValue (or 'All Users' if no breakdown)\n const matrix = useMemo(() => {\n if (!retentionData) return null\n\n const { rows, breakdownValues, periods } = retentionData\n\n // Determine segments: use breakdownValues if available, otherwise single 'All Users' segment\n const segments = breakdownValues || ['All Users']\n\n // Create a lookup map for quick access: segment:period -> row\n const lookup = new Map<string, RetentionResultRow>()\n for (const row of rows) {\n const segment = row.breakdownValue || 'All Users'\n lookup.set(`${segment}:${row.period}`, row)\n }\n\n // Build matrix structure - one row per segment\n return segments.map((segment: string) => {\n const segmentRows = periods.map(period => {\n const row = lookup.get(`${segment}:${period}`)\n return row || null\n })\n\n // Get cohort size from period 0\n const period0 = segmentRows[0]\n const cohortSize = period0?.cohortSize ?? 0\n\n return {\n cohort: segment, // Keep 'cohort' key for compatibility with rendering\n cohortSize,\n periods: segmentRows,\n }\n })\n }, [retentionData])\n\n // Handle mouse enter on cell\n const handleMouseEnter = (\n event: React.MouseEvent,\n cohort: string,\n period: number,\n row: RetentionResultRow | null\n ) => {\n if (!row) return\n\n const rect = event.currentTarget.getBoundingClientRect()\n setTooltip({\n cohort,\n period,\n cohortSize: row.cohortSize,\n retainedUsers: row.retainedUsers,\n retentionRate: row.retentionRate,\n x: rect.left + rect.width / 2,\n y: rect.top,\n })\n }\n\n const handleMouseLeave = () => {\n setTooltip(null)\n }\n\n // Handle empty/loading states\n if (!data || (Array.isArray(data) && data.length === 0)) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.noData')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.noDataHint.retention')}\n </div>\n </div>\n </div>\n )\n }\n\n if (!matrix || matrix.length === 0) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.unableToRender')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.dataFormatIncorrect')}\n </div>\n </div>\n </div>\n )\n }\n\n const periods = retentionData?.periods ?? []\n const showLegend = displayConfig?.showLegend ?? true\n\n return (\n <div className=\"dc:relative dc:w-full dc:h-full dc:overflow-auto\" style={{ height }}>\n {/* Retention Matrix Table */}\n <table className=\"dc:w-full dc:border-collapse dc:text-sm\">\n <thead className=\"dc:sticky dc:top-0 bg-dc-bg dc:z-10\">\n <tr>\n <th className=\"dc:text-left dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[100px]\">\n {t('chart.runtime.retention.cohort')}\n </th>\n <th className=\"dc:text-right dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[80px]\">\n {t('chart.runtime.retention.users')}\n </th>\n {periods.map(period => (\n <th\n key={period}\n className=\"dc:text-center dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[60px]\"\n >\n P{period}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {matrix.map((row, rowIndex) => (\n <tr key={row.cohort} className={rowIndex % 2 === 0 ? 'bg-dc-bg' : 'bg-dc-surface-secondary'}>\n <td className=\"dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:whitespace-nowrap\">\n {formatCohortPeriod(row.cohort)}\n </td>\n <td className=\"dc:p-2 dc:text-right text-dc-text-secondary dc:border-b border-dc-border\">\n {row.cohortSize.toLocaleString()}\n </td>\n {row.periods.map((cell, periodIndex) => {\n const period = periods[periodIndex]\n const rate = cell?.retentionRate ?? 0\n const bgColor = cell ? getRetentionColor(rate) : 'transparent'\n const textColor = cell ? getTextColor(rate) : 'var(--dc-text-muted)'\n\n return (\n <td\n key={period}\n className=\"dc:p-2 dc:text-center dc:border-b border-dc-border dc:cursor-default dc:transition-opacity dc:hover:opacity-80\"\n style={{ backgroundColor: bgColor, color: textColor }}\n onMouseEnter={(e) => handleMouseEnter(e, row.cohort, period, cell)}\n onMouseLeave={handleMouseLeave}\n >\n {cell ? formatPercentage(rate) : '-'}\n </td>\n )\n })}\n </tr>\n ))}\n </tbody>\n </table>\n\n {/* Legend */}\n {showLegend && (\n <div className=\"dc:flex dc:items-center dc:justify-center dc:mt-4 dc:gap-2 dc:text-xs text-dc-text-secondary\">\n <span>0%</span>\n <div className=\"dc:flex dc:h-4\">\n {[0, 0.2, 0.4, 0.6, 0.8, 1].map(rate => (\n <div\n key={rate}\n className=\"dc:w-6 dc:h-4\"\n style={{ backgroundColor: getRetentionColor(rate) }}\n />\n ))}\n </div>\n <span>100%</span>\n </div>\n )}\n\n {/* Tooltip */}\n {tooltip && (\n <div\n className=\"dc:fixed dc:z-50 dc:px-3 dc:py-2 bg-dc-surface dc:border border-dc-border dc:rounded-sm dc:shadow-lg dc:text-sm dc:pointer-events-none\"\n style={{\n left: tooltip.x,\n top: tooltip.y - 10,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <div className=\"dc:font-medium text-dc-text dc:mb-1\">\n {t('chart.runtime.retention.periodLabel', {\n cohort: formatCohortPeriod(tooltip.cohort),\n period: tooltip.period\n })}\n </div>\n <div className=\"text-dc-text-secondary dc:space-y-0.5\">\n <div>{t('chart.runtime.retention.cohortSize', { count: tooltip.cohortSize.toLocaleString() })}</div>\n <div>{t('chart.runtime.retention.retained', { count: tooltip.retainedUsers.toLocaleString() })}</div>\n <div className=\"dc:font-medium text-dc-text\">\n {t('chart.runtime.retention.rate', { rate: formatPercentage(tooltip.retentionRate) })}\n </div>\n </div>\n </div>\n )}\n </div>\n )\n})\n\nexport default RetentionHeatmap\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RetentionHeatmap-LkTt89pK.js","names":[],"sources":["../../../src/client/components/charts/RetentionHeatmap.tsx"],"sourcesContent":["/**\n * RetentionHeatmap Component\n *\n * Visualizes retention analysis data as a cohort × period matrix.\n * Displays retention rates with color intensity based on percentage.\n *\n * Features:\n * - Cohort labels in first column (e.g., \"2024-01\", \"2024-02\")\n * - Cohort size in second column\n * - Period columns (P0, P1, P2, ... PN)\n * - Cell background color intensity based on retention rate\n * - Hover tooltip with detailed stats\n */\n\nimport React, { useMemo, useState } from 'react'\nimport { useTranslation } from '../../hooks/useTranslation.js'\nimport type { ChartProps } from '../../types.js'\nimport type { RetentionChartData, RetentionResultRow } from '../../types/retention.js'\nimport { isRetentionData } from '../../types/retention.js'\n\n/**\n * Get color with opacity based on retention rate\n * Uses a green gradient: higher retention = more saturated green\n */\nfunction getRetentionColor(rate: number): string {\n // Clamp rate between 0 and 1\n const clampedRate = Math.max(0, Math.min(1, rate))\n\n // Use CSS variable for theming support\n // Fallback to a green color if CSS var not available\n const alpha = 0.1 + clampedRate * 0.7 // Range from 0.1 to 0.8 opacity\n\n // Green color (success color)\n return `rgba(34, 197, 94, ${alpha})`\n}\n\n/**\n * Get text color that contrasts with background\n */\nfunction getTextColor(rate: number): string {\n // Use dark text for lower rates, light text for higher rates\n return rate > 0.5 ? '#ffffff' : 'var(--dc-text)'\n}\n\n/**\n * Format cohort period for display\n * Converts date strings to readable format\n */\nfunction formatCohortPeriod(cohort: string): string {\n // If it's already in YYYY-MM format, return as-is\n if (/^\\d{4}-\\d{2}$/.test(cohort)) {\n return cohort\n }\n\n // Try to parse as date\n const date = new Date(cohort)\n if (!isNaN(date.getTime())) {\n return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short' })\n }\n\n return cohort\n}\n\n/**\n * Format percentage for display\n */\nfunction formatPercentage(rate: number): string {\n return `${Math.round(rate * 100)}%`\n}\n\ninterface TooltipData {\n cohort: string\n period: number\n cohortSize: number\n retainedUsers: number\n retentionRate: number\n x: number\n y: number\n}\n\n/**\n * RetentionHeatmap Component\n */\nconst RetentionHeatmap = React.memo(function RetentionHeatmap({\n data,\n height = '100%',\n displayConfig,\n}: ChartProps) {\n const { t } = useTranslation()\n const [tooltip, setTooltip] = useState<TooltipData | null>(null)\n\n // Parse retention data\n const retentionData = useMemo<RetentionChartData | null>(() => {\n if (!data) return null\n\n // Check if data is already in RetentionChartData format\n if (isRetentionData(data)) {\n return data\n }\n\n // If data is an array of RetentionResultRow, convert it\n if (Array.isArray(data) && data.length > 0) {\n const rows = data as RetentionResultRow[]\n const breakdownValues = [...new Set(rows.map(r => r.breakdownValue || 'All Users'))].sort()\n const periods = [...new Set(rows.map(r => r.period))].sort((a, b) => a - b)\n\n return {\n rows,\n breakdownValues: breakdownValues.length > 1 || breakdownValues[0] !== 'All Users' ? breakdownValues : undefined,\n periods,\n }\n }\n\n return null\n }, [data])\n\n // Build matrix for display\n // In the new simplified format, rows are grouped by breakdownValue (or 'All Users' if no breakdown)\n const matrix = useMemo(() => {\n if (!retentionData) return null\n\n const { rows, breakdownValues, periods } = retentionData\n\n // Determine segments: use breakdownValues if available, otherwise single 'All Users' segment\n const segments = breakdownValues || ['All Users']\n\n // Create a lookup map for quick access: segment:period -> row\n const lookup = new Map<string, RetentionResultRow>()\n for (const row of rows) {\n const segment = row.breakdownValue || 'All Users'\n lookup.set(`${segment}:${row.period}`, row)\n }\n\n // Build matrix structure - one row per segment\n return segments.map((segment: string) => {\n const segmentRows = periods.map(period => {\n const row = lookup.get(`${segment}:${period}`)\n return row || null\n })\n\n // Get cohort size from period 0\n const period0 = segmentRows[0]\n const cohortSize = period0?.cohortSize ?? 0\n\n return {\n cohort: segment, // Keep 'cohort' key for compatibility with rendering\n cohortSize,\n periods: segmentRows,\n }\n })\n }, [retentionData])\n\n // Handle mouse enter on cell\n const handleMouseEnter = (\n event: React.MouseEvent,\n cohort: string,\n period: number,\n row: RetentionResultRow | null\n ) => {\n if (!row) return\n\n const rect = event.currentTarget.getBoundingClientRect()\n setTooltip({\n cohort,\n period,\n cohortSize: row.cohortSize,\n retainedUsers: row.retainedUsers,\n retentionRate: row.retentionRate,\n x: rect.left + rect.width / 2,\n y: rect.top,\n })\n }\n\n const handleMouseLeave = () => {\n setTooltip(null)\n }\n\n // Handle empty/loading states\n if (!data || (Array.isArray(data) && data.length === 0)) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.noData')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.noDataHint.retention')}\n </div>\n </div>\n </div>\n )\n }\n\n if (!matrix || matrix.length === 0) {\n return (\n <div\n className=\"dc:flex dc:items-center dc:justify-center dc:w-full text-dc-text-muted\"\n style={{ height }}\n >\n <div className=\"dc:text-center\">\n <div className=\"dc:text-sm dc:font-semibold dc:mb-1\">{t('chart.runtime.unableToRender')}</div>\n <div className=\"dc:text-xs text-dc-text-secondary\">\n {t('chart.runtime.dataFormatIncorrect')}\n </div>\n </div>\n </div>\n )\n }\n\n const periods = retentionData?.periods ?? []\n const showLegend = displayConfig?.showLegend ?? true\n\n return (\n <div className=\"dc:relative dc:w-full dc:h-full dc:overflow-auto\" style={{ height }}>\n {/* Retention Matrix Table */}\n <table className=\"dc:w-full dc:border-collapse dc:text-sm\">\n <thead className=\"dc:sticky dc:top-0 bg-dc-bg dc:z-10\">\n <tr>\n <th className=\"dc:text-left dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[100px]\">\n {t('chart.runtime.retention.cohort')}\n </th>\n <th className=\"dc:text-right dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[80px]\">\n {t('chart.runtime.retention.users')}\n </th>\n {periods.map(period => (\n <th\n key={period}\n className=\"dc:text-center dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:min-w-[60px]\"\n >\n P{period}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {matrix.map((row, rowIndex) => (\n <tr key={row.cohort} className={rowIndex % 2 === 0 ? 'bg-dc-bg' : 'bg-dc-surface-secondary'}>\n <td className=\"dc:p-2 dc:font-medium text-dc-text dc:border-b border-dc-border dc:whitespace-nowrap\">\n {formatCohortPeriod(row.cohort)}\n </td>\n <td className=\"dc:p-2 dc:text-right text-dc-text-secondary dc:border-b border-dc-border\">\n {row.cohortSize.toLocaleString()}\n </td>\n {row.periods.map((cell, periodIndex) => {\n const period = periods[periodIndex]\n const rate = cell?.retentionRate ?? 0\n const bgColor = cell ? getRetentionColor(rate) : 'transparent'\n const textColor = cell ? getTextColor(rate) : 'var(--dc-text-muted)'\n\n return (\n <td\n key={period}\n className=\"dc:p-2 dc:text-center dc:border-b border-dc-border dc:cursor-default dc:transition-opacity dc:hover:opacity-80\"\n style={{ backgroundColor: bgColor, color: textColor }}\n onMouseEnter={(e) => handleMouseEnter(e, row.cohort, period, cell)}\n onMouseLeave={handleMouseLeave}\n >\n {cell ? formatPercentage(rate) : '-'}\n </td>\n )\n })}\n </tr>\n ))}\n </tbody>\n </table>\n\n {/* Legend */}\n {showLegend && (\n <div className=\"dc:flex dc:items-center dc:justify-center dc:mt-4 dc:gap-2 dc:text-xs text-dc-text-secondary\">\n <span>0%</span>\n <div className=\"dc:flex dc:h-4\">\n {[0, 0.2, 0.4, 0.6, 0.8, 1].map(rate => (\n <div\n key={rate}\n className=\"dc:w-6 dc:h-4\"\n style={{ backgroundColor: getRetentionColor(rate) }}\n />\n ))}\n </div>\n <span>100%</span>\n </div>\n )}\n\n {/* Tooltip */}\n {tooltip && (\n <div\n className=\"dc:fixed dc:z-50 dc:px-3 dc:py-2 bg-dc-surface dc:border border-dc-border dc:rounded-sm dc:shadow-lg dc:text-sm dc:pointer-events-none\"\n style={{\n left: tooltip.x,\n top: tooltip.y - 10,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <div className=\"dc:font-medium text-dc-text dc:mb-1\">\n {t('chart.runtime.retention.periodLabel', {\n cohort: formatCohortPeriod(tooltip.cohort),\n period: tooltip.period\n })}\n </div>\n <div className=\"text-dc-text-secondary dc:space-y-0.5\">\n <div>{t('chart.runtime.retention.cohortSize', { count: tooltip.cohortSize.toLocaleString() })}</div>\n <div>{t('chart.runtime.retention.retained', { count: tooltip.retainedUsers.toLocaleString() })}</div>\n <div className=\"dc:font-medium text-dc-text\">\n {t('chart.runtime.retention.rate', { rate: formatPercentage(tooltip.retentionRate) })}\n </div>\n </div>\n </div>\n )}\n </div>\n )\n})\n\nexport default RetentionHeatmap\n"],"mappings":";;;;;AAwBA,SAAS,EAAkB,GAAsB;CAS/C,OAAO,qBAHO,KAJM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAI,CAI5B,IAAc,GAGA;AACpC;AAKA,SAAS,EAAa,GAAsB;CAE1C,OAAO,IAAO,KAAM,YAAY;AAClC;AAMA,SAAS,EAAmB,GAAwB;CAElD,IAAI,gBAAgB,KAAK,CAAM,GAC7B,OAAO;CAIT,IAAM,IAAO,IAAI,KAAK,CAAM;CAK5B,OAJK,MAAM,EAAK,QAAQ,CAAC,IAIlB,IAHE,EAAK,mBAAmB,SAAS;EAAE,MAAM;EAAW,OAAO;CAAQ,CAAC;AAI/E;AAKA,SAAS,EAAiB,GAAsB;CAC9C,OAAO,GAAG,KAAK,MAAM,IAAO,GAAG,EAAE;AACnC;AAeA,IAAM,IAAmB,EAAM,KAAK,SAA0B,EAC5D,SACA,YAAS,QACT,oBACa;CACb,IAAM,EAAE,SAAM,EAAe,GACvB,CAAC,GAAS,KAAc,EAA6B,IAAI,GAGzD,IAAgB,QAAyC;EAC7D,IAAI,CAAC,GAAM,OAAO;EAGlB,IAAI,EAAgB,CAAI,GACtB,OAAO;EAIT,IAAI,MAAM,QAAQ,CAAI,KAAK,EAAK,SAAS,GAAG;GAC1C,IAAM,IAAO,GACP,IAAkB,CAAC,GAAG,IAAI,IAAI,EAAK,KAAI,MAAK,EAAE,kBAAkB,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,GACpF,IAAU,CAAC,GAAG,IAAI,IAAI,EAAK,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;GAE1E,OAAO;IACL;IACA,iBAAiB,EAAgB,SAAS,KAAK,EAAgB,OAAO,cAAc,IAAkB,KAAA;IACtG;GACF;EACF;EAEA,OAAO;CACT,GAAG,CAAC,CAAI,CAAC,GAIH,IAAS,QAAc;EAC3B,IAAI,CAAC,GAAe,OAAO;EAE3B,IAAM,EAAE,SAAM,oBAAiB,eAAY,GAGrC,IAAW,KAAmB,CAAC,WAAW,GAG1C,oBAAS,IAAI,IAAgC;EACnD,KAAK,IAAM,KAAO,GAEhB,EAAO,IAAI,GADK,EAAI,kBAAkB,YAChB,GAAG,EAAI,UAAU,CAAG;EAI5C,OAAO,EAAS,KAAK,MAAoB;GACvC,IAAM,IAAc,EAAQ,KAAI,MAClB,EAAO,IAAI,GAAG,EAAQ,GAAG,GAC9B,KAAO,IACf;GAMD,OAAO;IACL,QAAQ;IACR,YALc,EAAY,EACT,EAAS,cAAc;IAKxC,SAAS;GACX;EACF,CAAC;CACH,GAAG,CAAC,CAAa,CAAC,GAGZ,KACJ,GACA,GACA,GACA,MACG;EACH,IAAI,CAAC,GAAK;EAEV,IAAM,IAAO,EAAM,cAAc,sBAAsB;EACvD,EAAW;GACT;GACA;GACA,YAAY,EAAI;GAChB,eAAe,EAAI;GACnB,eAAe,EAAI;GACnB,GAAG,EAAK,OAAO,EAAK,QAAQ;GAC5B,GAAG,EAAK;EACV,CAAC;CACH,GAEM,UAAyB;EAC7B,EAAW,IAAI;CACjB;CAGA,IAAI,CAAC,KAAS,MAAM,QAAQ,CAAI,KAAK,EAAK,WAAW,GACnD,OACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO,EAAE,UAAO;EAEhB,UAAA,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAuC,UAAA,EAAE,sBAAsB;GAAO,CAAA,GACrF,kBAAC,OAAD;IAAK,WAAU;IACZ,UAAA,EAAE,oCAAoC;GACpC,CAAA,CACF;;CACF,CAAA;CAIT,IAAI,CAAC,KAAU,EAAO,WAAW,GAC/B,OACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO,EAAE,UAAO;EAEhB,UAAA,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAuC,UAAA,EAAE,8BAA8B;GAAO,CAAA,GAC7F,kBAAC,OAAD;IAAK,WAAU;IACZ,UAAA,EAAE,mCAAmC;GACnC,CAAA,CACF;;CACF,CAAA;CAIT,IAAM,IAAU,GAAe,WAAW,CAAC,GACrC,IAAa,GAAe,cAAc;CAEhD,OACE,kBAAC,OAAD;EAAK,WAAU;EAAmD,OAAO,EAAE,UAAO;EAAlF,UAAA;GAEE,kBAAC,SAAD;IAAO,WAAU;IAAjB,UAAA,CACE,kBAAC,SAAD;KAAO,WAAU;KACf,UAAA,kBAAC,MAAD,EAAA,UAAA;MACE,kBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,gCAAgC;MACjC,CAAA;MACJ,kBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,+BAA+B;MAChC,CAAA;MACH,EAAQ,KAAI,MACX,kBAAC,MAAD;OAEE,WAAU;OAFZ,UAAA,CAGC,KACG,CACA;MAJG,GAAA,CAIH,CACL;KACC,EAAA,CAAA;IACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAO,KAAK,GAAK,MAChB,kBAAC,MAAD;KAAqB,WAAW,IAAW,KAAM,IAAI,aAAa;KAAlE,UAAA;MACE,kBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAmB,EAAI,MAAM;MAC5B,CAAA;MACJ,kBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAI,WAAW,eAAe;MAC7B,CAAA;MACH,EAAI,QAAQ,KAAK,GAAM,MAAgB;OACtC,IAAM,IAAS,EAAQ,IACjB,IAAO,GAAM,iBAAiB,GAC9B,IAAU,IAAO,EAAkB,CAAI,IAAI,eAC3C,IAAY,IAAO,EAAa,CAAI,IAAI;OAE9C,OACE,kBAAC,MAAD;QAEE,WAAU;QACV,OAAO;SAAE,iBAAiB;SAAS,OAAO;QAAU;QACpD,eAAe,MAAM,EAAiB,GAAG,EAAI,QAAQ,GAAQ,CAAI;QACjE,cAAc;QAEb,UAAA,IAAO,EAAiB,CAAI,IAAI;OAC/B,GAPG,CAOH;MAER,CAAC;KACC;IAzBK,GAAA,EAAI,MAyBT,CACL,EACI,CAAA,CACF;;GAGN,KACC,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;KACd,kBAAC,OAAD;MAAK,WAAU;MACZ,UAAA;OAAC;OAAG;OAAK;OAAK;OAAK;OAAK;MAAC,CAAC,CAAC,KAAI,MAC9B,kBAAC,OAAD;OAEE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAkB,CAAI,EAAE;MACnD,GAHM,CAGN,CACF;KACE,CAAA;KACL,kBAAC,QAAD,EAAA,UAAM,OAAU,CAAA;IACb;;GAIN,KACC,kBAAC,OAAD;IACE,WAAU;IACV,OAAO;KACL,MAAM,EAAQ;KACd,KAAK,EAAQ,IAAI;KACjB,WAAW;IACb;IANF,UAAA,CAQE,kBAAC,OAAD;KAAK,WAAU;KACZ,UAAA,EAAE,uCAAuC;MACxC,QAAQ,EAAmB,EAAQ,MAAM;MACzC,QAAQ,EAAQ;KAClB,CAAC;IACE,CAAA,GACL,kBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,kBAAC,OAAD,EAAA,UAAM,EAAE,sCAAsC,EAAE,OAAO,EAAQ,WAAW,eAAe,EAAE,CAAC,EAAO,CAAA;MACnG,kBAAC,OAAD,EAAA,UAAM,EAAE,oCAAoC,EAAE,OAAO,EAAQ,cAAc,eAAe,EAAE,CAAC,EAAO,CAAA;MACpG,kBAAC,OAAD;OAAK,WAAU;OACZ,UAAA,EAAE,gCAAgC,EAAE,MAAM,EAAiB,EAAQ,aAAa,EAAE,CAAC;MACjF,CAAA;KACF;IACF,CAAA,CAAA;;EAEJ;;AAET,CAAC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { A as e, C as t, _ as n, gt as r, l as i, u as a, y as o } from "./chart-data-table-
|
|
2
|
-
import { T as s, j as c, o as l, s as u, v as d, x as f } from "./DashboardEditModal-
|
|
1
|
+
import { A as e, C as t, _ as n, gt as r, l as i, u as a, y as o } from "./chart-data-table-Cou9qOab.js";
|
|
2
|
+
import { T as s, j as c, o as l, s as u, v as d, x as f } from "./DashboardEditModal-BliiIHcI.js";
|
|
3
3
|
import { i as p, n as m, o as h, r as g, s as _ } from "./retention-ChW9jYdy.js";
|
|
4
|
-
import { B as v, F as y } from "./chart-activity-grid-
|
|
5
|
-
import { r as b, t as x } from "./useExplainAI-
|
|
6
|
-
import { a as S, t as C } from "./charts-loader-
|
|
7
|
-
import { F as w, M as T, N as E, S as D, c as O, n as k, p as A, t as j, u as M } from "./analysis-builder-shared-
|
|
8
|
-
import { n as N } from "./charts-core-
|
|
4
|
+
import { B as v, F as y } from "./chart-activity-grid-CJzxC18G.js";
|
|
5
|
+
import { r as b, t as x } from "./useExplainAI-MuC9jBn-.js";
|
|
6
|
+
import { a as S, t as C } from "./charts-loader-tCSbqT7H.js";
|
|
7
|
+
import { F as w, M as T, N as E, S as D, c as O, n as k, p as A, t as j, u as M } from "./analysis-builder-shared-dqcpl_WL.js";
|
|
8
|
+
import { n as N } from "./charts-core-DnirPG1F.js";
|
|
9
9
|
import P, { Component as F, forwardRef as I, memo as L, useCallback as R, useEffect as z, useImperativeHandle as ee, useMemo as B, useRef as V, useState as H, useSyncExternalStore as te } from "react";
|
|
10
10
|
import { Fragment as U, jsx as W, jsxs as G } from "react/jsx-runtime";
|
|
11
11
|
//#region src/client/shared/components/QueryAnalysisPanel.sections.tsx
|
|
@@ -671,7 +671,7 @@ function We(e, t, n) {
|
|
|
671
671
|
}
|
|
672
672
|
//#endregion
|
|
673
673
|
//#region src/client/components/AnalysisBuilder/AnalysisResultsPanel.tsx
|
|
674
|
-
var Ge = P.lazy(() => import("./schema-visualization-
|
|
674
|
+
var Ge = P.lazy(() => import("./schema-visualization-o1zzuMQy.js").then((e) => e.n).then((e) => ({ default: e.SchemaVisualizationLazy }))), Ke = L(function({ executionStatus: t, executionResults: n, executionError: r, totalRowCount: i, resultsStale: a = !1, chartType: s = "line", chartConfig: u = {}, displayConfig: d = {}, colorPalette: f, currentPaletteName: p, onColorPaletteChange: m, allQueries: h, funnelExecutedQueries: g, activeView: _ = "chart", onActiveViewChange: v, displayLimit: w = 100, onDisplayLimitChange: T, chartAvailability: D, debugDataPerQuery: O = [], onShareClick: k, canShare: A = !1, shareButtonState: j = "idle", onRefreshClick: M, canRefresh: N = !1, isRefreshing: F = !1, needsRefresh: I = !1, onClearClick: L, canClear: ee = !1, enableAI: te = !1, isAIOpen: ne = !1, onAIToggle: re, queryCount: ie = 1, perQueryResults: ae, activeTableIndex: oe = 0, onActiveTableChange: se, analysisType: K, isFunnelMode: ce = !1, funnelServerQuery: le, funnelDebugData: ue, flowServerQuery: de, flowDebugData: fe, retentionServerQuery: pe, retentionDebugData: me, retentionChartData: q, retentionValidation: ge, warnings: _e, highlightedFields: ve, onSchemaFieldClick: ye }) {
|
|
675
675
|
let { t: J } = y(), Y = K === "funnel" || ce, X = K === "flow", Z = K === "retention", { features: be } = e(), [xe, Se] = H(!1), [Ce, we] = H(!1), [Q, Ee] = H(0), [De, Oe] = H(!1), [ke, Ae] = H(!1), [je, Me] = H(!1), [Ne, Pe] = H("idle");
|
|
676
676
|
z(() => {
|
|
677
677
|
let e = (e) => {
|
|
@@ -5411,4 +5411,4 @@ vr.displayName = "AnalysisBuilder";
|
|
|
5411
5411
|
//#endregion
|
|
5412
5412
|
export { vr as default };
|
|
5413
5413
|
|
|
5414
|
-
//# sourceMappingURL=analysis-builder-
|
|
5414
|
+
//# sourceMappingURL=analysis-builder-C0gJ3kDD.js.map
|