-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathd3_work.js
295 lines (251 loc) · 11.8 KB
/
d3_work.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// All your javascript code will go here
let store = {}
// function loadData() {
// let promise = d3.csv('routes.csv') //TODO 1: Add the code to load the CSV file named "routes.csv" | 1 Line
// return promise.then(routes => {
// store.routes = routes //TODO 2: Save the routes into our store variable;
// return store;
// })
// }
function loadData() {
return Promise.all([
d3.csv("routes.csv"),
d3.json("countries.geo.json"),
]).then(datasets => {
store.routes = datasets[0];
store.geoJSON = datasets[1]
return store;
})
}
function groupByAirline(data) {
//Iterate over each route, producing a dictionary where the keys is are the ailines ids and the values are the information of the airline.
let result = data.reduce((result, d) => {
let currentData = result[d.AirlineID] || {
"AirlineID": d.AirlineID,
"AirlineName": d.AirlineName,
"Count": 0
}
currentData.Count += 1 //TODO: Increment the count (number of routes) of ariline.
result[d.AirlineID] = currentData //TODO: Save the updated information in the dictionary using the airline id as key.
return result;
}, {})
//We use this to convert the dictionary produced by the code above, into a list, that will make it easier to create the visualization.
result = Object.keys(result).map(key => result[key])
// result = //TODO: Sort the data in descending order of count.
result.sort((a, b) => b.Count - a.Count)
return result
}
function getAirlinesChartConfig() {
let width = 350;
let height = 400;
let margin = {
top: 10,
bottom: 50,
left: 130,
right: 10
}
//The body is the area that will be occupied by the bars.
let bodyHeight = height - margin.top - margin.bottom
let bodyWidth = width - margin.left - margin.right//TODO: Compute the width of the body by subtracting the left and right margins from the width.
//The container is the SVG where we will draw the chart. In our HTML is the svg tag with the id AirlinesChart
let container = d3.select('#AirlinesChart') //TODO: use d3.select to select the element with id AirlinesChart
container
.attr("width", width)
//TODO: Set the height of the container
.attr('height', height)
return { width, height, margin, bodyHeight, bodyWidth, container }
}
function getAirlinesChartScales(airlines, config) {
let { bodyWidth, bodyHeight } = config;
let maximumCount = d3.max(airlines, (a) => a.Count) //TODO: Use d3.max to get the highest Count value we have on the airlines list.
let xScale = d3.scaleLinear()
//TODO: Set the range to go from 0 to the width of the body
.range([0, bodyWidth])
//TODO: Set the domain to go from 0 to the maximun value fount for the field 'Count'
.domain([0, maximumCount])
let yScale = d3.scaleBand()
.range([0, bodyHeight])
.domain(airlines.map(a => a.AirlineName)) //The domain is the list of ailines names
.padding(0.2)
return { xScale, yScale }
}
function drawBarsAirlinesChart(airlines, scales, config) {
let {margin, container} = config; // this is equivalent to 'let margin = config.margin; let container = config.container'
let {xScale, yScale} = scales
let body = container.append("g")
.style("transform",
`translate(${margin.left}px,${margin.top}px)`
)
let bars = body.selectAll(".bar")
//TODO: Use the .data method to bind the airlines to the bars (elements with class bar)
.data(airlines)
//Adding a rect tag for each airline
bars.enter().append("rect")
.attr("height", yScale.bandwidth())
.attr("y", (d) => yScale(d.AirlineName))
//TODO: set the width of the bar to be proportional to the airline count using the xScale
.attr("width", (d) => xScale(d.Count))
.attr("fill", "#2a5599")
//... Here is our original code
.on("mouseenter", function(d) { // <- this is the new code
//TODO: call the drawRoutes function passing the AirlineID id 'd'
drawRoutes(d.AirlineID)
//TODO: change the fill color of the bar to "#992a5b" as a way to highlight the bar. Hint: use d3.select(this)
d3.select(this)
.attr('fill', "#992a5b")
})
//TODO: Add another listener, this time for mouseleave
.on("mouseout", function(d) {
//TODO: In this listener, call drawRoutes(null), this will cause the function to remove all lines in the chart since there is no airline withe AirlineID == null.
drawRoutes(null)
//TODO: change the fill color of the bar back to "#2a5599"
d3.select(this)
.attr('fill', "#2a5599")
})
}
function drawAirlinesChart(airlines) {
let config = getAirlinesChartConfig();
let scales = getAirlinesChartScales(airlines, config);
drawBarsAirlinesChart(airlines, scales, config)
drawAxesAirlinesChart(airlines, scales, config)
}
function drawAxesAirlinesChart(airlines, scales, config){
let {xScale, yScale} = scales
let {container, margin, height} = config;
let axisX = d3.axisBottom(xScale)
.ticks(5)
container.append("g")
.style("transform",
`translate(${margin.left}px,${height - margin.bottom}px)`
)
.call(axisX)
let axisY = d3.axisLeft(yScale) //TODO: Create an axis on the left for the Y scale
//TODO: Append a g tag to the container, translate it based on the margins and call the axisY axis to draw the left axis.
container.append("g")
.style("transform",
`translate(${margin.left}px,${margin.top}px)`
)
.call(axisY)
}
function getMapConfig(){
let width = 600;
let height = 400;
let container = d3.select('#map') //TODO: select the svg with id Map
//TODO: set the width and height of the conatiner to be equal the width and height variables.
.attr('width', width)
.attr('height', height)
return {width, height, container}
}
function getMapProjection(config) {
let {width, height} = config;
let projection = d3.geoMercator() //TODO: Create a projection of type Mercator.
projection.scale(97)
.translate([width / 2, height / 2 + 20])
store.mapProjection = projection;
return projection;
}
function drawBaseMap(container, countries, projection){
let path = d3.geoPath() //TODO: create a geoPath generator and set its projection to be the projection passed as parameter.
.projection(projection)
container.selectAll("path").data(countries)
.enter().append("path")
.attr("d", d => path(d)) //TODO: use the path generator to draw each country
.attr("stroke", "#ccc")
.attr("fill", "#eee")
}
function drawMap(geoJeon) {
let config = getMapConfig();
let projection = getMapProjection(config)
drawBaseMap(config.container, geoJeon.features, projection)
}
function groupByAirport(data) {
//We use reduce to transform a list into a object where each key points to an aiport. This way makes it easy to check if is the first time we are seeing the airport.
let result = data.reduce((result, d) => {
//The || sign in the line below means that in case the first option is anything that Javascript consider false (this insclude undefined, null and 0), the second option will be used. Here if result[d.DestAirportID] is false, it means that this is the first time we are seeing the airport, so we will create a new one (second part after ||)
let currentDest = result[d.DestAirportID] || {
"AirportID": d.DestAirportID,
"Airport": d.DestAirport,
"Latitude": +d.DestLatitude,
"Longitude": +d.DestLongitude,
"City": d.DestCity,
"Country": d.DestCountry,
"Count": 0
}
currentDest.Count += 1
result[d.DestAirportID] = currentDest
//After doing for the destination airport, we also update the airport the airplane is departing from.
let currentSource = result[d.SourceAirportID] || {
"AirportID": d.SourceAirportID,
"Airport": d.SourceAirport,
"Latitude": +d.SourceLatitude,
"Longitude": +d.SourceLongitude,
"City": d.SourceCity,
"Country": d.SourceCountry,
"Count": 0
}
currentSource.Count += 1
result[d.SourceAirportID] = currentSource
return result
}, {})
//We map the keys to the actual ariorts, this is an way to transform the object we got in the previous step into a list.
result = Object.keys(result).map(key => result[key])
return result
}
function drawAirports(airports) {
let config = getMapConfig(); //get the config
let projection = getMapProjection(config) //get the projection
let container = config.container; //get the container
let circles = container.selectAll("circle");
//TODO: bind the airports to the circles using the .data method.
//TODO: for each new airport (hint: .enter)
// - Set the radius to 1
// - set the x and y position of the circle using the projection to convert longitude and latitude to x and y porision.
// - Set the fill color of the circle to "#2a5599"
circles.data(airports)
.enter()
.append('circle')
.attr('r', 1)
.attr('cx', d => projection([d.Longitude, d.Latitude])[0])
.attr('cy', d => projection([d.Longitude, d.Latitude])[1])
.attr('fill', '#2a5599')
}
function drawRoutes(airlineID) {
let routes = store.routes //TODO: get the routes from store
let projection = store.mapProjection //TODO: get the projection from the store
let container = d3.select('#Map') //TODO: select the svg with id "Map" (our map container)
let selectedRoutes = routes.filter((d) => d.AirlineID === airlineID) //TODO: filter the routes to keep only the routes which AirlineID is equal to the parameter airlineID received by the function
// console.log(selectedRoutes)
let bindedData = container.selectAll("line")
.data(selectedRoutes, d => d.ID) //This second parameter tells D3 what to use to identify the routes, this hepls D3 to correctly find which routes have been added or removed.
//TODO: Use the .enter selector to append a line for each new route.
bindedData
.enter()
.append('line')
//TODO: for each line set the start of the line (x1 and y1) to be the position of the source airport (SourceLongitude and SourceLatitude) Hint: you can use projection to convert longitude and latitude to x and y.
.attr('x1', d => projection([d.SourceLongitude, d.SourceLatitude])[0])
.attr('y1', d => projection([d.SourceLongitude, d.SourceLatitude])[1])
//TODO: for each line set the end of the line (x2 and y2) to be the position of the source airport (DestLongitude and DestLongitude)
.attr('x2', d => projection([d.DestLongitude, d.DestLatitude])[0])
.attr('y2', d => projection([d.DestLongitude, d.DestLatitude])[1])
//TODO: set the color of the stroke of the line to "#992a2a"
.attr('stroke', "#992a2a")
//TODO: set the opacity to 0.1
.attr('opacity', 0.1)
//TODO: use exit function over bindedData to remove any routes that does not satisfy the filter.
bindedData
.exit()
.remove()
}
function showData() {
//Get the routes from our store variable
let routes = store.routes
// Compute the number of routes per airline.
let airlines = groupByAirline(store.routes);
// Draw airlines barchart
drawAirlinesChart(airlines)
drawMap(store.geoJSON) //Using the data saved on loadData
let airports = groupByAirport(store.routes);
drawAirports(airports)
console.log(store)
}
loadData().then(showData);