ui: add drill to summary charts, add more options to card category picker

This commit is contained in:
nick-delirium 2025-01-13 11:53:07 +01:00
parent 7e3ae1c22e
commit 1672113aff
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
8 changed files with 136 additions and 33 deletions

View file

@ -25,6 +25,7 @@ export interface DataProps {
interface ColumnChartProps extends DataProps {
label?: string;
onSeriesFocus?: (name: string) => void;
}
function ColumnChart(props: ColumnChartProps) {
@ -80,6 +81,10 @@ function ColumnChart(props: ColumnChartProps) {
const obs = new ResizeObserver(() => chart.resize());
obs.observe(chartRef.current);
chart.on('click', (event) => {
const focusedSeriesName = event.name;
props.onSeriesFocus?.(focusedSeriesName);
})
return () => {
chart.dispose();

View file

@ -18,7 +18,7 @@ interface PieChartProps {
};
label?: string;
inGrid?: boolean;
onClick?: (filters: any[]) => void;
onSeriesFocus?: (seriesName: string) => void;
}
function PieChart(props: PieChartProps) {
@ -40,10 +40,10 @@ function PieChart(props: PieChartProps) {
return;
}
const largestSlice = pieData.reduce((acc, curr) =>
curr.value > acc.value ? curr : acc
);
const largestVal = largestSlice.value || 1; // avoid divide-by-zero
// const largestSlice = pieData.reduce((acc, curr) =>
// curr.value > acc.value ? curr : acc
// );
// const largestVal = largestSlice.value || 1; // avoid divide-by-zero
const option = {
...defaultOptions,
@ -75,14 +75,14 @@ function PieChart(props: PieChartProps) {
name: d.name,
value: d.value,
label: {
show: d.value / largestVal >= 0.03,
show: false, //d.value / largestVal >= 0.03,
position: 'outside',
formatter: (params: any) => {
return params.value;
},
},
labelLine: {
show: d.value / largestVal >= 0.03,
show: false, // d.value / largestVal >= 0.03,
length: 10,
length2: 20,
lineStyle: { color: '#3EAAAF' },
@ -105,7 +105,8 @@ function PieChart(props: PieChartProps) {
obs.observe(chartRef.current);
chartInstance.on('click', function (params) {
onClick([{ name: params.name, value: params.value }]);
const focusedSeriesName = params.name
props.onSeriesFocus?.(focusedSeriesName);
});
return () => {

View file

@ -1,19 +1,21 @@
import React from 'react'
import { CompareTag } from "./CustomChartTooltip";
import cn from 'classnames'
interface Props {
colors: any;
onClick?: (event, index) => void;
yaxis?: any;
label?: string;
hideLegend?: boolean;
values: { value: number, compData?: number, series: string, valueLabel?: string }[];
onSeriesFocus?: (name: string) => void;
}
function BigNumChart(props: Props) {
const {
colors,
label = 'Number of Sessions',
values,
onSeriesFocus,
} = props;
return (
<div className={'flex flex-row flex-wrap gap-2'} style={{ height: 240 }}>
@ -26,19 +28,21 @@ function BigNumChart(props: Props) {
label={label}
compData={val.compData}
valueLabel={val.valueLabel}
onSeriesFocus={onSeriesFocus}
/>
))}
</div>
)
}
function BigNum({ color, series, value, label, compData, valueLabel }: {
function BigNum({ color, series, value, label, compData, valueLabel, onSeriesFocus }: {
color: string,
series: string,
value: number,
label: string,
compData?: number,
valueLabel?: string,
onSeriesFocus?: (name: string) => void
}) {
const formattedNumber = (num: number) => {
return Intl.NumberFormat().format(num);
@ -53,7 +57,13 @@ function BigNum({ color, series, value, label, compData, valueLabel }: {
return value - compData;
}, [value, compData])
return (
<div className={'flex flex-col flex-auto justify-center items-center rounded-lg transition-all hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer'}>
<div
onClick={() => onSeriesFocus?.(series)}
className={cn(
'flex flex-col flex-auto justify-center items-center rounded-lg transition-all',
'hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer'
)}
>
<div className={'flex items-center gap-2 font-medium text-gray-darkest'}>
<div className={'rounded w-4 h-4'} style={{ background: color }} />
<div>{series}</div>

View file

@ -12,6 +12,8 @@ import {
Combine,
Users,
Sparkles,
Globe,
MonitorSmartphone,
} from 'lucide-react';
import { Icon } from 'UI';
import FilterSeries from 'App/mstore/types/filterSeries';
@ -129,6 +131,18 @@ export const tabItems: Record<string, TabItem[]> = {
type: FilterKey.USERID,
description: 'Identify the users with the most interactions.',
},
{
icon: <Globe width={16} />,
title: 'Top Countries',
type: FilterKey.LOCATION,
description: 'Track the geographical distribution of your audience.',
},
{
icon: <MonitorSmartphone width={16} />,
title: 'Top Devices',
type: FilterKey.USER_DEVICE,
description: 'Explore the devices used by your users.',
}
// { TODO: 1.23+ maybe
// icon: <ArrowDown10 width={16} />,
// title: 'Speed Index by Country',

View file

@ -8,25 +8,40 @@ import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { DROPDOWN_OPTIONS } from 'App/constants/card';
const options = [
{
key: 'all',
label: 'All Types',
},
...DROPDOWN_OPTIONS.map((option) => ({
key: option.value,
label: option.label,
})),
{
key: 'monitors',
label: 'Monitors',
},
{
key: 'web_analytics',
label: 'Web Analytics',
},
]
function MetricViewHeader() {
const { metricStore } = useStore();
const filter = metricStore.filter;
useEffect(() => {
// Set the default sort order to 'desc'
metricStore.updateKey('sort', { by: 'desc' });
}, [metricStore]);
// Handler for dropdown menu selection
const handleMenuClick = ({ key }) => {
metricStore.updateKey('filter', { ...filter, type: key });
};
// Dropdown menu options
const menu = (
<Menu onClick={handleMenuClick}>
<Menu.Item key="all">All Types</Menu.Item>
{DROPDOWN_OPTIONS.map((option) => (
<Menu.Item key={option.value}>{option.label}</Menu.Item>
{options.map((option) => (
<Menu.Item key={option.key}>{option.label}</Menu.Item>
))}
</Menu>
);
@ -39,10 +54,7 @@ function MetricViewHeader() {
<Space>
<Dropdown overlay={menu} trigger={['click']} className="">
<Button type="text" size="small" className="mt-1">
{filter.type === 'all'
? 'All Types'
: DROPDOWN_OPTIONS.find((opt) => opt.value === filter.type)
?.label || 'Select Type'}
{options.find(opt => opt.key === filter.type)?.label || 'Select Type'}
<DownOutlined />
</Button>
</Dropdown>

View file

@ -214,6 +214,10 @@ function WidgetChart(props: Props) {
]);
useEffect(loadPage, [_metric.page]);
const onFocus = (seriesName: string)=> {
metricStore.setFocusedSeriesName(seriesName);
}
const renderChart = React.useCallback(() => {
const { metricType, metricOf } = _metric;
const viewType = _metric.viewType;
@ -351,7 +355,7 @@ function WidgetChart(props: Props) {
compData={compData}
params={params}
colors={colors}
onClick={onChartClick}
onSeriesFocus={onFocus}
label={
_metric.metricOf === 'sessionCount'
? 'Number of Sessions'
@ -366,7 +370,7 @@ function WidgetChart(props: Props) {
<PieChart
inGrid={!props.isPreview}
data={chartData}
onClick={onChartClick}
onSeriesFocus={onFocus}
label={
_metric.metricOf === 'sessionCount'
? 'Number of Sessions'
@ -413,7 +417,7 @@ function WidgetChart(props: Props) {
values={values}
inGrid={!props.isPreview}
colors={colors}
onClick={onChartClick}
onSeriesFocus={onFocus}
label={
_metric.metricOf === 'sessionCount'
? 'Number of Sessions'

View file

@ -24,8 +24,10 @@ function WidgetSessions(props: Props) {
const [data, setData] = useState<any>([]);
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const filteredSessions = getListSessionsBySeries(data, activeSeries);
// all filtering done through series now
const filteredSessions = getListSessionsBySeries(data, 'all');
const { dashboardStore, metricStore, sessionStore, customFieldStore } = useStore();
const focusedSeries = metricStore.focusedSeriesName;
const filter = dashboardStore.drillDownFilter;
const widget = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
@ -44,15 +46,14 @@ function WidgetSessions(props: Props) {
)
}));
const writeOption = ({ value }: any) => setActiveSeries(value.value);
useEffect(() => {
if (!data) return;
const seriesOptions = data.map((item: any) => ({
label: item.seriesName,
if (!widget.series) return;
const seriesOptions = widget.series.map((item: any) => ({
label: item.name,
value: item.seriesId
}));
setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]);
}, [data]);
}, [widget.series]);
const fetchSessions = (metricId: any, filter: any) => {
if (!isMounted()) return;
@ -99,9 +100,10 @@ function WidgetSessions(props: Props) {
};
debounceClickMapSearch(customFilter);
} else {
const usedSeries = focusedSeries ? widget.series.filter((s) => s.name === focusedSeries) : widget.series;
debounceRequest(widget.metricId, {
...filter,
series: widget.series.map((s) => s.toJson()),
series: usedSeries.map((s) => s.toJson()),
page: metricStore.sessionsPage,
limit: metricStore.sessionsPageSize
});
@ -116,9 +118,23 @@ function WidgetSessions(props: Props) {
filter.filters,
depsString,
metricStore.clickMapSearch,
activeSeries
focusedSeries
]);
useEffect(loadData, [metricStore.sessionsPage]);
useEffect(() => {
if (activeSeries === 'all') {
metricStore.setFocusedSeriesName(null);
} else {
metricStore.setFocusedSeriesName(seriesOptions.find((option) => option.value === activeSeries)?.label, false);
}
}, [activeSeries])
useEffect(() => {
if (focusedSeries) {
setActiveSeries(seriesOptions.find((option) => option.label === focusedSeries)?.value || 'all');
} else {
setActiveSeries('all');
}
}, [focusedSeries])
const clearFilters = () => {
metricStore.updateKey('sessionsPage', 1);

View file

@ -18,6 +18,37 @@ import { clickmapFilter } from 'App/types/filter/newFilter';
import { getRE } from 'App/utils';
import { FilterKey } from 'Types/filter/filterType';
const handleFilter = (card: Widget, filterType?: string) => {
const metricType = card.metricType;
if (filterType === 'all' || !filterType || !metricType) {
return true;
}
if ([CATEGORIES.monitors, CATEGORIES.web_analytics].includes(filterType)) {
if (metricType !== 'table') return false;
const metricOf = card.metricOf;
if (filterType === CATEGORIES.monitors) {
return [
FilterKey.ERRORS,
FilterKey.FETCH,
TIMESERIES + '_4xx_requests',
TIMESERIES + '_slow_network_requests'
].includes(metricOf)
}
if (filterType === CATEGORIES.web_analytics) {
return [
FilterKey.LOCATION,
FilterKey.USER_BROWSER,
FilterKey.REFERRER,
FilterKey.USERID,
FilterKey.LOCATION,
FilterKey.USER_DEVICE,
].includes(metricOf)
}
} else {
return filterType === metricType;
}
}
const cardToCategory = (cardType: string) => {
switch (cardType) {
case TIMESERIES:
@ -70,6 +101,8 @@ export default class MetricStore {
cardCategory: string | null = CATEGORIES.product_analytics;
focusedSeriesName: string | null = null;
constructor() {
makeAutoObservable(this);
}
@ -89,7 +122,7 @@ export default class MetricStore {
(this.filter.showMine
? card.owner === JSON.parse(localStorage.getItem('user')!).account.email
: true) &&
(this.filter.type === 'all' || card.metricType === this.filter.type) &&
handleFilter(card, this.filter.type) &&
(!dbIds.length ||
card.dashboards.map((i) => i.dashboardId).some((id) => dbIds.includes(id))) &&
// @ts-ignore
@ -105,6 +138,14 @@ export default class MetricStore {
this.instance.update(metric || new Widget());
}
setFocusedSeriesName(name: string | null, resetOnSame = true) {
if (this.focusedSeriesName === name && resetOnSame) {
this.focusedSeriesName = null;
} else {
this.focusedSeriesName = name;
}
}
setCardCategory(category: string) {
this.cardCategory = category;
}