1- import { useEffect , useRef , useState } from 'react'
1+ import { memo , useEffect , useMemo , useRef , useState } from 'react'
22import { cn } from '@/lib/core/utils/cn'
33import { formatDate , formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
44
@@ -15,7 +15,7 @@ export interface LineChartMultiSeries {
1515 dashed ?: boolean
1616}
1717
18- export function LineChart ( {
18+ function LineChartComponent ( {
1919 data,
2020 label,
2121 color,
@@ -95,92 +95,92 @@ export function LineChart({
9595
9696 const hasExternalWrapper = ! label || label === ''
9797
98- if ( containerWidth === null ) {
99- return (
100- < div
101- ref = { containerRef }
102- className = { cn ( 'w-full' , ! hasExternalWrapper && 'rounded-lg border bg-card p-4' ) }
103- style = { { height } }
104- />
105- )
106- }
107-
108- if ( data . length === 0 ) {
109- return (
110- < div
111- className = { cn (
112- 'flex items-center justify-center' ,
113- ! hasExternalWrapper && 'rounded-lg border bg-card p-4'
114- ) }
115- style = { { width, height } }
116- >
117- < p className = 'text-muted-foreground text-sm' > No data</ p >
118- </ div >
119- )
120- }
98+ const allSeries = useMemo (
99+ ( ) =>
100+ ( Array . isArray ( series ) && series . length > 0
101+ ? [ { id : 'base' , label, color, data } , ...series ]
102+ : [ { id : 'base' , label, color, data } ]
103+ ) . map ( ( s , idx ) => ( { ...s , id : s . id || s . label || String ( idx ) } ) ) ,
104+ [ series , label , color , data ]
105+ )
121106
122- const allSeries = (
123- Array . isArray ( series ) && series . length > 0
124- ? [ { id : 'base' , label, color, data } , ...series ]
125- : [ { id : 'base' , label, color, data } ]
126- ) . map ( ( s , idx ) => ( { ...s , id : s . id || s . label || String ( idx ) } ) )
127-
128- const flatValues = allSeries . flatMap ( ( s ) => s . data . map ( ( d ) => d . value ) )
129- const rawMax = Math . max ( ...flatValues , 1 )
130- const rawMin = Math . min ( ...flatValues , 0 )
131- const paddedMax = rawMax === 0 ? 1 : rawMax * 1.1
132- const paddedMin = Math . min ( 0 , rawMin )
133- const unitSuffixPre = ( unit || '' ) . trim ( ) . toLowerCase ( )
134- let maxValue = Math . ceil ( paddedMax )
135- let minValue = Math . floor ( paddedMin )
136- if ( unitSuffixPre === 'ms' || unitSuffixPre === 'latency' ) {
137- minValue = 0
138- if ( paddedMax < 10 ) {
139- maxValue = Math . ceil ( paddedMax )
140- } else if ( paddedMax < 100 ) {
141- maxValue = Math . ceil ( paddedMax / 10 ) * 10
142- } else if ( paddedMax < 1000 ) {
143- maxValue = Math . ceil ( paddedMax / 50 ) * 50
144- } else if ( paddedMax < 10000 ) {
145- maxValue = Math . ceil ( paddedMax / 500 ) * 500
146- } else {
147- maxValue = Math . ceil ( paddedMax / 1000 ) * 1000
107+ const { maxValue, minValue, valueRange } = useMemo ( ( ) => {
108+ const flatValues = allSeries . flatMap ( ( s ) => s . data . map ( ( d ) => d . value ) )
109+ const rawMax = Math . max ( ...flatValues , 1 )
110+ const rawMin = Math . min ( ...flatValues , 0 )
111+ const paddedMax = rawMax === 0 ? 1 : rawMax * 1.1
112+ const paddedMin = Math . min ( 0 , rawMin )
113+ const unitSuffixPre = ( unit || '' ) . trim ( ) . toLowerCase ( )
114+ let maxVal = Math . ceil ( paddedMax )
115+ let minVal = Math . floor ( paddedMin )
116+ if ( unitSuffixPre === 'ms' || unitSuffixPre === 'latency' ) {
117+ minVal = 0
118+ if ( paddedMax < 10 ) {
119+ maxVal = Math . ceil ( paddedMax )
120+ } else if ( paddedMax < 100 ) {
121+ maxVal = Math . ceil ( paddedMax / 10 ) * 10
122+ } else if ( paddedMax < 1000 ) {
123+ maxVal = Math . ceil ( paddedMax / 50 ) * 50
124+ } else if ( paddedMax < 10000 ) {
125+ maxVal = Math . ceil ( paddedMax / 500 ) * 500
126+ } else {
127+ maxVal = Math . ceil ( paddedMax / 1000 ) * 1000
128+ }
148129 }
149- }
150- const valueRange = maxValue - minValue || 1
130+ return {
131+ maxValue : maxVal ,
132+ minValue : minVal ,
133+ valueRange : maxVal - minVal || 1 ,
134+ }
135+ } , [ allSeries , unit ] )
151136
152137 const yMin = padding . top + 3
153138 const yMax = padding . top + chartHeight - 3
154139
155- const scaledPoints = data . map ( ( d , i ) => {
156- const usableW = Math . max ( 1 , chartWidth )
157- const x = padding . left + ( i / ( data . length - 1 || 1 ) ) * usableW
158- const rawY = padding . top + chartHeight - ( ( d . value - minValue ) / valueRange ) * chartHeight
159- const y = Math . max ( yMin , Math . min ( yMax , rawY ) )
160- return { x, y }
161- } )
162-
163- const scaledSeries = allSeries . map ( ( s ) => {
164- const pts = s . data . map ( ( d , i ) => {
165- const usableW = Math . max ( 1 , chartWidth )
166- const x = padding . left + ( i / ( s . data . length - 1 || 1 ) ) * usableW
167- const rawY = padding . top + chartHeight - ( ( d . value - minValue ) / valueRange ) * chartHeight
168- const y = Math . max ( yMin , Math . min ( yMax , rawY ) )
169- return { x, y }
170- } )
171- return { ...s , pts }
172- } )
140+ const scaledPoints = useMemo (
141+ ( ) =>
142+ data . map ( ( d , i ) => {
143+ const usableW = Math . max ( 1 , chartWidth )
144+ const x = padding . left + ( i / ( data . length - 1 || 1 ) ) * usableW
145+ const rawY = padding . top + chartHeight - ( ( d . value - minValue ) / valueRange ) * chartHeight
146+ const y = Math . max ( yMin , Math . min ( yMax , rawY ) )
147+ return { x, y }
148+ } ) ,
149+ [ data , chartWidth , chartHeight , minValue , valueRange , yMin , yMax , padding . left , padding . top ]
150+ )
151+
152+ const scaledSeries = useMemo (
153+ ( ) =>
154+ allSeries . map ( ( s ) => {
155+ const pts = s . data . map ( ( d , i ) => {
156+ const usableW = Math . max ( 1 , chartWidth )
157+ const x = padding . left + ( i / ( s . data . length - 1 || 1 ) ) * usableW
158+ const rawY = padding . top + chartHeight - ( ( d . value - minValue ) / valueRange ) * chartHeight
159+ const y = Math . max ( yMin , Math . min ( yMax , rawY ) )
160+ return { x, y }
161+ } )
162+ return { ...s , pts }
163+ } ) ,
164+ [
165+ allSeries ,
166+ chartWidth ,
167+ chartHeight ,
168+ minValue ,
169+ valueRange ,
170+ yMin ,
171+ yMax ,
172+ padding . left ,
173+ padding . top ,
174+ ]
175+ )
173176
174177 const getSeriesById = ( id ?: string | null ) => scaledSeries . find ( ( s ) => s . id === id )
175- const visibleSeries = activeSeriesId
176- ? scaledSeries . filter ( ( s ) => s . id === activeSeriesId )
177- : scaledSeries
178- const orderedSeries = ( ( ) => {
179- if ( ! activeSeriesId ) return visibleSeries
180- return visibleSeries
181- } ) ( )
182-
183- const pathD = ( ( ) => {
178+ const visibleSeries = useMemo (
179+ ( ) => ( activeSeriesId ? scaledSeries . filter ( ( s ) => s . id === activeSeriesId ) : scaledSeries ) ,
180+ [ activeSeriesId , scaledSeries ]
181+ )
182+
183+ const pathD = useMemo ( ( ) => {
184184 if ( scaledPoints . length <= 1 ) return ''
185185 const p = scaledPoints
186186 const tension = 0.2
@@ -199,7 +199,7 @@ export function LineChart({
199199 d += ` C ${ cp1x } ${ cp1y } , ${ cp2x } ${ cp2y } , ${ p2 . x } ${ p2 . y } `
200200 }
201201 return d
202- } ) ( )
202+ } , [ scaledPoints , yMin , yMax ] )
203203
204204 const getCompactDateLabel = ( timestamp ?: string ) => {
205205 if ( ! timestamp ) return ''
@@ -222,6 +222,30 @@ export function LineChart({
222222 const currentHoverDate =
223223 hoverIndex !== null && data [ hoverIndex ] ? getCompactDateLabel ( data [ hoverIndex ] . timestamp ) : ''
224224
225+ if ( containerWidth === null ) {
226+ return (
227+ < div
228+ ref = { containerRef }
229+ className = { cn ( 'w-full' , ! hasExternalWrapper && 'rounded-lg border bg-card p-4' ) }
230+ style = { { height } }
231+ />
232+ )
233+ }
234+
235+ if ( data . length === 0 ) {
236+ return (
237+ < div
238+ className = { cn (
239+ 'flex items-center justify-center' ,
240+ ! hasExternalWrapper && 'rounded-lg border bg-card p-4'
241+ ) }
242+ style = { { width, height } }
243+ >
244+ < p className = 'text-muted-foreground text-sm' > No data</ p >
245+ </ div >
246+ )
247+ }
248+
225249 return (
226250 < div
227251 ref = { containerRef }
@@ -386,7 +410,7 @@ export function LineChart({
386410 )
387411 } ) ( ) }
388412
389- { orderedSeries . map ( ( s , idx ) => {
413+ { visibleSeries . map ( ( s , idx ) => {
390414 const isActive = activeSeriesId ? activeSeriesId === s . id : true
391415 const isHovered = hoverSeriesId ? hoverSeriesId === s . id : false
392416 const baseOpacity = isActive ? 1 : 0.12
@@ -682,4 +706,8 @@ export function LineChart({
682706 )
683707}
684708
709+ /**
710+ * Memoized LineChart component to prevent re-renders when parent updates.
711+ */
712+ export const LineChart = memo ( LineChartComponent )
685713export default LineChart
0 commit comments