Product analytics refinements (#3006)
* Various UX, UI and Functional Improvements in Dashboards & Cards - Depth filter of Sankey chart data in frontend - Dashboard & Cards empty state view updates - Disabled save image feature on cards * Fixed empty views and headers * Various improvements across dashboards and cards. * Dashboard and Sankey refinements.
This commit is contained in:
parent
cb8d87e367
commit
f88ff53e15
8 changed files with 97 additions and 44 deletions
|
|
@ -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> = (props) => {
|
||||
const { data, height = 240, onChartClick } = props;
|
||||
const { data, height = 240, onChartClick, isUngrouped } = props;
|
||||
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
|
|
@ -64,7 +66,6 @@ const EChartsSankey: React.FC<Props> = (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> = (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> = (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> = (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> = (props) => {
|
|||
};
|
||||
}, [data, height, onChartClick]);
|
||||
|
||||
return <div ref={chartRef} style={{ width: '100%', height }} />;
|
||||
const containerStyle: React.CSSProperties = isUngrouped
|
||||
? {width: '100%', minHeight: 500, height: '100%', overflowY: 'auto' }
|
||||
: { width: '100%', height };
|
||||
|
||||
return <div ref={chartRef} style={containerStyle} />;
|
||||
};
|
||||
|
||||
export default EChartsSankey;
|
||||
|
|
@ -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 (
|
||||
<SankeyChart
|
||||
height={props.isPreview ? 500 : 240}
|
||||
height={height}
|
||||
data={data}
|
||||
onChartClick={(filters: any) => {
|
||||
dashboardStore.drillDownFilter.merge({ filters, page: 1 });
|
||||
}}
|
||||
isUngrouped={isUngrouped}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card styles={{ body: { padding: '20px 20px' } }} className="rounded-lg">
|
||||
<Form.Item>
|
||||
<div className='flex flex-col lg:flex-row lg:items-center gap-6 flex-wrap'>
|
||||
<Form.Item className='mb-0 flex-1'>
|
||||
<div className="flex flex-wrap gap-2 items-center justify-start">
|
||||
<span className="font-medium">User journeys with: </span>
|
||||
|
||||
<div className="flex sm:flex-wrap lg:flex-nowrap gap-2 items-start">
|
||||
<span className="font-medium">Journeys with: </span>
|
||||
<div className="flex gap-2 items-start">
|
||||
<Select
|
||||
className="w-36 rounded-xl"
|
||||
className="w-36 rounded-lg"
|
||||
name="startType"
|
||||
options={[
|
||||
{ value: 'start', label: 'Start Point' },
|
||||
|
|
@ -219,7 +219,8 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
|
||||
<Select
|
||||
mode="multiple"
|
||||
className="min-w-36 rounded-xl"
|
||||
className="rounded-lg w-max min-w-44 max-w-58"
|
||||
// style={{ width: 'auto', minWidth: '9rem', maxWidth: '12rem' }}
|
||||
allowClear
|
||||
name="metricValue"
|
||||
options={metricValueOptions}
|
||||
|
|
@ -227,19 +228,19 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
onChange={(value) => writeOption({ name: 'metricValue', value })}
|
||||
placeholder="Select Metrics"
|
||||
size="small"
|
||||
maxTagCount={'responsive'}
|
||||
showSearch={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<div className="flex items-center">
|
||||
<Form.Item
|
||||
label={
|
||||
metric.startType === 'start'
|
||||
? 'Specify Start Point'
|
||||
: 'Specify End Point'
|
||||
}
|
||||
className="m0-0 font-medium p-0 h-fit"
|
||||
>
|
||||
<Form.Item className='mb-0 flex-1'>
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
<span className="font-medium mr-2">{
|
||||
metric.startType === 'start'
|
||||
? 'Start Point'
|
||||
: 'End Point'
|
||||
}</span>
|
||||
<span className="font-normal">
|
||||
<FilterItem
|
||||
hideDelete
|
||||
|
|
@ -254,6 +255,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
|
|||
onRemoveFilter={() => {}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function WidgetOptions() {
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
metric.update({ hideExcess: !metric.hideExcess });
|
||||
metric.updateKey('hasChanged', true);
|
||||
// metric.updateKey('hasChanged', true);
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function WidgetView(props: Props) {
|
|||
{
|
||||
value: 'flex-row',
|
||||
icon: (
|
||||
<Tooltip title="Horizontal Layout">
|
||||
<Tooltip title="Filters on Left">
|
||||
<LayoutPanelLeft size={16} />
|
||||
</Tooltip>
|
||||
)
|
||||
|
|
@ -179,7 +179,7 @@ function WidgetView(props: Props) {
|
|||
{
|
||||
value: 'flex-col',
|
||||
icon: (
|
||||
<Tooltip title="Vertical Layout">
|
||||
<Tooltip title="Filters on Top">
|
||||
<LayoutPanelTop size={16} />
|
||||
</Tooltip>
|
||||
)
|
||||
|
|
@ -187,7 +187,7 @@ function WidgetView(props: Props) {
|
|||
{
|
||||
value: 'flex-row-reverse',
|
||||
icon: (
|
||||
<Tooltip title="Reversed Horizontal Layout">
|
||||
<Tooltip title="Filters on Right">
|
||||
<div className={'rotate-180'}>
|
||||
<LayoutPanelLeft size={16} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between items-center bg-white rounded-lg shadow-sm px-4 ps-2 py-2 border border-gray-lighter input-card-title'
|
||||
'flex justify-between items-center bg-white rounded-lg shadow-sm px-4 ps-2 py-2 border border-gray-lighter input-card-title flex-wrap'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
<Loader loading={isLoading}>
|
||||
<>
|
||||
|
|
@ -163,13 +169,23 @@ export function AutocompleteModal({
|
|||
) : null}
|
||||
</>
|
||||
</Loader>
|
||||
<div className={'flex gap-2 items-center pt-2'}>
|
||||
<Button type={'primary'} onClick={applyValues} className="btn-apply-event-value">
|
||||
Apply
|
||||
</Button>
|
||||
<Button onClick={onClose} className="btn-cancel-event-value">
|
||||
Cancel
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button type="primary" onClick={applyValues} className="btn-apply-event-value">
|
||||
Apply
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} className="btn-cancel-event-value">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tooltip title='Clear all selection'>
|
||||
<Button onClick={clearSelection} type='text' className="btn-clear-selection" disabled={selectedValues.length === 0}>
|
||||
<RedoOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
</OutsideClickDetectingDiv>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ function FilterModal(props: Props) {
|
|||
placeholder={'Search'}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<div className={'flex gap-2 items-start'}>
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue