Skip to content

Commit a3eac9f

Browse files
jiayiw5j0weiss
authored andcommitted
[IT-2766]added brush for zooming
1 parent 0669ed2 commit a3eac9f

File tree

1 file changed

+120
-61
lines changed

1 file changed

+120
-61
lines changed

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

Lines changed: 120 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,8 @@ 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 idleTimeout;
66+
private brush;
6267

6368

6469
constructor(private translationService: TranslateService) {}
@@ -97,11 +102,12 @@ export class LineChartService {
97102

98103
d3Select('.x-axis').transition().call(this.updateXAxis, xScale);
99104
d3Select('.y-axis').transition().call(this.updateYAxis, yScale, this._width, this._margin);
100-
105+
this.brush = d3BrushX().extent([[0,0], [this._width, this._height]]);
106+
this.addBrush(chart, xScale, yScale, data);
101107
this.addDataLinesToChart(chart, xScale, yScale, data);
102-
103108
}
104109

110+
105111
/**
106112
* Prepares the incoming data for drawing with D3.js
107113
*/
@@ -125,8 +131,8 @@ export class LineChartService {
125131

126132
private generateKey(data: EventResultSeriesDTO): string {
127133
return data.jobGroup
128-
+ data.measuredEvent
129-
+ data.data.length;
134+
+ data.measuredEvent
135+
+ data.data.length;
130136
}
131137

132138
/**
@@ -137,13 +143,13 @@ export class LineChartService {
137143
this._width = svgElement.nativeElement.parentElement.offsetWidth - this._margin.left - this._margin.right;
138144
//this._height = svgElement.nativeElement.parentElement.offsetHeight - this._margin.top - this._margin.bottom;
139145
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);
146+
.attr('id', 'time-series-chart')
147+
.attr('width', this._width + this._margin.left + this._margin.right)
148+
.attr('height', 0);
143149

144150
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)
151+
.attr('id', 'time-series-chart-drawing-area')
152+
.attr('transform', 'translate(' + this._margin.left + ', ' + this._margin.top + ')'); // translates the origin to the top left corner (default behavior of D3)
147153
}
148154

149155
public startResize(svgElement: ElementRef): void {
@@ -166,8 +172,8 @@ export class LineChartService {
166172
*/
167173
private getXScale(data: TimeSeries[]): D3ScaleTime<number, number> {
168174
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)]);
175+
.range([0, this._width]) // Display the X-Axis over the complete width
176+
.domain([this.getMinDate(data), this.getMaxDate(data)]);
171177
}
172178

173179
private getMinDate(data: TimeSeries[]): Date {
@@ -191,9 +197,9 @@ export class LineChartService {
191197
*/
192198
private getYScale(data: TimeSeries[]): D3ScaleLinear<number, number> {
193199
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();
200+
.range([this._height, 0]) // Display the Y-Axis over the complete height - origin is top left corner, so height comes first
201+
.domain([0, this.getMaxValue(data)])
202+
.nice();
197203
}
198204

199205
private getMaxValue(data: TimeSeries[]): number {
@@ -214,9 +220,9 @@ export class LineChartService {
214220

215221
// Add the X-Axis to the chart
216222
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);
223+
.attr('class', 'axis x-axis') // a css class to style it later
224+
.attr('transform', 'translate(0, ' + this._height + ')') // even if the D3 method called `axisBottom` we have to move it to the bottom by ourselfs
225+
.call(xAxis);
220226
}
221227

222228
/**
@@ -229,15 +235,15 @@ export class LineChartService {
229235

230236
// Add the Y-Axis to the chart
231237
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);
238+
.attr('class', 'axis y-axis') // a css class to style it later
239+
.call(yAxis);
234240

235241
// Add the axis description
236242
this.translationService.get("frontend.de.iteratec.osm.timeSeries.loadTimes").pipe(take(1)).subscribe(title => {
237243
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]');
244+
.attr('class', 'description')
245+
.attr('transform', 'translate(-' + (this._margin.left - 20) + ', ' + (this._height/2 - this._margin.bottom) +') rotate(-90)')
246+
.text(title + ' [ms]');
241247
});
242248
}
243249

@@ -254,7 +260,7 @@ export class LineChartService {
254260
lines.forEach((line, index) => {
255261
let tspan = element.append('tspan').text(line);
256262
if (index > 0)
257-
tspan.attr('x', 0).attr('dy', '15');
263+
tspan.attr('x', 0).attr('dy', '15');
258264
});
259265
});
260266
}
@@ -296,9 +302,9 @@ export class LineChartService {
296302
transition.on('end.showTicks', function show() {
297303
d3Select(this).selectAll('g.tick text')
298304
.transition()
299-
.delay(100)
300-
.duration(500)
301-
.attr('opacity', '1.0')
305+
.delay(100)
306+
.duration(500)
307+
.attr('opacity', '1.0')
302308
});
303309
}
304310

@@ -307,28 +313,28 @@ export class LineChartService {
307313
d3AxisRight(yScale) // axis right, because we draw the background line with this
308314
.tickSize(width) // background line over complete chart width
309315
)
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));
316+
.attr('transform', 'translate(0, 0)') // move the axis to the left
317+
.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
318+
.attr("stroke-opacity", 0.5)
319+
.attr("stroke-dasharray", "1,1"))
320+
.call(g => g.selectAll(".tick text") // move the text a little so it does not overlap with the lines
321+
.attr("x", -5));
316322
}
317323

318324
/**
319325
* Configuration of the line generator which does print the lines
320326
*/
321327
private getLineGenerator(xScale: D3ScaleTime<number, number>,
322328
yScale: D3ScaleLinear<number, number>): D3Line<TimeSeriesPoint> {
323-
329+
console.log(xScale.ticks());
324330
return d3Line<TimeSeriesPoint>() // Setup a line generator
325331
.x((point: TimeSeriesPoint) => {
326332
return xScale(point.date);
327333
}) // ... specify the data for the X-Coordinate
328334
.y((point: TimeSeriesPoint) => {
329335
return yScale(point.value);
330336
}) // ... and for the Y-Coordinate
331-
.curve(d3CurveMonotoneX); // smooth the line
337+
.curve(d3CurveMonotoneX); // smooth the line
332338

333339
}
334340

@@ -341,37 +347,90 @@ export class LineChartService {
341347
data: TimeSeries[]): void {
342348
// Create one group per line / data entry
343349
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);
350+
.data(data, (datum: TimeSeries) => datum.key) // ... for this data
351+
.join(
352+
enter => this.drawLine(enter, xScale, yScale))
353+
.attr('class', (dataItem: TimeSeries) => {
354+
return 'line line' + dataItem.key;
355+
});
356+
357+
//this.addDataPointsToChart(chartLineGroups, xScale, data);
358+
}
359+
360+
private addBrush(chart: D3Selection<D3BaseType, {}, D3ContainerElement, {}>,
361+
xScale: D3ScaleTime<number, number>,
362+
yScale: D3ScaleLinear<number, number>,
363+
data: TimeSeries[]){
364+
chart.selectAll('.brush')
365+
.remove();
366+
this.brush.on('end', () => this.updateChart(chart, xScale, yScale));
367+
chart.append('g')
368+
.attr('class', 'brush')
369+
.data([1])
370+
.call(this.brush)
371+
.on('dblclick', () => {
372+
xScale.domain([this.getMinDate(data), this.getMaxDate(data)]);
373+
this.resetChart(xScale, yScale);
374+
});
375+
}
376+
377+
private resetChart(xScale: D3ScaleTime<number, number>, yScale: D3ScaleLinear<number, number>){
378+
d3Select('.x-axis').transition().call(this.updateXAxis, xScale);
379+
d3Select('g#time-series-chart-drawing-area').selectAll('.line').each((data, index, nodes) => {
380+
d3Select(nodes[index]).transition()
381+
.attr('d', (dataItem: TimeSeries) => this.getLineGenerator(xScale,yScale)(dataItem.values))
382+
})
383+
}
384+
385+
private updateChart( selection: any, xScale: D3ScaleTime<number, number>, yScale: D3ScaleLinear<number, number>) {
386+
//selected boundaries
387+
let extent = d3Event.selection;
388+
// If no selection, back to initial coordinate. Otherwise, update X axis domain
389+
if(!extent){
390+
if (!this.idleTimeout) return this.idleTimeout = setTimeout(this.idleTimeout = null, 350); // This allows to wait a little bit
391+
//xScale = this.getXScale(data);
392+
393+
}else{
394+
xScale.domain([ xScale.invert(extent[0]), xScale.invert(extent[1]) ]);
395+
selection.select(".brush").call(this.brush.move, null);
396+
d3Select('.x-axis').transition().call(this.updateXAxis, xScale);// This remove the grey brush area as soon as the selection has been don
397+
selection.selectAll('.line').each((data, index, nodes) => {
398+
d3Select(nodes[index])
399+
.transition()
400+
.attr('d', (dataItem: TimeSeries) => {
401+
/*let newDataItem = dataItem.values.map((point: TimeSeriesPoint) => {
402+
point.date
403+
})*/
404+
console.log(xScale.ticks());
405+
return this.getLineGenerator(xScale,yScale)(dataItem.values);
406+
})
407+
}
408+
)
409+
}
410+
353411
}
354-
412+
355413
private drawLine(selection: any,
356414
xScale: D3ScaleTime<number, number>,
357415
yScale: D3ScaleLinear<number, number>
358416
) {
359417
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-
});
418+
.append('g')// Group each line so we can add dots to this group latter
419+
.append('path') // Draw one path for every item in the data set
420+
.attr('fill', 'none')
421+
.attr('stroke', (d, index: number) => { return getColorScheme()[index]; })
422+
.attr('stroke-width', 1.5)
423+
.attr('d', (dataItem: TimeSeries) => {
424+
return this.getLineGenerator(xScale, yScale)(dataItem.values);
425+
})
426+
.on('mouseover', () => {
427+
console.log("Mouse over line");
428+
//this.highlightLine(this);
429+
})
430+
.on('mouseout', () => {
431+
//normalizeColors();
432+
});
433+
375434
}
376435

377436
//private addDataPointsToChart(chartLineGroups: D3Selection<any, LineChartDataDTO, D3ContainerElement, {}>,

0 commit comments

Comments
 (0)