77 select as d3Select ,
88 Selection as D3Selection ,
99 BaseType as D3BaseType ,
10- ContainerElement as D3ContainerElement
10+ ContainerElement as D3ContainerElement ,
11+ event as d3Event
1112} from 'd3-selection' ;
1213
1314import { max as d3Max , min as d3Min } from 'd3-array' ;
@@ -29,14 +30,16 @@ import {
2930 axisRight as d3AxisRight
3031} from 'd3-axis' ;
3132
32- import {
33+ import {
3334 line as d3Line ,
3435 curveMonotoneX as d3CurveMonotoneX ,
3536 Line as D3Line
3637} from 'd3-shape' ;
3738
3839import 'd3-transition' ;
3940
41+ import { brushX as d3BrushX } from 'd3-brush' ;
42+
4043import { EventResultDataDTO } from 'src/app/modules/time-series/models/event-result-data.model' ;
4144import { EventResultSeriesDTO } from 'src/app/modules/time-series/models/event-result-series.model' ;
4245import { EventResultPointDTO } from 'src/app/modules/time-series/models/event-result-point.model' ;
@@ -59,6 +62,7 @@ export class LineChartService {
5962 private _margin = { top : 40 , right : 70 , bottom : 40 , left : 60 } ;
6063 private _width = 600 - this . _margin . left - this . _margin . right ;
6164 private _height = 500 - this . _margin . top - this . _margin . bottom ;
65+ private brush ;
6266
6367
6468 constructor ( private translationService : TranslateService ) { }
@@ -97,11 +101,12 @@ export class LineChartService {
97101
98102 d3Select ( '.x-axis' ) . transition ( ) . call ( this . updateXAxis , xScale ) ;
99103 d3Select ( '.y-axis' ) . transition ( ) . call ( this . updateYAxis , yScale , this . _width , this . _margin ) ;
100-
104+ this . brush = d3BrushX ( ) . extent ( [ [ 0 , 0 ] , [ this . _width , this . _height ] ] ) ;
105+ this . addBrush ( chart , xScale , yScale , data ) ;
101106 this . addDataLinesToChart ( chart , xScale , yScale , data ) ;
102-
103107 }
104108
109+
105110 /**
106111 * Prepares the incoming data for drawing with D3.js
107112 */
@@ -125,8 +130,8 @@ export class LineChartService {
125130
126131 private generateKey ( data : EventResultSeriesDTO ) : string {
127132 return data . jobGroup
128- + data . measuredEvent
129- + data . data . length ;
133+ + data . measuredEvent
134+ + data . data . length ;
130135 }
131136
132137 /**
@@ -137,13 +142,13 @@ export class LineChartService {
137142 this . _width = svgElement . nativeElement . parentElement . offsetWidth - this . _margin . left - this . _margin . right ;
138143 //this._height = svgElement.nativeElement.parentElement.offsetHeight - this._margin.top - this._margin.bottom;
139144 const svg = d3Select ( svgElement . nativeElement )
140- . attr ( 'id' , 'time-series-chart' )
141- . attr ( 'width' , this . _width + this . _margin . left + this . _margin . right )
142- . attr ( 'height' , 0 ) ;
145+ . attr ( 'id' , 'time-series-chart' )
146+ . attr ( 'width' , this . _width + this . _margin . left + this . _margin . right )
147+ . attr ( 'height' , 0 ) ;
143148
144149 return svg . append ( 'g' ) // g = grouping element; group all other stuff into the chart
145- . attr ( 'id' , 'time-series-chart-drawing-area' )
146- . attr ( 'transform' , 'translate(' + this . _margin . left + ', ' + this . _margin . top + ')' ) ; // translates the origin to the top left corner (default behavior of D3)
150+ . attr ( 'id' , 'time-series-chart-drawing-area' )
151+ . attr ( 'transform' , 'translate(' + this . _margin . left + ', ' + this . _margin . top + ')' ) ; // translates the origin to the top left corner (default behavior of D3)
147152 }
148153
149154 public startResize ( svgElement : ElementRef ) : void {
@@ -166,8 +171,8 @@ export class LineChartService {
166171 */
167172 private getXScale ( data : TimeSeries [ ] ) : D3ScaleTime < number , number > {
168173 return d3ScaleTime ( ) // Define a scale for the X-Axis
169- . range ( [ 0 , this . _width ] ) // Display the X-Axis over the complete width
170- . domain ( [ this . getMinDate ( data ) , this . getMaxDate ( data ) ] ) ;
174+ . range ( [ 0 , this . _width ] ) // Display the X-Axis over the complete width
175+ . domain ( [ this . getMinDate ( data ) , this . getMaxDate ( data ) ] ) ;
171176 }
172177
173178 private getMinDate ( data : TimeSeries [ ] ) : Date {
@@ -191,9 +196,9 @@ export class LineChartService {
191196 */
192197 private getYScale ( data : TimeSeries [ ] ) : D3ScaleLinear < number , number > {
193198 return d3ScaleLinear ( ) // Linear scale for the numbers on the Y-Axis
194- . range ( [ this . _height , 0 ] ) // Display the Y-Axis over the complete height - origin is top left corner, so height comes first
195- . domain ( [ 0 , this . getMaxValue ( data ) ] )
196- . nice ( ) ;
199+ . range ( [ this . _height , 0 ] ) // Display the Y-Axis over the complete height - origin is top left corner, so height comes first
200+ . domain ( [ 0 , this . getMaxValue ( data ) ] )
201+ . nice ( ) ;
197202 }
198203
199204 private getMaxValue ( data : TimeSeries [ ] ) : number {
@@ -214,9 +219,9 @@ export class LineChartService {
214219
215220 // Add the X-Axis to the chart
216221 chart . append ( 'g' ) // new group for the X-Axis (see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
217- . attr ( 'class' , 'axis x-axis' ) // a css class to style it later
218- . attr ( 'transform' , 'translate(0, ' + this . _height + ')' ) // even if the D3 method called `axisBottom` we have to move it to the bottom by ourselfs
219- . call ( xAxis ) ;
222+ . attr ( 'class' , 'axis x-axis' ) // a css class to style it later
223+ . attr ( 'transform' , 'translate(0, ' + this . _height + ')' ) // even if the D3 method called `axisBottom` we have to move it to the bottom by ourselfs
224+ . call ( xAxis ) ;
220225 }
221226
222227 /**
@@ -229,15 +234,15 @@ export class LineChartService {
229234
230235 // Add the Y-Axis to the chart
231236 chart . append ( 'g' ) // new group for the y-axis
232- . attr ( 'class' , 'axis y-axis' ) // a css class to style it later
233- . call ( yAxis ) ;
237+ . attr ( 'class' , 'axis y-axis' ) // a css class to style it later
238+ . call ( yAxis ) ;
234239
235240 // Add the axis description
236241 this . translationService . get ( "frontend.de.iteratec.osm.timeSeries.loadTimes" ) . pipe ( take ( 1 ) ) . subscribe ( title => {
237242 d3Select ( '.y-axis' ) . append ( 'text' )
238- . attr ( 'class' , 'description' )
239- . attr ( 'transform' , 'translate(-' + ( this . _margin . left - 20 ) + ', ' + ( this . _height / 2 - this . _margin . bottom ) + ') rotate(-90)' )
240- . text ( title + ' [ms]' ) ;
243+ . attr ( 'class' , 'description' )
244+ . attr ( 'transform' , 'translate(-' + ( this . _margin . left - 20 ) + ', ' + ( this . _height / 2 - this . _margin . bottom ) + ') rotate(-90)' )
245+ . text ( title + ' [ms]' ) ;
241246 } ) ;
242247 }
243248
@@ -254,7 +259,7 @@ export class LineChartService {
254259 lines . forEach ( ( line , index ) => {
255260 let tspan = element . append ( 'tspan' ) . text ( line ) ;
256261 if ( index > 0 )
257- tspan . attr ( 'x' , 0 ) . attr ( 'dy' , '15' ) ;
262+ tspan . attr ( 'x' , 0 ) . attr ( 'dy' , '15' ) ;
258263 } ) ;
259264 } ) ;
260265 }
@@ -296,9 +301,9 @@ export class LineChartService {
296301 transition . on ( 'end.showTicks' , function show ( ) {
297302 d3Select ( this ) . selectAll ( 'g.tick text' )
298303 . transition ( )
299- . delay ( 100 )
300- . duration ( 500 )
301- . attr ( 'opacity' , '1.0' )
304+ . delay ( 100 )
305+ . duration ( 500 )
306+ . attr ( 'opacity' , '1.0' )
302307 } ) ;
303308 }
304309
@@ -307,28 +312,27 @@ export class LineChartService {
307312 d3AxisRight ( yScale ) // axis right, because we draw the background line with this
308313 . tickSize ( width ) // background line over complete chart width
309314 )
310- . attr ( 'transform' , 'translate(0, 0)' ) // move the axis to the left
311- . call ( g => g . selectAll ( ".tick:not(:first-of-type) line" ) // make all line dotted, except the one on the bottom as this will indicate the x-axis
312- . attr ( "stroke-opacity" , 0.5 )
313- . attr ( "stroke-dasharray" , "1,1" ) )
314- . call ( g => g . selectAll ( ".tick text" ) // move the text a little so it does not overlap with the lines
315- . attr ( "x" , - 5 ) ) ;
315+ . attr ( 'transform' , 'translate(0, 0)' ) // move the axis to the left
316+ . call ( g => g . selectAll ( ".tick:not(:first-of-type) line" ) // make all line dotted, except the one on the bottom as this will indicate the x-axis
317+ . attr ( "stroke-opacity" , 0.5 )
318+ . attr ( "stroke-dasharray" , "1,1" ) )
319+ . call ( g => g . selectAll ( ".tick text" ) // move the text a little so it does not overlap with the lines
320+ . attr ( "x" , - 5 ) ) ;
316321 }
317322
318323 /**
319324 * Configuration of the line generator which does print the lines
320325 */
321326 private getLineGenerator ( xScale : D3ScaleTime < number , number > ,
322327 yScale : D3ScaleLinear < number , number > ) : D3Line < TimeSeriesPoint > {
323-
324328 return d3Line < TimeSeriesPoint > ( ) // Setup a line generator
325329 . x ( ( point : TimeSeriesPoint ) => {
326330 return xScale ( point . date ) ;
327331 } ) // ... specify the data for the X-Coordinate
328332 . y ( ( point : TimeSeriesPoint ) => {
329333 return yScale ( point . value ) ;
330334 } ) // ... and for the Y-Coordinate
331- . curve ( d3CurveMonotoneX ) ; // smooth the line
335+ . curve ( d3CurveMonotoneX ) ; // smooth the line
332336
333337 }
334338
@@ -341,37 +345,89 @@ export class LineChartService {
341345 data : TimeSeries [ ] ) : void {
342346 // Create one group per line / data entry
343347 chart . selectAll ( '.line' ) // Get all lines already drawn
344- . data ( data , ( datum : TimeSeries ) => datum . key ) // ... for this data
345- . join (
346- enter => this . drawLine ( enter , xScale , yScale )
347- )
348- . attr ( 'class' , ( dataItem : TimeSeries ) => {
349- return 'line line-' + dataItem . key ;
350- } )
351-
352- //this.addDataPointsToChart(chartLineGroups, xScale, yScale, data);
348+ . data ( data , ( datum : TimeSeries ) => datum . key ) // ... for this data
349+ . join (
350+ enter => this . drawLine ( enter , xScale , yScale ) )
351+ . attr ( 'class' , ( dataItem : TimeSeries ) => {
352+ return 'line line' + dataItem . key ;
353+ } ) ;
354+
355+ //this.addDataPointsToChart(chartLineGroups, xScale, data);
353356 }
354-
357+
358+ private addBrush ( chart : D3Selection < D3BaseType , { } , D3ContainerElement , { } > ,
359+ xScale : D3ScaleTime < number , number > ,
360+ yScale : D3ScaleLinear < number , number > ,
361+ data : TimeSeries [ ] ) {
362+ chart . selectAll ( '.brush' )
363+ . remove ( ) ;
364+ this . brush . on ( 'end' , ( ) => this . updateChart ( chart , xScale , yScale ) ) ;
365+ chart . append ( 'g' )
366+ . attr ( 'class' , 'brush' )
367+ . data ( [ 1 ] )
368+ . call ( this . brush )
369+ . on ( 'dblclick' , ( ) => {
370+ xScale . domain ( [ this . getMinDate ( data ) , this . getMaxDate ( data ) ] ) ;
371+ this . resetChart ( xScale , yScale ) ;
372+ } ) ;
373+ }
374+
375+ private resetChart ( xScale : D3ScaleTime < number , number > , yScale : D3ScaleLinear < number , number > ) {
376+ d3Select ( '.x-axis' ) . transition ( ) . call ( this . updateXAxis , xScale ) ;
377+ d3Select ( 'g#time-series-chart-drawing-area' ) . selectAll ( '.line' )
378+ . each ( ( data , index , nodes ) => {
379+ d3Select ( nodes [ index ] )
380+ . attr ( 'd' , ( dataItem : TimeSeries ) => this . getLineGenerator ( xScale , yScale ) ( dataItem . values ) ) ;
381+ } )
382+ }
383+
384+ private updateChart ( selection : any , xScale : D3ScaleTime < number , number > , yScale : D3ScaleLinear < number , number > ) {
385+ //selected boundaries
386+ let extent = d3Event . selection ;
387+ // If no selection, back to initial coordinate. Otherwise, update X axis domain
388+ if ( ! extent ) {
389+ return
390+ } else {
391+ let minDate = xScale . invert ( extent [ 0 ] ) ;
392+ let maxDate = xScale . invert ( extent [ 1 ] ) ;
393+ xScale . domain ( [ minDate , maxDate ] ) ;
394+ selection . select ( ".brush" ) . call ( this . brush . move , null ) ; // This remove the grey brush area
395+ d3Select ( '.x-axis' ) . transition ( ) . call ( this . updateXAxis , xScale ) ;
396+ selection . selectAll ( '.line' ) . each ( ( _ , index , nodes ) => {
397+ d3Select ( nodes [ index ] )
398+ . transition ( )
399+ . attr ( 'd' , ( dataItem : TimeSeries ) => {
400+ let newDataValues = dataItem . values . filter ( ( point ) => {
401+ return point . date <= maxDate && point . date >= minDate ;
402+ } ) ;
403+ return this . getLineGenerator ( xScale , yScale ) ( newDataValues ) ;
404+ } )
405+ }
406+ )
407+ }
408+ }
409+
355410 private drawLine ( selection : any ,
356411 xScale : D3ScaleTime < number , number > ,
357412 yScale : D3ScaleLinear < number , number >
358413 ) {
359414 return selection
360- . append ( 'g' ) // Group each line so we can add dots to this group latter
361- . append ( 'path' ) // Draw one path for every item in the data set
362- . attr ( 'fill' , 'none' )
363- . attr ( 'stroke' , ( d , index : number ) => { return getColorScheme ( ) [ index ] ; } )
364- . attr ( 'stroke-width' , 1.5 )
365- . attr ( 'd' , ( dataItem : TimeSeries ) => {
366- return this . getLineGenerator ( xScale , yScale ) ( dataItem . values ) ;
367- } )
368- . on ( 'mouseover' , ( ) => {
369- console . log ( "Mouse over line" ) ;
370- //this.highlightLine(this);
371- } )
372- . on ( 'mouseout' , ( ) => {
373- //normalizeColors();
374- } ) ;
415+ . append ( 'g' ) // Group each line so we can add dots to this group latter
416+ . append ( 'path' ) // Draw one path for every item in the data set
417+ . attr ( 'fill' , 'none' )
418+ . attr ( 'stroke' , ( d , index : number ) => { return getColorScheme ( ) [ index ] ; } )
419+ . attr ( 'stroke-width' , 1.5 )
420+ . attr ( 'd' , ( dataItem : TimeSeries ) => {
421+ return this . getLineGenerator ( xScale , yScale ) ( dataItem . values ) ;
422+ } )
423+ . on ( 'mouseover' , ( ) => {
424+ console . log ( "Mouse over line" ) ;
425+ //this.highlightLine(this);
426+ } )
427+ . on ( 'mouseout' , ( ) => {
428+ //normalizeColors();
429+ } ) ;
430+
375431 }
376432
377433 //private addDataPointsToChart(chartLineGroups: D3Selection<any, LineChartDataDTO, D3ContainerElement, {}>,
0 commit comments