ui: comparing for funnels, alternate column view, some refactoring to prepare for customizations...
This commit is contained in:
parent
7b0a41b743
commit
130e00748b
16 changed files with 458 additions and 386 deletions
|
|
@ -19,6 +19,7 @@ interface Props {
|
|||
yaxis?: Record<string, any>;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
inGrid?: boolean;
|
||||
}
|
||||
|
||||
function CustomAreaChart(props: Props) {
|
||||
|
|
@ -29,6 +30,7 @@ function CustomAreaChart(props: Props) {
|
|||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
inGrid,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
|
@ -39,7 +41,7 @@ function CustomAreaChart(props: Props) {
|
|||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ interface Props {
|
|||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
inGrid?: boolean;
|
||||
}
|
||||
|
||||
const getPath = (x, y, width, height) => {
|
||||
|
|
@ -75,6 +76,7 @@ function CustomBarChart(props: Props) {
|
|||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
inGrid,
|
||||
} = props;
|
||||
|
||||
const resultChart = data.chart.map((item, i) => {
|
||||
|
|
@ -112,7 +114,7 @@ function CustomBarChart(props: Props) {
|
|||
</pattern>
|
||||
</defs>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface Props {
|
|||
yaxis?: any;
|
||||
label?: string;
|
||||
hideLegend?: boolean;
|
||||
inGrid?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
|
|
@ -39,6 +40,7 @@ function CustomMetricLineChart(props: Props) {
|
|||
yaxis = { ...Styles.yaxis },
|
||||
label = 'Number of Sessions',
|
||||
hideLegend = false,
|
||||
inGrid,
|
||||
} = props;
|
||||
|
||||
const resultChart = data.chart.map((item, i) => {
|
||||
|
|
@ -54,7 +56,7 @@ function CustomMetricLineChart(props: Props) {
|
|||
onClick={onClick}
|
||||
>
|
||||
{!hideLegend && (
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
)}
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ interface Props {
|
|||
};
|
||||
colors: any;
|
||||
onClick?: (filters) => void;
|
||||
inGrid?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricPieChart(props: Props) {
|
||||
const { metric, data, onClick = () => null } = props;
|
||||
const { metric, data, onClick = () => null, inGrid } = props;
|
||||
|
||||
const onClickHandler = (event) => {
|
||||
if (event && !event.payload.group) {
|
||||
|
|
@ -62,7 +63,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<PieChart>
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: -26 }} />
|
||||
<Legend iconType={'circle'} wrapperStyle={{ top: inGrid ? undefined : -18 }} />
|
||||
<Pie
|
||||
isAnimationActive={false}
|
||||
data={values}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useStore } from 'App/mstore';
|
|||
import AreaChart from '../../Widgets/CustomMetricsWidgets/AreaChart';
|
||||
import BarChart from '../../Widgets/CustomMetricsWidgets/BarChart';
|
||||
import ProgressBarChart from '../../Widgets/CustomMetricsWidgets/ProgressBarChart';
|
||||
import BugNumChart from '../../Widgets/CustomMetricsWidgets/BigNumChart'
|
||||
import BugNumChart from '../../Widgets/CustomMetricsWidgets/BigNumChart';
|
||||
import WidgetDatatable from '../WidgetDatatable/WidgetDatatable';
|
||||
import WidgetPredefinedChart from '../WidgetPredefinedChart';
|
||||
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||
|
|
@ -56,7 +56,7 @@ function WidgetChart(props: Props) {
|
|||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const params = { density: dashboardStore.selectedDensity }
|
||||
const params = { density: dashboardStore.selectedDensity };
|
||||
const metricParams = _metric.params;
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
|
|
@ -76,13 +76,15 @@ function WidgetChart(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
if (!data.chart) return;
|
||||
const series = data.chart[0] ? Object.keys(data.chart[0]).filter(
|
||||
(key) => key !== 'time' && key !== 'timestamp'
|
||||
) : []
|
||||
const series = data.chart[0]
|
||||
? Object.keys(data.chart[0]).filter(
|
||||
(key) => key !== 'time' && key !== 'timestamp'
|
||||
)
|
||||
: [];
|
||||
if (series.length) {
|
||||
setEnabledRows(series)
|
||||
setEnabledRows(series);
|
||||
}
|
||||
}, [data.chart])
|
||||
}, [data.chart]);
|
||||
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
|
|
@ -132,7 +134,7 @@ function WidgetChart(props: Props) {
|
|||
.fetchMetricChartData(metric, payload, isSaved, period, isComparison)
|
||||
.then((res: any) => {
|
||||
if (isMounted()) {
|
||||
if (isComparison) setCompData(res)
|
||||
if (isComparison) setCompData(res);
|
||||
else setData(res);
|
||||
}
|
||||
})
|
||||
|
|
@ -169,12 +171,27 @@ function WidgetChart(props: Props) {
|
|||
|
||||
const timestamps = dashboardStore.comparisonPeriod.toTimestamps();
|
||||
// TODO: remove after backend adds support for more view types
|
||||
const payload = { ...params, ...timestamps, ...metric.toJson(), viewType: 'lineChart' };
|
||||
fetchMetricChartData(metric, payload, isSaved, dashboardStore.comparisonPeriod, true);
|
||||
}
|
||||
const payload = {
|
||||
...params,
|
||||
...timestamps,
|
||||
...metric.toJson(),
|
||||
viewType: 'lineChart',
|
||||
};
|
||||
fetchMetricChartData(
|
||||
metric,
|
||||
payload,
|
||||
isSaved,
|
||||
dashboardStore.comparisonPeriod,
|
||||
true
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!dashboardStore.comparisonPeriod) {
|
||||
setCompData(null);
|
||||
return;
|
||||
}
|
||||
loadComparisonData();
|
||||
}, [dashboardStore.comparisonPeriod])
|
||||
}, [dashboardStore.comparisonPeriod]);
|
||||
useEffect(() => {
|
||||
_metric.updateKey('page', 1);
|
||||
loadPage();
|
||||
|
|
@ -202,6 +219,7 @@ function WidgetChart(props: Props) {
|
|||
<FunnelWidget
|
||||
metric={metric}
|
||||
data={data}
|
||||
compData={compData}
|
||||
isWidget={isSaved || isTemplate}
|
||||
/>
|
||||
);
|
||||
|
|
@ -224,10 +242,13 @@ function WidgetChart(props: Props) {
|
|||
|
||||
if (metricType === TIMESERIES) {
|
||||
const chartData = { ...data };
|
||||
chartData.namesMap = Array.isArray(chartData.namesMap) ? chartData.namesMap.map(n => enabledRows.includes(n) ? n : null) : chartData.namesMap
|
||||
chartData.namesMap = Array.isArray(chartData.namesMap)
|
||||
? chartData.namesMap.map((n) => (enabledRows.includes(n) ? n : null))
|
||||
: chartData.namesMap;
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetricLineChart
|
||||
inGrid={!props.isPreview}
|
||||
data={chartData}
|
||||
compData={compData}
|
||||
colors={colors}
|
||||
|
|
@ -245,6 +266,7 @@ function WidgetChart(props: Props) {
|
|||
return (
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
inGrid={!props.isPreview}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
|
|
@ -259,6 +281,7 @@ function WidgetChart(props: Props) {
|
|||
if (viewType === 'barChart') {
|
||||
return (
|
||||
<BarChart
|
||||
inGrid={!props.isPreview}
|
||||
data={chartData}
|
||||
compData={compData}
|
||||
params={params}
|
||||
|
|
@ -275,6 +298,7 @@ function WidgetChart(props: Props) {
|
|||
if (viewType === 'progressChart') {
|
||||
return (
|
||||
<ProgressBarChart
|
||||
inGrid={!props.isPreview}
|
||||
data={chartData}
|
||||
compData={compData}
|
||||
params={params}
|
||||
|
|
@ -291,14 +315,15 @@ function WidgetChart(props: Props) {
|
|||
if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
inGrid={!props.isPreview}
|
||||
metric={metric}
|
||||
data={chartData}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
@ -306,13 +331,14 @@ function WidgetChart(props: Props) {
|
|||
if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
inGrid={!props.isPreview}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
@ -324,16 +350,17 @@ function WidgetChart(props: Props) {
|
|||
return (
|
||||
<BugNumChart
|
||||
data={data}
|
||||
inGrid={!props.isPreview}
|
||||
compData={compData}
|
||||
colors={colors}
|
||||
onClick={onChartClick}
|
||||
label={
|
||||
metric.metricOf === 'sessionCount'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
? 'Number of Sessions'
|
||||
: 'Number of Users'
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,13 +455,27 @@ function WidgetChart(props: Props) {
|
|||
|
||||
return <div>Unknown metric type</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Loader loading={loading} style={{ height: `240px` }}>
|
||||
<div style={{ minHeight: 240 }}>
|
||||
<div
|
||||
style={{
|
||||
minHeight: 240,
|
||||
paddingTop:
|
||||
props.isPreview && metric.metricType === TIMESERIES
|
||||
? '1.5rem'
|
||||
: 0,
|
||||
}}
|
||||
>
|
||||
{renderChart()}
|
||||
{metric.metricType === TIMESERIES ? (
|
||||
<WidgetDatatable defaultOpen={metric.viewType === 'table'} data={data} enabledRows={enabledRows} setEnabledRows={setEnabledRows} />
|
||||
{props.isPreview && metric.metricType === TIMESERIES ? (
|
||||
<WidgetDatatable
|
||||
defaultOpen={metric.viewType === 'table'}
|
||||
data={data}
|
||||
enabledRows={enabledRows}
|
||||
setEnabledRows={setEnabledRows}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ import { observer } from 'mobx-react-lite';
|
|||
import { Space } from 'antd';
|
||||
import RangeGranularity from "./RangeGranularity";
|
||||
|
||||
function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeseries = false }: any) {
|
||||
function WidgetDateRange({
|
||||
viewType = undefined,
|
||||
label = 'Time Range',
|
||||
hasGranularSettings = false,
|
||||
hasGranularity = false,
|
||||
hasComparison = false,
|
||||
}: any) {
|
||||
const { dashboardStore } = useStore();
|
||||
const density = dashboardStore.selectedDensity
|
||||
const onDensityChange = (density: number) => {
|
||||
|
|
@ -25,7 +31,9 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser
|
|||
};
|
||||
|
||||
const onChangeComparison = (period: any) => {
|
||||
console.log(period)
|
||||
dashboardStore.setComparisonPeriod(period);
|
||||
if (!period) return;
|
||||
const periodTimestamps = period.toTimestamps();
|
||||
const compFilter = dashboardStore.cloneCompFilter();
|
||||
compFilter.merge({
|
||||
|
|
@ -34,8 +42,6 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser
|
|||
});
|
||||
}
|
||||
|
||||
const hasGranularity = ['lineChart', 'barChart', 'areaChart'].includes(viewType);
|
||||
const hasCompare = ['lineChart', 'barChart', 'table', 'progressChart'].includes(viewType);
|
||||
return (
|
||||
<Space>
|
||||
{label && <span className="mr-1 color-gray-medium">{label}</span>}
|
||||
|
|
@ -45,7 +51,7 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser
|
|||
isAnt={true}
|
||||
useButtonStyle={true}
|
||||
/>
|
||||
{isTimeseries ? (
|
||||
{hasGranularSettings ? (
|
||||
<>
|
||||
{hasGranularity ? (
|
||||
<RangeGranularity
|
||||
|
|
@ -54,7 +60,7 @@ function WidgetDateRange({ viewType = undefined, label = 'Time Range', isTimeser
|
|||
onDensityChange={onDensityChange}
|
||||
/>
|
||||
) : null}
|
||||
{hasCompare ?
|
||||
{hasComparison ?
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
compPeriod={compPeriod}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import {
|
|||
Hash,
|
||||
Users,
|
||||
Library,
|
||||
ChartColumnBig,
|
||||
ChartBarBig,
|
||||
} from 'lucide-react';
|
||||
|
||||
function WidgetOptions() {
|
||||
|
|
@ -32,6 +34,8 @@ function WidgetOptions() {
|
|||
metric.update({ metricFormat: value });
|
||||
};
|
||||
|
||||
// const hasSeriesTypes = [TIMESERIES, FUNNEL, TABLE].includes(metric.metricType);
|
||||
const hasViewTypes = [TIMESERIES, FUNNEL].includes(metric.metricType);
|
||||
return (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{metric.metricType === USER_PATH && (
|
||||
|
|
@ -50,10 +54,7 @@ function WidgetOptions() {
|
|||
)}
|
||||
|
||||
{metric.metricType === TIMESERIES ? (
|
||||
<>
|
||||
<SeriesTypeOptions metric={metric} />
|
||||
<WidgetViewTypeOptions metric={metric} />
|
||||
</>
|
||||
) : null}
|
||||
{(metric.metricType === FUNNEL || metric.metricType === TABLE) &&
|
||||
metric.metricOf != FilterKey.USERID &&
|
||||
|
|
@ -63,11 +64,12 @@ function WidgetOptions() {
|
|||
onChange={handleChange}
|
||||
variant="borderless"
|
||||
options={[
|
||||
{ value: 'sessionCount', label: 'Sessions' },
|
||||
{ value: 'userCount', label: 'Users' },
|
||||
{ value: 'sessionCount', label: 'All Sessions' },
|
||||
{ value: 'userCount', label: 'Unique Users' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{hasViewTypes ? <WidgetViewTypeOptions metric={metric} /> : null}
|
||||
|
||||
{metric.metricType === HEATMAP ? <ClickMapRagePicker /> : null}
|
||||
</div>
|
||||
|
|
@ -121,6 +123,8 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
|
|||
progressChart: 'Bar',
|
||||
table: 'Table',
|
||||
metric: 'Metric',
|
||||
chart: 'Funnel Bar',
|
||||
columnChart: 'Funnel Column',
|
||||
};
|
||||
const chartIcons = {
|
||||
lineChart: <ChartLine size={16} strokeWidth={1} />,
|
||||
|
|
@ -130,16 +134,23 @@ const WidgetViewTypeOptions = observer(({ metric }: { metric: any }) => {
|
|||
progressChart: <ChartBar size={16} strokeWidth={1} />,
|
||||
table: <Table size={16} strokeWidth={1} />,
|
||||
metric: <Hash size={16} strokeWidth={1} />,
|
||||
// funnel specific
|
||||
columnChart: <ChartColumnBig size={16} strokeWidth={1} />,
|
||||
chart: <ChartBarBig size={16} strokeWidth={1} />,
|
||||
};
|
||||
const allowedTypes = {
|
||||
[TIMESERIES]: ['lineChart', 'barChart', 'areaChart', 'pieChart', 'progressChart', 'table', 'metric',],
|
||||
[FUNNEL]: ['chart', 'columnChart', ] // + table + metric
|
||||
}
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: Object.entries(chartTypes).map(([key, name]) => ({
|
||||
items: allowedTypes[metric.metricType].map((key) => ({
|
||||
key,
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{chartIcons[key]}
|
||||
<div>{name}</div>
|
||||
<div>{chartTypes[key]}</div>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import React from 'react';
|
||||
import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { TIMESERIES } from "App/constants/card";
|
||||
import { FUNNEL, TIMESERIES } from "App/constants/card";
|
||||
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import WidgetOptions from 'Components/Dashboard/components/WidgetOptions';
|
||||
|
|
@ -16,96 +16,26 @@ interface Props {
|
|||
|
||||
function WidgetPreview(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const { metricStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
|
||||
// compare logic
|
||||
const hasGranularSettings = [TIMESERIES, FUNNEL].includes(metric.metricType)
|
||||
const hasGranularity = ['lineChart', 'barChart', 'areaChart'].includes(metric.viewType);
|
||||
const hasComparison = metric.metricType === FUNNEL || ['lineChart', 'barChart', 'table', 'progressChart'].includes(metric.viewType);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-4 pt-2">
|
||||
<WidgetDateRange label="" isTimeseries={metric.metricType === TIMESERIES} viewType={metric.viewType} />
|
||||
<div className="flex items-center ml-auto">
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b">
|
||||
<WidgetDateRange
|
||||
label=""
|
||||
hasGranularSettings={hasGranularSettings}
|
||||
hasGranularity={hasGranularity}
|
||||
hasComparison={hasComparison}
|
||||
/>
|
||||
<div className="ml-auto">
|
||||
<WidgetOptions />
|
||||
{/*{metric.metricType === USER_PATH && (*/}
|
||||
{/* <a*/}
|
||||
{/* href="#"*/}
|
||||
{/* onClick={(e) => {*/}
|
||||
{/* e.preventDefault();*/}
|
||||
{/* metric.update({ hideExcess: !metric.hideExcess });*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* <Space>*/}
|
||||
{/* <Switch checked={metric.hideExcess} size="small" />*/}
|
||||
{/* <span className="mr-4 color-gray-medium">*/}
|
||||
{/* Hide Minor Paths*/}
|
||||
{/* </span>*/}
|
||||
{/* </Space>*/}
|
||||
{/* </a>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/*{isTimeSeries && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={ [*/}
|
||||
{/* { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },*/}
|
||||
{/* { value: 'progress', name: 'Progress', icon: 'hash' },*/}
|
||||
{/* ]}*/}
|
||||
{/* />*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/*{!disableVisualization && isTable && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary={true}*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={[*/}
|
||||
{/* { value: 'table', name: 'Table', icon: 'table' },*/}
|
||||
{/* { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },*/}
|
||||
{/* ]}*/}
|
||||
{/* disabledMessage="Chart view is not supported"*/}
|
||||
{/* />*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/*{isRetention && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary={true}*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={[*/}
|
||||
{/* { value: 'trend', name: 'Trend', icon: 'graph-up-arrow' },*/}
|
||||
{/* { value: 'cohort', name: 'Cohort', icon: 'dice-3' },*/}
|
||||
{/* ]}*/}
|
||||
{/* disabledMessage="Chart view is not supported"*/}
|
||||
{/* />*/}
|
||||
{/*</>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/* add to dashboard */}
|
||||
{/*{metric.exists() && (*/}
|
||||
{/* <AddToDashboardButton metricId={metric.metricId}/>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-0">
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import { useStore } from 'App/mstore';
|
|||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { withSiteId, dashboardMetricDetails } from 'App/routes';
|
||||
import TemplateOverlay from './TemplateOverlay';
|
||||
import AlertButton from './AlertButton';
|
||||
import stl from './widgetWrapper.module.css';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { TIMESERIES } from "App/constants/card";
|
||||
import { TIMESERIES } from 'App/constants/card';
|
||||
|
||||
const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart'));
|
||||
const WidgetChart = lazy(
|
||||
() => import('Components/Dashboard/components/WidgetChart')
|
||||
);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -74,14 +74,20 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
});
|
||||
|
||||
const onDelete = async () => {
|
||||
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId!, widget.widgetId);
|
||||
dashboardStore.deleteDashboardWidget(
|
||||
dashboard?.dashboardId!,
|
||||
widget.widgetId
|
||||
);
|
||||
};
|
||||
|
||||
const onChartClick = () => {
|
||||
if (!isSaved || isPredefined) return;
|
||||
|
||||
props.history.push(
|
||||
withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId)
|
||||
withSiteId(
|
||||
dashboardMetricDetails(dashboard?.dashboardId, widget.metricId),
|
||||
siteId
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -90,7 +96,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
const addOverlay =
|
||||
isTemplate ||
|
||||
(!isPredefined &&
|
||||
isSaved &&
|
||||
isSaved &&
|
||||
widget.metricOf !== FilterKey.ERRORS &&
|
||||
widget.metricOf !== FilterKey.SESSIONS);
|
||||
|
||||
|
|
@ -106,73 +112,39 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
userSelect: 'none',
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
borderColor:
|
||||
(canDrop && isOver) || active ? '#394EFF' : isPreview ? 'transparent' : '#EEEEEE',
|
||||
(canDrop && isOver) || active
|
||||
? '#394EFF'
|
||||
: isPreview
|
||||
? 'transparent'
|
||||
: '#EEEEEE',
|
||||
}}
|
||||
ref={dragDropRef}
|
||||
onClick={props.onClick ? props.onClick : () => {}}
|
||||
id={`widget-${widget.widgetId}`}
|
||||
>
|
||||
{!isTemplate && isSaved && isPredefined && (
|
||||
<div
|
||||
className={cn(
|
||||
stl.drillDownMessage,
|
||||
'disabled text-gray text-sm invisible group-hover:visible'
|
||||
)}
|
||||
>
|
||||
{'Cannot drill down system provided metrics'}
|
||||
</div>
|
||||
{addOverlay && (
|
||||
<TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />
|
||||
)}
|
||||
|
||||
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
|
||||
<div
|
||||
className={cn('p-3 pb-4 flex items-center justify-between', {
|
||||
'cursor-move': !isTemplate && isSaved,
|
||||
})}
|
||||
>
|
||||
{!props.hideName ? (
|
||||
{!props.hideName ? (
|
||||
<div
|
||||
className={cn('p-3 pb-4 flex items-center justify-between', {
|
||||
'cursor-move': !isTemplate && isSaved,
|
||||
})}
|
||||
>
|
||||
<div className="capitalize-first w-full font-medium">
|
||||
<TextEllipsis text={widget.name} />
|
||||
</div>
|
||||
) : null}
|
||||
{isSaved && (
|
||||
<div className="flex items-center" id="no-print">
|
||||
{!isPredefined && isTimeSeries && !isGridView && (
|
||||
<>
|
||||
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
|
||||
<div className="mx-2" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTemplate && !isGridView && (
|
||||
<ItemMenu
|
||||
items={[
|
||||
{
|
||||
text:
|
||||
widget.metricType === 'predefined'
|
||||
? 'Cannot edit system generated metrics'
|
||||
: 'Edit',
|
||||
onClick: onChartClick,
|
||||
disabled: widget.metricType === 'predefined',
|
||||
},
|
||||
{
|
||||
text: 'Hide',
|
||||
onClick: onDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart
|
||||
isPreview={isPreview}
|
||||
metric={widget}
|
||||
isTemplate={isTemplate}
|
||||
isSaved={isSaved}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart
|
||||
isPreview={isPreview}
|
||||
metric={widget}
|
||||
isTemplate={isTemplate}
|
||||
isSaved={isSaved}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,77 +4,162 @@ import FunnelStepText from './FunnelStepText';
|
|||
import { Icon } from 'UI';
|
||||
import { Space } from 'antd';
|
||||
import { Styles } from 'Components/Dashboard/Widgets/common';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
filter: any;
|
||||
compData?: any;
|
||||
index?: number;
|
||||
focusStage?: (index: number, isFocused: boolean) => void;
|
||||
focusedFilter?: number | null;
|
||||
metricLabel?: string;
|
||||
isHorizontal?: boolean;
|
||||
}
|
||||
|
||||
function FunnelBar(props: Props) {
|
||||
const { filter, index, focusStage, focusedFilter, metricLabel = 'Sessions' } = props;
|
||||
const { filter, index, focusStage, focusedFilter, compData, isHorizontal } = props;
|
||||
|
||||
const isFocused = focusedFilter && index ? focusedFilter === index - 1 : false;
|
||||
const isFocused =
|
||||
focusedFilter && index ? focusedFilter === index - 1 : false;
|
||||
return (
|
||||
<div className="w-full mb-4">
|
||||
<div className="w-full mb-2">
|
||||
<FunnelStepText filter={filter} />
|
||||
<div className={isHorizontal ? 'flex gap-1' : 'flex flex-col'}>
|
||||
<FunnelBarData
|
||||
data={props.filter}
|
||||
isHorizontal={isHorizontal}
|
||||
isComp={false}
|
||||
index={index}
|
||||
isFocused={isFocused}
|
||||
focusStage={focusStage}
|
||||
/>
|
||||
{compData ? (
|
||||
<FunnelBarData
|
||||
data={props.compData}
|
||||
isHorizontal={isHorizontal}
|
||||
isComp
|
||||
index={index}
|
||||
isFocused={isFocused}
|
||||
focusStage={focusStage}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FunnelBarData({
|
||||
data,
|
||||
isComp,
|
||||
isFocused,
|
||||
focusStage,
|
||||
index,
|
||||
isHorizontal,
|
||||
}: {
|
||||
data: any;
|
||||
isComp?: boolean;
|
||||
isFocused?: boolean;
|
||||
focusStage?: (index: number, isFocused: boolean) => void;
|
||||
index?: number;
|
||||
isHorizontal?: boolean;
|
||||
}) {
|
||||
|
||||
const vertFillBarStyle = {
|
||||
width: `${data.completedPercentageTotal}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: isComp ? Styles.compareColors[2] : Styles.compareColors[1]
|
||||
};
|
||||
const horizontalFillBarStyle = {
|
||||
width: '100%',
|
||||
height: `${data.completedPercentageTotal}%`,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: isComp ? Styles.compareColors[2] : Styles.compareColors[1]
|
||||
}
|
||||
|
||||
const vertEmptyBarStyle = {
|
||||
width: `${100.1 - data.completedPercentageTotal}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: isFocused
|
||||
? 'rgba(204, 0, 0, 0.3)'
|
||||
: 'repeating-linear-gradient(325deg, lightgray, lightgray 2px, #FFF1F0 2px, #FFF1F0 6px)',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
const horizontalEmptyBarStyle = {
|
||||
height: `${100.1 - data.completedPercentageTotal}%`,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
background: isFocused
|
||||
? 'rgba(204, 0, 0, 0.3)'
|
||||
: 'repeating-linear-gradient(325deg, lightgray, lightgray 2px, #FFF1F0 2px, #FFF1F0 6px)',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
|
||||
const fillBarStyle = isHorizontal ? horizontalFillBarStyle : vertFillBarStyle;
|
||||
const emptyBarStyle = isHorizontal ? horizontalEmptyBarStyle : vertEmptyBarStyle
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={isHorizontal ? 'rounded-t' : ''}
|
||||
style={{
|
||||
height: '25px',
|
||||
width: '99.8%',
|
||||
height: isHorizontal ? '210px' : '21px',
|
||||
width: isHorizontal ? '200px' : '99.8%',
|
||||
backgroundColor: '#f5f5f5',
|
||||
position: 'relative',
|
||||
borderRadius: '.5rem',
|
||||
overflow: 'hidden'
|
||||
borderRadius: isHorizontal ? undefined : '.5rem',
|
||||
overflow: 'hidden',
|
||||
opacity: isComp ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
width: `${filter.completedPercentageTotal}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: Styles.compareColors[1]
|
||||
}}
|
||||
style={fillBarStyle}
|
||||
>
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
|
||||
{filter.completedPercentageTotal}%
|
||||
{data.completedPercentageTotal}%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: `${100.1 - filter.completedPercentageTotal}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: isFocused ? 'rgba(204, 0, 0, 0.3)' : '#fff0f0',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => focusStage?.(index! - 1, filter.isActive)}
|
||||
className={'hover:opacity-75'}
|
||||
style={emptyBarStyle}
|
||||
onClick={() => focusStage?.(index! - 1, data.isActive)}
|
||||
className={'hover:opacity-70'}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<div
|
||||
className={cn('flex justify-between', isComp ? 'opacity-60' : '')}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<div className="flex items-center">
|
||||
<Icon name="arrow-right-short" size="20" color="green" />
|
||||
<span className="mx-1">{filter.count} {metricLabel}</span>
|
||||
<span className="color-gray-medium text-sm">
|
||||
({filter.completedPercentage}%) Completed
|
||||
{`${data.completedPercentage}% . ${data.count}`}
|
||||
</span>
|
||||
</div>
|
||||
{index && index > 1 && (
|
||||
<Space className="items-center">
|
||||
<Icon name="caret-down-fill" color={filter.droppedCount > 0 ? 'red' : 'gray-light'} size={16} />
|
||||
<Icon
|
||||
name="caret-down-fill"
|
||||
color={data.droppedCount > 0 ? 'red' : 'gray-light'}
|
||||
size={16}
|
||||
/>
|
||||
<span
|
||||
className={'mx-1 ' + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>{filter.droppedCount} {metricLabel}</span>
|
||||
<span
|
||||
className={'text-sm ' + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>({filter.droppedPercentage}%) Dropped</span>
|
||||
className={
|
||||
'mx-1 ' + (data.droppedCount > 0 ? 'color-red' : 'disabled')
|
||||
}
|
||||
>
|
||||
{data.droppedCount} Skipped
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -86,7 +171,7 @@ export function UxTFunnelBar(props: Props) {
|
|||
const { filter } = props;
|
||||
|
||||
return (
|
||||
<div className="w-full mb-4">
|
||||
<div className="w-full mb-2">
|
||||
<div className={'font-medium'}>{filter.title}</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -95,22 +180,28 @@ export function UxTFunnelBar(props: Props) {
|
|||
backgroundColor: '#f5f5f5',
|
||||
position: 'relative',
|
||||
borderRadius: '.5rem',
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
width: `${(filter.completed / (filter.completed + filter.skipped)) * 100}%`,
|
||||
width: `${
|
||||
(filter.completed / (filter.completed + filter.skipped)) * 100
|
||||
}%`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#6272FF'
|
||||
backgroundColor: '#6272FF',
|
||||
}}
|
||||
>
|
||||
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
|
||||
{((filter.completed / (filter.completed + filter.skipped)) * 100).toFixed(1)}%
|
||||
{(
|
||||
(filter.completed / (filter.completed + filter.skipped)) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -119,22 +210,22 @@ export function UxTFunnelBar(props: Props) {
|
|||
<div className={'flex items-center gap-4'}>
|
||||
<div className="flex items-center">
|
||||
<Icon name="arrow-right-short" size="20" color="green" />
|
||||
<span className="mx-1 font-medium">{filter.completed}</span><span>completed this step</span>
|
||||
<span className="mx-1 font-medium">{filter.completed}</span>
|
||||
<span>completed this step</span>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<Icon name="clock" size="16" />
|
||||
<span className="mx-1 font-medium">
|
||||
{durationFormatted(filter.avgCompletionTime)}
|
||||
</span>
|
||||
<span>
|
||||
avg. completion time
|
||||
</span>
|
||||
<span>avg. completion time</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* @ts-ignore */}
|
||||
<div className="flex items-center">
|
||||
<Icon name="caret-down-fill" color="red" size={16} />
|
||||
<span className="font-medium mx-1">{filter.skipped}</span><span> skipped</span>
|
||||
<span className="font-medium mx-1">{filter.skipped}</span>
|
||||
<span> skipped</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,11 +233,3 @@ export function UxTFunnelBar(props: Props) {
|
|||
}
|
||||
|
||||
export default FunnelBar;
|
||||
|
||||
const calculatePercentage = (completed: number, dropped: number) => {
|
||||
const total = completed + dropped;
|
||||
if (dropped === 0) return 100;
|
||||
if (total === 0) return 0;
|
||||
|
||||
return Math.round((completed / dropped) * 100);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function FunnelStepText(props: Props) {
|
|||
const { filter } = props;
|
||||
const total = filter.value.length;
|
||||
return (
|
||||
<div className="mb-2 color-gray-medium">
|
||||
<div className="color-gray-medium">
|
||||
<span className="color-gray-darkest">{filter.label}</span>
|
||||
<span className="mx-1">{filter.operator}</span>
|
||||
{filter.value.map((value: any, index: number) => (
|
||||
|
|
|
|||
|
|
@ -1,23 +1,3 @@
|
|||
.step {
|
||||
/* display: flex; */
|
||||
position: relative;
|
||||
transition: all 0.5s ease;
|
||||
&:before {
|
||||
content: '';
|
||||
border-left: 2px solid $gray-lightest;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
bottom: 9px;
|
||||
left: 10px;
|
||||
/* width: 1px; */
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
&:last-child:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.step-disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.8;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import Funnelbar, { UxTFunnelBar } from "./FunnelBar";
|
||||
import Funnel from 'App/mstore/types/funnel'
|
||||
import cn from 'classnames';
|
||||
import stl from './FunnelWidget.module.css';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -11,134 +12,186 @@ import { useModal } from 'App/components/Modal';
|
|||
interface Props {
|
||||
metric?: Widget;
|
||||
isWidget?: boolean;
|
||||
data: any;
|
||||
data: { funnel: Funnel };
|
||||
compData: { funnel: Funnel };
|
||||
}
|
||||
|
||||
function FunnelWidget(props: Props) {
|
||||
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
|
||||
const { isWidget = false, data, metric } = props;
|
||||
const funnel = data.funnel || { stages: [] };
|
||||
const totalSteps = funnel.stages.length;
|
||||
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
|
||||
const hasMoreSteps = funnel.stages.length > 2;
|
||||
const lastStage = funnel.stages[funnel.stages.length - 1];
|
||||
const remainingSteps = totalSteps - 2;
|
||||
const { hideModal } = useModal();
|
||||
const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
|
||||
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
|
||||
const { isWidget = false, data, metric, compData } = props;
|
||||
const funnel = data.funnel || { stages: [] };
|
||||
const totalSteps = funnel.stages.length;
|
||||
const stages = isWidget
|
||||
? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]]
|
||||
: funnel.stages;
|
||||
const hasMoreSteps = funnel.stages.length > 2;
|
||||
const lastStage = funnel.stages[funnel.stages.length - 1];
|
||||
const remainingSteps = totalSteps - 2;
|
||||
const { hideModal } = useModal();
|
||||
const metricLabel =
|
||||
metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isWidget) return;
|
||||
hideModal();
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isWidget) return;
|
||||
hideModal();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const focusStage = (index: number) => {
|
||||
funnel.stages.forEach((s, i) => {
|
||||
// turning on all filters if one was focused already
|
||||
if (focusedFilter === index) {
|
||||
s.updateKey('isActive', true);
|
||||
setFocusedFilter(null);
|
||||
} else {
|
||||
setFocusedFilter(index);
|
||||
if (i === index) {
|
||||
s.updateKey('isActive', true);
|
||||
} else {
|
||||
s.updateKey('isActive', false);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const focusStage = (index: number) => {
|
||||
funnel.stages.forEach((s, i) => {
|
||||
// turning on all filters if one was focused already
|
||||
if (focusedFilter === index) {
|
||||
s.updateKey('isActive', true)
|
||||
setFocusedFilter(null)
|
||||
} else {
|
||||
setFocusedFilter(index)
|
||||
if (i === index) {
|
||||
s.updateKey('isActive', true)
|
||||
} else {
|
||||
s.updateKey('isActive', false)
|
||||
}
|
||||
}
|
||||
})
|
||||
const shownStages = React.useMemo(() => {
|
||||
const stages: { data: Funnel['stages'][0], compData?: Funnel['stages'][0] }[] = [];
|
||||
for (let i = 0; i < funnel.stages.length; i++) {
|
||||
const stage: any = { data: funnel.stages[i], compData: undefined }
|
||||
const compStage = compData?.funnel.stages[i];
|
||||
if (compStage) {
|
||||
stage.compData = compStage;
|
||||
}
|
||||
stages.push(stage)
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
title={
|
||||
<div className="flex items-center text-lg">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
}
|
||||
show={!stages || stages.length === 0}
|
||||
>
|
||||
<div className="w-full">
|
||||
{ !isWidget && (
|
||||
stages.map((filter: any, index: any) => (
|
||||
<Stage
|
||||
key={index}
|
||||
index={index + 1}
|
||||
isWidget={isWidget}
|
||||
stage={filter}
|
||||
focusStage={focusStage}
|
||||
focusedFilter={focusedFilter}
|
||||
metricLabel={metricLabel}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
return stages;
|
||||
}, [data, compData])
|
||||
|
||||
{ isWidget && (
|
||||
<>
|
||||
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
|
||||
const viewType = metric?.viewType;
|
||||
const isHorizontal = viewType === 'columnChart';
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
title={
|
||||
<div className="flex items-center text-lg">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
No data available for the selected period.
|
||||
</div>
|
||||
}
|
||||
show={!stages || stages.length === 0}
|
||||
>
|
||||
<div className={cn('w-full border-b -mx-4 px-4', isHorizontal ? 'flex gap-2 flex-wrap justify-around' : '')}>
|
||||
{!isWidget &&
|
||||
shownStages.map((stage: any, index: any) => (
|
||||
<Stage
|
||||
key={index}
|
||||
isHorizontal={isHorizontal}
|
||||
index={index + 1}
|
||||
isWidget={isWidget}
|
||||
stage={stage.data}
|
||||
compData={stage.compData}
|
||||
focusStage={focusStage}
|
||||
focusedFilter={focusedFilter}
|
||||
metricLabel={metricLabel}
|
||||
/>
|
||||
))}
|
||||
|
||||
{ hasMoreSteps && (
|
||||
<>
|
||||
<EmptyStage total={remainingSteps} />
|
||||
</>
|
||||
)}
|
||||
{isWidget && (
|
||||
<>
|
||||
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
|
||||
|
||||
{funnel.stages.length > 1 && (
|
||||
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center pb-4">
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Lost conversion</span>
|
||||
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="red" className='text-lg font-medium rounded-lg'>
|
||||
{funnel.lostConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mx-3" />
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Total conversion</span>
|
||||
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
|
||||
<Tag bordered={false} color="green" className='text-lg font-medium rounded-lg'>
|
||||
{funnel.totalConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}
|
||||
</NoContent>
|
||||
);
|
||||
{hasMoreSteps && (
|
||||
<>
|
||||
<EmptyStage total={remainingSteps} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{funnel.stages.length > 1 && (
|
||||
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center py-2 gap-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Total conversion</span>
|
||||
<Tooltip
|
||||
title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color="#F5F8FF"
|
||||
className="text-lg rounded-lg !text-black"
|
||||
>
|
||||
{funnel.totalConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-medium mr-2">Lost conversion</span>
|
||||
<Tooltip
|
||||
title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}
|
||||
>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color="#FFEFEF"
|
||||
className="text-lg rounded-lg !text-black"
|
||||
>
|
||||
{funnel.lostConversions}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{funnel.totalDropDueToIssues > 0 && (
|
||||
<div className="flex items-center mb-2">
|
||||
<Icon name="magic" />{' '}
|
||||
<span className="ml-2">
|
||||
{funnel.totalDropDueToIssues} sessions dropped due to issues.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export const EmptyStage = observer(({ total }: any) => {
|
||||
return (
|
||||
<div className={cn("flex items-center mb-4 pb-3", stl.step)}>
|
||||
<IndexNumber index={0} />
|
||||
<div className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal" style={{ width: '100px'}}>
|
||||
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
|
||||
</div>
|
||||
<div className="border-b w-full border-dashed"></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={cn('flex items-center mb-4 pb-3 relative border-b -mx-4 px-4 pt-2')}>
|
||||
<IndexNumber index={0} />
|
||||
<div
|
||||
className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal"
|
||||
style={{ width: '100px' }}
|
||||
>
|
||||
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
|
||||
</div>
|
||||
<div className="border-b w-full border-dashed"></div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Stage = observer(({ metricLabel, stage, index, isWidget, uxt, focusStage, focusedFilter }: any) => {
|
||||
export const Stage = observer(({
|
||||
metricLabel,
|
||||
stage,
|
||||
index,
|
||||
uxt,
|
||||
focusStage,
|
||||
focusedFilter,
|
||||
compData,
|
||||
isHorizontal,
|
||||
}: any) => {
|
||||
return stage ? (
|
||||
<div
|
||||
className={cn('flex items-start', stl.step, { [stl['step-disabled']]: !stage.isActive })}
|
||||
className={cn(
|
||||
'flex items-start relative pt-2',
|
||||
{ [stl['step-disabled']]: !stage.isActive },
|
||||
)}
|
||||
>
|
||||
<IndexNumber index={index} />
|
||||
{!uxt ? <Funnelbar metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage} focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
|
||||
{/*{!isWidget && !uxt && <BarActions bar={stage} />}*/}
|
||||
{!uxt ? <Funnelbar isHorizontal={isHorizontal} compData={compData} metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage} focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
) : null
|
||||
})
|
||||
|
||||
export const IndexNumber = observer(({ index }: any) => {
|
||||
|
|
@ -149,15 +202,4 @@ export const IndexNumber = observer(({ index }: any) => {
|
|||
);
|
||||
})
|
||||
|
||||
|
||||
const BarActions = observer(({ bar }: any) => {
|
||||
return (
|
||||
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
|
||||
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
|
||||
<Icon name="eye-slash-fill" color={bar.isActive ? "gray-light" : "gray-darkest"} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default observer(FunnelWidget);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ function SelectDateRange(props: Props) {
|
|||
);
|
||||
|
||||
const onChange = (value: any) => {
|
||||
if (props.comparison) {
|
||||
if (props.comparison && props.onChangeComparison) {
|
||||
if (!value) return props.onChangeComparison(null);
|
||||
const newPeriod = new Period({
|
||||
start: props.period.start,
|
||||
end: props.period.end,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default class Funnel {
|
|||
}
|
||||
this.totalDropDueToIssues = json.totalDropDueToIssues;
|
||||
|
||||
if (json.stages.length >= 1) {
|
||||
if (json.stages?.length >= 1) {
|
||||
const firstStage = json.stages[0]
|
||||
this.stages = json.stages ? json.stages.map((stage: any, index: number) => new FunnelStage().fromJSON(stage, firstStage.count, index > 0 ? json.stages[index - 1].count : stage.count)) : []
|
||||
const filteredStages = this.stages.filter((stage: any) => stage.isActive)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {FilterKey} from 'Types/filter/filterType';
|
|||
import Period, {LAST_24_HOURS} from 'Types/app/period';
|
||||
import Funnel from '../types/funnel';
|
||||
import {metricService} from 'App/services';
|
||||
import { FUNNEL, HEATMAP, INSIGHTS, TABLE, USER_PATH } from "App/constants/card";
|
||||
import { FUNNEL, HEATMAP, INSIGHTS, TABLE, TIMESERIES, USER_PATH } from "App/constants/card";
|
||||
import { ErrorInfo } from '../types/error';
|
||||
import {getChartFormatter} from 'Types/dashboard/helper';
|
||||
import FilterItem from './filterItem';
|
||||
|
|
@ -295,7 +295,7 @@ export default class Widget {
|
|||
|
||||
setData(data: { timestamp: number, [seriesName: string]: number}[], period: any, isComparison?: boolean) {
|
||||
const _data: any = {};
|
||||
if (isComparison) {
|
||||
if (isComparison && this.metricType === TIMESERIES) {
|
||||
data.forEach((point, i) => {
|
||||
Object.keys(point).forEach((key) => {
|
||||
if (key === 'timestamp') return;
|
||||
|
|
@ -321,10 +321,9 @@ export default class Widget {
|
|||
new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew)
|
||||
);
|
||||
} else if (this.metricType === FUNNEL) {
|
||||
_data.funnel = new Funnel().fromJSON(_data);
|
||||
_data.funnel = new Funnel().fromJSON(data);
|
||||
} else if (this.metricType === TABLE) {
|
||||
const total = data[0]['total'];
|
||||
const count = data[0]['count'];
|
||||
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, total, this.metricOf));
|
||||
} else {
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue