import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Http } from '@angular/http';
import { BarChartComponent, BarChartData } from './bar-chart.component';
@Component({
selector: 'my-app',
directives: [
BarChartComponent
],
template: `
<div class="container">
<bar-chart [data]="data | async"></bar-chart>
</div>
`
})
export class AppComponent implements OnInit {
data: Observable<BarChartData>;
constructor(private http: Http) {}
ngOnInit() {
console.log(this.http);
this.data = this.http.get('data/chart.json').map(res => res.json().items);
}
}
import { bootstrap } from '@angular/platform-browser-dynamic';
import { HTTP_PROVIDERS } from '@angular/http';
// RxJs
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { AppComponent } from './app.component';
bootstrap(AppComponent, [
...HTTP_PROVIDERS
]);
/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
body {
width: 100%;
height: 100vh;
margin: 0;
display: flex;
}
.flex {
flex: 1; // Flex to fit parent
display: flex; // make this element a flexbox
}
.container {
flex: 1;
display: flex;
padding: 2rem;
}
my-app {
flex: 1;
display: flex;
}
/*
BarChart
*/
bar-chart {
flex: 1;
display: flex;
position: relative;
svg {
flex: 1;
}
.axis path,
.axis line {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.grid {
fill: none;
stroke: #e6e6e6;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.dot {
//fill: rgba(0,0,0,0.1);
fill: none;
pointer-events: visible;
}
.no-data {
font-family: "Open Sans";
font-size: 20px;
text-anchor: middle;
}
}
<!DOCTYPE html>
<html>
<head>
<title>Angular 2 QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- 1. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="https://npmcdn.com/core-js/client/shim.min.js"></script>
<script src="https://npmcdn.com/zone.js@0.6.12?main=browser"></script>
<script src="https://npmcdn.com/reflect-metadata@0.1.3"></script>
<script src="https://npmcdn.com/systemjs@0.19.27/dist/system.src.js"></script>
<!-- 2. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
<!--
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
-->
/**
* PLUNKER VERSION (based on systemjs.config.js in angular.io)
* System configuration for Angular 2 samples
* Adjust as necessary for your application needs.
*/
(function(global) {
var ngVer = '@2.0.0-rc.3'; // lock in the angular package version; do not let it float to current!
var routerVer = '@3.0.0-alpha.7'; // lock router version
var formsVer = '@0.1.1'; // lock forms version
//map tells the System loader where to look for things
var map = {
'app': 'app',
'@angular': 'https://npmcdn.com/@angular', // sufficient if we didn't pin the version
'@angular/router': 'https://npmcdn.com/@angular/router' + routerVer,
'@angular/forms': 'https://npmcdn.com/@angular/forms' + formsVer,
'angular2-in-memory-web-api': 'https://npmcdn.com/angular2-in-memory-web-api', // get latest
'rxjs': 'https://npmcdn.com/rxjs@5.0.0-beta.6',
'ts': 'https://npmcdn.com/plugin-typescript@4.0.10/lib/plugin.js',
'typescript': 'https://npmcdn.com/typescript@1.9.0-dev.20160409/lib/typescript.js',
'd3': 'https://npmcdn.com/d3@3.5.17',
};
//packages tells the System loader how to load when no filename and/or no extension
var packages = {
'app': { main: 'main.ts', defaultExtension: 'ts' },
'rxjs': { defaultExtension: 'js' },
'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
};
var ngPackageNames = [
'common',
'compiler',
'core',
'http',
'platform-browser',
'platform-browser-dynamic',
'router-deprecated',
'upgrade',
];
// Add map entries for each angular package
// only because we're pinning the version with `ngVer`.
ngPackageNames.forEach(function(pkgName) {
map['@angular/'+pkgName] = 'https://npmcdn.com/@angular/' + pkgName + ngVer;
});
// Add package entries for angular packages
ngPackageNames.forEach(function(pkgName) {
// Bundled (~40 requests):
packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
// Individual files (~300 requests):
//packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' };
});
// No umd for router yet
packages['@angular/router'] = { main: 'index.js', defaultExtension: 'js' };
// Forms not on rc yet
packages['@angular/forms'] = { main: 'index.js', defaultExtension: 'js' };
var config = {
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
tsconfig: true
},
meta: {
'typescript': {
"exports": "ts"
}
},
map: map,
packages: packages
};
System.config(config);
})(this);
/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
import {Component, OnInit, OnDestroy, OnChanges, SimpleChange, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/debounceTime';
import * as d3 from 'd3';
export interface Margin {
top: number;
right: number;
bottom: number;
left: number;
}
export abstract class D3Base implements OnInit, OnDestroy, OnChanges {
margin: Margin = {top: 0, right: 0, bottom: 0, left: 0};
svg: d3.Selection<any>;
chart: d3.Selection<any>;
containerWidth: number = 0;
containerHeight: number = 0;
chartWidth: number = 0;
chartHeight: number = 0;
resize$: Observable<Window>;
resize$Subscription: Subscription;
constructor(private element: ElementRef) {}
ngOnInit() {
// Create base d3 dom nodes
this.svg = d3.select(this.element.nativeElement).append('svg');
this.chart = this.svg.append('g').attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
// Attach to window resize
this.resize$ = Observable.fromEvent<Window>(window, 'resize').debounceTime(100);
// When window resizes calculate sizes and re-render
this.resize$Subscription = this.resize$.subscribe(() => {
this.resize();
this.render();
});
// On first run
this.resize(); // to get size of container
this.init(); // setup axis and more
this.render(); // render the chart
}
ngOnDestroy() {
this.resize$Subscription.unsubscribe();
}
ngOnChanges(changes: {[propName: string]: SimpleChange}) {
// if changes are pushed we need to re-render
if (this.chart) {
this.render();
}
}
/**
* Calculate the size of the container so that the chart fits
*/
protected resize(): void {
this.containerWidth = Math.abs(this.element.nativeElement.offsetWidth);
this.containerHeight = Math.abs(this.element.nativeElement.offsetHeight);
this.chartWidth = this.containerWidth - this.margin.left - this.margin.right;
this.chartHeight = this.containerHeight - this.margin.top - this.margin.bottom;
}
/**
* Initialize the D3 chart components
* In this method we should do the one time setup stuff
*/
abstract init(): void;
/**
* Render the chart using enter(), transition(), exit()
*/
abstract render(): void;
}
import {Component, Input, ChangeDetectionStrategy, ElementRef} from '@angular/core';
import * as d3 from 'd3';
import {D3Base} from './d3-base';
export interface BarChartData {
label: string;
value: number;
}
@Component({
moduleId: module.id,
selector: 'bar-chart',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
})
export class BarChartComponent extends D3Base {
@Input() data: BarChartData[];
color: d3.scale.Ordinal<string, string>;
xScale: d3.scale.Ordinal<string, number>;
yScale: d3.scale.Linear<number, number>;
xAxisElement: d3.Selection<any>;
yAxisElement: d3.Selection<any>;
xAxis: d3.svg.Axis;
yAxis: d3.svg.Axis;
barElement: d3.Selection<any>;
constructor(element: ElementRef) {
super(element);
this.margin = {top: 30, right: 0, bottom: 30, left: 80};
}
init() {
/************************************************************
* Set Axis and Colors
***********************************************************/
// this.color = d3.scale.ordinal();
this.color = d3.scale.ordinal<string, string>().range(['#000033', '#003462', '#006699', '#0099cc', '#666666', '#999999', '#cccccc', '#db9815', '#999900', '#d1d17c', '#669933', '#666633', '#333333']);
this.xScale = d3.scale.ordinal<number>()
.range([0, 0]);
this.yScale = d3.scale.linear()
// Default domain
.domain([0, 100])
.range([0, 0]);
this.xAxis = d3.svg.axis()
.scale(this.xScale)
.orient('bottom');
this.yAxis = d3.svg.axis()
.scale(this.yScale)
.orient('left')
.ticks(6);
/************************************************************
* Add Elements
***********************************************************/
this.xAxisElement = this.chart
.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,0)')
.call(this.xAxis);
this.yAxisElement = this.chart
.append('g')
.attr('class', 'y axis')
.call(this.yAxis);
this.barElement = this.chart
.append('g')
.attr('class', 'bars');
}
render() {
if (!this.data || this.data.length === 0) {
return;
}
this.xScale
.domain(this.data.map(d => d.label))
.rangeRoundBands([0, this.chartWidth], .1);
this.yScale
.domain([0, d3.max(this.data, d => d.value)])
.range([this.chartHeight, 0]);
this.xAxisElement.attr('transform', `translate(0, ${this.chartHeight})`);
// this.yGridElement.call(this.yGrid.tickSize(-this.chartWidth, 0, 0));
this.xAxis.scale(this.xScale);
this.yAxis.scale(this.yScale);
this.xAxisElement.transition().call(this.xAxis);
this.yAxisElement.transition().call(this.yAxis);
let bars = this.barElement
.selectAll('.bar')
.data(this.data);
/************************************************************
* D3 Enters
***********************************************************/
bars.enter()
.append('rect')
.attr('class', 'bar')
.style('fill', d => this.color(d.label))
.attr('x', d => this.xScale(d.label))
.attr('width', this.xScale.rangeBand())
.attr('y', d => this.yScale(d.value))
.attr('height', d => this.chartHeight - this.yScale(d.value));
/************************************************************
* D3 Transitions
***********************************************************/
bars.transition()
.attr('x', d => this.xScale(d.label))
.attr('width', this.xScale.rangeBand())
.attr('y', d => this.yScale(d.value))
.attr('height', d => this.chartHeight - this.yScale(d.value));
/************************************************************
* D3 Exits
***********************************************************/
bars.exit().transition().remove();
}
}
{
"items": [
{ "label": "One", "value": 25 },
{ "label": "Two", "value": 50 },
{ "label": "Three", "value": 100 },
{ "label": "Four", "value": 75 }
]
}
# Angular 2 - D3 resizable flexbox chart
By making the chart and svg element a flexbox, it can size itself to it's parent. In this case it's the body.
I found it hard to use flexbox because if you want it to fill up it's parent everything needs to have
```
flex: 1;
display: flex;
```
Also D3's type definitions still need refining, and they didn't work well with timecharts.
The D3 base class fetches the elements offsetWidth and offsetHeight and stores them in variables that can be used by d3.
## Init
We have an init method that is used by the chart to initialize it's axis, containers...
## Render
We have a render method that takes the incoming data and calculates the axis extents, and uses d3´s `.enter()`, `.transition()`, `exit()` selections to create the chart.