diff --git a/frontend/app/components/Charts/SankeyChart.tsx b/frontend/app/components/Charts/SankeyChart.tsx index aac036ad8..15dda871e 100644 --- a/frontend/app/components/Charts/SankeyChart.tsx +++ b/frontend/app/components/Charts/SankeyChart.tsx @@ -4,6 +4,7 @@ import { SankeyChart } from 'echarts/charts'; import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils'; import { NoContent } from 'App/components/ui'; import {InfoCircleOutlined} from '@ant-design/icons'; +import { X } from 'lucide-react'; echarts.use([SankeyChart]); interface SankeyNode { @@ -30,10 +31,11 @@ interface Props { data: Data; height?: number; onChartClick?: (filters: any[]) => void; + isUngrouped?: boolean; } const EChartsSankey: React.FC = (props) => { - const { data, height = 240, onChartClick } = props; + const { data, height = 240, onChartClick, isUngrouped } = props; const chartRef = React.useRef(null); if (data.nodes.length === 0 || data.links.length === 0) { @@ -64,7 +66,6 @@ const EChartsSankey: React.FC = (props) => { const targetNode = data.nodes.find((n) => n.id === l.target); return (sourceNode?.depth ?? 0) <= maxDepth && (targetNode?.depth ?? 0) <= maxDepth; }); - const nodeValues = new Array(filteredNodes.length).fill(0); @@ -74,11 +75,11 @@ const EChartsSankey: React.FC = (props) => { .map((n) => { let computedName = getNodeName(n.eventType || 'Minor Paths', n.name); if (computedName === 'Other') { - computedName = 'Minor Paths'; + computedName = 'Others'; } const itemColor = - computedName === 'Minor Paths' - ? '#222F99' + computedName === 'Others' + ? 'rgba(34,44,154,.9)' : n.eventType === 'DROP' ? '#B5B7C8' : '#394eff'; @@ -98,9 +99,9 @@ const EChartsSankey: React.FC = (props) => { return (a.depth as number) - (b.depth as number); } }); - console.log('EChart Nodes:', echartNodes); - + const distinctSteps = new Set(data.nodes.map(node => node.depth)).size; + console.log('Number of steps returned by the backend:', distinctSteps); const echartLinks = filteredLinks.map((l) => ({ source: echartNodes.findIndex((n) => n.id === l.source), @@ -144,22 +145,45 @@ const EChartsSankey: React.FC = (props) => { }, }, label: { - formatter: '{b} - {c}', + show: true, + position: 'top', + textShadowColor: "transparent", + textBorderColor: "transparent", + align: 'left', + overflow: "truncate", + distance: 3, + offset: [-20, 0], + formatter: function(params: any) { + const totalSessions = nodeValues.reduce((sum: number, v: number) => sum + v, 0); + const percentage = totalSessions ? ((params.value / totalSessions) * 100).toFixed(1) + '%' : '0%'; + return `{header|${params.name}}\n{body|${percentage} ${params.value} Sessions}`; + }, + rich: { + header: { + fontWeight: 'bold', + fontSize: 12, + color: '#333' + }, + body: { + fontSize: 12, + color: '#666' + } + } }, tooltip: { formatter: sankeyTooltip(echartNodes, nodeValues), }, - nodeAlign: 'jusitfy', + nodeAlign: 'left', nodeWidth: 40, - nodeGap: 8, + nodeGap: 40, lineStyle: { color: 'source', - curveness: 0.2, + curveness: 0.6, opacity: 0.1, }, itemStyle: { color: '#394eff', - borderRadius: 2, + borderRadius: 7, }, }, ], @@ -279,7 +303,11 @@ const EChartsSankey: React.FC = (props) => { }; }, [data, height, onChartClick]); - return
; + const containerStyle: React.CSSProperties = isUngrouped + ? {width: '100%', minHeight: 500, height: '100%', overflowY: 'auto' } + : { width: '100%', height }; + + return
; }; export default EChartsSankey; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 9a0fa5efd..aae0af7a4 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -513,14 +513,16 @@ function WidgetChart(props: Props) { } if (metricType === USER_PATH && data && data.links) { - // const usedData = _metric.hideExcess ? filterMinorPaths(data) : data; + const isUngrouped = props.isPreview ? (!(_metric.hideExcess ?? true)) : false; + const height = props.isPreview ? 550 : 500; return ( { dashboardStore.drillDownFilter.merge({ filters, page: 1 }); }} + isUngrouped={isUngrouped} /> ); } diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx index be35a51c4..231d1ccec 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx @@ -193,17 +193,17 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => { { value: 'location', label: 'Pages' }, { value: 'click', label: 'Clicks' }, { value: 'input', label: 'Input' }, - { value: 'custom', label: 'Custom' }, + { value: 'custom', label: 'Custom Events' }, ]; return ( - +
+
- User journeys with: - -
+ Journeys with: +
{ onChange={(value) => writeOption({ name: 'metricValue', value })} placeholder="Select Metrics" size="small" + maxTagCount={'responsive'} + showSearch={false} />
-
- + +
+ { + metric.startType === 'start' + ? 'Start Point' + : 'End Point' + } { onRemoveFilter={() => {}} /> +
diff --git a/frontend/app/components/Dashboard/components/WidgetOptions.tsx b/frontend/app/components/Dashboard/components/WidgetOptions.tsx index 8d45d4ff3..b278f8723 100644 --- a/frontend/app/components/Dashboard/components/WidgetOptions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetOptions.tsx @@ -45,7 +45,7 @@ function WidgetOptions() { onClick={(e) => { e.preventDefault(); metric.update({ hideExcess: !metric.hideExcess }); - metric.updateKey('hasChanged', true); + // metric.updateKey('hasChanged', true); }} > diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index e8ebd3913..84d66952d 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -171,7 +171,7 @@ function WidgetView(props: Props) { { value: 'flex-row', icon: ( - + ) @@ -179,7 +179,7 @@ function WidgetView(props: Props) { { value: 'flex-col', icon: ( - + ) @@ -187,7 +187,7 @@ function WidgetView(props: Props) { { value: 'flex-row-reverse', icon: ( - +
diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx index 550cd3f87..1deb6fbd4 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx @@ -3,7 +3,7 @@ import cn from 'classnames'; import WidgetName from 'Components/Dashboard/components/WidgetName'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; - +import { USER_PATH } from 'App/constants/card'; import { Button, Space, Tooltip } from 'antd'; import CardViewMenu from 'Components/Dashboard/components/WidgetView/CardViewMenu'; import { Link2 } from 'lucide-react' @@ -14,16 +14,20 @@ interface Props { onSave: () => void; undoChanges: () => void; layoutControl?: React.ReactNode; + isPreview?: boolean; } const defaultText = 'Copy link to clipboard' -function WidgetViewHeader({ onClick, onSave, layoutControl }: Props) { +function WidgetViewHeader({ onClick, onSave, layoutControl, isPreview }: Props) { const [tooltipText, setTooltipText] = React.useState(defaultText); const { metricStore } = useStore(); const widget = metricStore.instance; const handleSave = () => { + if (!isPreview && widget.metricType === USER_PATH) { + widget.hideExcess = true; // Force grouped view + } onSave(); }; @@ -36,7 +40,7 @@ function WidgetViewHeader({ onClick, onSave, layoutControl }: Props) { return (
diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx index efd2e0127..ffe8064a4 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx @@ -1,5 +1,6 @@ import React, { useRef, useState, useEffect } from 'react'; import { Button, Checkbox, Input, Tooltip } from 'antd'; +import { RedoOutlined } from '@ant-design/icons'; import cn from 'classnames'; import { Loader } from 'UI'; import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv'; @@ -85,6 +86,10 @@ export function AutocompleteModal({ onApply(vals); }; + const clearSelection = () => { + setSelectedValues([]); + }; + const sortedOptions = React.useMemo(() => { if (values[0] && values[0].length) { const sorted = options.sort((a, b) => { @@ -128,6 +133,7 @@ export function AutocompleteModal({ onChange={(e) => handleInputChange(e.target.value)} placeholder={placeholder} className="rounded-lg" + autoFocus /> <> @@ -163,13 +169,23 @@ export function AutocompleteModal({ ) : null} -
- - + + +
+ + + + +
); diff --git a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx index 0b6fce772..3c764b8da 100644 --- a/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx +++ b/frontend/app/components/shared/Filters/FilterModal/FilterModal.tsx @@ -240,6 +240,7 @@ function FilterModal(props: Props) { placeholder={'Search'} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + autoFocus />