Better d3 charts with tdd
-
Upload
marcos-iglesias-valle -
Category
Engineering
-
view
98 -
download
1
Transcript of Better d3 charts with tdd
Better D3 Charts with TDD
Slides: Code:
http://golodhros.github.io/https://github.com/Golodhros/d3-meetup
Marcos Iglesias
El Bierzo
OnlinePersonal BlogEventbrite Engineering Blog@golodhros
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
Bubble charts
Choropleth
Map projections
Algorithm visualization
Artistic visualizations
Interactive data explorations
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
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