From 615f78e355134543c9d685ebf0e43c2b65e745e7 Mon Sep 17 00:00:00 2001 From: atrincas Date: Mon, 11 Nov 2024 11:15:36 +0100 Subject: [PATCH] Added breakdown implementation --- client/src/containers/sandbox/index.tsx | 4 +- .../sidebar/breakdown-selector/index.tsx | 85 +++++++++++++++++++ .../sidebar/filter-settings/index.tsx | 12 +-- .../src/containers/widget/breakdown/index.tsx | 28 +++--- client/src/containers/widget/index.tsx | 18 ++++ client/src/hooks/use-sandbox-widget.ts | 14 ++- client/src/lib/normalize-widget-data.ts | 7 ++ client/src/lib/queryKeys.ts | 3 +- client/src/lib/utils.ts | 1 + client/tests/widget/breakdown.test.tsx | 23 +++-- client/tests/widget/widget.test.tsx | 23 +++++ 11 files changed, 182 insertions(+), 36 deletions(-) create mode 100644 client/src/containers/sidebar/breakdown-selector/index.tsx diff --git a/client/src/containers/sandbox/index.tsx b/client/src/containers/sandbox/index.tsx index fbd45b5a..6363dfdf 100644 --- a/client/src/containers/sandbox/index.tsx +++ b/client/src/containers/sandbox/index.tsx @@ -9,7 +9,8 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; export default function Sandbox() { - const { visualization, setVisualization, widget } = useSandboxWidget(); + const { breakdown, visualization, setVisualization, widget } = + useSandboxWidget(); const menuItems = ( <> + + + + w.visualisations.some((v) => v !== "horizontal_bar_chart"), + )} + itemKey="indicator" + onItemClick={(w) => { + setBreakdown(w.indicator); + setShowIndicators(false); + setshowOverlay(false); + }} + /> + + + ); +}; + +export default BreakdownSelector; diff --git a/client/src/containers/sidebar/filter-settings/index.tsx b/client/src/containers/sidebar/filter-settings/index.tsx index 7d342af0..0fa546f4 100644 --- a/client/src/containers/sidebar/filter-settings/index.tsx +++ b/client/src/containers/sidebar/filter-settings/index.tsx @@ -5,10 +5,9 @@ import { queryKeys } from "@/lib/queryKeys"; import useFilters from "@/hooks/use-filters"; +import BreakdownSelector from "@/containers/sidebar/breakdown-selector"; import FilterPopup from "@/containers/sidebar/filter-settings/filter-popup"; -import { Button } from "@/components/ui/button"; - const DEFAULT_FILTERS = ["location-country-region", "sector"]; const DEFAULT_FILTERS_LABEL_MAP: { [key: string]: { selected: string; unSelected: string }; @@ -65,14 +64,7 @@ const FilterSettings: FC<{ withDataBreakdown?: boolean }> = ({ name="Add a custom filter" items={customFilters} /> - {withDataBreakdown && ( - - )} + {withDataBreakdown && } ); }; diff --git a/client/src/containers/widget/breakdown/index.tsx b/client/src/containers/widget/breakdown/index.tsx index e4ef5a6e..48f50682 100644 --- a/client/src/containers/widget/breakdown/index.tsx +++ b/client/src/containers/widget/breakdown/index.tsx @@ -1,7 +1,7 @@ "use client"; import React, { FC } from "react"; -import { WidgetChartData } from "@shared/dto/widgets/base-widget-data.interface"; +import { WidgetBreakdownData } from "@shared/dto/widgets/base-widget-data.interface"; import { Bar, BarChart, Cell, XAxis, YAxis } from "recharts"; import { CSS_CHART_COLORS, TW_CHART_COLORS } from "@/lib/constants"; @@ -16,7 +16,7 @@ import { } from "@/components/ui/chart"; interface BreakdownProps { - data?: { label: string; data: WidgetChartData }[]; + data?: WidgetBreakdownData; } const Breakdown: FC = ({ data }) => { @@ -81,17 +81,19 @@ const Breakdown: FC = ({ data }) => { data-testid="breakdown-chart-legend" className="flex items-center gap-6 pl-6" > - {data[0].data.map((d, i) => ( -

- - {d.label} -

- ))} + {[...data] + .sort((a, b) => b.data.length - a.data.length)[0] + .data.map((d, i) => ( +

+ + {d.label} +

+ ))} ); diff --git a/client/src/containers/widget/index.tsx b/client/src/containers/widget/index.tsx index 38c2ccb7..d5a7c826 100644 --- a/client/src/containers/widget/index.tsx +++ b/client/src/containers/widget/index.tsx @@ -17,6 +17,7 @@ import MenuButton from "@/containers/menu-button"; import NoData from "@/containers/no-data"; import { showOverlayAtom } from "@/containers/overlay/store"; import AreaChart from "@/containers/widget/area-chart"; +import Breakdown from "@/containers/widget/breakdown"; import Filter from "@/containers/widget/filter"; import HorizontalBarChart from "@/containers/widget/horizontal-bar-chart"; import Map from "@/containers/widget/map"; @@ -48,6 +49,7 @@ export interface WidgetProps { indicator: string; data: BaseWidgetWithData["data"]; visualization: WidgetVisualizationsType; + breakdown?: string; visualisations?: WidgetVisualizationsType[]; question?: string; menuItems?: React.ReactNode; @@ -68,6 +70,7 @@ export default function Widget({ indicator, visualization, visualisations, + breakdown, data, question, menuItems, @@ -130,6 +133,21 @@ export default function Widget({ ); } + if (breakdown) { + return ( + + + + + ); + } + switch (selectedVisualization) { case WIDGET_VISUALIZATIONS.SINGLE_VALUE: return ( diff --git a/client/src/hooks/use-sandbox-widget.ts b/client/src/hooks/use-sandbox-widget.ts index 55b9d23a..c1a5648f 100644 --- a/client/src/hooks/use-sandbox-widget.ts +++ b/client/src/hooks/use-sandbox-widget.ts @@ -4,12 +4,14 @@ import { } from "@shared/dto/widgets/widget-visualizations.constants"; import { parseAsStringEnum, useQueryState } from "nuqs"; +import { normalizeWidgetData } from "@/lib/normalize-widget-data"; import { client } from "@/lib/queryClient"; import { queryKeys } from "@/lib/queryKeys"; import useFilters from "@/hooks/use-filters"; function useSandboxWidget() { + const [breakdown] = useQueryState("breakdown"); const [indicator, setIndicator] = useQueryState("indicator", { defaultValue: "", }); @@ -20,19 +22,27 @@ function useSandboxWidget() { ); const { filters } = useFilters(); const { data } = client.widgets.getWidget.useQuery( - queryKeys.widgets.one(indicator, filters).queryKey, + queryKeys.widgets.one(indicator, filters, breakdown || undefined).queryKey, { params: { id: indicator }, query: { filters, + breakdown: breakdown || undefined, }, }, - { enabled: !!indicator, select: (res) => res.body.data }, + { + enabled: !!indicator, + select: (res) => ({ + ...res.body.data, + data: normalizeWidgetData(res.body.data.data), + }), + }, ); return { indicator, visualization, + breakdown, setIndicator, setVisualization, widget: data, diff --git a/client/src/lib/normalize-widget-data.ts b/client/src/lib/normalize-widget-data.ts index deffc71e..670ab79e 100644 --- a/client/src/lib/normalize-widget-data.ts +++ b/client/src/lib/normalize-widget-data.ts @@ -18,6 +18,13 @@ function normalizeWidgetData(widgetData: WidgetData): WidgetData { result.chart = normalizeChartData(result.chart); } + if (result.breakdown) { + result.breakdown = result.breakdown.map((b) => ({ + ...b, + data: normalizeChartData(b.data), + })); + } + return result; } diff --git a/client/src/lib/queryKeys.ts b/client/src/lib/queryKeys.ts index bb72e029..8138f484 100644 --- a/client/src/lib/queryKeys.ts +++ b/client/src/lib/queryKeys.ts @@ -25,9 +25,10 @@ export const pageFiltersKeys = createQueryKeys("pageFilters", { export const widgetsKeys = createQueryKeys("widgets", { all: null, - one: (indicator: string, filters: FilterQueryParam[]) => [ + one: (indicator: string, filters: FilterQueryParam[], breakdown?: string) => [ indicator, { filters }, + breakdown, ], }); diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts index d1eaec48..9f40e8f9 100644 --- a/client/src/lib/utils.ts +++ b/client/src/lib/utils.ts @@ -13,6 +13,7 @@ export const getInPageLinkId = (slug?: string): string => `inPage-${slug}-link`; export function isEmptyWidget(data: BaseWidgetWithData["data"]): boolean { return ( !data.counter && + !data.breakdown && !data.navigation && !data.chart?.length && !data.map?.length diff --git a/client/tests/widget/breakdown.test.tsx b/client/tests/widget/breakdown.test.tsx index 135e38bb..dfb22260 100644 --- a/client/tests/widget/breakdown.test.tsx +++ b/client/tests/widget/breakdown.test.tsx @@ -3,6 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import BreakdownChart from "@/containers/widget/breakdown"; import { CSS_CHART_COLORS, TW_CHART_COLORS } from "@/lib/constants"; import type { ResponsiveContainerProps } from "recharts"; +import { WidgetBreakdownData } from "@shared/dto/widgets/base-widget-data.interface"; vi.mock("recharts", async () => { const mockRecharts = await vi.importActual("recharts"); @@ -20,21 +21,25 @@ vi.mock("@/containers/no-data", () => ({ default: () =>
No data
, })); -const mockData = [ +const mockData: WidgetBreakdownData = [ { label: "Category 1", + data: [{ label: "Option A", value: 100, total: 100 }], + }, + { + label: "Category 2", data: [ - { label: "Option A", value: 50 }, - { label: "Option B", value: 30 }, - { label: "Option C", value: 20 }, + { label: "Option A", value: 60, total: 100 }, + { label: "Option B", value: 25, total: 100 }, + { label: "Option C", value: 15, total: 100 }, ], }, { - label: "Category 2", + label: "Category 3", data: [ - { label: "Option A", value: 60 }, - { label: "Option B", value: 25 }, - { label: "Option C", value: 15 }, + { label: "Option A", value: 50, total: 100 }, + { label: "Option B", value: 30, total: 100 }, + { label: "Option C", value: 20, total: 100 }, ], }, ]; @@ -91,7 +96,7 @@ describe("BreakdownChart", () => { await waitFor(() => { const legendItems = screen.getByTestId("breakdown-chart-legend").children; - const categoryData = mockData[0].data; + const categoryData = mockData[1].data; expect(legendItems).toHaveLength(categoryData.length); diff --git a/client/tests/widget/widget.test.tsx b/client/tests/widget/widget.test.tsx index 3acb02af..0e0491a9 100644 --- a/client/tests/widget/widget.test.tsx +++ b/client/tests/widget/widget.test.tsx @@ -59,6 +59,10 @@ vi.mock("@/containers/no-data", () => ({ default: () =>
No data
, })); +vi.mock("@/containers/widget/breakdown", () => ({ + default: () =>
Breakdown Chart
, +})); + describe("Widget", () => { const mockProps: WidgetProps = { indicator: "Test Indicator", @@ -151,6 +155,25 @@ describe("Widget", () => { }); }); + it("renders breakdown chart when breakdown props is passed", () => { + render( + , + ); + + expect(screen.getByTestId("breakdown")).toBeInTheDocument(); + }); + it("renders null for unsupported visualization type", () => { const consoleWarnSpy = vi .spyOn(console, "warn")