Last active
March 5, 2018 14:30
-
-
Save bentedder/58b269d3c5aa172dec3beb2e88de5740 to your computer and use it in GitHub Desktop.
Stacked Bar Chart
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
#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; | |
} | |
} | |
} | |
} |
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 { 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); | |
} | |
} |
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 * 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