mirror of https://github.com/buster-so/buster.git
commit
e3c1bb9d8f
|
@ -1,3 +1,4 @@
|
||||||
|
import { AppPageLayout } from '@/components/ui/layouts';
|
||||||
import { checkIfUserIsAdmin_server } from '@/context/Users/checkIfUserIsAdmin';
|
import { checkIfUserIsAdmin_server } from '@/context/Users/checkIfUserIsAdmin';
|
||||||
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
|
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
@ -14,5 +15,5 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <AppPageLayout scrollable>{children}</AppPageLayout>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default function Page() {
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="px-[30px] pt-[46px]">
|
<div className="px-[30px] pt-[46px]">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="User Management"
|
title="User management"
|
||||||
description="Manage your organization's users and their permissions"
|
description="Manage your organization's users and their permissions"
|
||||||
type="alternate"
|
type="alternate"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -88,8 +88,8 @@ export const LoginForm: React.FC<{}> = ({}) => {
|
||||||
if (res?.error) throw res.error;
|
if (res?.error) throw res.error;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
errorFallback(error);
|
errorFallback(error);
|
||||||
|
setLoading(null);
|
||||||
}
|
}
|
||||||
setLoading('azure');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSignUp = useMemoizedFn(async (d: { email: string; password: string }) => {
|
const onSignUp = useMemoizedFn(async (d: { email: string; password: string }) => {
|
||||||
|
@ -211,18 +211,11 @@ const LoginOptions: React.FC<{
|
||||||
<WelcomeText signUpFlow={signUpFlow} />
|
<WelcomeText signUpFlow={signUpFlow} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<div className="mt-6 mb-4 flex flex-col space-y-3">
|
||||||
className="my-6 space-y-3"
|
|
||||||
onSubmit={(v) => {
|
|
||||||
v.preventDefault();
|
|
||||||
onSubmitClickPreflight({
|
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
<Button
|
<Button
|
||||||
prefix={<Google />}
|
prefix={<Google />}
|
||||||
size={'tall'}
|
size={'tall'}
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearAllCookies();
|
clearAllCookies();
|
||||||
onSignInWithGoogle();
|
onSignInWithGoogle();
|
||||||
|
@ -235,6 +228,7 @@ const LoginOptions: React.FC<{
|
||||||
<Button
|
<Button
|
||||||
prefix={<Github />}
|
prefix={<Github />}
|
||||||
size={'tall'}
|
size={'tall'}
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearAllCookies();
|
clearAllCookies();
|
||||||
onSignInWithGithub();
|
onSignInWithGithub();
|
||||||
|
@ -247,6 +241,7 @@ const LoginOptions: React.FC<{
|
||||||
<Button
|
<Button
|
||||||
prefix={<Microsoft />}
|
prefix={<Microsoft />}
|
||||||
size={'tall'}
|
size={'tall'}
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearAllCookies();
|
clearAllCookies();
|
||||||
onSignInWithAzure();
|
onSignInWithAzure();
|
||||||
|
@ -256,8 +251,18 @@ const LoginOptions: React.FC<{
|
||||||
tabIndex={3}>
|
tabIndex={3}>
|
||||||
{!signUpFlow ? `Continue with Azure` : `Sign up with Azure`}
|
{!signUpFlow ? `Continue with Azure` : `Sign up with Azure`}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-border h-[0.5px] w-full" />
|
<form
|
||||||
|
className="space-y-3"
|
||||||
|
onSubmit={(v) => {
|
||||||
|
v.preventDefault();
|
||||||
|
onSubmitClickPreflight({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<div className="bg-border mb-4 h-[0.5px] w-full" />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
|
@ -333,7 +338,7 @@ const LoginOptions: React.FC<{
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-2 pt-0">
|
<div className="mt-2 flex flex-col gap-y-2">
|
||||||
<AlreadyHaveAccount
|
<AlreadyHaveAccount
|
||||||
setErrorMessages={setErrorMessages}
|
setErrorMessages={setErrorMessages}
|
||||||
setPassword2={setPassword2}
|
setPassword2={setPassword2}
|
||||||
|
|
|
@ -16,9 +16,11 @@ export const AvatarUserButton = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="hover:bg-item-hover active:bg-item-active flex w-full cursor-pointer items-center gap-x-2 rounded-md p-2">
|
className="hover:bg-item-hover active:bg-item-active flex w-full cursor-pointer items-center gap-x-2 rounded-md p-2">
|
||||||
<Avatar size={32} fallbackClassName="text-base" image={avatarUrl} name={username} />
|
<Avatar size={32} fallbackClassName="text-base" image={avatarUrl} name={username} />
|
||||||
<div className="flex flex-col gap-y-0.5">
|
<div className="flex w-full flex-col gap-y-0.5 overflow-hidden">
|
||||||
<Text className="flex-grow">{username}</Text>
|
<Text truncate className="flex-grow">
|
||||||
<Text size={'sm'} variant={'secondary'}>
|
{username}
|
||||||
|
</Text>
|
||||||
|
<Text truncate size={'sm'} variant={'secondary'}>
|
||||||
{email}
|
{email}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,13 +22,15 @@ export const BusterChartComponent: React.FC<BusterChartRenderComponentProps> = (
|
||||||
selectedChartType,
|
selectedChartType,
|
||||||
selectedAxis
|
selectedAxis
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
datasetOptions,
|
datasetOptions,
|
||||||
dataTrendlineOptions,
|
dataTrendlineOptions,
|
||||||
y2AxisKeys,
|
y2AxisKeys,
|
||||||
yAxisKeys,
|
yAxisKeys,
|
||||||
tooltipKeys,
|
tooltipKeys,
|
||||||
hasMismatchedTooltipsAndMeasures
|
hasMismatchedTooltipsAndMeasures,
|
||||||
|
isDownsampled
|
||||||
} = useDatasetOptions({
|
} = useDatasetOptions({
|
||||||
data: dataProp,
|
data: dataProp,
|
||||||
selectedAxis,
|
selectedAxis,
|
||||||
|
@ -52,7 +54,8 @@ export const BusterChartComponent: React.FC<BusterChartRenderComponentProps> = (
|
||||||
y2AxisKeys,
|
y2AxisKeys,
|
||||||
yAxisKeys,
|
yAxisKeys,
|
||||||
tooltipKeys,
|
tooltipKeys,
|
||||||
hasMismatchedTooltipsAndMeasures
|
hasMismatchedTooltipsAndMeasures,
|
||||||
|
isDownsampled
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
props,
|
props,
|
||||||
|
@ -62,7 +65,8 @@ export const BusterChartComponent: React.FC<BusterChartRenderComponentProps> = (
|
||||||
y2AxisKeys,
|
y2AxisKeys,
|
||||||
yAxisKeys,
|
yAxisKeys,
|
||||||
hasMismatchedTooltipsAndMeasures,
|
hasMismatchedTooltipsAndMeasures,
|
||||||
tooltipKeys
|
tooltipKeys,
|
||||||
|
isDownsampled
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { DEFAULT_CHART_CONFIG, DEFAULT_COLUMN_METADATA } from '@/api/asset_interfaces/metric';
|
import { DEFAULT_CHART_CONFIG, DEFAULT_COLUMN_METADATA } from '@/api/asset_interfaces/metric';
|
||||||
import { BusterChartJSLegendWrapper } from './BusterChartJSLegendWrapper';
|
import { BusterChartJSLegendWrapper } from './BusterChartJSLegendWrapper';
|
||||||
import { ChartJSOrUndefined } from './core/types';
|
import { ChartJSOrUndefined } from './core/types';
|
||||||
import { useMemoizedFn, useMount } from '@/hooks';
|
import { useMemoizedFn } from '@/hooks';
|
||||||
import { BusterChartJSComponent } from './BusterChartJSComponent';
|
import { BusterChartJSComponent } from './BusterChartJSComponent';
|
||||||
import type { BusterChartComponentProps } from '../interfaces';
|
import type { BusterChartComponentProps } from '../interfaces';
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ export const BusterChartJS: React.FC<BusterChartComponentProps> = ({
|
||||||
colors={colors}
|
colors={colors}
|
||||||
chartRef={chartRef}
|
chartRef={chartRef}
|
||||||
datasetOptions={datasetOptions}
|
datasetOptions={datasetOptions}
|
||||||
|
isDownsampled={props.isDownsampled}
|
||||||
pieMinimumSlicePercentage={pieMinimumSlicePercentage}>
|
pieMinimumSlicePercentage={pieMinimumSlicePercentage}>
|
||||||
<BusterChartJSComponent
|
<BusterChartJSComponent
|
||||||
ref={chartRef}
|
ref={chartRef}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { useChartSpecificOptions } from './hooks/useChartSpecificOptions';
|
||||||
import { BusterChartTypeComponentProps } from '../interfaces/chartComponentInterfaces';
|
import { BusterChartTypeComponentProps } from '../interfaces/chartComponentInterfaces';
|
||||||
import { useTrendlines } from './hooks/useTrendlines';
|
import { useTrendlines } from './hooks/useTrendlines';
|
||||||
import { ScatterAxis } from '@/api/asset_interfaces/metric/charts';
|
import { ScatterAxis } from '@/api/asset_interfaces/metric/charts';
|
||||||
import { useMount } from '@/hooks';
|
|
||||||
|
|
||||||
export const BusterChartJSComponent = React.memo(
|
export const BusterChartJSComponent = React.memo(
|
||||||
React.forwardRef<ChartJSOrUndefined, BusterChartTypeComponentProps>(
|
React.forwardRef<ChartJSOrUndefined, BusterChartTypeComponentProps>(
|
||||||
|
|
|
@ -24,6 +24,7 @@ interface BusterChartJSLegendWrapperProps {
|
||||||
chartRef: React.RefObject<ChartJSOrUndefined | null>;
|
chartRef: React.RefObject<ChartJSOrUndefined | null>;
|
||||||
datasetOptions: DatasetOption[];
|
datasetOptions: DatasetOption[];
|
||||||
pieMinimumSlicePercentage: NonNullable<BusterChartProps['pieMinimumSlicePercentage']>;
|
pieMinimumSlicePercentage: NonNullable<BusterChartProps['pieMinimumSlicePercentage']>;
|
||||||
|
isDownsampled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperProps>(
|
export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperProps>(
|
||||||
|
@ -45,7 +46,8 @@ export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperP
|
||||||
barGroupType,
|
barGroupType,
|
||||||
colors,
|
colors,
|
||||||
datasetOptions,
|
datasetOptions,
|
||||||
pieMinimumSlicePercentage
|
pieMinimumSlicePercentage,
|
||||||
|
isDownsampled
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
renderLegend,
|
renderLegend,
|
||||||
|
@ -81,6 +83,7 @@ export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperP
|
||||||
renderLegend={renderLegend}
|
renderLegend={renderLegend}
|
||||||
legendItems={legendItems}
|
legendItems={legendItems}
|
||||||
showLegend={showLegend}
|
showLegend={showLegend}
|
||||||
|
isDownsampled={isDownsampled}
|
||||||
showLegendHeadline={showLegendHeadline}
|
showLegendHeadline={showLegendHeadline}
|
||||||
inactiveDatasets={inactiveDatasets}
|
inactiveDatasets={inactiveDatasets}
|
||||||
onHoverItem={onHoverItem}
|
onHoverItem={onHoverItem}
|
||||||
|
|
|
@ -113,7 +113,7 @@ ChartJS.defaults.font = {
|
||||||
scale.ticks.autoSkipPadding = 4;
|
scale.ticks.autoSkipPadding = 4;
|
||||||
scale.ticks.align = 'center';
|
scale.ticks.align = 'center';
|
||||||
scale.ticks.callback = function (value, index, values) {
|
scale.ticks.callback = function (value, index, values) {
|
||||||
return truncateText(this.getLabelForValue(index), 18);
|
return truncateText(this.getLabelForValue(value as number), 18);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
ChartType,
|
ChartType,
|
||||||
ComboChartAxis
|
ComboChartAxis
|
||||||
} from '@/api/asset_interfaces/metric/charts';
|
} from '@/api/asset_interfaces/metric/charts';
|
||||||
import { useDebounceEffect, useMemoizedFn } from '@/hooks';
|
import { useDebounceEffect, useDebounceFn, useMemoizedFn } from '@/hooks';
|
||||||
import type { IBusterMetricChartConfig } from '@/api/asset_interfaces/metric';
|
import type { IBusterMetricChartConfig } from '@/api/asset_interfaces/metric';
|
||||||
import {
|
import {
|
||||||
addLegendHeadlines,
|
addLegendHeadlines,
|
||||||
|
@ -18,7 +18,8 @@ import {
|
||||||
} from '../../../BusterChartLegend';
|
} from '../../../BusterChartLegend';
|
||||||
import { getLegendItems } from './helper';
|
import { getLegendItems } from './helper';
|
||||||
import { DatasetOption } from '../../../chartHooks';
|
import { DatasetOption } from '../../../chartHooks';
|
||||||
import { ANIMATION_THRESHOLD } from '../../../config';
|
import { ANIMATION_THRESHOLD, LEGEND_ANIMATION_THRESHOLD } from '../../../config';
|
||||||
|
import { timeout } from '@/lib';
|
||||||
|
|
||||||
interface UseBusterChartJSLegendProps {
|
interface UseBusterChartJSLegendProps {
|
||||||
chartRef: React.RefObject<ChartJSOrUndefined | null>;
|
chartRef: React.RefObject<ChartJSOrUndefined | null>;
|
||||||
|
@ -57,6 +58,9 @@ export const useBusterChartJSLegend = ({
|
||||||
}: UseBusterChartJSLegendProps): UseChartLengendReturnValues => {
|
}: UseBusterChartJSLegendProps): UseChartLengendReturnValues => {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [isUpdatingChart, setIsUpdatingChart] = useState(false);
|
const [isUpdatingChart, setIsUpdatingChart] = useState(false);
|
||||||
|
const [numberOfDataPoints, setNumberOfDataPoints] = useState(0);
|
||||||
|
const isLargeDataset = numberOfDataPoints > LEGEND_ANIMATION_THRESHOLD;
|
||||||
|
const legendTimeoutDuration = isLargeDataset ? 95 : 0;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
inactiveDatasets,
|
inactiveDatasets,
|
||||||
|
@ -105,6 +109,13 @@ export const useBusterChartJSLegend = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numberOfPoints =
|
||||||
|
chartRef.current?.data.datasets.reduce<number>((acc, dataset) => {
|
||||||
|
if (dataset.hidden) return acc;
|
||||||
|
return acc + dataset.data.length;
|
||||||
|
}, 0) || 0;
|
||||||
|
setNumberOfDataPoints(numberOfPoints);
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setLegendItems(items);
|
setLegendItems(items);
|
||||||
});
|
});
|
||||||
|
@ -144,19 +155,33 @@ export const useBusterChartJSLegend = ({
|
||||||
chartjs.update();
|
chartjs.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
const onLegendItemClick = useMemoizedFn((item: BusterChartLegendItem) => {
|
const { run: debouncedChartUpdate } = useDebounceFn(
|
||||||
|
useMemoizedFn((timeoutDuration: number) => {
|
||||||
|
const chartjs = chartRef.current;
|
||||||
|
if (!chartjs) return;
|
||||||
|
// Schedule the heavy update operation with minimal delay to allow UI to remain responsive
|
||||||
|
setTimeout(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
chartjs.update();
|
||||||
|
|
||||||
|
// Set a timeout to turn off loading state after the update is complete
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsUpdatingChart(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, timeoutDuration);
|
||||||
|
}),
|
||||||
|
{ wait: isLargeDataset ? 250 : 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLegendItemClick = useMemoizedFn(async (item: BusterChartLegendItem) => {
|
||||||
const chartjs = chartRef.current;
|
const chartjs = chartRef.current;
|
||||||
|
|
||||||
if (!chartjs) return;
|
if (!chartjs) return;
|
||||||
|
|
||||||
const data = chartjs.data;
|
const data = chartjs.data;
|
||||||
const hasAnimation = chartjs.options.animation !== false;
|
|
||||||
const numberOfPoints = data.datasets.reduce((acc, dataset) => acc + dataset.data.length, 0);
|
|
||||||
const isLargeChart = numberOfPoints > ANIMATION_THRESHOLD;
|
|
||||||
const timeoutDuration = isLargeChart && hasAnimation ? 125 : 0;
|
|
||||||
|
|
||||||
// Set updating state
|
// Set updating state
|
||||||
if (timeoutDuration) setIsUpdatingChart(true);
|
if (legendTimeoutDuration) setIsUpdatingChart(true);
|
||||||
|
|
||||||
// Update dataset visibility state
|
// Update dataset visibility state
|
||||||
setInactiveDatasets((prev) => ({
|
setInactiveDatasets((prev) => ({
|
||||||
|
@ -164,6 +189,8 @@ export const useBusterChartJSLegend = ({
|
||||||
[item.id]: prev[item.id] ? !prev[item.id] : true
|
[item.id]: prev[item.id] ? !prev[item.id] : true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
await timeout(legendTimeoutDuration);
|
||||||
|
|
||||||
// Defer visual updates to prevent UI blocking
|
// Defer visual updates to prevent UI blocking
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// This is a synchronous, lightweight operation that toggles visibility flags
|
// This is a synchronous, lightweight operation that toggles visibility flags
|
||||||
|
@ -177,53 +204,48 @@ export const useBusterChartJSLegend = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the heavy update operation with minimal delay to allow UI to remain responsive
|
debouncedChartUpdate(legendTimeoutDuration);
|
||||||
setTimeout(() => {
|
|
||||||
startTransition(() => {
|
|
||||||
chartjs.update();
|
|
||||||
|
|
||||||
// Set a timeout to turn off loading state after the update is complete
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setIsUpdatingChart(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, timeoutDuration);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const onLegendItemFocus = useMemoizedFn((item: BusterChartLegendItem) => {
|
const onLegendItemFocus = useMemoizedFn(async (item: BusterChartLegendItem) => {
|
||||||
const chartjs = chartRef.current;
|
const chartjs = chartRef.current;
|
||||||
if (!chartjs) return;
|
if (!chartjs) return;
|
||||||
|
|
||||||
const datasets = chartjs.data.datasets.filter((dataset) => !dataset.hidden);
|
if (legendTimeoutDuration) setIsUpdatingChart(true);
|
||||||
const hasMultipleDatasets = datasets?.length > 1;
|
|
||||||
const assosciatedDatasetIndex = datasets?.findIndex((dataset) => dataset.label === item.id);
|
|
||||||
|
|
||||||
if (hasMultipleDatasets) {
|
// Defer visual updates to prevent UI blocking
|
||||||
const hasOtherDatasetsVisible = datasets?.some(
|
requestAnimationFrame(() => {
|
||||||
(dataset, index) =>
|
const datasets = chartjs.data.datasets.filter((dataset) => !dataset.hidden);
|
||||||
dataset.label !== item.id && chartjs.isDatasetVisible(index) && !dataset.hidden
|
const hasMultipleDatasets = datasets?.length > 1;
|
||||||
);
|
const assosciatedDatasetIndex = datasets?.findIndex((dataset) => dataset.label === item.id);
|
||||||
const inactiveDatasetsRecord: Record<string, boolean> = {};
|
|
||||||
if (hasOtherDatasetsVisible) {
|
if (hasMultipleDatasets) {
|
||||||
datasets?.forEach((dataset, index) => {
|
const hasOtherDatasetsVisible = datasets?.some(
|
||||||
const value = index === assosciatedDatasetIndex;
|
(dataset, index) =>
|
||||||
chartjs.setDatasetVisibility(index, value);
|
dataset.label !== item.id && chartjs.isDatasetVisible(index) && !dataset.hidden
|
||||||
inactiveDatasetsRecord[dataset.label!] = !value;
|
);
|
||||||
});
|
const inactiveDatasetsRecord: Record<string, boolean> = {};
|
||||||
} else {
|
if (hasOtherDatasetsVisible) {
|
||||||
datasets?.forEach((dataset, index) => {
|
datasets?.forEach((dataset, index) => {
|
||||||
chartjs.setDatasetVisibility(index, true);
|
const value = index === assosciatedDatasetIndex;
|
||||||
inactiveDatasetsRecord[dataset.label!] = false;
|
chartjs.setDatasetVisibility(index, value);
|
||||||
});
|
inactiveDatasetsRecord[dataset.label!] = !value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
datasets?.forEach((dataset, index) => {
|
||||||
|
chartjs.setDatasetVisibility(index, true);
|
||||||
|
inactiveDatasetsRecord[dataset.label!] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setInactiveDatasets((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...inactiveDatasetsRecord
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
setInactiveDatasets((prev) => ({
|
|
||||||
...prev,
|
|
||||||
...inactiveDatasetsRecord
|
|
||||||
}));
|
|
||||||
|
|
||||||
chartjs.update();
|
debouncedChartUpdate(legendTimeoutDuration);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useDebounceEffect(
|
useDebounceEffect(
|
||||||
|
@ -231,7 +253,7 @@ export const useBusterChartJSLegend = ({
|
||||||
calculateLegendItems();
|
calculateLegendItems();
|
||||||
},
|
},
|
||||||
[selectedChartType],
|
[selectedChartType],
|
||||||
{ wait: 3 }
|
{ wait: 4 }
|
||||||
);
|
);
|
||||||
|
|
||||||
//immediate items
|
//immediate items
|
||||||
|
|
|
@ -167,8 +167,8 @@ export const useOptions = ({
|
||||||
const interaction = useInteractions({ selectedChartType, barLayout });
|
const interaction = useInteractions({ selectedChartType, barLayout });
|
||||||
|
|
||||||
const numberOfSources = useMemo(() => {
|
const numberOfSources = useMemo(() => {
|
||||||
return datasetOptions.reduce((acc, curr) => {
|
return datasetOptions.reduce((acc, dataset) => {
|
||||||
return acc + curr.source.length;
|
return acc + dataset.source.length;
|
||||||
}, 0);
|
}, 0);
|
||||||
}, [datasetOptions]);
|
}, [datasetOptions]);
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ interface UseTooltipOptionsProps {
|
||||||
colors: string[];
|
colors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_TOOLTIP_CACHE_SIZE = 30;
|
||||||
|
|
||||||
export const useTooltipOptions = ({
|
export const useTooltipOptions = ({
|
||||||
columnLabelFormats,
|
columnLabelFormats,
|
||||||
selectedChartType,
|
selectedChartType,
|
||||||
|
@ -129,7 +131,6 @@ export const useTooltipOptions = ({
|
||||||
selectedChartType,
|
selectedChartType,
|
||||||
matchedCacheItem,
|
matchedCacheItem,
|
||||||
keyToUsePercentage,
|
keyToUsePercentage,
|
||||||
columnSettings,
|
|
||||||
hasCategoryAxis,
|
hasCategoryAxis,
|
||||||
hasMultipleMeasures,
|
hasMultipleMeasures,
|
||||||
barGroupType,
|
barGroupType,
|
||||||
|
@ -137,6 +138,9 @@ export const useTooltipOptions = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (Object.keys(tooltipCache.current).length > MAX_TOOLTIP_CACHE_SIZE) {
|
||||||
|
tooltipCache.current = {};
|
||||||
|
}
|
||||||
tooltipCache.current[key] = result;
|
tooltipCache.current[key] = result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -225,7 +229,6 @@ const externalTooltip = (
|
||||||
selectedChartType: NonNullable<BusterChartProps['selectedChartType']>,
|
selectedChartType: NonNullable<BusterChartProps['selectedChartType']>,
|
||||||
matchedCacheItem: string | undefined,
|
matchedCacheItem: string | undefined,
|
||||||
keyToUsePercentage: string[],
|
keyToUsePercentage: string[],
|
||||||
columnSettings: NonNullable<BusterChartProps['columnSettings']>,
|
|
||||||
hasCategoryAxis: boolean,
|
hasCategoryAxis: boolean,
|
||||||
hasMultipleMeasures: boolean,
|
hasMultipleMeasures: boolean,
|
||||||
barGroupType: BusterChartProps['barGroupType'],
|
barGroupType: BusterChartProps['barGroupType'],
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from '../chartHooks/useChartWrapperProvider';
|
} from '../chartHooks/useChartWrapperProvider';
|
||||||
import { cn } from '@/lib/classMerge';
|
import { cn } from '@/lib/classMerge';
|
||||||
import { CircleSpinnerLoader } from '../../loaders';
|
import { CircleSpinnerLoader } from '../../loaders';
|
||||||
|
import { DownsampleAlert } from './DownsampleAlert';
|
||||||
|
|
||||||
export type BusterChartLegendWrapper = {
|
export type BusterChartLegendWrapper = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -18,6 +19,7 @@ export type BusterChartLegendWrapper = {
|
||||||
className: string | undefined;
|
className: string | undefined;
|
||||||
animateLegend: boolean;
|
animateLegend: boolean;
|
||||||
isUpdatingChart?: boolean;
|
isUpdatingChart?: boolean;
|
||||||
|
isDownsampled: boolean;
|
||||||
onHoverItem: (item: BusterChartLegendItem, isHover: boolean) => void;
|
onHoverItem: (item: BusterChartLegendItem, isHover: boolean) => void;
|
||||||
onLegendItemClick: (item: BusterChartLegendItem) => void;
|
onLegendItemClick: (item: BusterChartLegendItem) => void;
|
||||||
onLegendItemFocus: ((item: BusterChartLegendItem) => void) | undefined;
|
onLegendItemFocus: ((item: BusterChartLegendItem) => void) | undefined;
|
||||||
|
@ -34,6 +36,7 @@ export const BusterChartLegendWrapper: React.FC<BusterChartLegendWrapper> = Reac
|
||||||
animateLegend,
|
animateLegend,
|
||||||
className,
|
className,
|
||||||
isUpdatingChart,
|
isUpdatingChart,
|
||||||
|
isDownsampled,
|
||||||
onHoverItem,
|
onHoverItem,
|
||||||
onLegendItemClick,
|
onLegendItemClick,
|
||||||
onLegendItemFocus
|
onLegendItemFocus
|
||||||
|
@ -42,7 +45,8 @@ export const BusterChartLegendWrapper: React.FC<BusterChartLegendWrapper> = Reac
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartLegendWrapperProvider inactiveDatasets={inactiveDatasets}>
|
<ChartLegendWrapperProvider inactiveDatasets={inactiveDatasets}>
|
||||||
<div className={cn(className, 'flex h-full w-full flex-col overflow-hidden')}>
|
<div
|
||||||
|
className={cn('legend-wrapper flex h-full w-full flex-col overflow-hidden', className)}>
|
||||||
{renderLegend && (
|
{renderLegend && (
|
||||||
<BusterChartLegend
|
<BusterChartLegend
|
||||||
show={showLegend}
|
show={showLegend}
|
||||||
|
@ -56,9 +60,10 @@ export const BusterChartLegendWrapper: React.FC<BusterChartLegendWrapper> = Reac
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center overflow-hidden">
|
<div className="relative flex h-full w-full flex-col items-center justify-center overflow-hidden">
|
||||||
{isUpdatingChart && <LoadingOverlay />}
|
{isUpdatingChart && <LoadingOverlay />}
|
||||||
{children}
|
{children}
|
||||||
|
{isDownsampled && <DownsampleAlert isDownsampled={isDownsampled} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ChartLegendWrapperProvider>
|
</ChartLegendWrapperProvider>
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Text } from '../../typography/Text';
|
||||||
|
import { TriangleWarning } from '../../icons/NucleoIconFilled';
|
||||||
|
import { Popover } from '../../popover';
|
||||||
|
import { DOWNSIZE_SAMPLE_THRESHOLD } from '../config';
|
||||||
|
import { cn } from '@/lib/classMerge';
|
||||||
|
import { Xmark } from '../../icons';
|
||||||
|
|
||||||
|
export const DownsampleAlert = React.memo(({ isDownsampled }: { isDownsampled: boolean }) => {
|
||||||
|
const [close, setClose] = useState(false);
|
||||||
|
const [onHover, setOnHover] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setClose(false);
|
||||||
|
}, [isDownsampled]);
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute right-0 bottom-0 left-0 w-full px-1 pb-0"
|
||||||
|
onMouseEnter={() => setOnHover(true)}
|
||||||
|
onMouseLeave={() => setOnHover(false)}>
|
||||||
|
<Popover
|
||||||
|
align="center"
|
||||||
|
side="top"
|
||||||
|
open={open}
|
||||||
|
sideOffset={8}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
content={
|
||||||
|
<div className="max-w-68">
|
||||||
|
<Text>{`This chart has been downsampled to ${DOWNSIZE_SAMPLE_THRESHOLD} data points to improve performance. Click the results tab or download the data to see all points.`}</Text>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div
|
||||||
|
onClick={() => open && setClose(true)}
|
||||||
|
className={cn(
|
||||||
|
'group relative z-10 flex h-6 w-full cursor-pointer items-center justify-center overflow-hidden rounded-sm border border-yellow-300 bg-yellow-200 px-1.5 text-sm text-yellow-700 shadow transition-all duration-300 hover:bg-yellow-300'
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute flex items-center space-x-1 transition-all duration-300',
|
||||||
|
open ? 'scale-50 opacity-0' : 'scale-100 opacity-100'
|
||||||
|
)}>
|
||||||
|
<TriangleWarning />
|
||||||
|
<span>Downsampled</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute flex items-center space-x-1 transition-all duration-300',
|
||||||
|
!open ? 'scale-50 opacity-0' : 'scale-100 opacity-100'
|
||||||
|
)}>
|
||||||
|
<Xmark />
|
||||||
|
<span>Close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DownsampleAlert.displayName = 'DownsampleAlert';
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { type BusterChartProps, type ScatterAxis } from '@/api/asset_interfaces/metric/charts';
|
||||||
type BusterChartProps,
|
|
||||||
type ColumnLabelFormat,
|
|
||||||
type ScatterAxis
|
|
||||||
} from '@/api/asset_interfaces/metric/charts';
|
|
||||||
import { createDimension } from './datasetHelpers_BarLinePie';
|
import { createDimension } from './datasetHelpers_BarLinePie';
|
||||||
import { appendToKeyValueChain } from './groupingHelpers';
|
import { appendToKeyValueChain } from './groupingHelpers';
|
||||||
import { DatasetOption } from './interfaces';
|
import { DatasetOption } from './interfaces';
|
||||||
|
@ -68,10 +64,7 @@ export const processScatterData = (
|
||||||
measureFields.forEach((measure) => {
|
measureFields.forEach((measure) => {
|
||||||
categories.forEach((category) => {
|
categories.forEach((category) => {
|
||||||
const columnLabelFormat = columnLabelFormats[measure];
|
const columnLabelFormat = columnLabelFormats[measure];
|
||||||
const replaceMissingDataWith =
|
const replaceMissingDataWith = defaultReplaceMissingDataWith;
|
||||||
columnLabelFormat?.replaceMissingDataWith !== undefined
|
|
||||||
? columnLabelFormat?.replaceMissingDataWith
|
|
||||||
: defaultReplaceMissingDataWith;
|
|
||||||
if (categoryKey === category) {
|
if (categoryKey === category) {
|
||||||
const value = item[measure] || replaceMissingDataWith;
|
const value = item[measure] || replaceMissingDataWith;
|
||||||
row.push(value as string | number);
|
row.push(value as string | number);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
import { type TrendlineDataset, useDataTrendlineOptions } from './useDataTrendlineOptions';
|
import { type TrendlineDataset, useDataTrendlineOptions } from './useDataTrendlineOptions';
|
||||||
import type { DatasetOption } from './interfaces';
|
import type { DatasetOption } from './interfaces';
|
||||||
import { DEFAULT_COLUMN_LABEL_FORMAT } from '@/api/asset_interfaces/metric';
|
import { DEFAULT_COLUMN_LABEL_FORMAT } from '@/api/asset_interfaces/metric';
|
||||||
|
import { DOWNSIZE_SAMPLE_THRESHOLD } from '../../config';
|
||||||
|
|
||||||
type DatasetHookResult = {
|
type DatasetHookResult = {
|
||||||
datasetOptions: DatasetOption[];
|
datasetOptions: DatasetOption[];
|
||||||
|
@ -42,6 +43,7 @@ type DatasetHookResult = {
|
||||||
y2AxisKeys: string[];
|
y2AxisKeys: string[];
|
||||||
tooltipKeys: string[];
|
tooltipKeys: string[];
|
||||||
hasMismatchedTooltipsAndMeasures: boolean;
|
hasMismatchedTooltipsAndMeasures: boolean;
|
||||||
|
isDownsampled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DatasetHookParams = {
|
type DatasetHookParams = {
|
||||||
|
@ -125,6 +127,10 @@ export const useDatasetOptions = (params: DatasetHookParams): DatasetHookResult
|
||||||
return uniq([...yAxisFields, ...y2AxisFields, ...tooltipFields]);
|
return uniq([...yAxisFields, ...y2AxisFields, ...tooltipFields]);
|
||||||
}, [yAxisFieldsString, y2AxisFieldsString, tooltipFieldsString]);
|
}, [yAxisFieldsString, y2AxisFieldsString, tooltipFieldsString]);
|
||||||
|
|
||||||
|
const isDownsampled = useMemo(() => {
|
||||||
|
return data.length > DOWNSIZE_SAMPLE_THRESHOLD;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const sortedAndLimitedData = useMemo(() => {
|
const sortedAndLimitedData = useMemo(() => {
|
||||||
if (isScatter) return downsampleScatterData(data);
|
if (isScatter) return downsampleScatterData(data);
|
||||||
return sortLineBarData(data, columnMetadata, xFieldSorts, xFields);
|
return sortLineBarData(data, columnMetadata, xFieldSorts, xFields);
|
||||||
|
@ -139,18 +145,25 @@ export const useDatasetOptions = (params: DatasetHookParams): DatasetHookResult
|
||||||
return measureFields
|
return measureFields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const value = columnLabelFormats[field]?.replaceMissingDataWith;
|
const value = columnLabelFormats[field]?.replaceMissingDataWith;
|
||||||
if (value === undefined) return 0;
|
if (value === undefined) return 'undefined';
|
||||||
if (value === null) return null;
|
if (value === null) return 'null';
|
||||||
if (value === '') return '';
|
if (value === '') return 'empty';
|
||||||
return value;
|
return value;
|
||||||
})
|
})
|
||||||
.join(',');
|
.join(',');
|
||||||
}, [measureFields.join(''), columnLabelFormats]);
|
}, [measureFields.join(''), columnLabelFormats]);
|
||||||
|
|
||||||
|
const dimensions: string[] = useMemo(() => {
|
||||||
|
if (isScatter) {
|
||||||
|
return getScatterDimensions(categoriesSet, xAxisField, measureFields, sizeField);
|
||||||
|
}
|
||||||
|
return getLineBarPieDimensions(categoriesSet, measureFields, xFields);
|
||||||
|
}, [categoriesSet, measureFields, xFieldsString, sizeFieldString, isScatter]);
|
||||||
|
|
||||||
const processedData = useMemo(() => {
|
const processedData = useMemo(() => {
|
||||||
if (isScatter) {
|
if (isScatter) {
|
||||||
return processScatterData(
|
return processScatterData(
|
||||||
data,
|
sortedAndLimitedData,
|
||||||
xAxisField,
|
xAxisField,
|
||||||
measureFields,
|
measureFields,
|
||||||
categoryFields,
|
categoryFields,
|
||||||
|
@ -168,7 +181,7 @@ export const useDatasetOptions = (params: DatasetHookParams): DatasetHookResult
|
||||||
columnLabelFormats
|
columnLabelFormats
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
data,
|
sortedAndLimitedData,
|
||||||
xFieldSortsString,
|
xFieldSortsString,
|
||||||
xFieldsString,
|
xFieldsString,
|
||||||
isScatter,
|
isScatter,
|
||||||
|
@ -177,16 +190,10 @@ export const useDatasetOptions = (params: DatasetHookParams): DatasetHookResult
|
||||||
dataMap,
|
dataMap,
|
||||||
measureFields,
|
measureFields,
|
||||||
sizeFieldString,
|
sizeFieldString,
|
||||||
|
dimensions,
|
||||||
measureFieldsReplaceDataWithKey //use this instead of columnLabelFormats
|
measureFieldsReplaceDataWithKey //use this instead of columnLabelFormats
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dimensions: string[] = useMemo(() => {
|
|
||||||
if (isScatter) {
|
|
||||||
return getScatterDimensions(categoriesSet, xAxisField, measureFields, sizeField);
|
|
||||||
}
|
|
||||||
return getLineBarPieDimensions(categoriesSet, measureFields, xFields);
|
|
||||||
}, [categoriesSet, measureFields, xFieldsString, sizeFieldString, isScatter]);
|
|
||||||
|
|
||||||
const yAxisKeys = useMemo(() => {
|
const yAxisKeys = useMemo(() => {
|
||||||
if (isScatter) return getLineBarPieYAxisKeys(categoriesSet, yAxisFields); //not a typo. I want to use the same function for both scatter and bar/line/pie
|
if (isScatter) return getLineBarPieYAxisKeys(categoriesSet, yAxisFields); //not a typo. I want to use the same function for both scatter and bar/line/pie
|
||||||
return getLineBarPieYAxisKeys(categoriesSet, yAxisFields);
|
return getLineBarPieYAxisKeys(categoriesSet, yAxisFields);
|
||||||
|
@ -258,11 +265,12 @@ export const useDatasetOptions = (params: DatasetHookParams): DatasetHookResult
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasetOptions,
|
datasetOptions, //this is a matrix of dimensions x measures
|
||||||
dataTrendlineOptions,
|
dataTrendlineOptions,
|
||||||
yAxisKeys,
|
yAxisKeys,
|
||||||
y2AxisKeys,
|
y2AxisKeys,
|
||||||
tooltipKeys,
|
tooltipKeys,
|
||||||
hasMismatchedTooltipsAndMeasures
|
hasMismatchedTooltipsAndMeasures,
|
||||||
|
isDownsampled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const DOWNSIZE_SAMPLE_THRESHOLD = 500;
|
export const DOWNSIZE_SAMPLE_THRESHOLD = 1000;
|
||||||
export const LINE_DECIMATION_THRESHOLD = 300;
|
export const LINE_DECIMATION_THRESHOLD = 300;
|
||||||
export const LINE_DECIMATION_SAMPLES = 450;
|
export const LINE_DECIMATION_SAMPLES = 450;
|
||||||
export const ANIMATION_THRESHOLD = 150;
|
export const LEGEND_ANIMATION_THRESHOLD = 200; //we use this when there is a large dataset and need to show a loading animation
|
||||||
|
export const ANIMATION_THRESHOLD = 125; //testing 175 with scatter chart
|
||||||
export const ANIMATION_DURATION = 1000;
|
export const ANIMATION_DURATION = 1000;
|
||||||
export const TOOLTIP_THRESHOLD = 500;
|
export const TOOLTIP_THRESHOLD = 2500;
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface BusterChartComponentProps
|
||||||
>,
|
>,
|
||||||
ReturnType<typeof useDatasetOptions> {
|
ReturnType<typeof useDatasetOptions> {
|
||||||
selectedAxis: ChartEncodes;
|
selectedAxis: ChartEncodes;
|
||||||
|
isDownsampled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BusterChartRenderComponentProps
|
export interface BusterChartRenderComponentProps
|
||||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { useDebounceFn } from '@/hooks';
|
import { useDebounceFn } from '@/hooks';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { scatterConfig_problematic1, scatterDataProblematic1 } from './scatterData_problematic1';
|
||||||
|
|
||||||
type ScatterChartData = ReturnType<typeof generateScatterChartData>;
|
type ScatterChartData = ReturnType<typeof generateScatterChartData>;
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ export const Default: Story = {
|
||||||
data: generateScatterChartData(50),
|
data: generateScatterChartData(50),
|
||||||
scatterAxis: {
|
scatterAxis: {
|
||||||
x: ['x'],
|
x: ['x'],
|
||||||
y: ['y'],
|
y: ['y', 'y2'],
|
||||||
size: [],
|
size: [],
|
||||||
category: ['category']
|
category: ['category']
|
||||||
},
|
},
|
||||||
|
@ -57,7 +58,7 @@ export const Default: Story = {
|
||||||
style: 'string'
|
style: 'string'
|
||||||
} satisfies IColumnLabelFormat
|
} satisfies IColumnLabelFormat
|
||||||
} satisfies Record<keyof ScatterChartData, IColumnLabelFormat>,
|
} satisfies Record<keyof ScatterChartData, IColumnLabelFormat>,
|
||||||
className: 'w-[400px] h-[400px]'
|
className: 'w-[400px] h-[400px] max-w-[400px] max-h-[400px]'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -437,3 +438,18 @@ export const ScatterWithTrendline_DateXAxisLogarithmicRegression: Story = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ProblematicDataset: Story = {
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
data: scatterDataProblematic1.data,
|
||||||
|
columnMetadata: scatterDataProblematic1.data_metadata.column_metadata,
|
||||||
|
...(scatterConfig_problematic1 as any),
|
||||||
|
barAndLineAxis: {
|
||||||
|
x: ['eligible_orders'],
|
||||||
|
y: ['attach_rate'],
|
||||||
|
category: ['merchant']
|
||||||
|
},
|
||||||
|
selectedChartType: ChartType.Scatter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,7 +15,7 @@ export const Sidebar: React.FC<SidebarProps> = React.memo(
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{footer && <div className="mt-auto mb-2 pt-5">{footer}</div>}
|
{footer && <div className="mt-auto mb-2 overflow-hidden pt-5">{footer}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export interface TooltipProps
|
||||||
extends Pick<React.ComponentProps<typeof TooltipContentBase>, 'align' | 'side' | 'sideOffset'>,
|
extends Pick<React.ComponentProps<typeof TooltipContentBase>, 'align' | 'side' | 'sideOffset'>,
|
||||||
Pick<React.ComponentProps<typeof TooltipProvider>, 'delayDuration' | 'skipDelayDuration'> {
|
Pick<React.ComponentProps<typeof TooltipProvider>, 'delayDuration' | 'skipDelayDuration'> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title: string | undefined;
|
title: string | React.ReactNode | undefined;
|
||||||
shortcuts?: string[];
|
shortcuts?: string[];
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export const Tooltip = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const TooltipContent: React.FC<{
|
const TooltipContent: React.FC<{
|
||||||
title: string;
|
title: string | React.ReactNode;
|
||||||
shortcut?: string[];
|
shortcut?: string[];
|
||||||
}> = ({ title, shortcut }) => {
|
}> = ({ title, shortcut }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const HomePageController: React.FC<{}> = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center p-4.5">
|
<div className="flex flex-col items-center justify-center p-4.5">
|
||||||
<div className="mt-[150px] flex w-full max-w-[650px] flex-col space-y-6">
|
<div className="mt-[150px] flex w-full max-w-[650px] flex-col space-y-6">
|
||||||
<div className="flex flex-col justify-center gap-y-2.5 text-center">
|
<div className="flex flex-col justify-center gap-y-2 text-center">
|
||||||
<Title as="h1" className="mb-0!">
|
<Title as="h1" className="mb-0!">
|
||||||
{greeting}
|
{greeting}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
|
@ -62,7 +62,8 @@ export const generateScatterChartData = (pointCount = 30): IDataResult => {
|
||||||
const categories = ['Electronics', 'Clothing', 'Home Goods'];
|
const categories = ['Electronics', 'Clothing', 'Home Goods'];
|
||||||
return Array.from({ length: pointCount }, (_, index) => ({
|
return Array.from({ length: pointCount }, (_, index) => ({
|
||||||
x: (index % 10) * 10, // Values from 0-90 in steps of 10
|
x: (index % 10) * 10, // Values from 0-90 in steps of 10
|
||||||
y: Math.floor(index / 10) * 10, // Creates a grid pattern
|
y: Math.floor(index / 10) * 10, // Creates a grid pattern,
|
||||||
|
y2: Math.floor(index / 10) * 40, // Creates a grid pattern,
|
||||||
size: 10 + (index % 5) * 10, // Sizes cycle between 10-50 in steps of 10
|
size: 10 + (index % 5) * 10, // Sizes cycle between 10-50 in steps of 10
|
||||||
category: categories[index % categories.length]
|
category: categories[index % categories.length]
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue