diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/MPSubvComparison.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/components/MPSubvComparison.tsx index b7d45827..fd6e47d1 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/MPSubvComparison.tsx +++ b/front/app/community/[siren]/comparison/[comparedSiren]/components/MPSubvComparison.tsx @@ -1,5 +1,7 @@ 'use client'; +import { memo, useCallback, useMemo } from 'react'; + import type { Community } from '#app/models/community'; import EmptyState from '#components/EmptyState'; import Loading from '#components/ui/Loading'; @@ -27,12 +29,23 @@ export function MPSubvComparison({ }: MPSubvComparisonProperties) { const { year: selectedYear, setYear: setSelectedYear } = useComparisonYear(); + // Memoize section title to prevent recalculation + const sectionTitle = useMemo(() => getSectionTitle(comparisonType), [comparisonType]); + + // Stabilize year selection handler + const handleYearSelect = useCallback( + (year: number) => { + setSelectedYear(year); + }, + [setSelectedYear], + ); + return ( <> {/* Desktop layout */}
@@ -53,7 +66,6 @@ export function MPSubvComparison({ bgColor='bg-primary-light' /> } - className='my-10' />
@@ -77,37 +89,37 @@ type ComparingMPSubvProperties = { bgColor?: string; }; -function ComparingMPSubv({ siren, year, comparisonType, bgColor }: ComparingMPSubvProperties) { - const { data, isPending, isError } = useMPSubvComparison(siren, year, comparisonType); +const ComparingMPSubv = memo( + ({ siren, year, comparisonType, bgColor }: ComparingMPSubvProperties) => { + const { data, isPending, isError } = useMPSubvComparison(siren, year, comparisonType); - // Show loading state - if (isPending) { - return ; - } + // Show EmptyState for actual errors or missing data + if (isError || (!isPending && (!data || data.top5 === undefined))) { + return ( + + ); + } - // Show EmptyState for actual errors or missing data - if (isError || !data || data.top5 === undefined) { return ( - ); - } + }, +); - return ( - - ); -} +ComparingMPSubv.displayName = 'ComparingMPSubv'; function getSectionTitle(comparisonType: ComparisonType) { switch (comparisonType) { @@ -149,7 +161,7 @@ type MobileMPSubvCardProps = { comparisonType: ComparisonType; }; -function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubvCardProps) { +const MobileMPSubvCard = memo(({ siren1, siren2, year, comparisonType }: MobileMPSubvCardProps) => { const { data: data1, isPending: isPending1, @@ -162,6 +174,26 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv isError: isError2, } = useMPSubvComparison(siren2, year, comparisonType); + // Memoize comparison name to prevent recalculation + const comparisonName = useMemo(() => getName(comparisonType), [comparisonType]); + + const renderInfoBlock = useCallback( + (label: string, value1: string, value2: string) => ( + <> +

{label}

+
+
+ {value1} +
+
+ {value2} +
+
+ + ), + [], + ); + if (isPending1 || isPending2) { return ; } @@ -176,28 +208,14 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv ) { return ( ); } - const renderInfoBlock = (label: string, value1: string, value2: string) => ( - <> -

{label}

-
-
- {value1} -
-
- {value2} -
-
- - ); - return ( {/* Comparaison Montant Total */} @@ -212,7 +230,7 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv {/* Comparaison Nombre */}
{renderInfoBlock( - `Nombre de ${getName(comparisonType)}`, + `Nombre de ${comparisonName}`, data1.total_number.toString(), data2.total_number.toString(), )} @@ -224,7 +242,7 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv totalAmount={data1.total_amount} totalNumber={data1.total_number} top5Items={data1.top5} - comparisonName={getName(comparisonType)} + comparisonName={comparisonName} columnLabel={getColumnLabel(comparisonType)} communityName={formatLocationName(data1?.community_name || 'N/A')} bgColor='bg-brand-3' @@ -234,7 +252,7 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv totalAmount={data2.total_amount} totalNumber={data2.total_number} top5Items={data2.top5} - comparisonName={getName(comparisonType)} + comparisonName={comparisonName} columnLabel={getColumnLabel(comparisonType)} communityName={formatLocationName(data2?.community_name || 'N/A')} bgColor='bg-primary-light' @@ -242,4 +260,6 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
); -} +}); + +MobileMPSubvCard.displayName = 'MobileMPSubvCard'; diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/TableInfoBlock.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/components/TableInfoBlock.tsx index 44880ccd..e8cbdc38 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/TableInfoBlock.tsx +++ b/front/app/community/[siren]/comparison/[comparedSiren]/components/TableInfoBlock.tsx @@ -14,12 +14,13 @@ import { formatCompactPrice } from '#utils/utils'; type TableInfoBlockProps = { totalAmount: number; totalNumber: number; - top5Items: Array<{ label: string | null; value: number }>; + top5Items: Array<{ label: string | null; value: number }> | null; comparisonName: string; columnLabel: string; communityName?: string; bgColor?: string; className?: string; + isLoadingDetails?: boolean; }; export function TableInfoBlock({ @@ -31,6 +32,7 @@ export function TableInfoBlock({ communityName, bgColor = 'bg-brand-3', className = '', + isLoadingDetails = false, }: TableInfoBlockProps) { return (
@@ -45,15 +47,21 @@ export function TableInfoBlock({

Montant total{' '} - - {formatCompactPrice(totalAmount)} + + {isLoadingDetails ? '---' : formatCompactPrice(totalAmount)}

- Nombre de {comparisonName}{' '} - {totalNumber} + Nombre de {comparisonName} + + {isLoadingDetails ? '---' : totalNumber} +

@@ -61,22 +69,57 @@ export function TableInfoBlock({ Top 5 des {comparisonName} - + {columnLabel} Montant - {top5Items.map(({ label, value }) => ( - - - {label !== null ? label.toLocaleUpperCase() : 'Non précisé'} - - - {formatCompactPrice(value)} - - - ))} + {isLoadingDetails || !top5Items + ? Array.from({ length: 5 }, (_, index) => ( + + +
+
+
+ + +
+
+
+ + + )) + : top5Items.map(({ label, value }, index) => ( + + + {label !== null + ? label.charAt(0).toUpperCase() + label.slice(1).toLowerCase() + : 'Non précisé'} + + + {formatCompactPrice(value)} + + + ))}
diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/TransparencyComparison.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/components/TransparencyComparison.tsx index 4c587e53..3701815f 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/TransparencyComparison.tsx +++ b/front/app/community/[siren]/comparison/[comparedSiren]/components/TransparencyComparison.tsx @@ -16,9 +16,16 @@ import { SideBySideComparison } from './shared/SideBySideComparison'; type TransparencyComparisonProperties = { siren1: string; siren2: string; + community1Name: string; + community2Name: string; }; -export function TransparencyComparison({ siren1, siren2 }: TransparencyComparisonProperties) { +export function TransparencyComparison({ + siren1, + siren2, + community1Name, + community2Name, +}: TransparencyComparisonProperties) { const { year: selectedYear, setYear: setSelectedYear } = useComparisonYear(); return ( @@ -34,13 +41,18 @@ export function TransparencyComparison({ siren1, siren2 }: TransparencyCompariso } rightChild={} - className='my-10' /> {/* Mobile layout - unified card */}
- +
); @@ -120,9 +132,17 @@ type MobileComparisonCardProps = { siren1: string; siren2: string; year: number; + community1Name: string; + community2Name: string; }; -function MobileComparisonCard({ siren1, siren2, year }: MobileComparisonCardProps) { +function MobileComparisonCard({ + siren1, + siren2, + year, + community1Name, + community2Name, +}: MobileComparisonCardProps) { const { data: data1, isPending: isPending1, @@ -156,8 +176,8 @@ function MobileComparisonCard({ siren1, siren2, year }: MobileComparisonCardProp {/* Header avec les noms des villes */}
- Ville de Paris - Dijon Métropole + {community1Name} + {community2Name}
{/* Section Marchés publics */} diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/hooks/useComparisonYear.ts b/front/app/community/[siren]/comparison/[comparedSiren]/components/hooks/useComparisonYear.ts index 15d03000..26b86154 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/hooks/useComparisonYear.ts +++ b/front/app/community/[siren]/comparison/[comparedSiren]/components/hooks/useComparisonYear.ts @@ -9,7 +9,7 @@ export function useComparisonYear() { const [year, setYear] = useQueryState( 'year', - parseAsInteger.withDefault(currentYear).withOptions({ + parseAsInteger.withDefault(currentYear - 1).withOptions({ // Synchronise l'état dans l'URL shallow: false, // Utilise l'historique pour permettre le back/forward diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/shared/ComparisonModificationCard.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/components/shared/ComparisonModificationCard.tsx index 34900383..26f1736f 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/shared/ComparisonModificationCard.tsx +++ b/front/app/community/[siren]/comparison/[comparedSiren]/components/shared/ComparisonModificationCard.tsx @@ -27,9 +27,9 @@ export function ComparisonModificationCard({ return ( - + Modifier la ville de comparaison @@ -37,14 +37,14 @@ export function ComparisonModificationCard({ Comparer les dernières données de dépenses
publiques de vos collectivités locales.
- -
+ +
{currentCommunity.nom}
avec diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/components/skeletons/DataTableSkeleton.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/components/skeletons/DataTableSkeleton.tsx deleted file mode 100644 index 0c755506..00000000 --- a/front/app/community/[siren]/comparison/[comparedSiren]/components/skeletons/DataTableSkeleton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -type DataTableSkeletonProps = { - title: string; -}; - -export function DataTableSkeleton({ title }: DataTableSkeletonProps) { - return ( -
- {/* Section title */} -
-
- Chargement {title}... -
-
-
- - {/* Comparison cards */} -
- - -
-
- ); -} - -function DataCardSkeleton() { - return ( -
- {/* Header with total amount */} -
-
-
-
- - {/* Count */} -
-
-
-
- - {/* Data table entries */} -
- {Array.from({ length: 5 }).map((_, i) => ( -
-
-
-
-
-
-
- ))} -
- - {/* Footer link */} -
-
- ); -} diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/loading.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/loading.tsx deleted file mode 100644 index d2c9f811..00000000 --- a/front/app/community/[siren]/comparison/[comparedSiren]/loading.tsx +++ /dev/null @@ -1,44 +0,0 @@ -export default function Loading() { - return ( -
- {/* Header skeleton */} -
- -
- {/* Header comparison skeleton */} -
-
-
-
- - {/* Transparency section skeleton */} -
-
-
-
-
-
-
- - {/* Data sections skeleton */} -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- ); -} diff --git a/front/app/community/[siren]/comparison/[comparedSiren]/page.tsx b/front/app/community/[siren]/comparison/[comparedSiren]/page.tsx index 022fde76..c003ce31 100644 --- a/front/app/community/[siren]/comparison/[comparedSiren]/page.tsx +++ b/front/app/community/[siren]/comparison/[comparedSiren]/page.tsx @@ -1,5 +1,3 @@ -import { Suspense } from 'react'; - import type { Metadata } from 'next'; import { fetchCommunities } from '#utils/fetchers/communities/fetchCommunities-server'; @@ -10,8 +8,6 @@ import { MPSubvComparison } from './components/MPSubvComparison'; import { TransparencyComparison } from './components/TransparencyComparison'; import { ComparisonHeader } from './components/shared/ComparisonHeader'; import { ComparisonModificationCard } from './components/shared/ComparisonModificationCard'; -import { DataTableSkeleton } from './components/skeletons/DataTableSkeleton'; -import { TransparencySkeleton } from './components/skeletons/TransparencySkeleton'; // Activer Partial Prerendering pour Next.js 15 export const experimental_ppr = true; @@ -50,31 +46,30 @@ export default async function Page({ params }: PageProps) { return ( <> -
+
{/* Sections dynamiques avec Suspense pour streaming */} - }> - - - - }> - - - - }> - - + + + + +
); diff --git a/front/app/community/[siren]/components/FicheHeader/FicheActionButtons.tsx b/front/app/community/[siren]/components/FicheHeader/FicheActionButtons.tsx index 276ff08d..082ac6fb 100644 --- a/front/app/community/[siren]/components/FicheHeader/FicheActionButtons.tsx +++ b/front/app/community/[siren]/components/FicheHeader/FicheActionButtons.tsx @@ -2,9 +2,9 @@ import { useState } from 'react'; -import { Community } from '#app/models/community'; +import type { Community } from '#app/models/community'; import { ActionButton } from '#components/ui/action-button'; -import { Dialog, DialogContent, DialogTrigger } from '#components/ui/dialog'; +import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '#components/ui/dialog'; import { useToast } from '#hooks/use-toast'; import { Share } from 'lucide-react'; @@ -45,6 +45,7 @@ export function FicheActionButtons({ community, className }: FicheActionButtonsP + Comparer avec une autre collectivité diff --git a/front/app/components/Footer.tsx b/front/app/components/Footer.tsx index 24e2a330..52d6c068 100644 --- a/front/app/components/Footer.tsx +++ b/front/app/components/Footer.tsx @@ -37,7 +37,7 @@ const FOOTER_DATA = { { title: 'Comprendre', links: [ - { href: '/about', label: 'Qui sommes‑nous' }, + { href: '/qui-sommes-nous', label: 'Qui sommes‑nous' }, { href: '/project', label: 'Le projet' }, { href: '/interpeller', label: 'Interpeller' }, ], diff --git a/front/components/animate-ui/text/sliding-number.tsx b/front/components/animate-ui/text/sliding-number.tsx index bf10c7dd..607132f5 100644 --- a/front/components/animate-ui/text/sliding-number.tsx +++ b/front/components/animate-ui/text/sliding-number.tsx @@ -14,6 +14,13 @@ import { useTransform, } from 'motion/react'; +// Default transition object - created once to prevent object recreation +const defaultTransition: SpringOptions = { + stiffness: 200, + damping: 20, + mass: 0.4, +}; + type SlidingNumberRollerProps = { prevValue: number; value: number; @@ -21,36 +28,57 @@ type SlidingNumberRollerProps = { transition: SpringOptions; }; -function SlidingNumberRoller({ prevValue, value, place, transition }: SlidingNumberRollerProps) { - const startNumber = Math.floor(prevValue / place) % 10; - const targetNumber = Math.floor(value / place) % 10; - const animatedValue = useSpring(startNumber, transition); +// Memoized component to prevent unnecessary re-renders +const SlidingNumberRoller = React.memo( + function SlidingNumberRoller({ prevValue, value, place, transition }) { + const startNumber = Math.floor(prevValue / place) % 10; + const targetNumber = Math.floor(value / place) % 10; + const animatedValue = useSpring(startNumber, transition); - React.useEffect(() => { - animatedValue.set(targetNumber); - }, [targetNumber, animatedValue]); + React.useEffect(() => { + animatedValue.set(targetNumber); + }, [targetNumber, animatedValue]); - const [measureRef, { height }] = useMeasure(); + const [measureRef, { height }] = useMeasure(); - return ( - - 0 - {Array.from({ length: 10 }, (_, i) => ( - - ))} - - ); -} + // Memoize the displays array to prevent recreation + const displays = React.useMemo( + () => + Array.from({ length: 10 }, (_, i) => ( + + )), + [animatedValue, height, transition], + ); + + return ( + + 0 + {displays} + + ); + }, + (prevProps, nextProps) => { + // Custom comparison function for efficient memoization + return ( + prevProps.prevValue === nextProps.prevValue && + prevProps.value === nextProps.value && + prevProps.place === nextProps.place && + prevProps.transition.stiffness === nextProps.transition.stiffness && + prevProps.transition.damping === nextProps.transition.damping && + prevProps.transition.mass === nextProps.transition.mass + ); + }, +); type SlidingNumberDisplayProps = { motionValue: MotionValue; @@ -59,36 +87,52 @@ type SlidingNumberDisplayProps = { transition: SpringOptions; }; -function SlidingNumberDisplay({ - motionValue, - number, - height, - transition, -}: SlidingNumberDisplayProps) { - const y = useTransform(motionValue, (latest) => { - if (!height) return 0; - const currentNumber = latest % 10; - const offset = (10 + number - currentNumber) % 10; - let translateY = offset * height; - if (offset > 5) translateY -= 10 * height; - return translateY; - }); +// Memoized component with proper comparison to prevent unnecessary re-renders +const SlidingNumberDisplay = React.memo( + function SlidingNumberDisplay({ motionValue, number, height, transition }) { + // useTransform must be called at top level, not inside useMemo + const y = useTransform(motionValue, (latest) => { + if (!height) return 0; + const currentNumber = latest % 10; + const offset = (10 + number - currentNumber) % 10; + let translateY = offset * height; + if (offset > 5) translateY -= 10 * height; + return translateY; + }); - if (!height) { - return {number}; - } + // Memoize transition object to prevent recreation + const stableTransition = React.useMemo( + () => ({ ...transition, type: 'spring' as const }), + [transition], + ); - return ( - - {number} - - ); -} + if (!height) { + return {number}; + } + + return ( + + {number} + + ); + }, + (prevProps, nextProps) => { + // Custom comparison function for efficient memoization + return ( + prevProps.motionValue === nextProps.motionValue && + prevProps.number === nextProps.number && + prevProps.height === nextProps.height && + prevProps.transition.stiffness === nextProps.transition.stiffness && + prevProps.transition.damping === nextProps.transition.damping && + prevProps.transition.mass === nextProps.transition.mass + ); + }, +); type SlidingNumberProps = React.ComponentProps<'span'> & { number: number | string; @@ -111,11 +155,7 @@ function SlidingNumber({ padStart = false, decimalSeparator = '.', decimalPlaces = 0, - transition = { - stiffness: 200, - damping: 20, - mass: 0.4, - }, + transition = defaultTransition, ...props }: SlidingNumberProps) { const localRef = React.useRef(null); @@ -182,26 +222,33 @@ function SlidingNumber({ const newDecValue = newDecStrRaw ? Number.parseInt(newDecStrRaw, 10) : 0; const prevDecValue = adjustedPrevDec ? Number.parseInt(adjustedPrevDec, 10) : 0; - return ( - - {isInView && Number(number) < 0 && -} + // Memoize parsed integer values to prevent recreation + const parsedPrevInt = React.useMemo( + () => Number.parseInt(adjustedPrevInt, 10), + [adjustedPrevInt], + ); - {intPlaces.map((place) => ( + const parsedNewInt = React.useMemo(() => Number.parseInt(newIntStr ?? '0', 10), [newIntStr]); + + // Memoize the integer rollers to prevent recreation + const integerRollers = React.useMemo( + () => + intPlaces.map((place) => ( - ))} + )), + [intPlaces, parsedPrevInt, parsedNewInt, transition], + ); - {newDecStrRaw && ( + // Memoize the decimal rollers to prevent recreation + const decimalRollers = React.useMemo( + () => + newDecStrRaw ? ( <> {decimalSeparator} {decPlaces.map((place) => ( @@ -214,7 +261,20 @@ function SlidingNumber({ /> ))} - )} + ) : null, + [newDecStrRaw, decimalSeparator, decPlaces, prevDecValue, newDecValue, transition], + ); + + return ( + + {isInView && Number(number) < 0 && -} + {integerRollers} + {decimalRollers} ); } diff --git a/front/components/ui/command.tsx b/front/components/ui/command.tsx index 2c4be99f..405b0464 100644 --- a/front/components/ui/command.tsx +++ b/front/components/ui/command.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; -import { Dialog, DialogContent } from '#components/ui/dialog'; +import { Dialog, DialogContent, DialogTitle } from '#components/ui/dialog'; import { cn } from '#utils/utils'; -import { type DialogProps } from '@radix-ui/react-dialog'; +import type { DialogProps } from '@radix-ui/react-dialog'; import { Command as CommandPrimitive } from 'cmdk'; import { Search } from 'lucide-react'; @@ -27,6 +27,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => { return ( + Command Menu {children} diff --git a/front/components/utils/SectionSeparator.tsx b/front/components/utils/SectionSeparator.tsx index d1f1e4a4..65faa9e9 100644 --- a/front/components/utils/SectionSeparator.tsx +++ b/front/components/utils/SectionSeparator.tsx @@ -12,7 +12,7 @@ export default function SectionSeparator({ onSelectYear, }: SectionSeparatorProperties) { return ( -
+ <>

{sectionTitle}

@@ -24,6 +24,6 @@ export default function SectionSeparator({
)}
-
+ ); } diff --git a/front/utils/downloader/downloadSVGChart.ts b/front/utils/downloader/downloadSVGChart.ts index 9024a6a7..15cd0cff 100644 --- a/front/utils/downloader/downloadSVGChart.ts +++ b/front/utils/downloader/downloadSVGChart.ts @@ -1,7 +1,7 @@ import html2canvas from 'html2canvas'; import { downloadURL } from './downloadURL'; -import { Extension } from './types'; +import type { Extension } from './types'; type Size = { width: number; height: number }; @@ -40,7 +40,10 @@ export async function downloadSVGChart( const { fileName = DEFAULT_OPTIONS.fileName, extension = DEFAULT_OPTIONS.extension, - size = { width: chartContainer.clientWidth, height: chartContainer.clientHeight }, + size = { + width: chartContainer.clientWidth, + height: chartContainer.clientHeight, + }, } = options ?? DEFAULT_OPTIONS; const clonedChartContainer = chartContainer.cloneNode(true) as HTMLElement; @@ -70,8 +73,14 @@ async function createSvg(chartContainer: HTMLElement, header: Header, size: Size svgToDownload.setAttribute('width', `${width + margin}`); svgToDownload.setAttribute('height', `${svgHeight}`); - const communityNameSvg = createHeaderText(header.communityName, { fontSize: '24', y: '30' }); - const chartTitleSvg = createHeaderText(header.chartTitle, { fontSize: '20', y: '70' }); + const communityNameSvg = createHeaderText(header.communityName, { + fontSize: '24', + y: '30', + }); + const chartTitleSvg = createHeaderText(header.chartTitle, { + fontSize: '20', + y: '70', + }); const chartSvg = chartContainer.getElementsByTagName('svg')[0]; chartSvg.setAttribute('x', `${margin / 2}`); @@ -96,8 +105,8 @@ async function createChartCanvas(svgToDownload: SVGSVGElement) { svgContainer.appendChild(svgToDownload); document.body.appendChild(svgContainer); const chartCanvas = await html2canvas(svgContainer, { - width: parseInt(svgToDownload.getAttribute('width')!), - height: parseInt(svgToDownload.getAttribute('height')!), + width: Number.parseInt(svgToDownload.getAttribute('width')!), + height: Number.parseInt(svgToDownload.getAttribute('height')!), }); document.body.removeChild(svgContainer); return chartCanvas; @@ -158,14 +167,12 @@ function applyStylesToForeignObject(chartContainer: HTMLElement) { document.body.appendChild(chartContainer); const foreignObjects = chartContainer.querySelectorAll('foreignObject'); foreignObjects.forEach((object) => { - object.setAttribute('y', `${parseInt(object.getAttribute('y') ?? '0') + 50}`); + object.setAttribute('y', `${Number.parseInt(object.getAttribute('y') ?? '0') + 50}`); object.querySelector('button')?.remove(); if (object.children[0]?.children[0]) { applyStyleToAllChildren(object.children[0]); - object.children[0].children[0].innerHTML = object.children[0].children[0].textContent.replace( - ' ', - '
', - ); + object.children[0].children[0].innerHTML = + object.children[0].children[0].textContent?.replace(' ', '
') || ''; } }); document.body.removeChild(chartContainer);