D3: The Crash Course
Chad Stolper
chadstolper@gatech.edu
Chad Stolper CSE 6242 Guest Lecture 1
aka: D3: The Early Sticking Points aka: D3: Only the Beginning
D3: The Crash Course aka: D3: The Early Sticking Points aka: D3: Only - - PowerPoint PPT Presentation
D3: The Crash Course aka: D3: The Early Sticking Points aka: D3: Only the Beginning Chad Stolper chadstolper@gatech.edu Chad Stolper CSE 6242 Guest Lecture 1 http://bl.ocks.org/mbostock/1256572 Chad Stolper CSE 6242 Guest Lecture 6
D3: The Crash Course
Chad Stolper
chadstolper@gatech.edu
Chad Stolper CSE 6242 Guest Lecture 1
aka: D3: The Early Sticking Points aka: D3: Only the Beginning
http://bl.ocks.org/mbostock/1256572
Chad Stolper CSE 6242 Guest Lecture 6
http://www.bloomberg.com/graphics/2015-auto-sales/
Chad Stolper CSE 6242 Guest Lecture 7
BUT FIRST....
Chad Stolper CSE 6242 Guest Lecture 8
Chrome Inspector and Console
inspector to open the console as well
− (2nd from the left)
Chad Stolper CSE 6242 Guest Lecture 12
Starting a Local Webserver
Necessary for Chrome, not for Safari or Firefox
− python -m SimpleHTTPServer 8000
− python –m http.server 8000
Chad Stolper CSE 6242 Guest Lecture 13
If you’re new to Javascript…
https://www.destroyallsoftware.com/talks/wat (starting 1:25)
Chad Stolper CSE 6242 Guest Lecture 15
Prepare for a lot of hair pulling!!! I’m serious.
Javascript 101
using var
− x = 300 (global) − var x = 300 (local)
same syntax as python’s lists [ ] and dicts { }
Chad Stolper CSE 6242 Guest Lecture 16
Javascript 102: Functional Programming
− Functions are themselves objects − Functions can be stored as variables − Functions can be passed as parameters
Chad Stolper CSE 6242 Guest Lecture 17
Some people say javascript is a “multi-paradigm” programming language. http://stackoverflow.com/questions/3962604/is-javascript-a-functional- programming-language
What does that mean?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map Chad Stolper CSE 6242 Guest Lecture 19
Passing Math.sqrt (a function) as a parameter
Array.map( )
− d: a data point
Chad Stolper CSE 6242 Guest Lecture 20
Passing Math.sqrt (a function) as a parameter
MDN – the “best” Javascript reference
US/docs/Web/JavaScript/Reference
Chad Stolper CSE 6242 Guest Lecture 22
Method Chaining
method returns the object that it was called on
group .attr(“x”,5); .attr(“y”,5); //returns group is the same as group.attr(“x”,5) //returns group group.attr(“y”,5) //returns group
Chad Stolper CSE 6242 Guest Lecture 23
SVG BASICS
SVG = Scalable Vector Graphics
Chad Stolper CSE 6242 Guest Lecture 24 https://en.wikipedia.org/wiki/Scalable_Vector_Graphics
x y
(0,0)
Chad Stolper CSE 6242 Guest Lecture 27
x y
(0,0)
http://smg.photobucket.com/user/Pavan2099/ media/RvB/Descart-weeping.png.htmlChad Stolper CSE 6242 Guest Lecture 28
SVG Basics
SVG -> XML Vector Graphics (Scalable Vector Graphics)
Chad Stolper CSE 6242 Guest Lecture 29
SVG Basics
− Tags with Attributes
− <circle r=5 fill=“green”></circle>
− http://www.w3.org/TR/SVG/
Chad Stolper CSE 6242 Guest Lecture 30
SVG Basics
Chad Stolper CSE 6242 Guest Lecture 31
SVG Basics
<path>
Chad Stolper CSE 6242 Guest Lecture 32
<svg> element
− width − height
− d3.select(“#vis”).append(“svg:svg”)
<body> <div id=“vis”> </div> </body>
Chad Stolper CSE 6242 Guest Lecture 33
<svg> element
− width − height
− d3.select(“#vis”).append(“svg:svg”)
<body> <div id=“vis”> <svg></svg> </div> </body>
Chad Stolper CSE 6242 Guest Lecture 34
<circle> element
− cx (relative to the LEFT of the container) − cy (relative to the TOP of the container) − r (radius)
− fill (color) − stroke (the color of the stroke) − stroke-width (the width of the stroke)
− .append(“svg:circle”)
Chad Stolper CSE 6242 Guest Lecture 35
<rect> element
− x (relative to the LEFT of the container) − y (relative to the TOP of the container) − width (cannot be negative) − height (cannot be negative)
− fill (color) − stroke (the color of the stroke) − stroke-width (the width of the stroke)
− .append(“svg:rect”)
Chad Stolper CSE 6242 Guest Lecture 36
x y width height (0,0)
Chad Stolper CSE 6242 Guest Lecture 37
x y width height (0,0)
Chad Stolper CSE 6242 Guest Lecture 38
Rather than positioning each element, what if we want to position (or style) a group of elements?
Chad Stolper CSE 6242 Guest Lecture 39
<g> element
− transform − (fill,stroke,etc.)
− var group = vis.append(“svg:g”)
− group.append(“svg:circle”) − group.append(“svg:rect”) − group.append(“svg:text”)
Chad Stolper CSE 6242 Guest Lecture 40
CSS Selectors Reference
− AND
– circle.canary à <circle class=“canary”>
− OR
– circle,.canary à <circle> <rect class=“canary”>
Chad Stolper CSE 6242 Guest Lecture 41
AND NOW D3…
Chad Stolper CSE 6242 Guest Lecture 42
Mike Bostock and Jeff Heer @ Stanford 2009- Protovis
Chad Stolper CSE 6242 Guest Lecture 43
Mike Bostock and Jeff Heer @ Stanford 2009- Protovis
Chad Stolper CSE 6242 Guest Lecture 44
Mike Bostock and Jeff Heer @ Stanford 2009- Protovis 2011- D3.js
Chad Stolper CSE 6242 Guest Lecture 45
Mike Bostock and Jeff Heer @ Stanford 2009- Protovis 2011- D3.js
Chad Stolper CSE 6242 Guest Lecture 46
Mike Bostock and Jeff Heer @ Stanford 2009- Protovis 2011- D3.js
New York Times
Chad Stolper CSE 6242 Guest Lecture 47
D3
Chad Stolper CSE 6242 Guest Lecture 48
D3.js in a Nutshell
D3 is a really powerful for-loop with a ton of useful helper functions
Chad Stolper CSE 6242 Guest Lecture 49
D3
Declarative, domain-specific specification language for manipulating the DOM
Define a template for each type of element D3 draws one element for each data point
Chad Stolper CSE 6242 Guest Lecture 50
Importing D3
<html > <head> <script src='lib/d3.js’ charset=‘utf-8’></script> <script src='js/project.js'></script> </head> <body> <div id=“vis”></div> </body> </html>
Chad Stolper CSE 6242 Guest Lecture 51
Importing D3
<html > <head> <script src='lib/d3.js’ charset=‘utf-8’></script> <script src='js/project.js'></script> </head> <body> <div id=“vis”></div> </body> </html>
Chad Stolper CSE 6242 Guest Lecture 52
Importing D3
<html > <head> <script src='lib/d3.js’ charset=‘utf-8’></script> <script src='js/project.js'></script> </head> <body> <div id=“vis”></div> </body> </html>
Chad Stolper CSE 6242 Guest Lecture 53
Importing D3
<html > <head> <script src='lib/d3.js’ charset=‘utf-8’></script> <script src='js/project.js'></script> </head> <body> <div id=“vis”></div> </body> </html>
Chad Stolper CSE 6242 Guest Lecture 54
Importing D3
<html > <head> <script src='lib/d3.js’ charset=‘utf-8’></script> <script src='js/project.js'></script> </head> <body> <div id=“vis”></div> </body> </html>
Chad Stolper CSE 6242 Guest Lecture 55
Assigning the Canvas to a Variable var vis = d3.select(“#vis”) .append(“svg:svg”) <body> <div id=“vis”><svg></svg></div> </body>
Chad Stolper CSE 6242 Guest Lecture 56
Loading Data
− “data/datafile.csv”
Chad Stolper CSE 6242 Guest Lecture 57
rawdata from a CSV file
name school age Adam GT 18 Barbara Emory 22 Calvin GSU 30
[ { ‘name’: ‘Adam’, ‘school’: ‘GT’, ‘age’: ‘18’ }, { ‘name’: ‘Barbara’, ‘school’: ‘Emory’, ‘age’: ’22’ }, { ‘name’: ‘Calvin’, ‘school’: ‘GSU’, ‘age’: ‘30’ } ]
Chad Stolper CSE 6242 Guest Lecture 58
Problem
for(var d: data){
d = data[d] d.age = +d.age
}
[ { ‘name’: ‘Adam’, ‘school’: ‘GT’, ‘age’: ‘18’ }, { ‘name’: ‘Barbara’, ‘school’: ‘Emory’, ‘age’: ’22’ }, { ‘name’: ‘Calvin’, ‘school’: ‘GSU’, ‘age’: ‘30’ } ]
Chad Stolper CSE 6242 Guest Lecture 59
Problem
for(var d: data){
d = data[d] d.age = +d.age
}
[ { ‘name’: ‘Adam’, ‘school’: ‘GT’, ‘age’: ‘18’ }, { ‘name’: ‘Barbara’, ‘school’: ‘Emory’, ‘age’: ’22’ }, { ‘name’: ‘Calvin’, ‘school’: ‘GSU’, ‘age’: ‘30’ } ]
Chad Stolper CSE 6242 Guest Lecture 60 http://stackoverflow.com/questions/24473733/importing-a-csv-into-d3-cant-convert-strings-to-numbers
rawdata from a CSV file
name school age Adam GT 18 Barbara Emory 22 Calvin GSU 30
[ { ‘name’: ‘Adam’, ‘school’: ‘GT’, ‘age’: 18 }, { ‘name’: ‘Barbara’, ‘school’: ‘Emory’, ‘age’: 22 }, { ‘name’: ‘Calvin’, ‘school’: ‘GSU’, ‘age’: 30 } ]
Chad Stolper CSE 6242 Guest Lecture 61
rawdata from a CSV file
name school age Adam GT 18 Barbara Emory 22 Calvin GSU 30
[ { ‘name’: ‘Adam’, ‘school’: ‘GT’, ‘age’: 18 }, { ‘name’: ‘Barbara’, ‘school’: ‘Emory’, ‘age’: 22 }, { ‘name’: ‘Calvin’, ‘school’: ‘GSU’, ‘age’: 30 } ]
Ok, so let’s map this data to visual elements!
Chad Stolper CSE 6242 Guest Lecture 62
D3
Declarative, domain-specific specification language for manipulating the DOM
Define a template for each type of element D3 draws one element for each data point
Chad Stolper CSE 6242 Guest Lecture 63
D3
Declarative, domain-specific specification language for manipulating the DOM
Define a template for each type of element D3 draws one element for each data point
Chad Stolper CSE 6242 Guest Lecture 64
D3
Declarative, domain-specific specification language for manipulating the DOM
Define a template for each type of element D3 draws one element for each data point
Chad Stolper CSE 6242 Guest Lecture 65
Enter-Update-Exit
remember this...
Chad Stolper CSE 6242 Guest Lecture 66
Enter-Update-Exit
remember this...
Chad Stolper CSE 6242 Guest Lecture 67
Enter-Update-Exit
− Select a “group” of “elements” (e.g., circles) − Assign data to the group − Enter: Create new elements for data points that don’t have them yet and set constant or initial attribute values − Update: Set the attributes of all the elements based on the data − Exit: Remove elements that don’t have data anymore
Chad Stolper CSE 6242 Guest Lecture 68
Can be hard to grok: You can select groups of elements that DON’T EXIST YET
http://bost.ocks.org/mike/join/
Chad Stolper CSE 6242 Guest Lecture 69
.enter( ) and .exit( )
− New data points
− Old elements
has been called
New Data Old Elements Enter Exit Update
Chad Stolper CSE 6242 Guest Lecture 70
.enter( ) and .exit( )
− New data points
− Old elements
has been called
New Data Old Elements Enter Exit Update
Chad Stolper CSE 6242 Guest Lecture 71
.enter( ) and .exit( )
− Enter: [1,2,3,4] − Update: [1,2,3,4] − Exit: [ ]
− Enter: [5,6] − Update: [1,2,3,4,5,6] − Exit: [ ]
− Enter: [ ] − Update: ??? − Exit: [4,5,6] New Data Old Elements Enter Exit Update
Chad Stolper CSE 6242 Guest Lecture 72
.enter( ) and .exit( )
− Enter: [1,2,3,4] − Update: [1,2,3,4] − Exit: [ ]
− Enter: [5,6] − Update: [1,2,3,4,5,6] − Exit: [ ]
− Enter: [ ] − Update: [1,2,3,4,5,6] − Exit: [4,5,6] New Data Old Elements Enter Exit Update
Chad Stolper CSE 6242 Guest Lecture 73
Data Key Functions
index of the point is the key
set a key functions
− .data(rawdata, function(d,i){ return d.id; }) − .data(rawdata, function(d,i){ return d.name; })
Chad Stolper CSE 6242 Guest Lecture 75
E-U-E Pattern Template
var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .style( ) group //UPDATE! .attr( ) .style( ) group.exit( ).remove( ) //EXIT!
Chad Stolper CSE 6242 Guest Lecture 76
Chad Stolper CSE 6242 Guest Lecture 77
E-U-E Pattern Template
var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .style( ) group //UPDATE! .attr( ) .style( ) group.exit( ).remove( ) //EXIT!
Many online examples
Chad Stolper CSE 6242 Guest Lecture 78
E-U-E Pattern Template
var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .style( ) group //UPDATE! .attr( ) .style( ) group.exit( ).remove( ) //EXIT!
Many online examples drop the variable name before .enter()
Chad Stolper CSE 6242 Guest Lecture 79
E-U-E Pattern Template
var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .style( ) group //UPDATE! .attr( ) .style( ) group.exit( ).remove( ) //EXIT!
Many online examples drop the variable name before .enter() I highly recommend you don’t!
Chad Stolper CSE 6242 Guest Lecture 80
.attr( )
and fill
− group.attr(“x”, 5) − <rect x=“5”></rect>
Chad Stolper CSE 6242 Guest Lecture 81
.attr( ) and Functional Programming Input
[ {size: 10}, {size: 8}, {size: 12.2} ]
We want 3 rectangles:
<rect height=“10” x=“5”></rect> <rect height=“8” x=“10”></rect> <rect height=“12.2” x=“15”></rect>
.attr(“height”, function(d,i){ return d.size })
d: the data point
.attr(“x”, function(d,i){ return (i+1)*5; })
i: the index of the data point
Chad Stolper CSE 6242 Guest Lecture 82
<text> elements
Chad Stolper CSE 6242 Guest Lecture 83
<text> elements
the lousy job the W3C did with the <text> definition.
memorize these things or keep referring back to http://www.w3c.org/TR/SVG/text.html (first Google hit for “svg text”) like I do.
Chad Stolper CSE 6242 Guest Lecture 84
<text> elements
− .text(“Your Text Goes Here”) − <tag>Your Text Goes Here</tag>
− x − y
− text-anchor: start, middle, end − dominant-baseline: [nothing], hanging, middle
Chad Stolper CSE 6242 Guest Lecture 85
text-anchor style
start end middle
Where is (0,0)?
Chad Stolper CSE 6242 Guest Lecture 86
dominant-baseline style
This is my line of text.
hanging default middle
Where is (0,0)?
Chad Stolper CSE 6242 Guest Lecture 87
<text> example
Chad Stolper CSE 6242 Guest Lecture 88
http://tutorials.jenkov.com/svg/text-element.html
The .style() Function
Like attr, but for the style attribute
.style(“prop1”,“val1”) .style(“prop2”,“val2”) .style(“prop3”, function(d,i){ }) <ele style=“prop1: val1; prop2: val2;”>
Chad Stolper CSE 6242 Guest Lecture 89
<text> example
group.append(“svg:text”) .text(function(d){return d.name}) .attr(“x”, function(d,i){return i*5}) .attr(“y”, function(d,i){return height;}) .style(“dominant-baseline”,“hanging”) .style(“text-anchor”, “middle”)
Chad Stolper CSE 6242 Guest Lecture 90
Need to remember what to use .style and when to use .attr
What if you have two different types of circles?
Chad Stolper CSE 6242 Guest Lecture 91
Classing
− Any number of classes per element − Select using “.classname”
red = vis.selectAll(“circle.redcircle”) .data(reddata, function(d){return d.id;}) red.enter( ).append(“svg:circle”) .classed(“redcircle”,“true”) red.attr(“fill”,“red”) blue = vis.selectAll(“circle.bluecircle”) .data(bluedata, function(d){return d.id;}) blue.enter( ).append(“svg:circle”) .classed(“bluecircle”, “true”) vis.selectAll(“.bluecircle”).attr(“fill”,“blue”)
Chad Stolper CSE 6242 Guest Lecture 92
(e.g., sizing a circle based on data value)
Chad Stolper CSE 6242 Guest Lecture 93
blow up really quickly…
Chad Stolper CSE 6242 Guest Lecture 94
Scales
− Linear Scales − Ordinal Scales
Chad Stolper CSE 6242 Guest Lecture 95
Linear Scales
var xscale = d3.scale.linear( ) .domain( [min, max] ) .range( [minOut, maxOut] ) group.attr(“x”, function(d,i){ return xscale(d.size); })
Chad Stolper CSE 6242 Guest Lecture 96
Domain & Range
Chad Stolper CSE 6242 Guest Lecture 97
http://image.slidesharecdn.com/d3-140708145630-phpapp02/95/d3-17-638.jpg?cb=1404831405
Min and Max
But how do you figure out the min and max for the domain?
Chad Stolper CSE 6242 Guest Lecture 98
D3
A really powerful for-loop with a ton of useful helper functions
Chad Stolper CSE 6242 Guest Lecture 99
D3
A really powerful for-loop with a ton of useful helper functions
Chad Stolper CSE 6242 Guest Lecture 100
Min and Max
Chad Stolper CSE 6242 Guest Lecture 101
Min and Max
− .map( function(d){ } ), which returns an [ ]
Chad Stolper CSE 6242 Guest Lecture 102
d3.max( data.map( function(d){ return d.age; }) ) // returns the maximum age
Chad Stolper CSE 6242 Guest Lecture 103
var max = d3.max( data.map( function(d){ return d.age; }) ) // returns the maximum age var yscale = d3.scale.linear( ) .domain( [0, max] ) .range( [0, 100] )
Chad Stolper CSE 6242 Guest Lecture 104
Ordinal Scales
− (And they’re easy!)
− category20( ) − category20b( ) − category20c( ) − (and even a few more)
Chad Stolper CSE 6242 Guest Lecture 106
Ordinal Categorical Scales
− (And they’re easy!)
− category20( ) − category20b( ) − category20c( ) − (and even a few more)
Chad Stolper CSE 6242 Guest Lecture 107
Think carefully before using a rainbow palette for ordinal data!
http://www.mathworks.com/tagteam/81137_92 238v00_RainbowColorMap_57312.pdf
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Chad Stolper CSE 6242 Guest Lecture 108
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Bird Blue
Chad Stolper CSE 6242 Guest Lecture 109
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Bird Blue
Chad Stolper CSE 6242 Guest Lecture 110
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Bird Blue Rodent Orange
Chad Stolper CSE 6242 Guest Lecture 111
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Bird Blue Rodent Orange
Chad Stolper CSE 6242 Guest Lecture 112
Ordinal Categorical Scales
]
return colorscale(d.type) } − <rect fill=“blue”></rect> − <rect fill=“orange”></rect> − <rect fill=“blue”></rect>
Bird Blue Rodent Orange
Chad Stolper CSE 6242 Guest Lecture 113
D3 also has visual helper-functions
Chad Stolper CSE 6242 Guest Lecture 114
Axes
yaxisglyph = vis.append(“g”) yaxis = d3.svg.axis( ) .scale( yscale ) //must be a numerical scale .orient( ‘left’ ) //or ‘right’,‘top’, or ‘bottom’ .ticks( 6 ) //number of ticks, default is 10 yaxisglyph.call(yaxis)
Chad Stolper CSE 6242 Guest Lecture 115
D3 even has some entire techniques built in (e.g., treemap)… http://bl.ocks.org/mbostock/4063582
Chad Stolper CSE 6242 Guest Lecture 116
What if the data is changing?
Chad Stolper CSE 6242 Guest Lecture 117
E-U-E Pattern Template
var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .attr( ) group //UPDATE! .attr( ) .attr( ) group.exit( ).remove( ) //EXIT!
Chad Stolper CSE 6242 Guest Lecture 118
E-U-E Pattern Template
function redraw(rawdata){ var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .attr( ) group //UPDATE! .attr( ) .attr( ) group.exit( ).remove( ) //EXIT! }
Chad Stolper CSE 6242 Guest Lecture 119
E-U-E Pattern Template
function redraw(rawdata){ var group = vis.selectAll(“rect”) .data(rawdata) //rawdata must be an array! group.enter( ).append(“svg:rect”) //ENTER! .attr( ) .attr( ) group.transition( ) //UPDATE! .attr( ) .attr( ) group.exit( ).remove( ) //EXIT! }
Chad Stolper CSE 6242 Guest Lecture 120
Transitions
Chad Stolper CSE 6242 Guest Lecture 121
Transitions
rect.attr(“height”, 0) rect.transition( ) .delay( 500 ) //can be a function of data .duration(200) //can be a function of data .attr(“height”, 5) //can be a function of data .style(“fill”,”green”) //can be a function of data
Chad Stolper CSE 6242 Guest Lecture 122
So transitions allow a vis to be dynamic… But they’re not really interactive…
Chad Stolper CSE 6242 Guest Lecture 123
Interaction
The on( ) Method
Chad Stolper CSE 6242 Guest Lecture 124
.on( )
rect.on (“click”, function(d){
d.color = “blue”; redraw( rawdata )
}) HTML Events
− click − mouseover − mouseenter − mouseout − etc.
Chad Stolper CSE 6242 Guest Lecture 125
.on( )
rect.on (“click”, function(d){
d.color = “blue”; redraw( rawdata )
}) HTML Events
− click − mouseover − mouseenter − mouseout − etc. d is the data point backing the element clicked on
Chad Stolper CSE 6242 Guest Lecture 126
Where to get learn more…
− Tons of examples and basics.
Reference
− Official D3 documentation. Extremely well done.
− List of seemingly ALL the tutorials online
− (my personal favorite)
Chad Stolper CSE 6242 Guest Lecture 127
When You’re Bored…
http://www.koalastothemax.com/
Chad Stolper CSE 6242 Guest Lecture 128
Thanks! chadstolper@gatech.edu
Chad Stolper CSE 6242 Guest Lecture 129
Good Luck! chadstolper@gatech.edu
Chad Stolper CSE 6242 Guest Lecture 130
Questions? chadstolper@gatech.edu
Chad Stolper CSE 6242 Guest Lecture 131