Skip to content

Commit

Permalink
chore: extract bar chart events to a separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
dv-usama-ansari committed Feb 20, 2025
1 parent b1e2c43 commit a9b75f0
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 112 deletions.
117 changes: 5 additions & 112 deletions src/vis/bar/SingleEChartsBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { ErrorMessage } from '../general/ErrorMessage';
import { WarningMessage } from '../general/WarningMessage';
import { NAN_REPLACEMENT, VIS_NEUTRAL_COLOR } from '../general/constants';
import { EAggregateTypes, ICommonVisProps } from '../interfaces';
import { useBarSortHelper, useGetBarVisState } from './hooks';
import { EBarDirection, EBarDisplayType, EBarGroupingType, EBarSortParameters, IBarConfig } from './interfaces';
import { useGetBarChartMouseEvents, useGetBarVisState } from './hooks';
import { EBarDirection, EBarDisplayType, EBarGroupingType, IBarConfig } from './interfaces';
import { AggregatedDataType, BAR_WIDTH, CHART_HEIGHT_MARGIN, DEFAULT_BAR_CHART_HEIGHT, SERIES_ZERO } from './interfaces/internal';
import { numberFormatter } from './utils';

Expand Down Expand Up @@ -57,8 +57,6 @@ function EagerSingleEChartsBarChart({
selectedFacetValue?: string;
selectionCallback: (e: React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>, ids: string[]) => void;
}) {
const [getSortMetadata] = useBarSortHelper({ config: config! });

// NOTE: @dv-usama-ansari: Prepare the base series options for the bar chart.
const seriesBase = React.useMemo(
() =>
Expand Down Expand Up @@ -351,118 +349,13 @@ function EagerSingleEChartsBarChart({
}
}, [customTooltip.content, yAxisLabel]);

const { click } = useGetBarChartMouseEvents({ aggregatedData, config, selectedFacetIndex, selectedFacetValue, selectedList, selectionCallback, setConfig });

const { setRef, instance } = useChart({
options,
settings,
mouseEvents: {
click: [
{
query: { titleIndex: 0 },
handler: () => {
setConfig?.({ ...config!, focusFacetIndex: config?.focusFacetIndex === selectedFacetIndex ? null : selectedFacetIndex });
},
},
{
query: { seriesType: 'bar' },
handler: (params) => {
if (params.componentType === 'series') {
const event = params.event?.event as unknown as React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>;
// NOTE: @dv-usama-ansari: Sanitization is required here since the seriesName contains \u000 which make github confused.
const seriesName = sanitize(params.seriesName ?? '') === SERIES_ZERO ? params.name : params.seriesName;
const ids: string[] = config?.group
? config.group.id === config?.facets?.id
? [
...(aggregatedData?.categories[params.name]?.groups[selectedFacetValue!]?.unselected.ids ?? []),
...(aggregatedData?.categories[params.name]?.groups[selectedFacetValue!]?.selected.ids ?? []),
]
: [
...(aggregatedData?.categories[params.name]?.groups[seriesName as string]?.unselected.ids ?? []),
...(aggregatedData?.categories[params.name]?.groups[seriesName as string]?.selected.ids ?? []),
]
: (aggregatedData?.categories[params.name]?.ids ?? []);

if (event.shiftKey) {
// NOTE: @dv-usama-ansari: `shift + click` on a bar which is already selected will deselect it.
// Using `Set` to reduce time complexity to O(1).
const newSelectedSet = new Set(selectedList);
ids.forEach((id) => {
if (newSelectedSet.has(id)) {
newSelectedSet.delete(id);
} else {
newSelectedSet.add(id);
}
});
const newSelectedList = [...newSelectedSet];
selectionCallback(event, [...new Set([...newSelectedList])]);
} else {
// NOTE: @dv-usama-ansari: Early return if the bar is clicked and it is already selected?
const isSameBarClicked = (selectedList ?? []).length > 0 && (selectedList ?? []).every((id) => ids.includes(id));
selectionCallback(event, isSameBarClicked ? [] : ids);
}
}
},
},
{
query:
config?.direction === EBarDirection.HORIZONTAL
? { componentType: 'yAxis' }
: config?.direction === EBarDirection.VERTICAL
? { componentType: 'xAxis' }
: { componentType: 'unknown' }, // No event should be triggered when the direction is not set.

handler: (params) => {
if (params.targetType === 'axisLabel') {
const event = params.event?.event as unknown as React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>;
const ids = aggregatedData?.categories[params.value as string]?.ids ?? [];
if (event.shiftKey) {
const newSelectedSet = new Set(selectedList);
ids.forEach((id) => {
if (newSelectedSet.has(id)) {
newSelectedSet.delete(id);
} else {
newSelectedSet.add(id);
}
});
const newSelectedList = [...newSelectedSet];
selectionCallback(event, [...new Set([...newSelectedList])]);
} else {
const isSameBarClicked = (selectedList ?? []).length > 0 && (selectedList ?? []).every((id) => ids.includes(id));
selectionCallback(event, isSameBarClicked ? [] : ids);
}
}
},
},
{
query: { componentType: 'yAxis' },
handler: (params) => {
if (params.targetType === 'axisName' && params.componentType === 'yAxis') {
if (config?.direction === EBarDirection.HORIZONTAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.CATEGORIES);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
if (config?.direction === EBarDirection.VERTICAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.AGGREGATION);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
}
},
},
{
query: { componentType: 'xAxis' },
handler: (params) => {
if (params.targetType === 'axisName' && params.componentType === 'xAxis') {
if (config?.direction === EBarDirection.HORIZONTAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.AGGREGATION);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
if (config?.direction === EBarDirection.VERTICAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.CATEGORIES);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
}
},
},
],
click,
mouseover: [
{
query:
Expand Down
203 changes: 203 additions & 0 deletions src/vis/bar/hooks/BarChartMouseEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import * as React from 'react';

import { useBarSortHelper } from './BarSortHook';
import { CallbackArray, CallbackObject } from '../../../echarts';
import { sanitize } from '../../../utils';
import { ICommonVisProps } from '../../interfaces';
import { EBarDirection, EBarSortParameters, IBarConfig } from '../interfaces';
import { AggregatedDataType, SERIES_ZERO } from '../interfaces/internal';

function useGetClickEvents({
aggregatedData,
config,
selectedFacetIndex,
selectedFacetValue,
selectedList,
selectionCallback,
setConfig,
}: Pick<ICommonVisProps<IBarConfig>, 'config' | 'setConfig' | 'selectedList'> & {
aggregatedData: AggregatedDataType;
selectedFacetIndex?: number;
selectedFacetValue?: string;
selectionCallback: (e: React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>, ids: string[]) => void;
}) {
const [getSortMetadata] = useBarSortHelper({ config: config! });
const clickToFocusFacet = React.useMemo(
() =>
({
query: { titleIndex: 0 },
handler: () => {
setConfig?.({ ...config!, focusFacetIndex: config?.focusFacetIndex === selectedFacetIndex ? null : selectedFacetIndex });
},
}) as CallbackObject,
[config, selectedFacetIndex, setConfig],
);

const clickBarToToggleSelect = React.useMemo(
() =>
({
query: { seriesType: 'bar' },
handler: (params) => {
if (params.componentType === 'series') {
const event = params.event?.event as unknown as React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>;
// NOTE: @dv-usama-ansari: Sanitization is required here since the seriesName contains \u000 which make github confused.
const seriesName = sanitize(params.seriesName ?? '') === SERIES_ZERO ? params.name : params.seriesName;
const ids: string[] = config?.group
? config.group.id === config?.facets?.id
? [
...(aggregatedData?.categories[params.name]?.groups[selectedFacetValue!]?.unselected.ids ?? []),
...(aggregatedData?.categories[params.name]?.groups[selectedFacetValue!]?.selected.ids ?? []),
]
: [
...(aggregatedData?.categories[params.name]?.groups[seriesName as string]?.unselected.ids ?? []),
...(aggregatedData?.categories[params.name]?.groups[seriesName as string]?.selected.ids ?? []),
]
: (aggregatedData?.categories[params.name]?.ids ?? []);

if (event.shiftKey) {
// NOTE: @dv-usama-ansari: `shift + click` on a bar which is already selected will deselect it.
// Using `Set` to reduce time complexity to O(1).
const newSelectedSet = new Set(selectedList);
ids.forEach((id) => {
if (newSelectedSet.has(id)) {
newSelectedSet.delete(id);
} else {
newSelectedSet.add(id);
}
});
const newSelectedList = [...newSelectedSet];
selectionCallback(event, [...new Set([...newSelectedList])]);
} else {
// NOTE: @dv-usama-ansari: Early return if the bar is clicked and it is already selected?
const isSameBarClicked = (selectedList ?? []).length > 0 && (selectedList ?? []).every((id) => ids.includes(id));
selectionCallback(event, isSameBarClicked ? [] : ids);
}
}
},
}) as CallbackObject,
[aggregatedData?.categories, config?.facets?.id, config?.group, selectedFacetValue, selectedList, selectionCallback],
);

const clickAxisLabelToToggleSelect = React.useMemo(
() =>
({
query:
config?.direction === EBarDirection.HORIZONTAL
? { componentType: 'yAxis' }
: config?.direction === EBarDirection.VERTICAL
? { componentType: 'xAxis' }
: { componentType: 'unknown' }, // No event should be triggered when the direction is not set.

handler: (params) => {
if (params.targetType === 'axisLabel') {
const event = params.event?.event as unknown as React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>;
const ids = aggregatedData?.categories[params.value as string]?.ids ?? [];
if (event.shiftKey) {
const newSelectedSet = new Set(selectedList);
ids.forEach((id) => {
if (newSelectedSet.has(id)) {
newSelectedSet.delete(id);
} else {
newSelectedSet.add(id);
}
});
const newSelectedList = [...newSelectedSet];
selectionCallback(event, [...new Set([...newSelectedList])]);
} else {
const isSameBarClicked = (selectedList ?? []).length > 0 && (selectedList ?? []).every((id) => ids.includes(id));
selectionCallback(event, isSameBarClicked ? [] : ids);
}
}
},
}) as CallbackObject,
[aggregatedData?.categories, config?.direction, selectedList, selectionCallback],
);

const clickAxisTitleToSortAlongHorizontalAxis = React.useMemo(
() =>
({
query: { componentType: 'xAxis' },
handler: (params) => {
if (params.targetType === 'axisName' && params.componentType === 'xAxis') {
if (config?.direction === EBarDirection.HORIZONTAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.AGGREGATION);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
if (config?.direction === EBarDirection.VERTICAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.CATEGORIES);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
}
},
}) as CallbackObject,
[config, getSortMetadata, setConfig],
);

const clickAxisTitleToSortAlongVerticalAxis = React.useMemo(
() =>
({
query: { componentType: 'yAxis' },
handler: (params) => {
if (params.targetType === 'axisName' && params.componentType === 'yAxis') {
if (config?.direction === EBarDirection.HORIZONTAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.CATEGORIES);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
if (config?.direction === EBarDirection.VERTICAL) {
const sortMetadata = getSortMetadata(EBarSortParameters.AGGREGATION);
setConfig?.({ ...config!, sortState: sortMetadata.nextSortState });
}
}
},
}) as CallbackObject,
[config, getSortMetadata, setConfig],
);

return [
clickToFocusFacet,
clickBarToToggleSelect,
clickAxisLabelToToggleSelect,
clickAxisTitleToSortAlongHorizontalAxis,
clickAxisTitleToSortAlongVerticalAxis,
];
}

/**
* This is a placeholder function and will not be implemented.
* This function is written only for the sake of completeness.
*
* The underlying functionality will be addressed in
*
* @returns mouseoverEvents and mouseoutEvents
*/
function useGetHoverEvents() {
return { mouseoverEvents: [], mouseoutEvents: [] };
}

export function useGetBarChartMouseEvents({
aggregatedData,
config,
selectedFacetIndex,
selectedFacetValue,
selectedList,
selectionCallback,
setConfig,
}: Pick<ICommonVisProps<IBarConfig>, 'config' | 'setConfig' | 'selectedList'> & {
aggregatedData: AggregatedDataType;
selectedFacetIndex?: number;
selectedFacetValue?: string;
selectionCallback: (e: React.MouseEvent<SVGGElement | HTMLDivElement, MouseEvent>, ids: string[]) => void;
}): { click: CallbackArray; mouseover: CallbackArray; mouseout: CallbackArray } {
const clickEvents = useGetClickEvents({
aggregatedData,
config,
selectedFacetIndex,
selectedFacetValue,
selectedList,
selectionCallback,
setConfig,
});
const { mouseoverEvents, mouseoutEvents } = useGetHoverEvents();

return { click: clickEvents, mouseover: mouseoverEvents, mouseout: mouseoutEvents };
}
1 change: 1 addition & 0 deletions src/vis/bar/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './BarSortHook';
export * from './BarVisStateHook';
export * from './BarChartMouseEvents';

0 comments on commit a9b75f0

Please sign in to comment.