@@ -7,11 +7,11 @@ import { drag, D3DragEvent } from 'd3-drag'
7
7
import { XYComponentCore } from 'core/xy-component'
8
8
9
9
// Utils
10
- import { isNumber , unique , arrayOfIndices , getMin , getMax , getString , getNumber , getValue } from 'utils/data'
10
+ import { isNumber , arrayOfIndices , getMin , getMax , getString , getNumber , getValue , groupBy } from 'utils/data'
11
11
import { smartTransition } from 'utils/d3'
12
12
import { getColor } from 'utils/color'
13
13
import { textAlignToAnchor , trimSVGText } from 'utils/text'
14
- import { guid } from 'utils'
14
+ import { guid , isStringSvg , sanitizeSvgString } from 'utils'
15
15
16
16
// Types
17
17
import { TextAlign , Spacing , Arrangement } from 'types'
@@ -22,6 +22,9 @@ import { TimelineDefaultConfig, TimelineConfigInterface } from './config'
22
22
// Styles
23
23
import * as s from './style'
24
24
25
+ // Local Types
26
+ import type { TimelineRowLabel } from './types'
27
+
25
28
export class Timeline < Datum > extends XYComponentCore < Datum , TimelineConfigInterface < Datum > > {
26
29
static selectors = s
27
30
protected _defaultConfig = TimelineDefaultConfig as TimelineConfigInterface < Datum >
@@ -97,8 +100,8 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
97
100
if ( config . showRowLabels ?? config . showLabels ) {
98
101
if ( config . rowLabelWidth ?? config . labelWidth ) this . _labelWidth = ( config . rowLabelWidth ?? config . labelWidth ) + this . _labelMargin
99
102
else {
100
- const recordLabels = this . _getRecordLabels ( data )
101
- const longestLabel = recordLabels . reduce ( ( acc , val ) => acc . length > val . length ? acc : val , '' )
103
+ const rowLabels = this . _getRowLabels ( data )
104
+ const longestLabel = rowLabels . reduce ( ( longestLabel , l ) => longestLabel . length > l . formattedLabel . length ? longestLabel : l . formattedLabel , '' )
102
105
const label = this . _labelsGroup . append ( 'text' )
103
106
. attr ( 'class' , s . label )
104
107
. text ( longestLabel )
@@ -127,14 +130,13 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
127
130
const yRange = this . yScale . range ( )
128
131
const yStart = Math . min ( ...yRange )
129
132
const yHeight = Math . abs ( yRange [ 1 ] - yRange [ 0 ] )
130
- const recordLabels = this . _getRecordLabels ( data )
131
- const recordLabelsUnique = unique ( recordLabels )
132
- const numUniqueRecords = recordLabelsUnique . length
133
- const rowHeight = config . rowHeight || ( yHeight / numUniqueRecords )
133
+ const rowLabels = this . _getRowLabels ( data )
134
+ const numRowLabels = rowLabels . length
135
+ const rowHeight = config . rowHeight || ( yHeight / numRowLabels )
134
136
135
137
// Ordinal scale to handle records on the same type
136
138
const ordinalScale : ScaleOrdinal < string , number > = scaleOrdinal ( )
137
- ordinalScale . range ( arrayOfIndices ( numUniqueRecords ) )
139
+ ordinalScale . range ( arrayOfIndices ( numRowLabels ) )
138
140
139
141
// Invisible Background rect to track events
140
142
this . _background
@@ -144,7 +146,7 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
144
146
145
147
// Labels
146
148
const labels = this . _labelsGroup . selectAll < SVGTextElement , string > ( `.${ s . label } ` )
147
- . data ( ( config . showRowLabels ?? config . showLabels ) ? recordLabelsUnique : [ ] )
149
+ . data ( ( config . showRowLabels ?? config . showLabels ) ? rowLabels : [ ] )
148
150
149
151
const labelsEnter = labels . enter ( ) . append ( 'text' )
150
152
. attr ( 'class' , s . label )
@@ -155,20 +157,27 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
155
157
156
158
labelsEnter . merge ( labels )
157
159
. attr ( 'x' , xRange [ 0 ] - labelOffset )
158
- . attr ( 'y' , ( label , i ) => yStart + ( ordinalScale ( label ) + 0.5 ) * rowHeight )
159
- . text ( label => label )
160
- . style ( 'text-anchor' , textAlignToAnchor ( config . rowLabelTextAlign as TextAlign ) )
160
+ . attr ( 'y' , ( l , i ) => yStart + ( ordinalScale ( l . label ) + 0.5 ) * rowHeight )
161
+ . text ( l => l . formattedLabel )
161
162
. each ( ( label , i , els ) => {
162
- trimSVGText ( select ( els [ i ] ) , ( config . rowLabelWidth ?? config . labelWidth ) || ( config . rowMaxLabelWidth ?? config . maxLabelWidth ) )
163
+ const labelSelection = select ( els [ i ] )
164
+ trimSVGText ( labelSelection , ( config . rowLabelWidth ?? config . labelWidth ) || ( config . rowMaxLabelWidth ?? config . maxLabelWidth ) )
165
+
166
+ // Apply custom label style if it has been provided
167
+ const customStyle = getValue ( label , config . rowLabelStyle )
168
+ for ( const [ prop , value ] of Object . entries ( customStyle ) ) {
169
+ labelSelection . style ( prop , value )
170
+ }
163
171
} )
172
+ . style ( 'text-anchor' , textAlignToAnchor ( config . rowLabelTextAlign as TextAlign ) )
164
173
165
174
labels . exit ( ) . remove ( )
166
175
167
176
// Row background rects
168
177
const xStart = xRange [ 0 ]
169
178
const timelineWidth = xRange [ 1 ] - xRange [ 0 ]
170
- const numRows = Math . max ( Math . floor ( yHeight / rowHeight ) , numUniqueRecords )
171
- const recordTypes : ( string | undefined ) [ ] = Array ( numRows ) . fill ( null ) . map ( ( _ , i ) => recordLabelsUnique [ i ] )
179
+ const numRows = Math . max ( Math . floor ( yHeight / rowHeight ) , numRowLabels )
180
+ const recordTypes = Array ( numRows ) . fill ( null ) . map ( ( _ , i ) => rowLabels [ i ] )
172
181
const rects = this . _rowsGroup . selectAll < SVGRectElement , number > ( `.${ s . row } ` )
173
182
. data ( recordTypes )
174
183
@@ -199,9 +208,6 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
199
208
200
209
linesEnter . append ( 'rect' )
201
210
. attr ( 'class' , s . line )
202
- . classed ( s . rowOdd , config . alternatingRowColors
203
- ? ( d , i ) => ! ( recordLabelsUnique . indexOf ( this . _getRecordKey ( d , i ) ) % 2 )
204
- : null )
205
211
. style ( 'fill' , ( d , i ) => getColor ( d , config . color , ordinalScale ( this . _getRecordKey ( d , i ) ) ) )
206
212
. call ( this . _positionLines . bind ( this ) , rowHeight )
207
213
@@ -384,8 +390,16 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
384
390
return getString ( d , this . config . lineRow ?? this . config . type ) || `__${ i } `
385
391
}
386
392
387
- private _getRecordLabels ( data : Datum [ ] ) : string [ ] {
388
- return data . map ( ( d , i ) => getString ( d , this . config . lineRow ?? this . config . type ) || `${ i + 1 } ` )
393
+ private _getRowLabels ( data : Datum [ ] ) : TimelineRowLabel < Datum > [ ] {
394
+ const grouped = groupBy ( data , ( d , i ) => getString ( d , this . config . lineRow ?? this . config . type ) || `${ i + 1 } ` )
395
+
396
+ const rowLabels : TimelineRowLabel < Datum > [ ] = Object . entries ( grouped ) . map ( ( [ key , items ] ) => ( {
397
+ label : key ,
398
+ formattedLabel : this . config . rowLabelFormatter ?.( key ) ?? key ,
399
+ data : items ,
400
+ } ) )
401
+
402
+ return rowLabels
389
403
}
390
404
391
405
// Override the default XYComponent getXDataExtent method to take into account line lengths
@@ -396,3 +410,7 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
396
410
return [ min , max ]
397
411
}
398
412
}
413
+ function sanitizeSvg ( key : string ) : any {
414
+ throw new Error ( 'Function not implemented.' )
415
+ }
416
+
0 commit comments