better d3 charts with tdd

Post on 14-Apr-2017

98 Views

Category:

Engineering

1 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Better D3 Charts with TDD

Slides: Code:

http://golodhros.github.io/https://github.com/Golodhros/d3-meetup

Marcos Iglesias

El Bierzo

Next upPresentationLive codingQ&A

D3 introductionData-Driven DocumentsJavaScript library to manipulate data baseddocumentsOpen web standards (SVG, HTML and CSS)Allows interactions with your graphs

How does it work?Loads dataBinds data to elementsTransforms those elementsTransitions between statesExample

D3 NicetiesBased on attaching data to the DOMStyling of elements with CSSTransitions and animations baked inTotal control over our graphsAmazing communityDecent amount of publications

WHAT CAN YOU DO WITHD3?

Bar charts

Pie charts

Algorithm visualization

Artistic visualizations

CONTRACTING STORY

Marketing guy: Hey, I saw this nice chart,could we do something like that?

HE LOVED IT!

USUAL WORKFLOW

Search for an example

READ AND ADAPT CODE

ADD/REMOVE FEATURES

Polish it up

Usual workflowIdea or requirementSearch for an exampleAdapt the codeAdd/remove featuresPolish it up

THE STANDARD WAY

Code example

by Mike BostockBar chart example

Creating containervar margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom;

var svg = d3.select("body").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top +

Reference: Margin Convention

Setting up scales and axesvar x = d3.scale.ordinal() .rangeRoundBands([0, width], .1);

var y = d3.scale.linear() .range([height, 0]);

var xAxis = d3.svg.axis() .scale(x) .orient("bottom");

var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(10, "%");

Reference: Scales tutorial

Loading data// Loads Data d3.tsv("data.tsv", type, function(error, data) { if (error) throw error; // Chart Code here });

// Cleans Data function type(d) { d.frequency = +d.frequency; return d; }

Drawing axes// Rest of the scales x.domain(data.map(function(d) { return d.letter; })); y.domain([0, d3.max(data, function(d) { return d.frequency; })]);

// Draws X axis svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis);

// Draws Y axis svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em")

Drawing barssvg.selectAll(".bar") .data(data) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.letter); }) .attr("width", x.rangeBand()) .attr("y", function(d) { return y(d.frequency); }) .attr("height", function(d) { return height - y(d.frequency); });

Output

Standard D3: drawbacksMonolithic functionsChained method callsHard to change codeImpossible to reuseDelicate

STORY CONTINUES...

Marketing guy: What if we change this thinghere...

TRIAL AND ERROR

Done!

M-guy: nice, let’s change this other thing!

Done!

M-guy: Great! I love it so much I want it on theproduct!

M-guy: So good you have it almost ready,right?

I WAS HATING MYSELF!

Possible outcomesYou take it throughYou dump it and start all over againYou avoid refactoring

What if you could work with charts the sameway you work with the other code?

REUSABLE CHART API

jQuery VS MV*

Reusable Chart API - codereturn function module(){ // @param {D3Selection} _selection A d3 selection that represents // the container(s) where the chart(s) will be rendered function exports(_selection){

// @param {object} _data The data to generate the chart _selection.each(function(_data){ // Assigns private variables // Builds chart }); }

// @param {object} _x Margin object to get/set // @return { margin | module} Current margin or Bar Chart module to chain calls exports.margin = function(_x) { if (!arguments.length) return margin; margin = _x;

Reusable Chart API - use// Creates bar chart component and configures its margins barChart = chart() .margin({top: 5, left: 10});

container = d3.select('.chart-container');

// Calls bar chart with the data-fed selector container.datum(dataset).call(barChart);

Reusable Chart API - benefitsModularComposableConfigurableConsistentTeamwork EnablingTestable

THE TDD WAY

The "before" blockcontainer = d3.select('.test-container'); dataset = [ { letter: 'A', frequency: .08167 },{ letter: 'B', frequency: .01492 },... ]; barChart = barChart();

container.datum(dataset).call(barChart);

Test: basic chartit('should render a chart with minimal requirements', function(){ expect(containerFixture.select('.bar-chart').empty()).toBeFalsy(); });

Code: basic chartreturn function module(){ var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960, height = 500, svg;

function exports(_selection){ _selection.each(function(_data){ var chartWidth = width - margin.left - margin.right, chartHeight = height - margin.top - margin.bottom;

if (!svg) { svg = d3.select(this) .append('svg') .classed('bar-chart', true); } }); }; return exports;

Reference: Towards Reusable Charts

Test: containersit('should render container, axis and chart groups', function(){ expect(containerFixture.select('g.container-group').empty()).toBeFalsy(); expect(containerFixture.select('g.chart-group').empty()).toBeFalsy(); expect(containerFixture.select('g.x-axis-group').empty()).toBeFalsy(); expect(containerFixture.select('g.y-axis-group').empty()).toBeFalsy();});

Code: containersfunction buildContainerGroups(){ var container = svg.append("g").attr("class", "container-group");

container.append("g").attr("class", "chart-group"); container.append("g").attr("class", "x-axis-group axis"); container.append("g").attr("class", "y-axis-group axis"); }

Test: axesit('should render an X and Y axes', function(){ expect(containerFixture.select('.x-axis-group.axis').empty()).toBeFalsy(); expect(containerFixture.select('.y-axis-group.axis').empty()).toBeFalsy();});

Code: scalesfunction buildScales(){ xScale = d3.scale.ordinal() .domain(data.map(function(d) { return d.letter; })) .rangeRoundBands([0, chartWidth], .1);

yScale = d3.scale.linear() .domain([0, d3.max(data, function(d) { return d.frequency; })]) .range([chartHeight, 0]); }

Code: axesfunction buildAxis(){ xAxis = d3.svg.axis() .scale(xScale) .orient("bottom");

yAxis = d3.svg.axis() .scale(yScale) .orient("left") .ticks(10, "%"); }

Code: axes drawingfunction drawAxis(){ svg.select('.x-axis-group') .append("g") .attr("class", "x axis") .attr("transform", "translate(0," + chartHeight + ")") .call(xAxis);

svg.select(".y-axis-group") .append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Frequency"); }

Test: bars drawingit('should render a bar for each data entry', function(){ var numBars = dataset.length;

expect(containerFixture.selectAll('.bar').size()).toEqual(numBars);});

Code: bars drawingfunction drawBars(){ // Setup the enter, exit and update of the actual bars in the chart. // Select the bars, and bind the data to the .bar elements. var bars = svg.select('.chart-group').selectAll(".bar") .data(data);

// If there aren't any bars create them bars.enter().append('rect') .attr("class", "bar") .attr("x", function(d) { return xScale(d.letter); }) .attr("width", xScale.rangeBand()) .attr("y", function(d) { return yScale(d.frequency); }) .attr("height", function(d) { return chartHeight - yScale(d.frequency); });}

Reference: , Thinking with joins General Update Pattern

Test: margin accessorit('should provide margin getter and setter', function(){ var defaultMargin = barChart.margin(), testMargin = {top: 4, right: 4, bottom: 4, left: 4}, newMargin;

barChart.margin(testMargin); newMargin = barChart.margin();

expect(defaultMargin).not.toBe(testMargin); expect(newMargin).toBe(testMargin); });

Code: margin accessorexports.margin = function(_x) { if (!arguments.length) return margin; margin = _x; return this; };

Looks the same, but is not

Final code: standard wayvar margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1);

var y = d3.scale.linear() .range([height, 0]);

var xAxis = d3.svg.axis() .scale(x) .orient("bottom");

var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(10, "%");

Final code: TDD wayreturn function module(){ var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960, height = 500, chartWidth, chartHeight, xScale, yScale, xAxis, yAxis, data, svg;

function exports(_selection){ _selection.each(function(_data){ chartWidth = width - margin.left - margin.right; chartHeight = height - margin.top - margin.bottom; data = _data;

buildScales(); buildAxis(); buildSVG(this); drawBars();

TDD way - benefitsStress free refactorsGoal orientedDeeper understandingImproved communicationQuality, production ready output

HOW TO GET STARTED

Some ideasTest something that is in productionTDD the last chart you builtPick a block, refactor itTDD your next chart

REPOSITORYWALKTHROUGH

https://github.com/Golodhros/d3-meetup

What happened with my contracting gig?

I used the Reusable Chart API

Adding multiple dimensions?

I had tests!

Toogle dimensions, adding more y-axis?

ConclusionsExamples are great for exploration and prototyping,bad for production codeThere is a better way of building D3 ChartsReusable Chart API + TDD bring it to a Pro levelYou can build your own library and feel proud!

Thanks for listening!Twitter: Check out Slides: Code:

@golodhrosmy Blog

http://golodhros.github.io/https://github.com/Golodhros/d3-meetup

Live CodingRefactoring accessorsAdd EventsStart building a new chart

Learning resourcesD3.js Resources to Level UpDashing D3 Newsletter

Example searchSearch by chart type -> Search by D3 component ->

Christophe Viau's GalleryBlock Explorer

Books

top related