Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add on click event to ScatterChart #672

Merged
merged 11 commits into from Sep 20, 2023
37 changes: 35 additions & 2 deletions src/components/chart-elements/ScatterChart/ScatterChart.tsx
Expand Up @@ -13,7 +13,12 @@ import {
} from "recharts";
import { AxisDomain } from "recharts/types/util/types";

import { constructCategories, constructCategoryColors, getYAxisDomain } from "../common/utils";
import {
constructCategories,
constructCategoryColors,
deepEqual,
getYAxisDomain,
} from "../common/utils";
import NoData from "../common/NoData";
import BaseAnimationTimingProps from "../common/BaseAnimationTimingProps";
import ChartLegend from "components/chart-elements/common/ChartLegend";
Expand All @@ -35,6 +40,19 @@ export type ScatterChartValueFormatter = {
size?: ValueFormatter;
};

const renderShape = (props: any, activeNode: any | undefined) => {
const { cx, cy, width, node, fillOpacity } = props;

return (
<circle
cx={cx}
cy={cy}
r={width / 2}
opacity={activeNode ? (deepEqual(activeNode, node) ? fillOpacity : 0.3) : fillOpacity}
/>
);
};

export interface ScatterChartProps
extends BaseAnimationTimingProps,
React.HTMLAttributes<HTMLDivElement> {
Expand All @@ -61,6 +79,7 @@ export interface ScatterChartProps
minYValue?: number;
maxYValue?: number;
allowDecimals?: boolean;
onValueChange?: (value: any) => void;
noDataText?: string;
}

Expand Down Expand Up @@ -95,12 +114,24 @@ const ScatterChart = React.forwardRef<HTMLDivElement, ScatterChartProps>((props,
minYValue,
maxYValue,
allowDecimals = true,
onValueChange,
noDataText,
className,
...other
} = props;
const [legendHeight, setLegendHeight] = useState(60);
const [activeNode, setActiveNode] = React.useState<any | undefined>(undefined);

function onNodeClick(data: any, index: number, event: React.MouseEvent) {
event.stopPropagation();
if (onValueChange == null) return;
if (deepEqual(activeNode, data.node)) {
setActiveNode(undefined);
} else {
setActiveNode(data.node);
onValueChange?.(data.payload);
}
}
const categories = constructCategories(data, category);
const categoryColors = constructCategoryColors(categories, colors);

Expand All @@ -112,7 +143,7 @@ const ScatterChart = React.forwardRef<HTMLDivElement, ScatterChartProps>((props,
<div ref={ref} className={tremorTwMerge("w-full h-80", className)} {...other}>
<ResponsiveContainer className="h-full w-full">
{data?.length ? (
<ReChartsScatterChart>
<ReChartsScatterChart onClick={() => setActiveNode(undefined)}>
{showGridLines ? (
<CartesianGrid
className={tremorTwMerge(
Expand Down Expand Up @@ -225,6 +256,8 @@ const ScatterChart = React.forwardRef<HTMLDivElement, ScatterChartProps>((props,
data={category ? data.filter((d) => d[category] === cat) : data}
isAnimationActive={showAnimation}
animationDuration={animationDuration}
shape={(props) => renderShape(props, activeNode)}
onClick={onNodeClick}
/>
);
})}
Expand Down
18 changes: 18 additions & 0 deletions src/components/chart-elements/common/utils.ts
Expand Up @@ -32,3 +32,21 @@ export const constructCategories = (data: any[], color?: string): string[] => {
});
return Array.from(categories);
};

export const deepEqual = (obj1: any, obj2: any) => {
if (obj1 === obj2) return true;

if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null)
return false;

const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);

if (keys1.length !== keys2.length) return false;

for (const key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) return false;
}

return true;
};
35 changes: 24 additions & 11 deletions src/stories/chart-elements/ScatterChart.stories.tsx
Expand Up @@ -15,20 +15,25 @@ export default {
} as ComponentMeta<typeof ScatterChart>;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args

const ResponsiveTemplate: ComponentStory<typeof ScatterChart> = (args) => (
<>
<Title>Mobile</Title>
<div className="w-64">
const ResponsiveTemplate: ComponentStory<typeof ScatterChart> = (args) => {
if (args.onValueChange?.length === 0) {
args.onValueChange = undefined;
}
return (
<>
<Title>Mobile</Title>
<div className="w-64">
<Card>
<ScatterChart {...args} />
</Card>
</div>
<Title className="mt-5">Desktop</Title>
<Card>
<ScatterChart {...args} />
</Card>
</div>
<Title className="mt-5">Desktop</Title>
<Card>
<ScatterChart {...args} />
</Card>
</>
);
</>
);
};

const DefaultTemplate: ComponentStory<typeof ScatterChart> = ({ ...args }) => (
<Card>
Expand Down Expand Up @@ -101,6 +106,14 @@ WithNoDataText.args = {
noDataText: "No data, try again later.",
};

export const WithOnValueChange = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithOnValueChange.args = {
...args,
data,
onValueChange: (value) => alert(JSON.stringify(value)),
};

export const WithExampleDatas = ResponsiveTemplate.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
WithExampleDatas.args = {
Expand Down