diff --git a/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx b/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx index 112aa86cf..08474e55e 100644 --- a/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx +++ b/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx @@ -1,37 +1,45 @@ -import React from 'react' -import type { Data } from '../SankeyChart' +import React from 'react'; +import type { Data } from '../SankeyChart'; -function DroppedSessionsList({ data, colorMap }: { data: Data, colorMap: Map }) { +function DroppedSessionsList({ + data, + colorMap, +}: { + data: Data; + colorMap: Map; +}) { const dropsByUrl: Record = {}; - data.links.forEach(link => { - const targetNode = data.nodes.find(node => node.id === link.target) - const sourceNode = data.nodes.find(node => node.id === link.source) + data.links.forEach((link) => { + const targetNode = data.nodes.find((node) => node.id === link.target); + const sourceNode = data.nodes.find((node) => node.id === link.source); if (!targetNode || !sourceNode) return; - const isDrop = targetNode.eventType === 'DROP' + const isDrop = targetNode.eventType === 'DROP'; if (!isDrop) return; const sourceUrl = sourceNode.name; - + console.log(link, sourceUrl, dropsByUrl); if (sourceUrl) { dropsByUrl[sourceUrl] = (dropsByUrl[sourceUrl] || 0) + link.sessionsCount; } }); - const totalDropSessions = Object.values(dropsByUrl).reduce((sum, count) => sum + count, 0); + const totalDropSessions = Object.values(dropsByUrl).reduce( + (sum, count) => sum + count, + 0, + ); const sortedDrops = Object.entries(dropsByUrl) .map(([url, count]) => ({ url, count, - percentage: Math.round((count / totalDropSessions) * 100) + percentage: Math.round((count / totalDropSessions) * 100), })) .sort((a, b) => b.count - a.count); - return ( -
-

Droppe Sessions by Page

+
+

Sessions Drop by Page

{sortedDrops.length > 0 ? ( sortedDrops.map((item, index) => ( @@ -39,7 +47,17 @@ function DroppedSessionsList({ data, colorMap }: { data: Data, colorMap: Map -
+
{item.url} {item.count} ({item.percentage}%) @@ -51,6 +69,6 @@ function DroppedSessionsList({ data, colorMap }: { data: Data, colorMap: Map
); -}; +} export default DroppedSessionsList; diff --git a/frontend/app/components/Charts/SunburstChart/Sunburst.tsx b/frontend/app/components/Charts/SunburstChart/Sunburst.tsx index e6b3a624c..e7df0f4c9 100644 --- a/frontend/app/components/Charts/SunburstChart/Sunburst.tsx +++ b/frontend/app/components/Charts/SunburstChart/Sunburst.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { SunburstChart } from 'echarts/charts'; -// import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils'; import { echarts, defaultOptions } from '../init'; -import type { Data } from '../SankeyChart' +import type { Data } from '../SankeyChart'; import DroppedSessionsList from './DroppedSessions'; import { convertSankeyToSunburst, sunburstTooltip } from './sunburstUtils'; @@ -14,22 +13,28 @@ interface Props { } const EChartsSunburst = (props: Props) => { - const { data, height = 240, } = props; + const { data, height = 240 } = props; const chartRef = React.useRef(null); - const [colors, setColors] = React.useState>(new Map()) + const [colors, setColors] = React.useState>(new Map()); React.useEffect(() => { - if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return; + if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) + return; - const chart = echarts.init(chartRef.current) + const chart = echarts.init(chartRef.current); const { tree, colors } = convertSankeyToSunburst(data); - - tree.value = 100; + const singleRoot = + data.nodes.reduce((acc, node) => { + if (node.depth === 0) { + acc++; + } + return acc; + }, 0) === 1; const options = { ...defaultOptions, series: { type: 'sunburst', - data: tree.children, + data: singleRoot ? tree.children : [tree], radius: [30, '90%'], itemStyle: { borderRadius: 6, @@ -41,36 +46,45 @@ const EChartsSunburst = (props: Props) => { show: false, }, tooltip: { - formatter: sunburstTooltip(colors) - } + formatter: sunburstTooltip(colors), + }, }, - } - chart.setOption(options) + }; + chart.setOption(options); const ro = new ResizeObserver(() => chart.resize()); ro.observe(chartRef.current); setColors(colors); return () => { chart.dispose(); ro.disconnect(); - } - }, [data, height]) + }; + }, [data, height]); const containerStyle = { width: '100%', height, + flex: 1, }; - return
-
- -
; -} + return ( +
+
+ +
+ ); +}; export default EChartsSunburst; diff --git a/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts b/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts index 146abaf3d..ca6ca99e7 100644 --- a/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts +++ b/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts @@ -1,38 +1,64 @@ -import { colors } from '../utils' -import type { Data } from '../SankeyChart' -import { toJS } from 'mobx' +import { colors } from '../utils'; +import type { Data } from '../SankeyChart'; export interface SunburstChild { name: string; value: number; - children?: SunburstChild[] + children?: SunburstChild[]; itemStyle?: any; + dataIndex: number; } const colorMap = new Map(); -export function convertSankeyToSunburst(data: Data): { tree: SunburstChild, colors: Map } { - const nodesCopy: any = data.nodes.map(node => ({ +export function convertSankeyToSunburst(data: Data): { + tree: SunburstChild; + colors: Map; +} { + const dataLinks = data.links.filter((link) => { + const sourceNode = data.nodes.find((node) => node.id === link.source); + const targetNode = data.nodes.find((node) => node.id === link.target); + return ( + sourceNode && + targetNode && + ![sourceNode.eventType, targetNode.eventType].includes('OTHER') + ); + }); + const dataNodes = data.nodes.filter((node) => node.eventType !== 'OTHER'); + + const nodesCopy: any = dataNodes.map((node) => ({ ...node, children: [], childrenIds: new Set(), - value: 0 + value: 0, })); - const nodesById: Record = {}; + const nodesById: Record = {}; nodesCopy.forEach((node) => { nodesById[node.id as number] = node; }); - data.links.forEach(link => { + dataLinks.forEach((link) => { const sourceNode = nodesById[link.source as number]; const targetNode = nodesById[link.target as number]; + if (link.source === 0) { + if (sourceNode.value) { + sourceNode.value += link.sessionsCount; + } else { + sourceNode.value = link.sessionsCount; + } + } if (sourceNode && targetNode) { - if ((targetNode.depth) === (sourceNode.depth) + 1 && !sourceNode.childrenIds.has(targetNode.id)) { - const specificId = `${link.source}${link.target}` + if ( + targetNode.depth === sourceNode.depth + 1 && + !sourceNode.childrenIds.has(targetNode.id) + ) { + const specificId = `${link.source}_${link.target}`; const fakeNode = { - ...targetNode, id: specificId, value: link.sessionsCount - } + ...targetNode, + id: specificId, + value: link.sessionsCount, + }; sourceNode.children.push(fakeNode); sourceNode.childrenIds.add(specificId); } @@ -40,38 +66,54 @@ export function convertSankeyToSunburst(data: Data): { tree: SunburstChild, colo }); const rootNode = nodesById[0]; - + const nameCount: Record = {}; + let dataIndex = 0; function buildSunburstNode(node: SunburstChild): SunburstChild | null { if (!node) return null; + // eventType === DROP if (!node.name) { - // eventType = DROP - return null + // node.name = `DROP` + // colorMap.set('DROP', 'black') + return null; } - let color = colorMap.get(node.name) + let color = colorMap.get(node.name); if (!color) { - color = randomColor(colorMap.size) - colorMap.set(node.name, color) + color = randomColor(colorMap.size); + colorMap.set(node.name, color); + } + let nodeName; + if (nameCount[node.name]) { + nodeName = `${node.name}_$$$_${nameCount[node.name]++}`; + } else { + nodeName = node.name; + nameCount[node.name] = 1; } const result: SunburstChild = { - name: node.name, + name: nodeName, value: node.value || 0, + dataIndex: dataIndex++, itemStyle: { color, - } + }, }; if (node.children && node.children.length > 0) { - result.children = node.children.map(child => buildSunburstNode(child)).filter(Boolean) as SunburstChild[]; + result.children = node.children + .map((child) => buildSunburstNode(child)) + .filter(Boolean) as SunburstChild[]; } return result; } - return { tree: buildSunburstNode(rootNode) as SunburstChild, colors: colorMap } + return { + tree: buildSunburstNode(rootNode) as SunburstChild, + colors: colorMap, + }; } function randomColor(mapSize: number) { - const pointer = mapSize + const pointer = mapSize; if (pointer > colors.length) { colors.push(`#${Math.floor(Math.random() * 16777215).toString(16)}`); } @@ -82,15 +124,18 @@ export function sunburstTooltip(colorMap: Map) { return (params: any) => { if ('name' in params.data) { const color = colorMap.get(params.data.name); + const clearName = params.data.name + ? params.data.name.split('_$$$_')[0] + : 'Total'; return `
■︎ - ${params.data.name} + ${clearName}
${params.value} Sessions
`; } - } + }; } diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index fb9aedfc4..811a3c0ba 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -525,13 +525,13 @@ function WidgetChart(props: Props) { } if (metricType === USER_PATH && data && data.links) { - const isUngrouped = props.isPreview - ? !(_metric.hideExcess ?? true) - : false; - const height = props.isPreview ? 550 : 240; - return ( -
- - { - dashboardStore.drillDownFilter.merge({ filters, page: 1 }); - }} - isUngrouped={isUngrouped} - /> -
- ) + ); + } + if (viewType === 'chart') { + const isUngrouped = props.isPreview + ? !(_metric.hideExcess ?? true) + : false; + const height = props.isPreview ? 550 : 240; + return ( + { + dashboardStore.drillDownFilter.merge({ filters, page: 1 }); + }} + isUngrouped={isUngrouped} + /> + ) + } } if (metricType === RETENTION) { diff --git a/frontend/app/components/Dashboard/components/WidgetOptions.tsx b/frontend/app/components/Dashboard/components/WidgetOptions.tsx index 18f489615..6bcc5c277 100644 --- a/frontend/app/components/Dashboard/components/WidgetOptions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetOptions.tsx @@ -6,7 +6,7 @@ import { TIMESERIES, USER_PATH, } from 'App/constants/card'; -import { Select, Space, Switch, Dropdown, Button } from 'antd'; +import { Space, Switch, Dropdown, Button } from 'antd'; import { DownOutlined } from '@ant-design/icons'; import { useStore } from 'App/mstore'; import ClickMapRagePicker from 'Components/Dashboard/components/ClickMapRagePicker/ClickMapRagePicker'; @@ -24,6 +24,8 @@ import { Library, ChartColumnBig, ChartBarBig, + Split, + CircleDashed, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -38,7 +40,9 @@ function WidgetOptions() { }; // const hasSeriesTypes = [TIMESERIES, FUNNEL, TABLE].includes(metric.metricType); - const hasViewTypes = [TIMESERIES, FUNNEL].includes(metric.metricType); + const hasViewTypes = [TIMESERIES, FUNNEL, USER_PATH].includes( + metric.metricType, + ); return (
{metric.metricType === USER_PATH && ( @@ -154,19 +158,34 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { metric: 'Metric', table: 'Table', }; - const usedChartTypes = - metric.metricType === FUNNEL ? funnelChartTypes : chartTypes; + const pathTypes = { + chart: 'Flow Chart', + sunburst: 'Sunburst', + }; + + const usedChartTypes = { + [FUNNEL]: funnelChartTypes, + [TIMESERIES]: chartTypes, + [USER_PATH]: pathTypes, + }; const chartIcons = { - lineChart: , - barChart: , - areaChart: , - pieChart: , - progressChart: , - metric: , - table: , - // funnel specific - columnChart: , - chart: , + [TIMESERIES]: { + lineChart: , + barChart: , + areaChart: , + pieChart: , + progressChart: , + metric: , + table:
, + }, + [FUNNEL]: { + columnChart: , + chart: , + }, + [USER_PATH]: { + chart: , + sunburst: , + }, }; const allowedTypes = { [TIMESERIES]: [ @@ -179,18 +198,21 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { 'table', ], [FUNNEL]: ['chart', 'columnChart', 'metric', 'table'], + [USER_PATH]: ['chart', 'sunburst'], }; + const metricType = metric.metricType; + const viewType = metric.viewType; return ( ({ + items: allowedTypes[metricType].map((key) => ({ key, label: (
- {chartIcons[key]} -
{usedChartTypes[key]}
+ {chartIcons[metricType][key]} +
{usedChartTypes[metricType][key]}
), })), @@ -207,8 +229,8 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => { className="btn-visualization-type" > - {chartIcons[metric.viewType]} -
{usedChartTypes[metric.viewType]}
+ {chartIcons[metricType][viewType]} +
{usedChartTypes[metricType][viewType]}