@@ -2,6 +2,8 @@ import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
2
2
import type { Theme } from '@emotion/react' ;
3
3
import { useTheme } from '@emotion/react' ;
4
4
import styled from '@emotion/styled' ;
5
+ import type { Virtualizer } from '@tanstack/react-virtual' ;
6
+ import { useVirtualizer } from '@tanstack/react-virtual' ;
5
7
import type { TooltipComponentFormatterCallbackParams } from 'echarts' ;
6
8
import type { CallbackDataParams } from 'echarts/types/dist/shared' ;
7
9
@@ -17,16 +19,38 @@ const BASELINE_SERIES_NAME = 'baseline';
17
19
18
20
type Props = {
19
21
rankedAttributes : SuspectAttributesResult [ 'rankedAttributes' ] ;
22
+ searchQuery : string ;
20
23
} ;
21
24
22
25
// TODO Abdullah Khan: Add virtualization and search to the list of charts
23
- export function Charts ( { rankedAttributes} : Props ) {
26
+ export function Charts ( { rankedAttributes, searchQuery } : Props ) {
24
27
const theme = useTheme ( ) ;
28
+ const scrollContainerRef = useRef < HTMLDivElement > ( null ) ;
29
+
30
+ const virtualizer = useVirtualizer ( {
31
+ count : rankedAttributes . length ,
32
+ getScrollElement : ( ) => scrollContainerRef . current ,
33
+ estimateSize : ( ) => 200 ,
34
+ overscan : 5 ,
35
+ } ) ;
36
+
37
+ const virtualItems = virtualizer . getVirtualItems ( ) ;
38
+
25
39
return (
26
- < ChartsWrapper >
27
- { rankedAttributes . map ( attribute => (
28
- < Chart key = { attribute . attributeName } attribute = { attribute } theme = { theme } />
29
- ) ) }
40
+ < ChartsWrapper ref = { scrollContainerRef } >
41
+ < AllItemsContainer height = { virtualizer . getTotalSize ( ) } >
42
+ { virtualItems . map ( item => (
43
+ < VirtualOffset key = { item . index } offset = { item . start } >
44
+ < Chart
45
+ key = { `${ item . key } +${ searchQuery } ` }
46
+ index = { item . index }
47
+ virtualizer = { virtualizer }
48
+ attribute = { rankedAttributes [ item . index ] ! }
49
+ theme = { theme }
50
+ />
51
+ </ VirtualOffset >
52
+ ) ) }
53
+ </ AllItemsContainer >
30
54
</ ChartsWrapper >
31
55
) ;
32
56
}
@@ -103,9 +127,13 @@ function cohortTotals(
103
127
function Chart ( {
104
128
attribute,
105
129
theme,
130
+ index,
131
+ virtualizer,
106
132
} : {
107
133
attribute : SuspectAttributesResult [ 'rankedAttributes' ] [ number ] ;
134
+ index : number ;
108
135
theme : Theme ;
136
+ virtualizer : Virtualizer < HTMLDivElement , Element > ;
109
137
} ) {
110
138
const chartRef = useRef < ReactEchartsRef > ( null ) ;
111
139
const [ hideLabels , setHideLabels ] = useState ( false ) ;
@@ -181,7 +209,10 @@ function Chart({
181
209
? { adjective : 'different' , message : 'This is suspicious.' }
182
210
: { adjective : 'similar' , message : 'Nothing unusual here.' } ;
183
211
184
- return `<div style="max-width: 200px; white-space: normal; word-wrap: break-word; line-height: 1.2;">${ selectedParam ?. name } <span style="color: ${ theme . textColor } ;">is <strong>${ status . adjective } </strong> ${ isDifferent ? 'between' : 'across' } selected and baseline data. ${ status . message } </span></div>` ;
212
+ const name = selectedParam ?. name ?? baselineParam ?. name ?? '' ;
213
+ const truncatedName = name . length > 300 ? `${ name . slice ( 0 , 300 ) } ...` : name ;
214
+
215
+ return `<div style="max-width: 200px; white-space: normal; word-wrap: break-word; line-height: 1.2;">${ truncatedName } <span style="color: ${ theme . textColor } ;">is <strong>${ status . adjective } </strong> ${ isDifferent ? 'between' : 'across' } selected and baseline data. ${ status . message } </span></div>` ;
185
216
} ,
186
217
[ seriesTotals , theme . textColor ]
187
218
) ;
@@ -206,88 +237,103 @@ function Chart({
206
237
} , [ attribute ] ) ;
207
238
208
239
return (
209
- < ChartWrapper >
210
- < ChartTitle > { attribute . attributeName } </ ChartTitle >
211
- < BaseChart
212
- ref = { chartRef }
213
- autoHeightResize
214
- isGroupedByDate = { false }
215
- tooltip = { {
216
- renderMode : 'html' ,
217
- valueFormatter,
218
- formatAxisLabel,
219
- } }
220
- grid = { {
221
- left : 2 ,
222
- right : 8 ,
223
- containLabel : true ,
224
- } }
225
- xAxis = { {
226
- show : true ,
227
- type : 'category' ,
228
- data : seriesData [ SELECTED_SERIES_NAME ] . map ( cohort => cohort . label ) ,
229
- truncate : 14 ,
230
- axisLabel : hideLabels
231
- ? { show : false }
232
- : {
233
- hideOverlap : true ,
234
- showMaxLabel : false ,
235
- showMinLabel : false ,
236
- color : '#000' ,
237
- interval : 0 ,
238
- formatter : ( value : string ) => value ,
240
+ < div ref = { virtualizer . measureElement } data-index = { index } >
241
+ < ChartWrapper >
242
+ < ChartTitle > { attribute . attributeName } </ ChartTitle >
243
+ < BaseChart
244
+ ref = { chartRef }
245
+ autoHeightResize
246
+ isGroupedByDate = { false }
247
+ tooltip = { {
248
+ trigger : 'axis' ,
249
+ appendToBody : true ,
250
+ renderMode : 'html' ,
251
+ valueFormatter,
252
+ formatAxisLabel,
253
+ } }
254
+ grid = { {
255
+ left : 2 ,
256
+ right : 8 ,
257
+ containLabel : true ,
258
+ } }
259
+ xAxis = { {
260
+ show : true ,
261
+ type : 'category' ,
262
+ data : seriesData [ SELECTED_SERIES_NAME ] . map ( cohort => cohort . label ) ,
263
+ truncate : 14 ,
264
+ axisLabel : hideLabels
265
+ ? { show : false }
266
+ : {
267
+ hideOverlap : true ,
268
+ showMaxLabel : false ,
269
+ showMinLabel : false ,
270
+ color : '#000' ,
271
+ interval : 0 ,
272
+ formatter : ( value : string ) => value ,
273
+ } ,
274
+ } }
275
+ yAxis = { {
276
+ type : 'value' ,
277
+ axisLabel : {
278
+ show : false ,
279
+ width : 0 ,
280
+ } ,
281
+ } }
282
+ series = { [
283
+ {
284
+ type : 'bar' ,
285
+ data : seriesData [ SELECTED_SERIES_NAME ] . map ( cohort => cohort . value ) ,
286
+ name : SELECTED_SERIES_NAME ,
287
+ itemStyle : {
288
+ color : cohort1Color ,
239
289
} ,
240
- } }
241
- yAxis = { {
242
- type : 'value' ,
243
- axisLabel : {
244
- show : false ,
245
- width : 0 ,
246
- } ,
247
- } }
248
- series = { [
249
- {
250
- type : 'bar' ,
251
- data : seriesData [ SELECTED_SERIES_NAME ] . map ( cohort => cohort . value ) ,
252
- name : SELECTED_SERIES_NAME ,
253
- itemStyle : {
254
- color : cohort1Color ,
290
+ barMaxWidth : MAX_BAR_WIDTH ,
291
+ animation : false ,
255
292
} ,
256
- barMaxWidth : MAX_BAR_WIDTH ,
257
- animation : false ,
258
- } ,
259
- {
260
- type : 'bar' ,
261
- data : seriesData [ BASELINE_SERIES_NAME ] . map ( cohort => cohort . value ) ,
262
- name : BASELINE_SERIES_NAME ,
263
- itemStyle : {
264
- color : cohort2Color ,
293
+ {
294
+ type : 'bar' ,
295
+ data : seriesData [ BASELINE_SERIES_NAME ] . map ( cohort => cohort . value ) ,
296
+ name : BASELINE_SERIES_NAME ,
297
+ itemStyle : {
298
+ color : cohort2Color ,
299
+ } ,
300
+ barMaxWidth : MAX_BAR_WIDTH ,
301
+ animation : false ,
265
302
} ,
266
- barMaxWidth : MAX_BAR_WIDTH ,
267
- animation : false ,
268
- } ,
269
- ] }
270
- />
271
- </ ChartWrapper >
303
+ ] }
304
+ />
305
+ </ ChartWrapper >
306
+ </ div >
272
307
) ;
273
308
}
274
309
275
310
const ChartsWrapper = styled ( 'div' ) `
276
- flex: 1 ;
311
+ height: 100% ;
277
312
overflow: auto;
278
313
overflow-y: scroll;
279
314
overscroll-behavior: none;
280
315
` ;
281
316
317
+ const AllItemsContainer = styled ( 'div' ) < { height : number } > `
318
+ position: relative;
319
+ width: 100%;
320
+ height: ${ p => p . height } px;
321
+ ` ;
322
+
323
+ const VirtualOffset = styled ( 'div' ) < { offset : number } > `
324
+ position: absolute;
325
+ top: 0;
326
+ left: 0;
327
+ width: 100%;
328
+ transform: translateY(${ p => p . offset } px);
329
+ ` ;
330
+
282
331
const ChartWrapper = styled ( 'div' ) `
283
332
display: flex;
284
333
flex-direction: column;
285
334
height: 200px;
286
- padding: ${ space ( 2 ) } ${ space ( 2 ) } 0 ${ space ( 2 ) } ;
287
-
288
- &:not(:last-child) {
289
- border-bottom: 1px solid ${ p => p . theme . border } ;
290
- }
335
+ padding-top: ${ space ( 1.5 ) } ;
336
+ border-top: 1px solid ${ p => p . theme . border } ;
291
337
` ;
292
338
293
339
const ChartTitle = styled ( 'div' ) `
0 commit comments