From 817f039ddc8e72738676bc2c6bdfd9ac8ca35682 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Fri, 29 Nov 2024 17:34:34 +0100 Subject: [PATCH] ui: add comparison to more charts, add "metric" chart (BigNumChart.tsx) --- .../Widgets/CustomMetricsWidgets/BarChart.tsx | 60 ++++++------- .../CustomMetricsWidgets/BigNumChart.tsx | 85 +++++++++++++++++++ .../CustomChartTooltip.tsx | 37 +++++--- .../CustomMetricLineChart.tsx | 7 +- .../CustomMetricsWidgets/ProgressBarChart.tsx | 47 +++++++--- .../Dashboard/Widgets/common/Styles.js | 3 + .../components/WidgetChart/WidgetChart.tsx | 29 ++++++- .../WidgetDatatable/WidgetDatatable.tsx | 3 + .../WidgetDateRange/RangeGranularity.tsx | 13 ++- .../WidgetDateRange/WidgetDateRange.tsx | 1 - .../components/WidgetForm/WidgetFormNew.tsx | 15 ---- .../Dashboard/components/WidgetOptions.tsx | 4 +- .../WidgetView/WidgetViewHeader.tsx | 77 +++++++++-------- .../SelectDateRange/SelectDateRange.tsx | 10 +-- frontend/app/constants/card.ts | 1 - frontend/app/mstore/metricStore.ts | 1 - 16 files changed, 267 insertions(+), 126 deletions(-) create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx index a135afcbe..c99ac5b24 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BarChart.tsx @@ -68,7 +68,7 @@ const PillBar = (props) => { function CustomMetricLineChart(props: Props) { const { data = { chart: [], namesMap: [] }, - compData, + compData = { chart: [], namesMap: [] }, params, colors, onClick = () => null, @@ -82,6 +82,14 @@ function CustomMetricLineChart(props: Props) { return item; }); + // we mix 1 original, then 1 comparison, etc + const mergedNameMap: { data: any, isComp: boolean, index: number }[] = []; + for (let i = 0; i < data.namesMap.length; i++) { + mergedNameMap.push({ data: data.namesMap[i], isComp: false, index: i }); + if (compData && compData.namesMap[i]) { + mergedNameMap.push({ data: compData.namesMap[i], isComp: true, index: i }); + } + } return ( - {Array.isArray(data.namesMap) && - data.namesMap.map((key, index) => ( - ( - - )} - legendType={key === 'Total' ? 'none' : 'line'} - activeBar={ - - } - /> - ))} - {compData - ? compData.namesMap.map((key, i) => ( - ( - - )} - legendType={key === 'Total' ? 'none' : 'line'} - activeBar={ - - } - /> - )) - : null} + {mergedNameMap.map((item) => ( + ( + + )} + legendType={'line'} + activeBar={ + + } + /> + ))} ); diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx new file mode 100644 index 000000000..7584466f9 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/BigNumChart.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { CompareTag } from "./CustomChartTooltip"; + +interface Props { + data: { chart: any[], namesMap: string[] }; + compData: { chart: any[], namesMap: string[] } | null; + colors: any; + onClick?: (event, index) => void; + yaxis?: any; + label?: string; + hideLegend?: boolean; +} +function BigNumChart(props: Props) { + const { + data = { chart: [], namesMap: [] }, + compData = { chart: [], namesMap: [] }, + colors, + onClick = () => null, + label = 'Number of Sessions', + } = props; + + const values: { value: number, compData?: number, series: string }[] = []; + for (let i = 0; i < data.namesMap.length; i++) { + if (!data.namesMap[i]) { + continue; + } + + values.push({ + value: data.chart.reduce((acc, curr) => acc + curr[data.namesMap[i]], 0), + compData: compData ? compData.chart.reduce((acc, curr) => acc + curr[compData.namesMap[i]], 0) : undefined, + series: data.namesMap[i], + }); + } + console.log(values, data, compData) + return ( +
+ {values.map((val, i) => ( + + ))} +
+ ) +} + +function BigNum({ color, series, value, label, compData }: { + color: string, + series: string, + value: number, + label: string, + compData?: number, +}) { + const formattedNumber = (num: number) => { + return Intl.NumberFormat().format(num); + } + + const changePercent = React.useMemo(() => { + if (!compData) return 0; + return `${(((value - compData) / compData) * 100).toFixed(2)}%`; + }, [value, compData]) + return ( +
+
+
+
{series}
+
+
+ {formattedNumber(value)} +
+
+ {label} +
+ {compData ? ( + compData} prevValue={changePercent} /> + ) : null} +
+ ) +} + +export default BigNumChart; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx index 0baa35959..af9faf3c1 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { formatTimeOrDate } from 'App/date'; import cn from 'classnames'; -import { ArrowUp, ArrowDown } from 'lucide-react' +import { ArrowUp, ArrowDown } from 'lucide-react'; function CustomTooltip({ active, payload, label }) { if (!active) return; @@ -54,17 +54,10 @@ function CustomTooltip({ active, payload, label }) {
{p.value}
{p.prevValue !== null ? ( -
- {!isHigher(p) ? : } -
- {p.prevValue} -
-
+ ) : null}
@@ -74,4 +67,24 @@ function CustomTooltip({ active, payload, label }) { ); } +export function CompareTag({ + isHigher, + prevValue, +}: { + isHigher: boolean; + prevValue: number | string; +}) { + return ( +
+ {!isHigher ? : } +
{prevValue}
+
+ ); +} + export default CustomTooltip; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx index 884b39f49..46c5ec8cf 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart/CustomMetricLineChart.tsx @@ -32,7 +32,7 @@ interface Props { function CustomMetricLineChart(props: Props) { const { data = { chart: [], namesMap: [] }, - compData, + compData = { chart: [], namesMap: [] }, params, colors, onClick = () => null, @@ -86,13 +86,12 @@ function CustomMetricLineChart(props: Props) { strokeOpacity={key === 'Total' ? 0 : 0.6} legendType={key === 'Total' ? 'none' : 'line'} dot={false} - // strokeDasharray={'4 3'} FOR COPMARISON ONLY activeDot={{ fill: key === 'Total' ? 'transparent' : colors[index], }} /> ) : null)} - {compData ? compData.namesMap.map((key, i) => ( + {compData?.namesMap.map((key, i) => data.namesMap[i] ? ( - )) : null} + ) : null)} ); diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx index 570be0977..c5b717114 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ProgressBarChart.tsx @@ -2,6 +2,7 @@ import React from 'react'; interface Props { data: { chart: any[], namesMap: string[] }; + compData: { chart: any[], namesMap: string[] } | null; params: any; colors: any; onClick?: (event, index) => void; @@ -13,38 +14,64 @@ interface Props { function ProgressBarChart(props: Props) { const { data = { chart: [], namesMap: [] }, + compData = { chart: [], namesMap: [] }, colors, onClick = () => null, label = 'Number of Sessions', } = props; - const getTotalForSeries = (series: string) => { + const getTotalForSeries = (series: string, isComp: boolean) => { + if (isComp) { + return compData.chart.reduce((acc, curr) => acc + curr[series], 0); + } return data.chart.reduce((acc, curr) => acc + curr[series], 0); } - const values = data.namesMap.map((k, i) => { + + const formattedNumber = (num: number) => { + return Intl.NumberFormat().format(num); + } + + // we mix 1 original, then 1 comparison, etc + const mergedNameMap: { data: any, isComp: boolean, index: number }[] = []; + for (let i = 0; i < data.namesMap.length; i++) { + if (!data.namesMap[i]) { + continue; + } + mergedNameMap.push({ data: data.namesMap[i], isComp: false, index: i }); + if (compData && compData.namesMap[i]) { + mergedNameMap.push({ data: compData.namesMap[i], isComp: true, index: i }); + } + } + + const values = mergedNameMap.map((k, i) => { return { - name: k, - value: getTotalForSeries(k) + name: k.data, + value: getTotalForSeries(k.data, k.isComp), + isComp: k.isComp, + index: k.index, } }) 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) => (
-
+
{val.name}
-
+
{formattedNumber(val.value)}
diff --git a/frontend/app/components/Dashboard/Widgets/common/Styles.js b/frontend/app/components/Dashboard/Widgets/common/Styles.js index eef6ef5b8..9075bf6f2 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Styles.js +++ b/frontend/app/components/Dashboard/Widgets/common/Styles.js @@ -70,6 +70,9 @@ export default { lineHeight: '0.75rem', color: '#000', fontSize: '12px' + }, + cursor: { + fill: '#eee' } }, gradientDef: () => ( diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index a5a3f9090..26b4de0f5 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -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 WidgetDatatable from '../WidgetDatatable/WidgetDatatable'; import WidgetPredefinedChart from '../WidgetPredefinedChart'; import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; @@ -274,6 +274,7 @@ function WidgetChart(props: Props) { return ( ); } @@ -302,12 +307,32 @@ function WidgetChart(props: Props) { data={data[0]} colors={colors} params={params} + label={ + metric.metricOf === 'sessionCount' + ? 'Number of Sessions' + : 'Number of Users' + } /> ); } if (viewType === 'table') { return null; } + if (viewType === 'metric') { + return ( + + ) + } } if (metricType === TABLE) { diff --git a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx index a1565fb94..8fb9c60b4 100644 --- a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx @@ -11,12 +11,14 @@ const initTableProps = [ dataIndex: 'seriesName', key: 'seriesName', sorter: (a, b) => a.seriesName.localeCompare(b.seriesName), + fixed: 'left', }, { title: 'Avg.', dataIndex: 'average', key: 'average', sorter: (a, b) => a.average - b.average, + fixed: 'left', }, ]; @@ -132,6 +134,7 @@ function WidgetDatatable(props: Props) { pagination={false} rowSelection={rowSelection} size={'small'} + scroll={{ x: 'max-content' }} /> {/* 1.23+ export menu floater */} {/*
*/} diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx index f77c54546..566b3e574 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx @@ -15,6 +15,7 @@ function RangeGranularity({ return calculateGranularities(period.getDuration()); }, [period]); + const menuProps = { items: granularityOptions, onClick: (item: any) => onDensityChange(item.key), @@ -22,13 +23,19 @@ function RangeGranularity({ const selected = React.useMemo(() => { let selected = 'Custom'; for (const option of granularityOptions) { - if (option.key <= density) { + if (option.key === density) { selected = option.label; break; } } return selected; - }, []) + }, [period, density]) + + React.useEffect(() => { + const defaultOption = Math.max(granularityOptions.length - 2, 0); + onDensityChange(granularityOptions[defaultOption].key); + }, [period]); + return ( diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx index e2b2a60da..ee7284d7a 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx @@ -136,21 +136,6 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => { )} - - {metric.series[0] ? -
- -
- : null} ); }); diff --git a/frontend/app/components/Dashboard/components/WidgetOptions.tsx b/frontend/app/components/Dashboard/components/WidgetOptions.tsx index 8a5de8708..3bec940c1 100644 --- a/frontend/app/components/Dashboard/components/WidgetOptions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetOptions.tsx @@ -6,7 +6,7 @@ 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' +import { ChartLine, ChartArea, ChartColumn, ChartBar, ChartPie, Table, Hash } from 'lucide-react' function WidgetOptions() { const { metricStore } = useStore(); @@ -23,6 +23,7 @@ function WidgetOptions() { pieChart: 'Pie', progressChart: 'Bar', table: 'Table', + metric: 'Metric', } const chartIcons = { lineChart: , @@ -31,6 +32,7 @@ function WidgetOptions() { pieChart: , progressChart: , table: , + metric: , } return (
diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx index 3712e01ab..1f22def67 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetViewHeader.tsx @@ -1,47 +1,50 @@ import React from 'react'; -import cn from "classnames"; -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 {Button, Space} from "antd"; -import CardViewMenu from "Components/Dashboard/components/WidgetView/CardViewMenu"; +import cn from 'classnames'; +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 { Button, Space } from 'antd'; +import CardViewMenu from 'Components/Dashboard/components/WidgetView/CardViewMenu'; interface Props { - onClick?: () => void; - onSave: () => void; - undoChanges?: () => void; + onClick?: () => void; + onSave: () => void; + undoChanges?: () => void; } -function WidgetViewHeader({onClick, onSave, undoChanges}: Props) { - const {metricStore, dashboardStore} = useStore(); - const widget = useObserver(() => metricStore.instance); +function WidgetViewHeader({ onClick, onSave, undoChanges }: Props) { + const { metricStore, dashboardStore } = useStore(); + const widget = useObserver(() => metricStore.instance); - return ( -
+

+ metricStore.merge({ name })} + canEdit={true} + /> +

+ + + - - -
- ); + Update + + + +
+ ); } export default WidgetViewHeader; diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index e9ab568c9..7a6d7a25f 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -81,11 +81,11 @@ function SelectDateRange(props: Props) { setIsCustom(false); }; - const isCustomRange = period ? period.rangeName === CUSTOM_RANGE : false; + const isCustomRange = usedPeriod ? usedPeriod.rangeName === CUSTOM_RANGE : false; const isUSLocale = navigator.language === 'en-US' || navigator.language.startsWith('en-US'); const customRange = isCustomRange - ? period.rangeFormatted( + ? usedPeriod.rangeFormatted( isUSLocale ? 'MMM dd yyyy, hh:mm a' : 'MMM dd yyyy, HH:mm' ) : ''; @@ -97,6 +97,7 @@ function SelectDateRange(props: Props) { selectedValue={selectedValue} onChange={onChange} isCustomRange={isCustomRange} + isCustom={isCustom} customRange={customRange} setIsCustom={setIsCustom} onApplyDateRange={onApplyDateRange} @@ -189,6 +190,7 @@ function AndDateRange({ }, }; + const comparisonValue = isCustomRange && selectedValue ? customRange : selectedValue?.label; return (
{comparison ? ( @@ -200,9 +202,7 @@ function AndDateRange({ } > - {isCustomRange - ? customRange - : `Compare to ${selectedValue ? selectedValue?.label : ''}`} + {`Compare to ${comparisonValue || ''}`}
diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 70808fbdc..08bba396e 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -19,7 +19,6 @@ export const FUNNEL = 'funnel'; export const ERRORS = 'errors'; export const USER_PATH = 'pathAnalysis'; export const RETENTION = 'retention'; -export const FEATURE_ADOPTION = 'featureAdoption'; export const INSIGHTS = 'insights'; export const PERFORMANCE = 'performance'; diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 9e7d9bd53..43eb6b223 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -125,7 +125,6 @@ export default class MetricStore { } } - console.log('ch', obj) Object.assign(this.instance, obj); this.instance.updateKey('hasChanged', updateChangeFlag); }