Skip to content

Commit d7114e9

Browse files
authored
Merge branch 'develop' into bugfix/loadResultSelectionAfterUrlChanges
2 parents 3cbfac9 + 6e7b734 commit d7114e9

File tree

1 file changed

+117
-61
lines changed

1 file changed

+117
-61
lines changed

frontend/src/app/modules/time-series/services/line-chart.service.ts

Lines changed: 117 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
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

1314
import {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

3839
import 'd3-transition';
3940

41+
import {brushX as d3BrushX} from 'd3-brush';
42+
4043
import {EventResultDataDTO} from 'src/app/modules/time-series/models/event-result-data.model';
4144
import {EventResultSeriesDTO} from 'src/app/modules/time-series/models/event-result-series.model';
4245
import {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

Comments
 (0)