From b88fffb283812a40169d70410b7cc2b3122c4e32 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Wed, 22 May 2024 19:23:17 +0200 Subject: [PATCH 1/8] first draft bar chart --- src/components/BarChart/BarChart.tsx | 836 +++++++++++++++++++ src/components/BarChart/barchart.spec.ts | 61 ++ src/components/BarChart/barchart.stories.tsx | 302 +++++++ src/components/BarChart/changelog.md | 5 + 4 files changed, 1204 insertions(+) create mode 100644 src/components/BarChart/BarChart.tsx create mode 100644 src/components/BarChart/barchart.spec.ts create mode 100644 src/components/BarChart/barchart.stories.tsx create mode 100644 src/components/BarChart/changelog.md diff --git a/src/components/BarChart/BarChart.tsx b/src/components/BarChart/BarChart.tsx new file mode 100644 index 0000000..b3cfee8 --- /dev/null +++ b/src/components/BarChart/BarChart.tsx @@ -0,0 +1,836 @@ +// Tremor Raw BarChart [v0.0.0] + +"use client" + +import React from "react" +import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react" +import { + Bar, + CartesianGrid, + Label, + BarChart as RechartsBarChart, + Legend as RechartsLegend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts" +import { AxisDomain } from "recharts/types/util/types" + +import { useOnWindowResize } from "../../hooks/useOnWindowResize" +import { + AvailableChartColors, + AvailableChartColorsKeys, + constructCategoryColors, + getColorClassName, +} from "../../utils/chartColors" +import { cx } from "../../utils/cx" +import { getYAxisDomain } from "../../utils/getYAxisDomain" + +//#region Shape +export function 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 +} + +const renderShape = ( + props: any, + activeBar: any | undefined, + activeLegend: string | undefined, + layout: string, +) => { + const { fillOpacity, name, payload, value } = props + let { x, width, y, height } = props + + if (layout === "horizontal" && height < 0) { + y += height + height = Math.abs(height) // height must be a positive number + } else if (layout === "vertical" && width < 0) { + x += width + width = Math.abs(width) // width must be a positive number + } + + return ( + + ) +} + +//#region Legend + +interface LegendItemProps { + name: string + color: AvailableChartColorsKeys + onClick?: (name: string, color: AvailableChartColorsKeys) => void + activeLegend?: string +} + +const LegendItem = ({ + name, + color, + onClick, + activeLegend, +}: LegendItemProps) => { + const hasOnValueChange = !!onClick + return ( +
  • { + e.stopPropagation() + onClick?.(name, color) + }} + > + +

    + {name} +

    +
  • + ) +} + +interface ScrollButtonProps { + icon: React.ElementType + onClick?: () => void + disabled?: boolean +} + +const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => { + const Icon = icon + const [isPressed, setIsPressed] = React.useState(false) + const intervalRef = React.useRef(null) + + React.useEffect(() => { + if (isPressed) { + intervalRef.current = setInterval(() => { + onClick?.() + }, 300) + } else { + clearInterval(intervalRef.current as NodeJS.Timeout) + } + return () => clearInterval(intervalRef.current as NodeJS.Timeout) + }, [isPressed, onClick]) + + React.useEffect(() => { + if (disabled) { + clearInterval(intervalRef.current as NodeJS.Timeout) + setIsPressed(false) + } + }, [disabled]) + + return ( + + ) +} + +interface LegendProps extends React.OlHTMLAttributes { + categories: string[] + colors?: AvailableChartColorsKeys[] + onClickLegendItem?: (category: string, color: string) => void + activeLegend?: string + enableLegendSlider?: boolean +} + +type HasScrollProps = { + left: boolean + right: boolean +} + +const Legend = React.forwardRef((props, ref) => { + const { + categories, + colors = AvailableChartColors, + className, + onClickLegendItem, + activeLegend, + enableLegendSlider = false, + ...other + } = props + const scrollableRef = React.useRef(null) + const [hasScroll, setHasScroll] = React.useState(null) + const [isKeyDowned, setIsKeyDowned] = React.useState(null) + const intervalRef = React.useRef(null) + + const checkScroll = React.useCallback(() => { + const scrollable = scrollableRef?.current + if (!scrollable) return + + const hasLeftScroll = scrollable.scrollLeft > 0 + const hasRightScroll = + scrollable.scrollWidth - scrollable.clientWidth > scrollable.scrollLeft + + setHasScroll({ left: hasLeftScroll, right: hasRightScroll }) + }, [setHasScroll]) + + const scrollToTest = React.useCallback( + (direction: "left" | "right") => { + const element = scrollableRef?.current + const width = element?.clientWidth ?? 0 + + if (element && enableLegendSlider) { + element.scrollTo({ + left: + direction === "left" + ? element.scrollLeft - width + : element.scrollLeft + width, + behavior: "smooth", + }) + setTimeout(() => { + checkScroll() + }, 400) + } + }, + [enableLegendSlider, checkScroll], + ) + + React.useEffect(() => { + const keyDownHandler = (key: string) => { + if (key === "ArrowLeft") { + scrollToTest("left") + } else if (key === "ArrowRight") { + scrollToTest("right") + } + } + if (isKeyDowned) { + keyDownHandler(isKeyDowned) + intervalRef.current = setInterval(() => { + keyDownHandler(isKeyDowned) + }, 300) + } else { + clearInterval(intervalRef.current as NodeJS.Timeout) + } + return () => clearInterval(intervalRef.current as NodeJS.Timeout) + }, [isKeyDowned, scrollToTest]) + + const keyDown = (e: KeyboardEvent) => { + e.stopPropagation() + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + e.preventDefault() + setIsKeyDowned(e.key) + } + } + const keyUp = (e: KeyboardEvent) => { + e.stopPropagation() + setIsKeyDowned(null) + } + + React.useEffect(() => { + const scrollable = scrollableRef?.current + if (enableLegendSlider) { + checkScroll() + scrollable?.addEventListener("keydown", keyDown) + scrollable?.addEventListener("keyup", keyUp) + } + + return () => { + scrollable?.removeEventListener("keydown", keyDown) + scrollable?.removeEventListener("keyup", keyUp) + } + }, [checkScroll, enableLegendSlider]) + + return ( +
      +
      + {categories.map((category, index) => ( + + ))} +
      + {enableLegendSlider && (hasScroll?.right || hasScroll?.left) ? ( + <> +
      + { + setIsKeyDowned(null) + scrollToTest("left") + }} + disabled={!hasScroll?.left} + /> + { + setIsKeyDowned(null) + scrollToTest("right") + }} + disabled={!hasScroll?.right} + /> +
      + + ) : null} +
    + ) +}) + +Legend.displayName = "Legend" + +const ChartLegend = ( + { payload }: any, + categoryColors: Map, + setLegendHeight: React.Dispatch>, + activeLegend: string | undefined, + onClick?: (category: string, color: string) => void, + enableLegendSlider?: boolean, +) => { + const legendRef = React.useRef(null) + + useOnWindowResize(() => { + const calculateHeight = (height: number | undefined) => + height ? Number(height) + 15 : 60 + setLegendHeight(calculateHeight(legendRef.current?.clientHeight)) + }) + + const filteredPayload = payload.filter((item: any) => item.type !== "none") + + return ( +
    + entry.value)} + colors={filteredPayload.map((entry: any) => + categoryColors.get(entry.value), + )} + onClickLegendItem={onClick} + activeLegend={activeLegend} + enableLegendSlider={enableLegendSlider} + /> +
    + ) +} + +//#region Tooltip + +interface ChartTooltipRowProps { + value: string + name: string + color: string +} + +const ChartTooltipRow = ({ value, name, color }: ChartTooltipRowProps) => ( +
    +
    +
    +

    + {value} +

    +
    +) + +interface ChartTooltipProps { + active: boolean | undefined + payload: any + label: string + categoryColors: Map + valueFormatter: (value: number) => string +} + +const ChartTooltip = ({ + active, + payload, + label, + categoryColors, + valueFormatter, +}: ChartTooltipProps) => { + if (active && payload) { + const filteredPayload = payload.filter((item: any) => item.type !== "none") + + return ( +
    +
    +

    + {label} +

    +
    + +
    + {filteredPayload.map( + ( + { value, name }: { value: number; name: string }, + index: number, + ) => ( + + ), + )} +
    +
    + ) + } + return null +} + +//#region BarChart + +type BaseEventProps = { + eventType: "dot" | "category" + categoryClicked: string + [key: string]: number | string +} + +type BarChartEventProps = BaseEventProps | null | undefined + +interface BarChartProps extends React.HTMLAttributes { + data: Record[] + index: string + categories: string[] + colors?: AvailableChartColorsKeys[] + valueFormatter?: (value: number) => string + startEndOnly?: boolean + showXAxis?: boolean + showYAxis?: boolean + showGridLines?: boolean + yAxisWidth?: number + intervalType?: "preserveStartEnd" | "equidistantPreserveStart" + showTooltip?: boolean + showLegend?: boolean + autoMinValue?: boolean + minValue?: number + maxValue?: number + allowDecimals?: boolean + onValueChange?: (value: BarChartEventProps) => void + enableLegendSlider?: boolean + tickGap?: number + barCategoryGap?: string | number + xAxisLabel?: string + yAxisLabel?: string + layout?: "vertical" | "horizontal" + type?: "default" | "stacked" | "percent" +} + +const BarChart = React.forwardRef( + (props, forwardedRef) => { + const { + data = [], + categories = [], + index, + colors = AvailableChartColors, + valueFormatter = (value: number) => value.toString(), + startEndOnly = false, + showXAxis = true, + showYAxis = true, + showGridLines = true, + yAxisWidth = 56, + intervalType = "equidistantPreserveStart", + showTooltip = true, + showLegend = true, + autoMinValue = false, + minValue, + maxValue, + allowDecimals = true, + className, + onValueChange, + enableLegendSlider = false, + barCategoryGap, + tickGap = 5, + xAxisLabel, + yAxisLabel, + layout = "horizontal", + type = "default", + ...other + } = props + const paddingValue = !showXAxis && !showYAxis ? 0 : 20 + const [legendHeight, setLegendHeight] = React.useState(60) + const [activeLegend, setActiveLegend] = React.useState( + undefined, + ) + const categoryColors = constructCategoryColors(categories, colors) + const [activeBar, setActiveBar] = React.useState(undefined) + const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue) + const hasOnValueChange = !!onValueChange + const stacked = type === "stacked" || type === "percent" + function valueToPercent(value: number) { + return `${(value * 100).toFixed(0)}%` + } + + function onBarClick(data: any, _: any, event: React.MouseEvent) { + event.stopPropagation() + if (!onValueChange) return + if (deepEqual(activeBar, { ...data.payload, value: data.value })) { + setActiveLegend(undefined) + setActiveBar(undefined) + onValueChange?.(null) + } else { + setActiveLegend(data.tooltipPayload?.[0]?.dataKey) + setActiveBar({ + ...data.payload, + value: data.value, + }) + onValueChange?.({ + eventType: "bar", + categoryClicked: data.tooltipPayload?.[0]?.dataKey, + ...data.payload, + }) + } + } + + function onCategoryClick(dataKey: string) { + if (!hasOnValueChange) return + if (dataKey === activeLegend && !activeBar) { + setActiveLegend(undefined) + onValueChange?.(null) + } else { + setActiveLegend(dataKey) + onValueChange?.({ + eventType: "category", + categoryClicked: dataKey, + }) + } + setActiveBar(undefined) + } + + return ( +
    + + { + setActiveBar(undefined) + setActiveLegend(undefined) + onValueChange?.(null) + } + : undefined + } + margin={{ + bottom: xAxisLabel ? 30 : undefined, + left: yAxisLabel ? 20 : undefined, + right: yAxisLabel ? 5 : undefined, + top: 5, + }} + stackOffset={type === "percent" ? "expand" : undefined} + layout={layout} + > + {showGridLines ? ( + + ) : null} + + {xAxisLabel && ( + + )} + + + {yAxisLabel && ( + + )} + + ( + + ) + ) : ( + <> + ) + } + /> + {showLegend ? ( + + ChartLegend( + { payload }, + categoryColors, + setLegendHeight, + activeLegend, + hasOnValueChange + ? (clickedLegendItem: string) => + onCategoryClick(clickedLegendItem) + : undefined, + enableLegendSlider, + ) + } + /> + ) : null} + {categories.map((category) => ( + + renderShape(props, activeBar, activeLegend, layout) + } + onClick={onBarClick} + radius={20} + /> + ))} + + +
    + ) + }, +) + +BarChart.displayName = "BarChart" + +export { BarChart, type BarChartEventProps } diff --git a/src/components/BarChart/barchart.spec.ts b/src/components/BarChart/barchart.spec.ts new file mode 100644 index 0000000..49904bb --- /dev/null +++ b/src/components/BarChart/barchart.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test" + +test.beforeEach(async ({ page }) => { + await page.goto( + "http://localhost:6006/?path=/story/visualization-areachart--default", + ) +}) + +test.describe("Expect default area chart", () => { + test("to be rendered", async ({ page }) => { + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .getByTestId("area-chart"), + ).toBeVisible() + }) + + test("to render legend two items", async ({ page }) => { + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator("li") + .filter({ hasText: "SolarCells" }), + ).toBeVisible() + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator("li") + .filter({ hasText: "Glass" }), + ).toBeVisible() + }) + + test("to render an x-axis", async ({ page }) => { + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator(".recharts-xAxis"), + ).toHaveClass(/recharts-xAxis/) + }) + + test("to render an y-axis", async ({ page }) => { + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator(".recharts-yAxis"), + ).toHaveClass(/recharts-yAxis/) + }) + + test("to render two lines", async ({ page }) => { + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator('path.recharts-curve.recharts-area-curve[name="SolarCells"]'), + ).toBeVisible() + await expect( + page + .frameLocator('iframe[title="storybook-preview-iframe"]') + .locator('path.recharts-curve.recharts-area-curve[name="Glass"]'), + ).toBeVisible() + }) +}) diff --git a/src/components/BarChart/barchart.stories.tsx b/src/components/BarChart/barchart.stories.tsx new file mode 100644 index 0000000..b99e545 --- /dev/null +++ b/src/components/BarChart/barchart.stories.tsx @@ -0,0 +1,302 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { BarChart } from "./BarChart" + +const chartdata = [ + { + date: "Jan 23", + SolarCells: 2890, + Glass: 2338, + Encapsulant: 1450, + BackSheet: 1900, + Frame: 1600, + JunctionBox: 1800, + Adhesive: 1700, + }, + { + date: "Feb 23", + SolarCells: 2756, + Glass: 2103, + Encapsulant: 1200, + BackSheet: 1850, + Frame: 1700, + JunctionBox: 1750, + Adhesive: 1650, + }, + { + date: "Mar 23", + SolarCells: 3322, + Glass: 2194, + Encapsulant: 1300, + BackSheet: 2200, + Frame: 1400, + JunctionBox: 2000, + Adhesive: 800, + }, + { + date: "Apr 23", + SolarCells: 3470, + Glass: 2108, + Encapsulant: 1400, + BackSheet: 1600, + Frame: 1800, + JunctionBox: 1900, + Adhesive: -1950, + }, + { + date: "May 23", + SolarCells: 3475, + Glass: 1812, + Encapsulant: 1550, + BackSheet: 2300, + Frame: 1450, + JunctionBox: 2200, + Adhesive: -1600, + }, + { + date: "Jun 23", + SolarCells: 3129, + Glass: 1726, + Encapsulant: 1350, + BackSheet: 2100, + Frame: 1750, + JunctionBox: 2050, + Adhesive: -1700, + }, + { + date: "Jul 23", + SolarCells: 3490, + Glass: 1982, + Encapsulant: 1450, + BackSheet: 1950, + Frame: 1500, + JunctionBox: 2300, + Adhesive: -1800, + }, + { + date: "Aug 23", + SolarCells: 2903, + Glass: 2012, + Encapsulant: 1250, + BackSheet: 1700, + Frame: 1850, + JunctionBox: 2150, + Adhesive: -1900, + }, + { + date: "Sep 23", + SolarCells: 2643, + Glass: 2342, + Encapsulant: 1400, + BackSheet: 1600, + Frame: 1500, + JunctionBox: 2000, + Adhesive: -3750, + }, + { + date: "Oct 23", + SolarCells: 2837, + Glass: 2473, + Encapsulant: 1350, + BackSheet: 1850, + Frame: 1900, + JunctionBox: 2100, + Adhesive: -2600, + }, + { + date: "Nov 23", + SolarCells: 2954, + Glass: 3848, + Encapsulant: 1200, + BackSheet: 2000, + Frame: 1750, + JunctionBox: 2400, + Adhesive: -2950, + }, + { + date: "Dec 23", + SolarCells: 3239, + Glass: 3736, + Encapsulant: 1550, + BackSheet: 1700, + Frame: 1600, + JunctionBox: 2250, + Adhesive: -3800, + }, +] + +const meta: Meta = { + title: "visualization/BarChart", + component: BarChart, + args: { data: chartdata, index: "date", categories: ["SolarCells", "Glass"] }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + ), +} + +export const DefaultNegative: Story = { + args: { + categories: ["SolarCells", "Adhesive"], + }, +} + +export const WithValueFormatter: Story = { + args: { + valueFormatter: (v) => `$${Intl.NumberFormat("us").format(v).toString()}`, + }, +} + +export const WithAxisLabels: Story = { + args: { + xAxisLabel: "Month of Year", + yAxisLabel: "Revenue", + }, +} + +export const WithMinValue: Story = { + args: { + autoMinValue: true, + }, +} + +export const WithMinAndMaxValue: Story = { + args: { + maxValue: 5000, + minValue: -3000, + }, +} + +export const AllColors: Story = { + args: { + data: chartdata, + index: "date", + categories: [ + "SolarCells", + "Glass", + "Encapsulant", + "BackSheet", + "Frame", + "JunctionBox", + "Adhesive", + ], + }, +} + +export const WithLegendSlider: Story = { + args: { + className: "max-w-md", + data: chartdata, + index: "date", + categories: [ + "SolarCells", + "Glass", + "Encapsulant", + "BackSheet", + "Frame", + "JunctionBox", + "Adhesive", + ], + enableLegendSlider: true, + onValueChange: (v) => console.log(v), + }, +} + +export const ShiftColors: Story = { + args: { + colors: ["amber", "cyan"], + }, +} + +export const WithStartEndOnly: Story = { + args: { + startEndOnly: true, + }, +} + +export const WithNoAxis: Story = { + args: { + showXAxis: false, + showYAxis: false, + }, +} + +export const WithNoGridlines: Story = { + args: { + showGridLines: false, + }, +} + +export const WithNoLegend: Story = { + args: { + showLegend: false, + }, +} + +export const WithNoTooltip: Story = { + args: { + showTooltip: false, + }, +} + +export const WithOnValueChange: Story = { + args: { + onValueChange: (v) => console.log(v), + }, +} + +export const WithLargeTickGap: Story = { + args: { + tickGap: 300, + }, +} + +export const WithLayoutVertical: Story = { + args: { + layout: "vertical", + }, +} + +export const WithTypePercent: Story = { + args: { + type: "percent", + }, +} +export const WithTypePercentVertical: Story = { + args: { + layout: "vertical", + type: "percent", + }, +} + +export const stacked: Story = { + args: { + type: "stacked", + }, +} + +export const WithTypeStackedVertical: Story = { + args: { + layout: "vertical", + type: "stacked", + }, +} + +export const OneDataValue: Story = { + args: { + data: chartdata.slice(0, 1), + index: "date", + categories: ["SolarCells", "Glass"], + onValueChange: (v) => console.log(v), + }, +} diff --git a/src/components/BarChart/changelog.md b/src/components/BarChart/changelog.md new file mode 100644 index 0000000..a0709de --- /dev/null +++ b/src/components/BarChart/changelog.md @@ -0,0 +1,5 @@ +# Tremor Raw BarChart Changelog + +## 0.0.0 + +### Changes From 2f2ed7ff720c086e7095ea95910e126143a9ee9d Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Sat, 25 May 2024 17:26:18 +0200 Subject: [PATCH 2/8] update barchart --- src/components/BarChart/BarChart.tsx | 11 +++++------ src/components/BarChart/barchart.stories.tsx | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/BarChart/BarChart.tsx b/src/components/BarChart/BarChart.tsx index b3cfee8..a90b02b 100644 --- a/src/components/BarChart/BarChart.tsx +++ b/src/components/BarChart/BarChart.tsx @@ -666,9 +666,7 @@ const BarChart = React.forwardRef( hide={!showXAxis} tick={{ transform: - layout !== "vertical" - ? "translate(0, 6)" - : "translate(-3, 0)", + layout !== "vertical" ? "translate(0, 6)" : undefined, }} fill="" stroke="" @@ -697,7 +695,8 @@ const BarChart = React.forwardRef( : { type: "number", domain: yAxisDomain as AxisDomain, - tickFormatter: valueFormatter, + tickFormatter: + type === "percent" ? valueToPercent : valueFormatter, allowDecimals: allowDecimals, })} > @@ -728,7 +727,7 @@ const BarChart = React.forwardRef( transform: layout !== "vertical" ? "translate(-3, 0)" - : "translate(0, 6)", + : "translate(0, 0)", }} {...(layout !== "vertical" ? { @@ -744,7 +743,7 @@ const BarChart = React.forwardRef( ? [data[0][index], data[data.length - 1][index]] : undefined, type: "category", - interval: "preserveStartEnd", + interval: "equidistantPreserveStart", })} > {yAxisLabel && ( diff --git a/src/components/BarChart/barchart.stories.tsx b/src/components/BarChart/barchart.stories.tsx index b99e545..0bfacb7 100644 --- a/src/components/BarChart/barchart.stories.tsx +++ b/src/components/BarChart/barchart.stories.tsx @@ -263,6 +263,7 @@ export const WithLargeTickGap: Story = { export const WithLayoutVertical: Story = { args: { + categories: ["SolarCells"], layout: "vertical", }, } From 69ddd752241b2e987efa517cca16b3d85ecd35a0 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Sat, 25 May 2024 19:35:31 +0200 Subject: [PATCH 3/8] first draft linechart --- src/components/LineChart/LineChart.tsx | 14 +++++++++++++- src/components/LineChart/linechart.stories.tsx | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/components/LineChart/LineChart.tsx b/src/components/LineChart/LineChart.tsx index 53073b5..478863c 100644 --- a/src/components/LineChart/LineChart.tsx +++ b/src/components/LineChart/LineChart.tsx @@ -317,6 +317,7 @@ const ChartLegend = ( activeLegend: string | undefined, onClick?: (category: string, color: string) => void, enableLegendSlider?: boolean, + legendPosition?: "left" | "center" | "right", ) => { const legendRef = React.useRef(null) @@ -329,7 +330,15 @@ const ChartLegend = ( const filteredPayload = payload.filter((item: any) => item.type !== "none") return ( -
    +
    entry.value)} colors={filteredPayload.map((entry: any) => @@ -492,6 +501,7 @@ interface LineChartProps extends React.HTMLAttributes { connectNulls?: boolean xAxisLabel?: string yAxisLabel?: string + legendPosition?: "left" | "center" | "right" } const LineChart = React.forwardRef( @@ -521,6 +531,7 @@ const LineChart = React.forwardRef( tickGap = 5, xAxisLabel, yAxisLabel, + legendPosition = "right", ...other } = props const paddingValue = !showXAxis && !showYAxis ? 0 : 20 @@ -714,6 +725,7 @@ const LineChart = React.forwardRef( onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, + legendPosition, ) } /> diff --git a/src/components/LineChart/linechart.stories.tsx b/src/components/LineChart/linechart.stories.tsx index d90d8e2..cf7fd98 100644 --- a/src/components/LineChart/linechart.stories.tsx +++ b/src/components/LineChart/linechart.stories.tsx @@ -128,7 +128,11 @@ const chartdata = [ const meta: Meta = { title: "visualization/LineChart", component: LineChart, - args: { data: chartdata, index: "date", categories: ["SolarCells", "Glass"] }, + args: { + data: chartdata, + index: "date", + categories: ["SolarCells", "Glass"], + }, } export default meta @@ -193,6 +197,18 @@ export const AllColors: Story = { }, } +export const WithLegendLeft: Story = { + args: { + legendPosition: "left", + }, +} + +export const WithLegendCenter: Story = { + args: { + legendPosition: "center", + }, +} + export const WithLegendSlider: Story = { args: { className: "max-w-md", From bc7fb44d628ed092d42cc4687db0b38783ce90d4 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Sun, 26 May 2024 00:27:17 +0200 Subject: [PATCH 4/8] add legend position to areachart --- src/components/AreaChart/AreaChart.tsx | 14 +++++++++++++- src/components/AreaChart/areachart.stories.tsx | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/AreaChart/AreaChart.tsx b/src/components/AreaChart/AreaChart.tsx index 05e57af..72ce319 100644 --- a/src/components/AreaChart/AreaChart.tsx +++ b/src/components/AreaChart/AreaChart.tsx @@ -318,6 +318,7 @@ const ChartLegend = ( activeLegend: string | undefined, onClick?: (category: string, color: string) => void, enableLegendSlider?: boolean, + legendPosition?: "left" | "center" | "right", ) => { const legendRef = React.useRef(null) @@ -330,7 +331,15 @@ const ChartLegend = ( const filteredPayload = payload.filter((item: any) => item.type !== "none") return ( -
    +
    entry.value)} colors={filteredPayload.map((entry: any) => @@ -494,6 +503,7 @@ interface AreaChartProps extends React.HTMLAttributes { xAxisLabel?: string yAxisLabel?: string type?: "default" | "stacked" | "percent" + legendPosition?: "left" | "center" | "right" } const AreaChart = React.forwardRef( @@ -524,6 +534,7 @@ const AreaChart = React.forwardRef( xAxisLabel, yAxisLabel, type = "default", + legendPosition = "right", ...other } = props const paddingValue = !showXAxis && !showYAxis ? 0 : 20 @@ -724,6 +735,7 @@ const AreaChart = React.forwardRef( onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, + legendPosition, ) } /> diff --git a/src/components/AreaChart/areachart.stories.tsx b/src/components/AreaChart/areachart.stories.tsx index 93d422f..4ae0644 100644 --- a/src/components/AreaChart/areachart.stories.tsx +++ b/src/components/AreaChart/areachart.stories.tsx @@ -193,6 +193,18 @@ export const AllColors: Story = { }, } +export const WithLegendLeft: Story = { + args: { + legendPosition: "left", + }, +} + +export const WithLegendCenter: Story = { + args: { + legendPosition: "center", + }, +} + export const WithLegendSlider: Story = { args: { className: "max-w-md", From e63ee2e8c7acedc2174ac357b0250cc9601e9324 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Mon, 27 May 2024 13:19:27 +0200 Subject: [PATCH 5/8] Add legend left padding --- src/components/AreaChart/AreaChart.tsx | 6 ++++++ src/components/LineChart/LineChart.tsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/components/AreaChart/AreaChart.tsx b/src/components/AreaChart/AreaChart.tsx index 72ce319..0914312 100644 --- a/src/components/AreaChart/AreaChart.tsx +++ b/src/components/AreaChart/AreaChart.tsx @@ -319,6 +319,7 @@ const ChartLegend = ( onClick?: (category: string, color: string) => void, enableLegendSlider?: boolean, legendPosition?: "left" | "center" | "right", + yAxisWidth?: number, ) => { const legendRef = React.useRef(null) @@ -330,9 +331,13 @@ const ChartLegend = ( const filteredPayload = payload.filter((item: any) => item.type !== "none") + const paddingLeft = + legendPosition === "left" && yAxisWidth ? yAxisWidth - 8 : 0 + return (
    ( : undefined, enableLegendSlider, legendPosition, + yAxisWidth, ) } /> diff --git a/src/components/LineChart/LineChart.tsx b/src/components/LineChart/LineChart.tsx index 478863c..44a07d8 100644 --- a/src/components/LineChart/LineChart.tsx +++ b/src/components/LineChart/LineChart.tsx @@ -318,6 +318,7 @@ const ChartLegend = ( onClick?: (category: string, color: string) => void, enableLegendSlider?: boolean, legendPosition?: "left" | "center" | "right", + yAxisWidth?: number, ) => { const legendRef = React.useRef(null) @@ -329,9 +330,13 @@ const ChartLegend = ( const filteredPayload = payload.filter((item: any) => item.type !== "none") + const paddingLeft = + legendPosition === "left" && yAxisWidth ? yAxisWidth - 8 : 0 + return (
    ( : undefined, enableLegendSlider, legendPosition, + yAxisWidth, ) } /> From b4bd47f8bbb51fa53b2a94569a41c2986b796c32 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Fri, 31 May 2024 19:23:16 +0200 Subject: [PATCH 6/8] Update barchart.stories.tsx --- src/components/BarChart/barchart.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/BarChart/barchart.stories.tsx b/src/components/BarChart/barchart.stories.tsx index a80b64c..000e1f9 100644 --- a/src/components/BarChart/barchart.stories.tsx +++ b/src/components/BarChart/barchart.stories.tsx @@ -193,7 +193,6 @@ export const AllColors: Story = { }, } - export const WithLegendLeft: Story = { args: { legendPosition: "left", From 8de0c1b5721f78c44f7456f0310edc783dc7bc52 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Fri, 31 May 2024 19:31:45 +0200 Subject: [PATCH 7/8] add more workers --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 960ac9b..07e998b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,7 +18,7 @@ jobs: - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests - run: pnpm exec playwright test + run: pnpm exec playwright test --workers 4 - uses: actions/upload-artifact@v4 if: always() with: From b933a56acf65a884a9528475757ed2eb038b57f8 Mon Sep 17 00:00:00 2001 From: severinlandolt Date: Fri, 31 May 2024 19:56:15 +0200 Subject: [PATCH 8/8] revert workers --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 07e998b..960ac9b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,7 +18,7 @@ jobs: - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests - run: pnpm exec playwright test --workers 4 + run: pnpm exec playwright test - uses: actions/upload-artifact@v4 if: always() with: