ui: add drill to summary charts, add more options to card category picker
This commit is contained in:
parent
7e3ae1c22e
commit
1672113aff
8 changed files with 136 additions and 33 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue