diff --git a/src/app/elements/page.tsx b/src/app/elements/page.tsx index db7181ae..dc9a26e5 100644 --- a/src/app/elements/page.tsx +++ b/src/app/elements/page.tsx @@ -5,12 +5,12 @@ import { useState } from 'react'; import AccordionContainer from '@/components/Accordions/AccordionContainer'; import { CustomButton } from '@/components/Buttons/CustomButton'; import { CategoricalChart } from '@/components/Charts/CategoricalChart'; -import { LineChart } from '@/components/Charts/LineChart'; +import { ContinuousChart } from '@/components/Charts/ContinuousChart'; import MapSkeleton from '@/components/Map/MapSkeleton'; import SearchBar from '@/components/Search/SearchBar'; import { CategoricalChartData } from '@/domain/entities/charts/CategoricalChartData.ts'; -import { LineChartData } from '@/domain/entities/charts/LineChartData.ts'; -import { LineChartDataType } from '@/domain/enums/LineChartDataType.ts'; +import { ContinuousChartData } from '@/domain/entities/charts/ContinuousChartData.ts'; +import { ContinuousChartDataType } from '@/domain/enums/ContinuousChartDataType.ts'; import AccordionsOperations from '@/operations/accordions/AccordionOperations'; import { ReactComponent as FoodSvg } from '../../../public/Images/FoodConsumption.svg'; @@ -21,8 +21,8 @@ import { ReactComponent as FoodSvg } from '../../../public/Images/FoodConsumptio */ export default async function Elements() { const [searchTerm, setSearchTerm] = useState(''); - const simpleAndSmallLineChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, + const simpleAndSmallContinuousChartData: ContinuousChartData = { + type: ContinuousChartDataType.LINE_CHART_DATA, xAxisType: 'linear', lines: [ { @@ -37,8 +37,8 @@ export default async function Elements() { ], }; - const maxedOutLineChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, + const maxedOutContinuousChartData: ContinuousChartData = { + type: ContinuousChartDataType.LINE_CHART_DATA, xAxisType: 'linear', yAxisLabel: 'yield', lines: [ @@ -97,8 +97,8 @@ export default async function Elements() { ], }; - const predictionDummyChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, + const predictionDummyChartData: ContinuousChartData = { + type: ContinuousChartDataType.LINE_CHART_DATA, xAxisType: 'linear', yAxisLabel: 'Mill', predictionVerticalLineX: 3, @@ -129,7 +129,7 @@ export default async function Elements() { ], }; - const categoricalDummyChartData1: CategoricalChartData = { + const categoricalDummyChartData: CategoricalChartData = { yAxisLabel: 'Mill', categories: [ { @@ -143,8 +143,8 @@ export default async function Elements() { ], }; - const emptyDummyChartData: LineChartData = { - type: LineChartDataType.LINE_CHART_DATA, + const emptyDummyChartData: ContinuousChartData = { + type: ContinuousChartDataType.LINE_CHART_DATA, xAxisType: 'linear', yAxisLabel: 'Mill', predictionVerticalLineX: 3, @@ -174,8 +174,8 @@ export default async function Elements() {
-
-
- +
- +
- +
diff --git a/src/components/Charts/CategoricalChart.tsx b/src/components/Charts/CategoricalChart.tsx index 8f599c77..05d08646 100644 --- a/src/components/Charts/CategoricalChart.tsx +++ b/src/components/Charts/CategoricalChart.tsx @@ -1,8 +1,7 @@ 'use client'; import Highcharts from 'highcharts'; -import { useTheme } from 'next-themes'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { ChartContainer } from '@/components/Charts/helpers/ChartContainer'; import { ChartType } from '@/domain/enums/ChartType.ts'; @@ -11,7 +10,7 @@ import CategoricalChartOperations from '@/operations/charts/CategoricalChartOper /** * The `CategoricalChart` component is a box that primarily renders a title, description text, and a bar chart. - * It should be used to plot categorical data. For continues data please use the `LineChart` component. + * It should be used to plot categorical data. For continues data please use the `ContinuousChart` component. * This component has a width of 100%, so it adjusts to the width of its parent element in which it is used. * The height of the entire box depends on the provided text, while the chart itself has a fixed height. * It also provides the option to open the chart in a full-screen modal, where one can download the data as well. @@ -22,6 +21,8 @@ import CategoricalChartOperations from '@/operations/charts/CategoricalChartOper * @param small when selected, all components in the line chart box become slightly smaller (optional) * @param noPadding when selected, the main box has no padding on all sides (optional) * @param transparentBackground when selected, the background of the entire component is transparent (optional) + * @param chartHeight with this parameter, the height of the actual chart can be set to a fixed value in pixels. If + * chartHeight is not specified, the chart is given a fixed default height depending on `small`. * @param disableExpandable when selected, the functionality to open the chart in a larger modal is disabled (optional) * @param disablePieChartSwitch when selected, the functionality to switch to a pie chart is disabled (optional) * @param disableDownload when selected, the functionality to download the chart is disabled (optional) @@ -33,23 +34,21 @@ export function CategoricalChart({ small, noPadding, transparentBackground, + chartHeight, disableExpandable, disablePieChartSwitch, disableDownload, }: CategoricalChartProps) { - const { theme } = useTheme(); - - // build chart options for 'Highcharts' - const defaultChartOptions: Highcharts.Options | undefined = CategoricalChartOperations.getHighChartOptions(data); - // controlling if a bar or pie chart is rendered; bar chart is the default - const [showPieChart, setShowPieChart] = useState(false); - const [chartOptions, setChartOptions] = useState(defaultChartOptions); + const [showPieChart, setShowPieChart] = useState(false); + const [chartOptions, setChartOptions] = useState( + CategoricalChartOperations.getHighChartOptions(data, showPieChart) + ); - // handling the bar and pie chart switch and the theme switch; - useEffect(() => { + // function to update/recalculate the chart options + const recalculateChartOptions = () => { setChartOptions(CategoricalChartOperations.getHighChartOptions(data, showPieChart)); - }, [showPieChart, theme, data]); + }; const alternativeSwitchButtonProps = disablePieChartSwitch ? undefined @@ -62,13 +61,15 @@ export function CategoricalChart({ return ( { - return LineChartOperations.getDistinctXAxisValues(lineChartData).length; - }, [lineChartData]); + return ContinuousChartOperations.getDistinctXAxisValues(continuousChartData).length; + }, [continuousChartData]); + const [selectedXAxisRange, setSelectedXAxisRange] = useState([0, xAxisLength - 1]); + // we have to make sure that if the data changes we update the XAxis slider configuration as well useEffect(() => { setSelectedXAxisRange([0, xAxisLength - 1]); }, [xAxisLength]); // controlling if a line or bar chart is rendered; line chart is the default - const [showBarChart, setShowBarChart] = useState(false); - const [chartOptions, setChartOptions] = useState(lineChartOptions); + const [showBarChart, setShowBarChart] = useState(false); + const [chartOptions, setChartOptions] = useState( + ContinuousChartOperations.getHighChartOptions(continuousChartData) + ); - // handling the line and bar chart switch and the theme switch; - // also handling changing the x-axis range using the `LineChartXAxisSlider`; - // special: if the selected x-axis range has length 1 -> bar chart is displayed - useEffect(() => { + // function to update/recalculate the chart options + const recalculateChartOptions = () => { + // also handling changing the x-axis range using the `ChartXAxisSlider`; + // special: if the selected x-axis range has length 1 -> bar chart is displayed if (showBarChart || selectedXAxisRange[1] - selectedXAxisRange[0] === 0) { setChartOptions( - LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1], true) + ContinuousChartOperations.getHighChartOptions( + continuousChartData, + selectedXAxisRange[0], + selectedXAxisRange[1], + true + ) ); } else { setChartOptions( - LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1]) + ContinuousChartOperations.getHighChartOptions(continuousChartData, selectedXAxisRange[0], selectedXAxisRange[1]) ); } - }, [showBarChart, theme, selectedXAxisRange, lineChartData]); + }; // chart slider props - to manipulate the shown x-axis range const sliderProps = disableXAxisSlider @@ -105,13 +112,15 @@ export function LineChart({ return ( (null); + const [chartKey, setChartKey] = useState(0); // full screen modal state handling const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); + // handling the theme switch + useEffect(() => { + // `theme` change does not guarantee that the NextUI CSS colors have already been changed; + // therefore we synchronize the update with the next repaint cycle, ensuring the CSS variables are updated + const rafId = requestAnimationFrame(() => { + recalculateChartOptions(); + setChartKey((prev) => prev + 1); // forces chart to remount -> chart animation will be re-triggered + }); + return () => cancelAnimationFrame(rafId); + }, [theme]); + + // handling chart type switch, data changes and slider changes + useEffect(() => { + recalculateChartOptions(); + setChartKey((prev) => prev + 1); // forces chart to remount -> chart animation will be re-triggered + }, [alternativeSwitchButtonProps?.showAlternativeChart, sliderProps?.selectedSliderRange, chartData]); + // handling the x-axis range slider visibility const [showSlider, setShowSlider] = useState(false); @@ -122,10 +145,11 @@ export function ChartContainer({ highcharts={Highcharts} options={chartOptions} ref={chartRef} + key={chartKey} containerProps={{ style: { width: '100%', - height: `${CHART_HEIGHT}rem`, + height: CHART_HEIGHT, borderRadius: '0 0 0.375rem 0.375rem', }, }} diff --git a/src/components/Charts/helpers/ChartModal.tsx b/src/components/Charts/helpers/ChartModal.tsx index 9e598cda..e59fad15 100644 --- a/src/components/Charts/helpers/ChartModal.tsx +++ b/src/components/Charts/helpers/ChartModal.tsx @@ -3,7 +3,8 @@ import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@nextu import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; import { Minus } from 'iconsax-react'; -import { useRef } from 'react'; +import { useTheme } from 'next-themes'; +import { useEffect, useRef, useState } from 'react'; import ChartAlternativeSwitchButton from '@/components/Charts/helpers/buttons/ChartAlternativeSwitchButton'; import ChartDownloadButton from '@/components/Charts/helpers/buttons/ChartDownloadButton'; @@ -30,7 +31,15 @@ export function ChartModal({ showSlider, setShowSlider, }: ChartModalProps) { + const { theme } = useTheme(); + const chartRef = useRef(null); + const [chartKey, setChartKey] = useState(0); + + // if chart changes -> force chart to remount -> chart animation will be re-triggered + useEffect(() => { + setChartKey((prev) => prev + 1); + }, [theme, alternativeSwitchButtonProps?.showAlternativeChart, sliderProps?.selectedSliderRange, chartData]); return ( diff --git a/src/components/Charts/helpers/buttons/ChartSliderButton.tsx b/src/components/Charts/helpers/buttons/ChartSliderButton.tsx index 5b108ac0..48d1dc3d 100644 --- a/src/components/Charts/helpers/buttons/ChartSliderButton.tsx +++ b/src/components/Charts/helpers/buttons/ChartSliderButton.tsx @@ -2,12 +2,12 @@ import { Button } from '@nextui-org/button'; import { Settings } from 'iconsax-react'; import { Tooltip } from '@/components/Tooltip/Tooltip'; -import { LineChartSliderButtonProps } from '@/domain/props/ChartContainerProps'; +import { ChartSliderButtonProps } from '@/domain/props/ChartContainerProps'; /** * This component is tied to the `ChartContainer` and `ChartModal` component and should not be used independently. */ -export default function ChartSliderButton({ showSlider, setShowSlider, size = 4 }: LineChartSliderButtonProps) { +export default function ChartSliderButton({ showSlider, setShowSlider, size = 4 }: ChartSliderButtonProps) { return (