diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx
new file mode 100644
index 000000000..3f6e742ec
--- /dev/null
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import CustomTooltip from "./CustomChartTooltip";
+import { Styles } from '../common';
+import {
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ AreaChart,
+ Area,
+ Legend,
+} from 'recharts';
+
+interface Props {
+ data: any;
+ params: any;
+ colors: any;
+ onClick?: (event, index) => void;
+ yaxis?: any;
+ label?: string;
+ hideLegend?: boolean;
+}
+
+function CustomMetricLineChart(props: Props) {
+ const {
+ data = { chart: [], namesMap: [] },
+ params,
+ colors,
+ onClick = () => null,
+ yaxis = { ...Styles.yaxis },
+ label = 'Number of Sessions',
+ hideLegend = false,
+ } = props;
+
+
+ console.log(data.namesMap, data.chart)
+ return (
+
+
+ {!hideLegend && (
+
+ )}
+
+
+ Styles.tickFormatter(val)}
+ label={{
+ ...Styles.axisLabelLeft,
+ value: label || 'Number of Sessions',
+ }}
+ />
+
+ {Array.isArray(data.namesMap) &&
+ data.namesMap.map((key, index) => (
+
+ ))}
+
+
+ );
+}
+
+export default CustomMetricLineChart;
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx
new file mode 100644
index 000000000..5c6df8ee7
--- /dev/null
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import CustomTooltip from "./CustomChartTooltip";
+import { Styles } from '../common';
+import {
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ BarChart,
+ Bar,
+ Legend,
+ Rectangle,
+} from 'recharts';
+
+interface Props {
+ data: any;
+ params: any;
+ colors: any;
+ onClick?: (event, index) => void;
+ yaxis?: any;
+ label?: string;
+ hideLegend?: boolean;
+}
+const getPath = (x, y, width, height) => {
+ const radius = Math.min(width / 2, height / 2);
+ return `
+ M${x + radius},${y}
+ H${x + width - radius}
+ A${radius},${radius} 0 0 1 ${x + width},${y + radius}
+ V${y + height - radius}
+ A${radius},${radius} 0 0 1 ${x + width - radius},${y + height}
+ H${x + radius}
+ A${radius},${radius} 0 0 1 ${x},${y + height - radius}
+ V${y + radius}
+ A${radius},${radius} 0 0 1 ${x + radius},${y}
+ Z
+ `;
+};
+
+const PillBar = (props) => {
+ const { fill, x, y, width, height } = props;
+
+ return ;
+};
+
+
+function CustomMetricLineChart(props: Props) {
+ const {
+ data = { chart: [], namesMap: [] },
+ params,
+ colors,
+ onClick = () => null,
+ yaxis = { ...Styles.yaxis },
+ label = 'Number of Sessions',
+ hideLegend = false,
+ } = props;
+
+ return (
+
+
+ {!hideLegend && (
+
+ )}
+
+
+ Styles.tickFormatter(val)}
+ label={{
+ ...Styles.axisLabelLeft,
+ value: label || 'Number of Sessions',
+ }}
+ />
+
+ {Array.isArray(data.namesMap) &&
+ data.namesMap.map((key, index) => (
+ }
+ fill={colors[index]}
+ legendType={key === 'Total' ? 'none' : 'line'}
+ activeBar={}
+ // strokeDasharray={'4 3'} FOR COPMARISON ONLY
+ />
+ ))}
+
+
+ );
+}
+
+export default CustomMetricLineChart;
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx
new file mode 100644
index 000000000..6ad49bfda
--- /dev/null
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { formatTimeOrDate } from "App/date";
+
+function CustomTooltip({ active, payload, label }) {
+ if (!active) return;
+
+ const shownPayloads = payload.filter((p) => !p.hide);
+ return (
+
+ {shownPayloads.map((p, index) => (
+ <>
+
+
+
+ {label}, {formatTimeOrDate(p.payload.timestamp)}
+
+
{p.value}
+
+ >
+ ))}
+
+ );
+}
+
+export default CustomTooltip;
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx
index 8d3b424ef..6b267b714 100644
--- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { formatTimeOrDate } from 'App/date';
import { Button, Table } from 'antd';
import type { TableProps } from 'antd';
+import CustomTooltip from "../CustomChartTooltip";
import { Eye, EyeOff } from 'lucide-react';
import { Styles } from '../../common';
@@ -15,6 +16,7 @@ import {
Line,
Legend,
} from 'recharts';
+import cn from 'classnames';
interface Props {
data: any;
@@ -26,18 +28,6 @@ interface Props {
hideLegend?: boolean;
}
-const initTableProps = [{
- title: 'Series',
- dataIndex: 'seriesName',
- key: 'seriesName',
-},
- {
- title: 'Avg.',
- dataIndex: 'average',
- key: 'average',
- }
-]
-
function CustomMetricLineChart(props: Props) {
const {
data = { chart: [], namesMap: [] },
@@ -48,176 +38,55 @@ function CustomMetricLineChart(props: Props) {
label = 'Number of Sessions',
hideLegend = false,
} = props;
- const [showTable, setShowTable] = useState(false);
- const hasMultipleSeries = data.namesMap.length > 1;
- const [tableData, setTableData] = useState([]);
- const [tableProps, setTableProps] = useState(initTableProps);
- // console.log(params.density / 7, data.chart)
-
- const columnNames = new Set();
- /**
- * basically we have an array of
- * { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
- * which we turn into a table where each series of filters = row;
- * and each unique time = column
- * + average for each row
- * [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
- * */
- React.useEffect(()=> {
- setTableProps(initTableProps)
- const series = Object.keys(data.chart[0])
- .filter((key) => key !== 'time' && key !== 'timestamp')
- columnNames.clear()
- data.chart.forEach((p: any) => {
- columnNames.add(p.time)
- }) // for example: mon, tue, wed, thu, fri, sat, sun
- const avg: any = {} // { seriesName: {itemsCount: 0, total: 0} }
- const items: Record[] = []; // as many items (rows) as we have series in filter
- series.forEach(s => {
- items.push({ seriesName: s, average: 0 })
- avg[s] = { itemsCount: 0, total: 0 }
- })
- const tableCols: { title: string, dataIndex: string, key: string }[] = [];
- Array.from(columnNames).forEach((name: string) => {
- tableCols.push({
- title: name,
- dataIndex: name,
- key: name,
- })
- const values = data.chart.filter((p) => p.time === name)
- series.forEach((s) => {
- const toDateAvg = values.reduce((acc, curr) => acc + curr[s], 0) / values.length;
- avg[s].itemsCount += 1
- avg[s].total += toDateAvg
- const ind = items.findIndex((item) => item.seriesName === s)
- if (ind === -1) return
- items[ind][name] = (values.reduce((acc, curr) => acc + curr[s], 0) / values.length)
- .toFixed(2)
- })
- })
- Object.keys(avg).forEach((key) => {
- const ind = items.findIndex((item) => item.seriesName === key)
- if (ind === -1) return
- items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2)
- })
-
- setTableProps((prev) => [...prev, ...tableCols])
- setTableData(items)
- }, [data.chart.length])
return (
-
-
-
- {!hideLegend && (
-
- )}
-
-
- Styles.tickFormatter(val)}
- label={{
- ...Styles.axisLabelLeft,
- value: label || 'Number of Sessions',
- }}
- />
-
- {Array.isArray(data.namesMap) &&
- data.namesMap.map((key, index) => (
-
- ))}
-
-
- {hasMultipleSeries ? (
-
-
-
- : }
- size={'small'}
- type={'default'}
- onClick={() => setShowTable(!showTable)}
- >
- {showTable ? 'Hide Table' : 'Show Table'}
-
-
- {showTable ? (
-
+
+ {!hideLegend && (
+
+ )}
+
+
+ Styles.tickFormatter(val)}
+ label={{
+ ...Styles.axisLabelLeft,
+ value: label || 'Number of Sessions',
+ }}
+ />
+
+ {Array.isArray(data.namesMap) &&
+ data.namesMap.map((key, index) => key ? (
+
- ) : null}
-
- ) : null}
-
- );
-}
-
-function CustomTooltip({ active, payload, label }) {
- if (!active) return;
-
- const shownPayloads = payload.filter((p) => !p.hide);
- return (
-
- {shownPayloads.map((p, index) => (
- <>
-
-
-
- {label}, {formatTimeOrDate(p.payload.timestamp)}
-
-
{p.value}
-
- >
- ))}
-
+ ) : null)}
+
+
);
}
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx
index a453222e5..e155838db 100644
--- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx
@@ -1,126 +1,156 @@
//@ts-nocheck
-import React from 'react'
+import React from 'react';
import { ResponsiveContainer, Tooltip } from 'recharts';
-import { PieChart, Pie, Cell } from 'recharts';
+import { PieChart, Pie, Cell, Legend } from 'recharts';
import { Styles } from '../../common';
import { NoContent } from 'UI';
import { filtersMap } from 'Types/filter/newFilter';
import { numberWithCommas } from 'App/utils';
interface Props {
- metric: any,
- data: any;
- colors: any;
- onClick?: (filters) => void;
+ metric: any;
+ data: any;
+ colors: any;
+ onClick?: (filters) => void;
}
function CustomMetricPieChart(props: Props) {
- const { metric, data = { values: [] }, onClick = () => null } = props;
+ const { metric, data, onClick = () => null } = props;
- const onClickHandler = (event) => {
- if (event && !event.payload.group) {
- const filters = Array();
- let filter = { ...filtersMap[metric.metricOf] }
- filter.value = [event.payload.name]
- filter.type = filter.key
- delete filter.key
- delete filter.operatorOptions
- delete filter.category
- delete filter.icon
- delete filter.label
- delete filter.options
+ const onClickHandler = (event) => {
+ if (event && !event.payload.group) {
+ const filters = Array();
+ let filter = { ...filtersMap[metric.metricOf] };
+ filter.value = [event.payload.name];
+ filter.type = filter.key;
+ delete filter.key;
+ delete filter.operatorOptions;
+ delete filter.category;
+ delete filter.icon;
+ delete filter.label;
+ delete filter.options;
- filters.push(filter);
- onClick(filters);
- }
+ filters.push(filter);
+ onClick(filters);
}
- return (
-
-
-
- {
- const RADIAN = Math.PI / 180;
- let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
- let radius2 = innerRadius + (outerRadius - innerRadius);
- let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
- let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
- let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
- let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
+ };
- const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
-
- if (percentage<3){
- return null;
- }
-
- return(
-
- )
- }}
- label={({
- cx,
- cy,
- midAngle,
- innerRadius,
- outerRadius,
- value,
- index
- }) => {
- const RADIAN = Math.PI / 180;
- let radius = 20 + innerRadius + (outerRadius - innerRadius);
- let x = cx + radius * Math.cos(-midAngle * RADIAN);
- let y = cy + radius * Math.sin(-midAngle * RADIAN);
- const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
- let name = data.values[index].name || 'Unidentified';
- name = name.length > 20 ? name.substring(0, 20) + '...' : name;
- if (percentage<3){
- return null;
- }
- return (
- cx ? "start" : "end"}
- dominantBaseline="central"
- fill='#666'
- >
- {name || 'Unidentified'} {numberWithCommas(value)}
-
- );
- }}
- >
- {data && data.values && data.values.map((entry, index) => (
- |
- ))}
-
-
-
-
-
- Top 5
-
- )
+ const getTotalForSeries = (series: string) => {
+ return data.chart ? data.chart.reduce((acc, curr) => acc + curr[series], 0) : 0
+ }
+ const values = data.namesMap.map((k, i) => {
+ return {
+ name: k,
+ value: getTotalForSeries(k)
+ }
+ })
+ const highest = values.reduce(
+ (acc, curr) =>
+ acc.value > curr.value ? acc : curr,
+ { name: '', value: 0 });
+ return (
+
+
+
+
+ {
+ const RADIAN = Math.PI / 180;
+ let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
+ let radius2 = innerRadius + (outerRadius - innerRadius);
+ let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
+ let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
+ let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
+ let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
+
+ const percentage =
+ (value * 100) /
+ highest.value;
+
+ if (percentage < 3) {
+ return null;
+ }
+
+ return (
+
+ );
+ }}
+ label={({
+ cx,
+ cy,
+ midAngle,
+ innerRadius,
+ outerRadius,
+ value,
+ index,
+ }) => {
+ const RADIAN = Math.PI / 180;
+ let radius = 20 + innerRadius + (outerRadius - innerRadius);
+ let x = cx + radius * Math.cos(-midAngle * RADIAN);
+ let y = cy + radius * Math.sin(-midAngle * RADIAN);
+ const percentage =
+ (value / highest.value) *
+ 100;
+ let name = values[index].name || 'Unidentified';
+ name = name.length > 20 ? name.substring(0, 20) + '...' : name;
+ if (percentage < 3) {
+ return null;
+ }
+ return (
+ cx ? 'start' : 'end'}
+ dominantBaseline="central"
+ fill="#666"
+ >
+ {numberWithCommas(value)}
+
+ );
+ }}
+ >
+ {values && values.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+ );
}
export default CustomMetricPieChart;
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx
new file mode 100644
index 000000000..570be0977
--- /dev/null
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+interface Props {
+ data: { chart: any[], namesMap: string[] };
+ params: any;
+ colors: any;
+ onClick?: (event, index) => void;
+ yaxis?: any;
+ label?: string;
+ hideLegend?: boolean;
+}
+
+function ProgressBarChart(props: Props) {
+ const {
+ data = { chart: [], namesMap: [] },
+ colors,
+ onClick = () => null,
+ label = 'Number of Sessions',
+ } = props;
+
+ const getTotalForSeries = (series: string) => {
+ return data.chart.reduce((acc, curr) => acc + curr[series], 0);
+ }
+ const values = data.namesMap.map((k, i) => {
+ return {
+ name: k,
+ value: getTotalForSeries(k)
+ }
+ })
+ const highest = values.reduce(
+ (acc, curr) =>
+ acc.value > curr.value ? acc : curr,
+ { name: '', value: 0 });
+
+ const formattedNumber = (num: number) => {
+ return Intl.NumberFormat().format(num);
+ }
+ return (
+
+ {values.map((val, i) => (
+
+
+
+
+
{formattedNumber(val.value)}
+
+
+
+ ))}
+
+ );
+}
+
+export default ProgressBarChart;
diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
index 24288fe59..a5ad36c3e 100644
--- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
+++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { EventsList } from 'Shared/Filters/FilterList';
+import { EventsList, FilterList } from 'Shared/Filters/FilterList';
import SeriesName from './SeriesName';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
@@ -203,6 +203,7 @@ function FilterSeries(props: Props) {
)}
{expanded ? (
+ <>
+
+ >
) : null}
);
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
index da6173eb1..e20413305 100644
--- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx
@@ -1,26 +1,30 @@
-import React, {useState, useRef, useEffect} from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import CustomMetricLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart';
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
-import {Styles} from 'App/components/Dashboard/Widgets/common';
-import {observer} from 'mobx-react-lite';
+import { Styles } from 'App/components/Dashboard/Widgets/common';
+import { observer } from 'mobx-react-lite';
import { Icon, Loader } from 'UI';
-import {useStore} from 'App/mstore';
+import { useStore } from 'App/mstore';
+import AreaChart from '../../Widgets/CustomMetricsWidgets/AreaChart';
+import BarChart from '../../Widgets/CustomMetricsWidgets/BarChart';
+import ProgressBarChart from '../../Widgets/CustomMetricsWidgets/ProgressBarChart';
+
+import WidgetDatatable from '../WidgetDatatable/WidgetDatatable';
import WidgetPredefinedChart from '../WidgetPredefinedChart';
-import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
-import {getStartAndEndTimestampsByDensity} from 'Types/dashboard/helper';
-import {debounce} from 'App/utils';
+import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
+import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted';
-import {FilterKey} from 'Types/filter/filterType';
+import { FilterKey } from 'Types/filter/filterType';
import {
- TIMESERIES,
- TABLE,
- HEATMAP,
- FUNNEL,
- ERRORS,
- INSIGHTS,
- USER_PATH,
- RETENTION
+ TIMESERIES,
+ TABLE,
+ HEATMAP,
+ FUNNEL,
+ ERRORS,
+ INSIGHTS,
+ USER_PATH,
+ RETENTION,
} from 'App/constants/card';
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
@@ -29,245 +33,367 @@ import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
import SankeyChart from 'Shared/Insights/SankeyChart';
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
-import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
-import { useInView } from "react-intersection-observer";
+import SessionsBy from 'Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy';
+import { useInView } from 'react-intersection-observer';
interface Props {
- metric: any;
- isSaved?: boolean;
- isTemplate?: boolean;
- isPreview?: boolean;
+ metric: any;
+ isSaved?: boolean;
+ isTemplate?: boolean;
+ isPreview?: boolean;
}
function WidgetChart(props: Props) {
- const { ref, inView } = useInView({
- triggerOnce: true,
- rootMargin: "200px 0px",
- });
- const {isSaved = false, metric, isTemplate} = props;
- const {dashboardStore, metricStore, sessionStore} = useStore();
- const _metric: any = metricStore.instance;
- const period = dashboardStore.period;
- const drillDownPeriod = dashboardStore.drillDownPeriod;
- const drillDownFilter = dashboardStore.drillDownFilter;
- const colors = Styles.customMetricColors;
- const [loading, setLoading] = useState(true);
- const params = {density: 70};
- const metricParams = {...params};
- const prevMetricRef = useRef();
- const isMounted = useIsMounted();
- const [data, setData] = useState(metric.data);
- const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
- const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
+ const { ref, inView } = useInView({
+ triggerOnce: true,
+ rootMargin: '200px 0px',
+ });
+ const { isSaved = false, metric, isTemplate } = props;
+ const { dashboardStore, metricStore, sessionStore } = useStore();
+ const _metric: any = metricStore.instance;
+ const period = dashboardStore.period;
+ const drillDownPeriod = dashboardStore.drillDownPeriod;
+ const drillDownFilter = dashboardStore.drillDownFilter;
+ const colors = Styles.customMetricColors;
+ const [loading, setLoading] = useState(true);
+ const params = { density: 70 };
+ const metricParams = { ...params };
+ const prevMetricRef = useRef();
+ const isMounted = useIsMounted();
+ const [data, setData] = useState(metric.data);
+ const [enabledRows, setEnabledRows] = useState([]);
+ const isTableWidget =
+ metric.metricType === 'table' && metric.viewType === 'table';
+ const isPieChart =
+ metric.metricType === 'table' && metric.viewType === 'pieChart';
- useEffect(() => {
- return () => {
- dashboardStore.resetDrillDownFilter();
- };
- }, []);
-
- const onChartClick = (event: any) => {
- if (event) {
- if (isTableWidget || isPieChart) { // get the filter of clicked row
- const periodTimestamps = drillDownPeriod.toTimestamps();
- drillDownFilter.merge({
- filters: event,
- startTimestamp: periodTimestamps.startTimestamp,
- endTimestamp: periodTimestamps.endTimestamp
- });
- } else { // get the filter of clicked chart point
- const payload = event.activePayload[0].payload;
- const timestamp = payload.timestamp;
- const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
-
- drillDownFilter.merge({
- startTimestamp: periodTimestamps.startTimestamp,
- endTimestamp: periodTimestamps.endTimestamp
- });
- }
- }
+ useEffect(() => {
+ return () => {
+ dashboardStore.resetDrillDownFilter();
};
+ }, []);
- const depsString = JSON.stringify({
- ..._metric.series, ..._metric.excludes, ..._metric.startPoint,
- hideExcess: _metric.hideExcess
- });
- const fetchMetricChartData = (metric: any, payload: any, isSaved: any, period: any) => {
- if (!isMounted()) return;
- setLoading(true);
- dashboardStore.fetchMetricChartData(metric, payload, isSaved, period).then((res: any) => {
- if (isMounted()) setData(res);
- }).finally(() => {
- setLoading(false);
+ useEffect(() => {
+ const series = data.chart[0] ? Object.keys(data.chart[0]).filter(
+ (key) => key !== 'time' && key !== 'timestamp'
+ ) : []
+ if (series.length) {
+ setEnabledRows(series)
+ }
+ }, [data.chart.length])
+
+ const onChartClick = (event: any) => {
+ if (event) {
+ if (isTableWidget || isPieChart) {
+ // get the filter of clicked row
+ const periodTimestamps = drillDownPeriod.toTimestamps();
+ drillDownFilter.merge({
+ filters: event,
+ startTimestamp: periodTimestamps.startTimestamp,
+ endTimestamp: periodTimestamps.endTimestamp,
});
- };
+ } else {
+ // get the filter of clicked chart point
+ const payload = event.activePayload[0].payload;
+ const timestamp = payload.timestamp;
+ const periodTimestamps = getStartAndEndTimestampsByDensity(
+ timestamp,
+ drillDownPeriod.start,
+ drillDownPeriod.end,
+ params.density
+ );
- const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
- const loadPage = () => {
- if (!inView) return;
- if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
- prevMetricRef.current = metric;
- return;
- }
- prevMetricRef.current = metric;
- const timestmaps = drillDownPeriod.toTimestamps();
- const payload = isSaved ? {...params} : {...metricParams, ...timestmaps, ...metric.toJson()};
- debounceRequest(metric, payload, isSaved, !isSaved ? drillDownPeriod : period);
- };
- useEffect(() => {
- _metric.updateKey('page', 1);
- loadPage();
- }, [
- drillDownPeriod,
- period,
- depsString,
- metric.metricType,
- metric.metricOf,
- metric.viewType,
- metric.metricValue,
- metric.startType,
- metric.metricFormat,
- inView,
- ]);
- useEffect(loadPage, [_metric.page]);
+ drillDownFilter.merge({
+ startTimestamp: periodTimestamps.startTimestamp,
+ endTimestamp: periodTimestamps.endTimestamp,
+ });
+ }
+ }
+ };
+ const depsString = JSON.stringify({
+ ..._metric.series,
+ ..._metric.excludes,
+ ..._metric.startPoint,
+ hideExcess: _metric.hideExcess,
+ });
+ const fetchMetricChartData = (
+ metric: any,
+ payload: any,
+ isSaved: any,
+ period: any
+ ) => {
+ if (!isMounted()) return;
+ setLoading(true);
+ dashboardStore
+ .fetchMetricChartData(metric, payload, isSaved, period)
+ .then((res: any) => {
+ if (isMounted()) setData(res);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
- const renderChart = () => {
- const {metricType, viewType, metricOf} = metric;
- const metricWithData = {...metric, data};
-
- if (metricType === FUNNEL) {
- return ;
- }
-
- if (metricType === 'predefined' || metricType === ERRORS) {
- const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric;
- return ;
- }
-
- if (metricType === TIMESERIES) {
- if (viewType === 'lineChart') {
- return (
-
- );
- } else if (viewType === 'progress') {
- return (
-
- );
- }
- }
-
- if (metricType === TABLE) {
- if (metricOf === FilterKey.SESSIONS) {
- return (
-
- );
- }
- if (metricOf === FilterKey.ERRORS) {
- return (
-
- );
- }
- if (viewType === TABLE) {
- return (
-
- );
- } else if (viewType === 'pieChart') {
- return (
-
- );
- }
- }
- if (metricType === HEATMAP) {
- if (!props.isPreview) {
- return metric.thumbnail ? (
-
-

-
- ) : (
-
-
- No data available for the selected period.
-
- );
- }
- return (
-
- );
- }
-
- if (metricType === INSIGHTS) {
- return ;
- }
-
- if (metricType === USER_PATH && data && data.links) {
- // return ;
- return {
- dashboardStore.drillDownFilter.merge({filters, page: 1});
- }}/>;
- }
-
- if (metricType === RETENTION) {
- if (viewType === 'trend') {
- return (
-
- );
- } else if (viewType === 'cohort') {
- return (
-
- );
- }
- }
-
- return Unknown metric type
;
- };
- return (
-
+ const debounceRequest: any = React.useCallback(
+ debounce(fetchMetricChartData, 500),
+ []
+ );
+ const loadPage = () => {
+ if (!inView) return;
+ if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
+ prevMetricRef.current = metric;
+ return;
+ }
+ prevMetricRef.current = metric;
+ const timestmaps = drillDownPeriod.toTimestamps();
+ const payload = isSaved
+ ? { ...params }
+ : { ...metricParams, ...timestmaps, ...metric.toJson() };
+ debounceRequest(
+ metric,
+ payload,
+ isSaved,
+ !isSaved ? drillDownPeriod : period
);
+ };
+ useEffect(() => {
+ _metric.updateKey('page', 1);
+ loadPage();
+ }, [
+ drillDownPeriod,
+ period,
+ depsString,
+ metric.metricType,
+ metric.metricOf,
+ metric.metricValue,
+ metric.startType,
+ metric.metricFormat,
+ inView,
+ ]);
+ useEffect(loadPage, [_metric.page]);
+
+ const renderChart = () => {
+ const { metricType, metricOf } = metric;
+ const viewType = metric.viewType;
+ const metricWithData = { ...metric, data };
+
+ if (metricType === FUNNEL) {
+ return (
+
+ );
+ }
+
+ if (metricType === 'predefined' || metricType === ERRORS) {
+ const defaultMetric =
+ metric.data.chart && metric.data.chart.length === 0
+ ? metricWithData
+ : metric;
+ return (
+
+ );
+ }
+
+ if (metricType === TIMESERIES) {
+ const chartData = { ...data };
+ chartData.namesMap = Array.isArray(chartData.namesMap) ? chartData.namesMap.map(n => enabledRows.includes(n) ? n : null) : chartData.namesMap
+ if (viewType === 'lineChart') {
+ return (
+
+ );
+ }
+ if (viewType === 'areaChart') {
+ return (
+
+ );
+ }
+ if (viewType === 'barChart') {
+ return (
+
+ );
+ }
+ if (viewType === 'progressChart') {
+ return (
+
+ );
+ }
+ if (viewType === 'pieChart') {
+ return (
+
+ );
+ }
+ if (viewType === 'progress') {
+ return (
+
+ );
+ }
+ if (viewType === 'table') {
+ return null;
+ }
+ }
+
+ if (metricType === TABLE) {
+ if (metricOf === FilterKey.SESSIONS) {
+ return (
+
+ );
+ }
+ if (metricOf === FilterKey.ERRORS) {
+ return (
+
+ );
+ }
+ if (viewType === TABLE) {
+ return (
+
+ );
+ }
+ }
+ if (metricType === HEATMAP) {
+ if (!props.isPreview) {
+ return metric.thumbnail ? (
+
+

+
+ ) : (
+
+
+ No data available for the selected period.
+
+ );
+ }
+ return ;
+ }
+
+ if (metricType === INSIGHTS) {
+ return ;
+ }
+
+ if (metricType === USER_PATH && data && data.links) {
+ // return ;
+ return (
+ {
+ dashboardStore.drillDownFilter.merge({ filters, page: 1 });
+ }}
+ />
+ );
+ }
+
+ if (metricType === RETENTION) {
+ if (viewType === 'trend') {
+ return (
+
+ );
+ } else if (viewType === 'cohort') {
+ return ;
+ }
+ }
+
+ return Unknown metric type
;
+ };
+ return (
+
+
+
+ {renderChart()}
+ {metric.metricType === TIMESERIES ? (
+
+ ) : null}
+
+
+
+ );
}
export default observer(WidgetChart);
diff --git a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx
new file mode 100644
index 000000000..4fcf017e3
--- /dev/null
+++ b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx
@@ -0,0 +1,161 @@
+import { Button, Table } from 'antd';
+import type { TableProps } from 'antd';
+
+import { Eye, EyeOff } from 'lucide-react';
+import cn from 'classnames';
+import React, { useState } from 'react';
+
+const initTableProps = [
+ {
+ title: 'Series',
+ dataIndex: 'seriesName',
+ key: 'seriesName',
+ sorter: (a, b) => a.seriesName.localeCompare(b.seriesName),
+ },
+ {
+ title: 'Avg.',
+ dataIndex: 'average',
+ key: 'average',
+ sorter: (a, b) => a.average - b.average,
+ },
+];
+
+interface Props {
+ data: { chart: any[]; namesMap: string[] };
+ enabledRows: string[];
+ setEnabledRows: (rows: string[]) => void;
+}
+
+function WidgetDatatable(props: Props) {
+ const [tableProps, setTableProps] =
+ useState(initTableProps);
+ // console.log(params.density / 7, data.chart)
+ const data = props.data;
+
+ const [showTable, setShowTable] = useState(false);
+ const hasMultipleSeries = data.namesMap.length > 1;
+ const [tableData, setTableData] = useState([]);
+
+ const columnNames = new Set();
+ /**
+ * basically we have an array of
+ * { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
+ * which we turn into a table where each series of filters = row;
+ * and each unique time = column
+ * + average for each row
+ * [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
+ * */
+ const series = Object.keys(data.chart[0]).filter(
+ (key) => key !== 'time' && key !== 'timestamp'
+ );
+ React.useEffect(() => {
+ setTableProps(initTableProps);
+ columnNames.clear();
+ data.chart.forEach((p: any) => {
+ columnNames.add(p.time);
+ }); // for example: mon, tue, wed, thu, fri, sat, sun
+ const avg: any = {}; // { seriesName: {itemsCount: 0, total: 0} }
+ const items: Record[] = []; // as many items (rows) as we have series in filter
+ series.forEach((s, i) => {
+ items.push({ seriesName: s, average: 0, key: s });
+ avg[s] = { itemsCount: 0, total: 0 };
+ });
+ const tableCols: {
+ title: string;
+ dataIndex: string;
+ key: string;
+ sorter: any;
+ }[] = [];
+ const uniqueColArr = Array.from(columnNames);
+ uniqueColArr.forEach((name: string, i) => {
+ tableCols.push({
+ title: name,
+ dataIndex: name,
+ key: name,
+ sorter: (a, b) => a[name] - b[name],
+ });
+ const values = data.chart.filter((p) => p.time === name);
+ series.forEach((s) => {
+ avg[s].itemsCount += 1;
+ avg[s].total += values.reduce((acc, curr) => acc + curr[s], 0);
+ const ind = items.findIndex((item) => item.seriesName === s);
+ if (ind === -1) return;
+ items[ind][name] = values.reduce((acc, curr) => acc + curr[s], 0);
+ });
+ });
+ Object.keys(avg).forEach((key) => {
+ const ind = items.findIndex((item) => item.seriesName === key);
+ if (ind === -1) return;
+ items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2);
+ });
+
+ setTableProps((prev) => [...prev, ...tableCols]);
+ setTableData(items);
+ }, [data.chart.length]);
+
+ const rowSelection: TableProps['rowSelection'] = {
+ selectedRowKeys: props.enabledRows,
+ onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
+ props.setEnabledRows(selectedRowKeys as string[]);
+ console.log(
+ `selectedRowKeys: ${selectedRowKeys}`,
+ 'selectedRows: ',
+ selectedRows
+ );
+ },
+ getCheckboxProps: (record: any) => ({
+ name: record.name,
+ checked: false,
+ }),
+ type: 'checkbox',
+ };
+ return hasMultipleSeries ? (
+
+
+
+ : }
+ size={'small'}
+ type={'default'}
+ onClick={() => setShowTable(!showTable)}
+ >
+ {showTable ? 'Hide Table' : 'Show Table'}
+
+
+ {showTable ? (
+
+
+ {/* 1.23+ export menu floater */}
+ {/*
*/}
+ {/*
null },*/}
+ {/* ]}*/}
+ {/* bold*/}
+ {/* customTrigger={*/}
+ {/* */}
+ {/* */}
+ {/*
*/}
+ {/* }*/}
+ {/* />*/}
+ {/**/}
+
+ ) : null}
+
+ ) : null;
+}
+
+export default WidgetDatatable;
diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx
index d795c45bc..51c77c319 100644
--- a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx
@@ -1,37 +1,39 @@
import React from 'react';
import SelectDateRange from 'Shared/SelectDateRange';
-import {useStore} from 'App/mstore';
-import {useObserver} from 'mobx-react-lite';
-import {Space} from "antd";
+import { useStore } from 'App/mstore';
+import { observer } from 'mobx-react-lite';
+import { Space } from 'antd';
-function WidgetDateRange({
- label = 'Time Range',
- }: any) {
- const {dashboardStore} = useStore();
- const period = useObserver(() => dashboardStore.drillDownPeriod);
- const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
+function WidgetDateRange({ label = 'Time Range', comparison = false }: any) {
+ const { dashboardStore } = useStore();
+ const period = comparison ? dashboardStore.comparisonPeriod : dashboardStore.drillDownPeriod;
+ const drillDownFilter = dashboardStore.drillDownFilter;
- const onChangePeriod = (period: any) => {
- dashboardStore.setDrillDownPeriod(period);
- const periodTimestamps = period.toTimestamps();
- drillDownFilter.merge({
- startTimestamp: periodTimestamps.startTimestamp,
- endTimestamp: periodTimestamps.endTimestamp,
- })
+ const onChangePeriod = (period: any) => {
+ if (comparison) dashboardStore.setComparisonPeriod(period);
+ else {
+ dashboardStore.setDrillDownPeriod(period);
+ const periodTimestamps = period.toTimestamps();
+ drillDownFilter.merge({
+ startTimestamp: periodTimestamps.startTimestamp,
+ endTimestamp: periodTimestamps.endTimestamp,
+ });
}
+ };
- return (
-
- {label && {label}}
-
-
- );
+ return (
+
+ {label && {label}}
+
+
+ );
}
-export default WidgetDateRange;
+export default observer(WidgetDateRange);
diff --git a/frontend/app/components/Dashboard/components/WidgetOptions.tsx b/frontend/app/components/Dashboard/components/WidgetOptions.tsx
index 3a3298f8b..16fd82927 100644
--- a/frontend/app/components/Dashboard/components/WidgetOptions.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetOptions.tsx
@@ -1,17 +1,14 @@
import React from 'react';
-import { FUNNEL, HEATMAP, TABLE, USER_PATH } from 'App/constants/card';
-import { Select, Space, Switch } from 'antd';
+import { FUNNEL, HEATMAP, TABLE, TIMESERIES, USER_PATH } from "App/constants/card";
+import { Select, 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';
import { FilterKey } from 'Types/filter/filterType';
import { observer } from 'mobx-react-lite';
+import { ChartLine, ChartArea, ChartColumn, ChartBar, ChartPie, Table } from 'lucide-react'
-interface Props {
-
-}
-
-
-function WidgetOptions(props: Props) {
+function WidgetOptions() {
const { metricStore, dashboardStore } = useStore();
const metric: any = metricStore.instance;
@@ -19,6 +16,22 @@ function WidgetOptions(props: Props) {
metric.update({ metricFormat: value });
};
+ const chartTypes = {
+ lineChart: 'Chart',
+ barChart: 'Column',
+ areaChart: 'Area',
+ pieChart: 'Pie',
+ progressChart: 'Bar',
+ table: 'Table',
+ }
+ const chartIcons = {
+ lineChart: ,
+ barChart: ,
+ areaChart: ,
+ pieChart: ,
+ progressChart: ,
+ table: ,
+ }
return (
{metric.metricType === USER_PATH && (
@@ -31,28 +44,50 @@ function WidgetOptions(props: Props) {
>
-
- Hide Minor Paths
-
+ Hide Minor Paths
)}
- {(metric.metricType === FUNNEL || metric.metricType === TABLE) && metric.metricOf != FilterKey.USERID && metric.metricOf != FilterKey.ERRORS && (
-
- )}
-
- {metric.metricType === HEATMAP ? (
-
+ {metric.metricType === TIMESERIES ? (
+
({
+ key,
+ label:
+ {chartIcons[key]}
+
{name}
+
,
+ })),
+ onClick: ({ key }: any) => {
+ metric.updateKey('viewType', key);
+ },
+ }}
+ >
+
+
) : null}
+ {(metric.metricType === FUNNEL || metric.metricType === TABLE) &&
+ metric.metricOf != FilterKey.USERID &&
+ metric.metricOf != FilterKey.ERRORS && (
+
+ )}
+
+ {metric.metricType === HEATMAP ?
: null}
);
}
diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx
index 06fcddf88..f001c043e 100644
--- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx
@@ -1,11 +1,9 @@
-import { Button, Space, Switch } from 'antd';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React from 'react';
-
-import { HEATMAP, USER_PATH } from 'App/constants/card';
+import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
import { useStore } from 'App/mstore';
-import ClickMapRagePicker from 'Components/Dashboard/components/ClickMapRagePicker';
+import { TIMESERIES } from "../../../../constants/card";
import WidgetWrapper from '../WidgetWrapper';
import WidgetOptions from 'Components/Dashboard/components/WidgetOptions';
@@ -21,14 +19,16 @@ function WidgetPreview(props: Props) {
const { metricStore, dashboardStore } = useStore();
const metric: any = metricStore.instance;
+ // compare logic
return (
<>
-
-
{props.name}
-
+
+
+ {metric.metricType === TIMESERIES ?
: null}
+
{/*{metric.metricType === USER_PATH && (*/}
{/*
*/}
{/*)}*/}
-
{/*{isTimeSeries && (*/}
{/* <>*/}
{/* Visualization*/}
@@ -104,8 +103,6 @@ function WidgetPreview(props: Props) {
{/*>*/}
{/*)}*/}
-
-
{/* add to dashboard */}
{/*{metric.exists() && (*/}
{/* */}
diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx
index ea0cbf829..3712e01ab 100644
--- a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx
+++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx
@@ -4,7 +4,6 @@ import WidgetName from "Components/Dashboard/components/WidgetName";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
-import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
import {Button, Space} from "antd";
import CardViewMenu from "Components/Dashboard/components/WidgetView/CardViewMenu";
@@ -30,7 +29,6 @@ function WidgetViewHeader({onClick, onSave, undoChanges}: Props) {
/>
-