Skip to content

Instantly share code, notes, and snippets.

@bentedder
Last active March 5, 2018 14:30
Show Gist options
  • Save bentedder/58b269d3c5aa172dec3beb2e88de5740 to your computer and use it in GitHub Desktop.
Save bentedder/58b269d3c5aa172dec3beb2e88de5740 to your computer and use it in GitHub Desktop.
Stacked Bar Chart
#myChart {
.axis {
path {
stroke: #ccc;
}
text {
fill: #999;
font-size: 0.6rem;
}
}
.risk-group {
transition: fill 200ms ease-in-out;
fill: #ccc;
&:hover {
fill: #ccc;
}
&.low {
&:hover {
fill: green;
}
}
&.medium {
&:hover {
fill: yellow;
}
}
&.high {
&:hover {
fill: red;
}
}
}
}
import { AfterViewInit, Component, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core';
import * as d3 from 'd3';
import { ChartStructure, getChartDimensions, scaffoldChart } from './helpers/chart.helper';
interface MyInterface {
person: string;
high: number;
medium: number;
low: number;
}
@Component({
selector: 'my-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ChartComponent implements AfterViewInit {
@ViewChild('myChart') myChart: ElementRef;
chart: any;
dimensions: ChartStructure = {
width: 450,
height: 250,
margin: {
top: 20,
right: 10,
bottom: 10,
left: 50
}
};
innerDimensions = getChartDimensions(this.dimensions);
ngAfterViewInit(): void {
// Scaffold Chart
this.chart = scaffoldChart(this.myChart.nativeElement, this.dimensions);
// Inject data
const data = require('./data.json') as MyInterface[];
this.buildChart(data);
}
buildChart = (data: MyInterface[]): void => {
const x = this.createXScale(data);
const y = this.createYScale(data);
this.addBars(x, y, data);
this.addAxes(x, y);
}
createXScale = (data: MyInterface[]): d3.ScaleBand<string> => {
return d3.scaleBand()
.range([0, this.innerDimensions.width])
.domain(data.map((d: MyInterface): string => d.person))
.paddingOuter(0)
.paddingInner(0.3)
.align(0.5);
}
createYScale = (data: MyInterface[]): d3.ScaleLinear<number, number> => {
interface Totaled extends MyInterface {
total: number;
}
const totaledData: Totaled[] = data.map((d: MyInterface) => ({ ...d, total: d.high + d.medium + d.low }));
const maxTotal = d3.max(totaledData, (d: Totaled): number => d.total) || 0;
return d3.scaleLinear()
.range([this.innerDimensions.height, 0])
.domain([0, maxTotal]).nice();
}
addBars = (x: d3.ScaleBand<string>, y: d3.ScaleLinear<number, number>, data: MyInterface[]): void => {
const stack = d3.stack<MyInterface>()
.keys(['high', 'medium', 'low'])
(data);
// Create stacks from data
const levelGroup = this.chart
.selectAll('.level-group')
.data(stack)
.enter()
.append('g')
.attr('class', (d: d3.Series<MyInterface, string>) => `level-group ${d.key}`);
// Create bars within each level group
levelGroup
.selectAll('rect')
.data((d: d3.Series<MyInterface, string>) => d)
.enter()
.append('rect')
.attr('x', (d: d3.SeriesPoint<MyInterface>) => x(d.data.person) )
.attr('y', () => this.innerDimensions.height)
.attr('class', (d: d3.SeriesPoint<MyInterface>) => d.data.person)
.attr('height', 0)
.attr('width', x.bandwidth())
.transition()
.duration(600)
.attr('height', (d: d3.SeriesPoint<MyInterface>) => y(d[0]) - y(d[1]))
.attr('y', (d: d3.SeriesPoint<MyInterface>) => y(d[1]));
}
addAxes = (x: d3.ScaleBand<string>, y: d3.ScaleLinear<number, number>) => {
const xAxis = d3.axisBottom(x);
const yAxis = d3.axisLeft(y).ticks(2).tickSize(0).tickPadding(10);
// Create X Axis
this.chart.append('g')
.attr('class', 'axis axis--x')
.attr('transform', `translate(0, ${this.innerDimensions.height})`)
.call(xAxis);
// Create Y Axis
this.chart.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
}
}
import * as d3 from 'd3';
export interface ChartStructure {
width: number;
height: number;
margin: {
top: number;
right: number;
bottom: number;
left: number
};
}
// Generic Methods
export const scaffoldChart = (selector: string, chartWrap: ChartStructure): d3.Selection<any, {}, HTMLElement, undefined> => {
// A standard scaffold of a chartWrap that works with margins
return d3.select(selector)
.append('svg')
.attr('width', chartWrap.width + chartWrap.margin.left + chartWrap.margin.right)
.attr('height', chartWrap.height + chartWrap.margin.top + chartWrap.margin.bottom)
.call(this.responsivefy)
.append('g')
.attr('transform', `translate(${chartWrap.margin.left}, ${chartWrap.margin.top})`);
};
export const getChartDimensions = (chartWrap: ChartStructure): { width: number, height: number } => ({
width: chartWrap.width - chartWrap.margin.left - chartWrap.margin.right,
height: chartWrap.height - chartWrap.margin.top - chartWrap.margin.bottom
});
export const responsivefy = (svg: any) => {
// Pulled from Egghead.io tutorials
const container = d3.select(svg.node().parentNode);
const width = parseInt(svg.style('width'), 10);
const height = parseInt(svg.style('height'), 10);
const aspect = width / height;
// add viewBox and preserveAspectRatio properties,
// and call resize so that svg resizes on inital page load
svg.attr('viewBox', '0 0 ' + width + ' ' + height)
.attr('preserveAspectRatio', 'xMaxYMax meet')
.call(resize);
// to register multiple listeners for same event type,
// you need to add namespace, i.e., 'click.foo'
// necessary if you call invoke this function for multiple svgs
// api docs: https://github.com/mbostock/d3/wiki/Selections#on
d3.select(window).on('resize.' + container.attr('id'), resize);
// get width of container and resize svg to fit it
function resize(): void {
const targetWidth = parseInt(container.style('width'), 10);
svg.attr('width', targetWidth);
svg.attr('height', Math.round(targetWidth / aspect));
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment