Created
February 19, 2017 17:00
-
-
Save JonathanDn/14a66c707fab89ecbf3626fc170358a5 to your computer and use it in GitHub Desktop.
Angular 2 - D3 Horizontal Scrollable Timeline, with mock date data / real log data - circles re-rendering when zoom activated & when side-scrolling - change colors according to input v1.0.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Component, OnInit, Input, ChangeDetectionStrategy} from '@angular/core'; | |
import * as d3 from 'd3'; | |
import {Observable} from "rxjs"; | |
import {UIConsts} from "../../../../shared/app.consts"; | |
@Component({ | |
selector: 'timeline', | |
styleUrls: ['timeline.scss'], | |
template: ` | |
<div class="timeline-svg-container"> | |
<div class="chart"></div> | |
</div> | |
`, | |
changeDetection: ChangeDetectionStrategy.OnPush | |
}) | |
export class TimelineComponent implements OnInit { | |
private colorMap = { | |
itemType1: 'red', | |
itemType2: 'orange', | |
itemType3: '#00c2d6', | |
itemType4: 'green' | |
}; | |
// Time Formats | |
private ts = Math.round(new Date().getTime() / 1000); | |
private tsYesterday = this.ts - (24 * 3600); | |
private tsMonthAgo = this.ts - (24 * 3000 * 30); | |
private tsWeekAgo = this.ts - (24 * 3000 * 7); | |
private dateNow: any = new Date(this.ts * 1000); | |
private dateTwentyFourHoursAgo: any = new Date(this.tsYesterday * 1000); | |
private dateMonthAgo: any = new Date(this.tsMonthAgo * 1000); | |
private dateWeekAgo: any = new Date(this.tsWeekAgo * 1000); | |
// Domain in Timestamps: | |
private tsDateNow = Date.parse(this.dateNow); | |
private tsDateTwentyFourHoursAgo = Date.parse(this.dateTwentyFourHoursAgo); | |
private tsDateMonthAgo = Date.parse(this.dateMonthAgo); | |
private tsDateWeekAgo = Date.parse(this.dateWeekAgo); | |
private margin = {top: 0, right: 20, bottom: 30, left: 20}; | |
private width = 790 - this.margin.left - this.margin.right; | |
private height = 100 - this.margin.top - this.margin.bottom; | |
// Zoom Behavior | |
private zoom = d3.zoom() | |
// translateExtent - width - in charge of how many items can stack inside | |
.translateExtent([[0, +30], [this.width * 2, this.height]]) | |
// scaleExtent - how much zoom scales. | |
.scaleExtent([1, 100]) | |
.on ("zoom", () => { | |
// Zoom to a deeper Resolution: | |
this.zoomed(); | |
// Re-Render | |
this.reRender(); | |
}); | |
private svg; | |
private viewBox; | |
private xAxis; | |
private xScale; | |
private gX; | |
@Input() logs; | |
constructor() { } | |
ngOnInit() {}); | |
// Bar + scale + axis container | |
this.svg = d3.select('.chart') | |
.append('svg') | |
.attr('width', this.width + this.margin.left + this.margin.right) | |
.attr('height', this.height + this.margin.top + this.margin.bottom) | |
.append('g') | |
.attr('class', 'main-container') | |
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`); | |
// // Enable Zoom Behavior on SVG | |
this.svg.call(this.zoom); | |
// x Scale & Axis - domain in timestamps | |
this.xScale = d3 | |
.scaleTime() | |
.domain([this.tsDateMonthAgo, this.tsDateNow]) | |
.range([0, this.width]); | |
// // Bar above scale/axis | |
this.viewBox = this.svg.append('rect') | |
.attr('width', this.width) | |
.attr('height', this.height) | |
.style('fill', 'lightgrey') | |
.style('cursor', 'move'); | |
this.xAxis = d3.axisBottom(this.xScale); | |
// Bottom x scale | |
this.gX = this.svg.append('g') | |
.attr('transform', `translate(0, ${this.height})`) | |
.attr('class', 'x axis') | |
.call(this.xAxis); | |
} | |
// MOCK DATA | |
private logsMoq = [{ | |
CreationDateTime: "2017-02-12T07:02:44.033Z", | |
Type: "itemType1" | |
}, | |
{ | |
CreationDateTime: "2017-02-12T07:01:44.033Z", | |
Type: "itemType2" | |
}, | |
{ | |
CreationDateTime: "2017-02-12T07:00:44.033Z", | |
Type: "itemType3" | |
}, | |
{ | |
CreationDateTime: "2017-02-11T07:11:44.033Z", | |
Type: "itemType4" | |
}, | |
{ | |
CreationDateTime: "2017-02-11T07:10:44.033Z", | |
Type: "itemType1" | |
}]; | |
// Update these on zoom: | |
render(data) { | |
let circles; | |
// JOIN | |
circles = this.svg.selectAll("circle") | |
.data(data); | |
// UPDATE - add circles | |
circles | |
.enter() | |
.append('g') | |
.attr('class', 'dot') | |
.append("circle") | |
.attr("r", 5) | |
.attr("cy", this.height) | |
.attr("cx", (d) => this.xScale(new Date(d.CreationDateTime).getTime())) | |
.style('fill', (d) => this.colorMap[d.Type]) | |
.style('stroke', 'black') | |
.style('stroke-width', 2); | |
// EXIT | |
circles.exit().remove(); | |
} | |
reRender() { | |
// Preserve logs data | |
let oldLogs = this.logs; | |
// Remove | |
this.logs = []; | |
// Update Original; | |
this.logs = oldLogs; | |
// Get logs back and Re-Render | |
this.render(oldLogs); | |
} | |
rePosition(transitionX, kFactor, data) { | |
let oldX = this.xScale(new Date(data.CreationDateTime).getTime()); | |
let newXPosition = (transitionX + kFactor * oldX); | |
return newXPosition; | |
} | |
zoomed() { | |
// New Position: | |
let tX = d3.event.transform.x; | |
let k = d3.event.transform.k | |
// Zoom on bar | |
this.render(this.logs); | |
// Zoom / side-scroll on xAxis | |
this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale))); | |
// Draw the CIRCLES in their new position: | |
d3.selectAll('circle').attr('cx', (d) => this.rePosition(tX, k, d)); | |
} | |
ngAfterViewInit() { | |
this.render(this.logs); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.timeline-container { | |
position: relative; | |
width:792px; | |
height:102px; | |
.chart { | |
min-width: 790px; | |
min-height: 100px; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1. TODO - add types without exceptions: (and in other places like zoom); | |
// private svg: Selection<BaseType, {}, HTMLElement, any>; | |
// private viewBox: Selection<BaseType, {}, BaseType, {}>; | |
// private xAxis: Axis<any>; | |
// private xScale: ScaleTime<number, number>; | |
// private gX: Selection<BaseType, {}, BaseType, {}>; | |
2. TODO - make the initial zoom to be maximum zoom in --> instead of extent 0 initially --> | |
in a desired extent like 0-100 then initial view should be 50 if desired | |
3. TODO - time format --> should support both year / months / days / hours format, shorten strings of months from "February" --> "Feb" | |
without harming the zoom differnces in year / month / day.... | |
//.tickFormat(d3.timeFormat("%I %M %p")); | |
4. TODO - PRIORITY(project specific) - RE RENDER THE DOTS(DELETE OLD PLACE NEW ONES OF NEW INCIDENT) --> when clicking other item in list. | |
5. TODO - PRIORITY(project specific) - sort logs - by date from left to right - "past -----> future". | |
// Then We have the array in a left to right order and appending of SORTED logs would be | |
// properly z-indexed/ z-transformed from left to right. | |
// let tsLogs = logs.map((d) => { | |
// d.CreationDateTime = Date.parse(d.CreationDateTime); | |
// return d }); | |
// console.log('tsLogd', tsLogs); | |
// Sort Ascending | |
//let sortedLogs = tsLogs.sort((a, b) => a.CreationDateTime - b.CreationDateTime); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I honestly don't remember. I do remember I build it pretty close to when Angular 2 first stable release was out of beta. So you could track that and and find out. Feel free to share here if you do I'll update it in the description for future reference.