Commit ad528ad0f9c94904a9b7e205bb28cab57c2d011e

Authored by Antoine Goutenoir
1 parent e4138b41
Exists in master

fix: use DOMContentLoaded

Showing 1 changed file with 258 additions and 264 deletions   Show diff stats
flaskr/static/js/plots/emissions-per-distance.js
1 1 // export draw_emissions_per_distance
2 2  
3 3 function draw_emissions_per_distance(divId, csvUrl) {
4   - (() => {
5   - // let divId = "emissions_per_distance_histogram";
  4 + // let divId = "emissions_per_distance_histogram";
6 5  
7   - // set the dimensions and margins of the graph
8   - let margin = {top: 30, right: 62, bottom: 62, left: 72},
9   - width = 960 - margin.left - margin.right,
10   - height = 540 - margin.top - margin.bottom;
  6 + // set the dimensions and margins of the graph
  7 + let margin = {top: 30, right: 62, bottom: 62, left: 72},
  8 + width = 960 - margin.left - margin.right,
  9 + height = 540 - margin.top - margin.bottom;
11 10  
12   - function getTicks(maxValue, interval, startValue = 0) {
13   - let range = [];
14   - for (let i = startValue; i <= maxValue; i += interval) {
15   - range.push(i);
16   - }
17   - return range;
  11 + function getTicks(maxValue, interval, startValue = 0) {
  12 + let range = [];
  13 + for (let i = startValue; i <= maxValue; i += interval) {
  14 + range.push(i);
18 15 }
  16 + return range;
  17 + }
19 18  
20   - function getBottomTicks(maxDistance) {
21   - let range = getTicks(maxDistance, 2500, 2500);
22   - range.push(500);
23   - return range;
24   - }
  19 + function getBottomTicks(maxDistance) {
  20 + let range = getTicks(maxDistance, 2500, 2500);
  21 + range.push(500);
  22 + return range;
  23 + }
25 24  
26   - function getLeftTicks(maxemissions) {
27   - return getTicks(maxemissions, Math.floor((maxemissions / 8) / 1000) * 1000);
28   - }
  25 + function getLeftTicks(maxemissions) {
  26 + return getTicks(maxemissions, Math.floor((maxemissions / 8) / 1000) * 1000);
  27 + }
29 28  
30   - function getRightTicks(maxemissionsPercent) {
31   - return getTicks(maxemissionsPercent, 2);
  29 + function getRightTicks(maxemissionsPercent) {
  30 + return getTicks(maxemissionsPercent, 2);
  31 + }
  32 +
  33 + function getAttendeeOnRight(sliceId, attendeeNumberPerGroup) {
  34 + let attendeeSum = 0;
  35 + let sliceInt = Math.floor(sliceId);
  36 + let sliceFloat = sliceId - sliceInt;
  37 + for (let i = sliceInt; i < attendeeNumberPerGroup.length; i++) {
  38 + attendeeSum += attendeeNumberPerGroup[i] * (1 - sliceFloat);
  39 + sliceFloat = 0;
32 40 }
  41 + return attendeeSum;
33 42  
34   - function getAttendeeOnRight(sliceId, attendeeNumberPerGroup) {
35   - let attendeeSum = 0;
36   - let sliceInt = Math.floor(sliceId);
37   - let sliceFloat = sliceId - sliceInt;
38   - for (let i = sliceInt; i < attendeeNumberPerGroup.length; i++) {
39   - attendeeSum += attendeeNumberPerGroup[i] * (1 - sliceFloat);
40   - sliceFloat = 0;
41   - }
42   - return attendeeSum;
  43 + }
43 44  
  45 + function setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea) {
  46 + // mousex = d3.mouse(this);
  47 + // const x = d3.mouse(this)[0];
  48 + // const y = d3.mouse(this)[1];
  49 + const x = d3.pointer(event)[0];
  50 + const y = d3.pointer(event)[1];
  51 + // var y = d3.event.pageY - document.getElementById(<id-of-your-svg>).getBoundingClientRect().y + 10
  52 + // x += 5;
  53 + vertical.style("left", x + "px");
  54 + rightArea.style("left", x + "px");
  55 + box.style("left", (x + 10) + "px");
  56 + box.style("top", (y - 20) + "px");
  57 + let sliceId = xScale.invert(x - d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().right) / 500;
  58 + // let sliceId = xScale.invert(x * 1.025 - d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().x- margin.left)/500 +1;
  59 + let attendeePercent = (getAttendeeOnRight(sliceId, attendeeNumberPerGroup) / attendeeSum * 100.0).toFixed(1);
  60 + if (x > d3.selectAll("g.yr.axis")._groups[0][0].getBoundingClientRect().x ||
  61 + x < d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().right) {
  62 + rightArea.style("width", 0);
  63 + vertical.style("width", 0);
  64 + box.style("display", "none");
44 65 }
45   -
46   - function setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea) {
47   - // mousex = d3.mouse(this);
48   - // const x = d3.mouse(this)[0];
49   - // const y = d3.mouse(this)[1];
50   - const x = d3.pointer(event)[0];
51   - const y = d3.pointer(event)[1];
52   - // var y = d3.event.pageY - document.getElementById(<id-of-your-svg>).getBoundingClientRect().y + 10
53   - // x += 5;
54   - vertical.style("left", x + "px");
55   - rightArea.style("left", x + "px");
56   - box.style("left", (x + 10) + "px");
57   - box.style("top", (y - 20) + "px");
58   - let sliceId = xScale.invert(x - d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().right) / 500;
59   - // let sliceId = xScale.invert(x * 1.025 - d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().x- margin.left)/500 +1;
60   - let attendeePercent = (getAttendeeOnRight(sliceId, attendeeNumberPerGroup) / attendeeSum * 100.0).toFixed(1);
61   - if (x > d3.selectAll("g.yr.axis")._groups[0][0].getBoundingClientRect().x ||
62   - x < d3.selectAll("g.yl.axis")._groups[0][0].getBoundingClientRect().right) {
63   - rightArea.style("width", 0);
64   - vertical.style("width", 0);
65   - box.style("display", "none");
66   - }
67   - else {
68   - rightArea.style("width", d3.selectAll("g.yr.axis")._groups[0][0].getBoundingClientRect().x - x);
69   - vertical.style("width", "2px");
70   - box.style("display", "inherit");
71   - }
72   - box.text(attendeePercent + " % of attendees");
73   - box.text(attendeePercent + " % of attendees");
74   - // console.log(d3.selectAll("g.x.axis")._groups[0][0]);
75   - // console.log(d3.selectAll("g.x.axis")._groups[0][0].getBoundingClientRect().x);
  66 + else {
  67 + rightArea.style("width", d3.selectAll("g.yr.axis")._groups[0][0].getBoundingClientRect().x - x);
  68 + vertical.style("width", "2px");
  69 + box.style("display", "inherit");
76 70 }
  71 + box.text(attendeePercent + " % of attendees");
  72 + box.text(attendeePercent + " % of attendees");
  73 + // console.log(d3.selectAll("g.x.axis")._groups[0][0]);
  74 + // console.log(d3.selectAll("g.x.axis")._groups[0][0].getBoundingClientRect().x);
  75 + }
77 76  
78   - function addVerticalLineAndListenCursor(xScale, attendeeNumberPerGroup, attendeeSum) {
79   - let vertical = d3.select("#" + divId)
80   - .append("div")
81   - .attr("class", "remove")
82   - .style("position", "absolute")
83   - .style("z-index", "19")
84   - .style("width", "2px")
85   - .style("height", (height) + "px")
86   - .style("top", (10 + margin.top) + "px")
87   - .style("bottom", "30px")
88   - .style("left", "0px")
89   - .style("background", "#000");
  77 + function addVerticalLineAndListenCursor(xScale, attendeeNumberPerGroup, attendeeSum) {
  78 + let vertical = d3.select("#" + divId)
  79 + .append("div")
  80 + .attr("class", "remove")
  81 + .style("position", "absolute")
  82 + .style("z-index", "19")
  83 + .style("width", "2px")
  84 + .style("height", (height) + "px")
  85 + .style("top", (10 + margin.top) + "px")
  86 + .style("bottom", "30px")
  87 + .style("left", "0px")
  88 + .style("background", "#000");
90 89  
91   - let rightArea = d3.select("#" + divId)
92   - .append("div")
93   - .attr("class", "remove")
94   - .style("position", "absolute")
95   - .style("z-index", "-50")
96   - .style("width", "2000px")
97   - .style("height", (height) + "px")
98   - .style("top", (10 + margin.top) + "px")
99   - .style("bottom", "30px")
100   - .style("left", "0px")
101   - .style("background", "rgba(60, 200, 60, 0.3)");
  90 + let rightArea = d3.select("#" + divId)
  91 + .append("div")
  92 + .attr("class", "remove")
  93 + .style("position", "absolute")
  94 + .style("z-index", "-50")
  95 + .style("width", "2000px")
  96 + .style("height", (height) + "px")
  97 + .style("top", (10 + margin.top) + "px")
  98 + .style("bottom", "30px")
  99 + .style("left", "0px")
  100 + .style("background", "rgba(60, 200, 60, 0.3)");
102 101  
103 102  
104   - let box = d3.select("#" + divId)
105   - .append("div")
106   - .attr("class", "remove")
107   - .style("position", "absolute")
108   - .style("z-index", "20")
109   - .style("width", "150px")
110   - .style("height", "22px")
111   - .style("top", "10px")
112   - .style("bottom", "30px")
113   - .style("left", "0px")
114   - .style("border", "1px solid grey")
115   - .style("background", "rgba(255, 255, 255, 0.7)");
116   - d3.select("#" + divId)
117   - .on("mousemove", function (event) {
118   - setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea);
119   - })
120   - .on("mouseover", function (event) {
121   - setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea);
122   -
123   - });
124   - }
  103 + let box = d3.select("#" + divId)
  104 + .append("div")
  105 + .attr("class", "remove")
  106 + .style("position", "absolute")
  107 + .style("z-index", "20")
  108 + .style("width", "150px")
  109 + .style("height", "22px")
  110 + .style("top", "10px")
  111 + .style("bottom", "30px")
  112 + .style("left", "0px")
  113 + .style("border", "1px solid grey")
  114 + .style("background", "rgba(255, 255, 255, 0.7)");
  115 + d3.select("#" + divId)
  116 + .on("mousemove", function (event) {
  117 + setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea);
  118 + })
  119 + .on("mouseover", function (event) {
  120 + setupCursorBoxes(event, attendeeSum, attendeeNumberPerGroup, xScale, box, vertical, rightArea);
125 121  
  122 + });
  123 + }
126 124  
127   - document.onreadystatechange = () => {
128   - if (document.readyState === 'complete') {
129   - let maxemissions = 0;
130   - let maxemissionsPercent = 0;
131   - let maxDistance = 0;
132   - let svg = d3.select("#" + divId)
133   - .append("svg")
134   - .attr("id", divId + "-svg")
135   - .attr("width", width + margin.left + margin.right)
136   - .attr("height", height + margin.top + margin.bottom)
137   - .append("g")
138   - .attr("transform",
139   - "translate(" + margin.left + "," + margin.top + ")");
  125 + document.addEventListener("DOMContentLoaded", () => {
  126 + let maxemissions = 0;
  127 + let maxemissionsPercent = 0;
  128 + let maxDistance = 0;
  129 + let svg = d3.select("#" + divId)
  130 + .append("svg")
  131 + .attr("id", divId + "-svg")
  132 + .attr("width", width + margin.left + margin.right)
  133 + .attr("height", height + margin.top + margin.bottom)
  134 + .append("g")
  135 + .attr("transform",
  136 + "translate(" + margin.left + "," + margin.top + ")");
140 137  
141   - let emissionsPerGroup = [];
142   - let attendeeNumberPerGroup = [];
143   - let emissionsSum = 0;
144   - let attendeeSum = 0;
145   - let rows = [];
  138 + let emissionsPerGroup = [];
  139 + let attendeeNumberPerGroup = [];
  140 + let emissionsSum = 0;
  141 + let attendeeSum = 0;
  142 + let rows = [];
146 143  
147   - const on_csv_datum = function (datum) {
148   - let trainAttendee = parseInt(datum["train trips_amount"]);
149   - let planeAttendee = parseInt(datum["plane trips_amount"]);
150   - if (trainAttendee === 0 && planeAttendee === 0) {
151   - return;
152   - }
153   - let attendeeNumber = trainAttendee + planeAttendee;
154   - let distance_km = datum.distance_km / attendeeNumber;
155   - let co2_kg = parseFloat(datum.co2_kg);
156   - if (co2_kg === "NaN" || distance_km / 500 > 37 || distance_km === "NaN") {
157   - return;
158   - }
159   - rows.push(datum);
160   - maxDistance = Math.max(maxDistance, distance_km);
161   - emissionsSum += co2_kg;
162   -
163   - };
  144 + const on_csv_datum = function (datum) {
  145 + let trainAttendee = parseInt(datum["train trips_amount"]);
  146 + let planeAttendee = parseInt(datum["plane trips_amount"]);
  147 + if (trainAttendee === 0 && planeAttendee === 0) {
  148 + return;
  149 + }
  150 + let attendeeNumber = trainAttendee + planeAttendee;
  151 + let distance_km = datum.distance_km / attendeeNumber;
  152 + let co2_kg = parseFloat(datum.co2_kg);
  153 + if (co2_kg === "NaN" || distance_km / 500 > 37 || distance_km === "NaN") {
  154 + return;
  155 + }
  156 + rows.push(datum);
  157 + maxDistance = Math.max(maxDistance, distance_km);
  158 + emissionsSum += co2_kg;
164 159  
165   - const on_csv_ready = function () {
166   - for (let i = 0; i <= maxDistance / 500; i++) {
167   - emissionsPerGroup[i] = 0;
168   - attendeeNumberPerGroup[i] = 0;
169   - }
170   - rows.forEach((element, index) => {
171   - let trainAttendee = parseInt(element["train trips_amount"]);
172   - let planeAttendee = parseInt(element["plane trips_amount"]);
173   - let attendeeNumber = trainAttendee + planeAttendee;
174   - let distance_km = element.distance_km / attendeeNumber;
175   - let co2_kg = parseFloat(element.co2_kg);
176   - emissionsPerGroup[Math.floor(distance_km / 500)] += parseFloat(co2_kg);
177   - attendeeNumberPerGroup[Math.floor(distance_km / 500)] += attendeeNumber;
178   - attendeeSum += attendeeNumber;
179   - });
180   - emissionsPerGroup.forEach((element, index) => {
181   - maxemissions = Math.max(maxemissions, element);
182   - maxemissionsPercent = Math.max(maxemissionsPercent, element / emissionsSum * 100.0)
183   - });
184   - maxDistance += 2000;
185   - // console.log(maxDistance);
  160 + };
186 161  
187   - //Title
188   - svg.append("text")
189   - .attr("transform",
190   - "translate(" + (70 + margin.left) + ", -12)")
191   - .style("text-anchor", "middle")
192   - .style("font-weight", "bold")
193   - .style("font-size", "130%")
194   - .text("Emissions per distance");
  162 + const on_csv_ready = function () {
  163 + for (let i = 0; i <= maxDistance / 500; i++) {
  164 + emissionsPerGroup[i] = 0;
  165 + attendeeNumberPerGroup[i] = 0;
  166 + }
  167 + rows.forEach((element, index) => {
  168 + let trainAttendee = parseInt(element["train trips_amount"]);
  169 + let planeAttendee = parseInt(element["plane trips_amount"]);
  170 + let attendeeNumber = trainAttendee + planeAttendee;
  171 + let distance_km = element.distance_km / attendeeNumber;
  172 + let co2_kg = parseFloat(element.co2_kg);
  173 + emissionsPerGroup[Math.floor(distance_km / 500)] += parseFloat(co2_kg);
  174 + attendeeNumberPerGroup[Math.floor(distance_km / 500)] += attendeeNumber;
  175 + attendeeSum += attendeeNumber;
  176 + });
  177 + emissionsPerGroup.forEach((element, index) => {
  178 + maxemissions = Math.max(maxemissions, element);
  179 + maxemissionsPercent = Math.max(maxemissionsPercent, element / emissionsSum * 100.0)
  180 + });
  181 + maxDistance += 2000;
  182 + // console.log(maxDistance);
195 183  
196   - // X axis: scale and draw:
197   - let x = d3.scaleLinear()
198   - .domain([0, maxDistance])
199   - .range([0, width]);
200   - let xAxis = d3.axisBottom(x)
201   - .tickValues(getBottomTicks(maxDistance));
202   - svg.append("g")
203   - .attr("transform", "translate(0," + height + ")")
204   - .attr("class", "x axis")
205   - .call(xAxis);
206   - d3.selectAll("g.x.axis")
207   - .selectAll(".tick")
208   - .filter(function (d) {
209   - return d === 0;
210   - })
211   - .remove();
  184 + //Title
  185 + svg.append("text")
  186 + .attr("transform",
  187 + "translate(" + (70 + margin.left) + ", -12)")
  188 + .style("text-anchor", "middle")
  189 + .style("font-weight", "bold")
  190 + .style("font-size", "130%")
  191 + .text("Emissions per distance");
212 192  
213   - // Y axis Left
214   - let yl = d3.scaleLinear()
215   - .domain([0, maxemissions])
216   - .range([height, 0]);
217   - let ylAxis = d3.axisLeft(yl)
218   - .tickValues(getLeftTicks(maxemissions));
219   - svg.append("g")
220   - .attr("class", "yl axis")
221   - .call(ylAxis);
  193 + // X axis: scale and draw:
  194 + let x = d3.scaleLinear()
  195 + .domain([0, maxDistance])
  196 + .range([0, width]);
  197 + let xAxis = d3.axisBottom(x)
  198 + .tickValues(getBottomTicks(maxDistance));
  199 + svg.append("g")
  200 + .attr("transform", "translate(0," + height + ")")
  201 + .attr("class", "x axis")
  202 + .call(xAxis);
  203 + d3.selectAll("g.x.axis")
  204 + .selectAll(".tick")
  205 + .filter(function (d) {
  206 + return d === 0;
  207 + })
  208 + .remove();
222 209  
223   - // Y axis Right
224   - let yr = d3.scaleLinear()
225   - .domain([0, maxemissionsPercent])
226   - .range([height, 0]);
227   - let yrAxis = d3.axisRight(yr)
228   - .tickValues(getRightTicks(maxemissionsPercent));
229   - svg.append("g")
230   - .attr("transform", "translate(" + width + ", 0)")
231   - .attr("class", "yr axis")
232   - .call(yrAxis);
  210 + // Y axis Left
  211 + let yl = d3.scaleLinear()
  212 + .domain([0, maxemissions])
  213 + .range([height, 0]);
  214 + let ylAxis = d3.axisLeft(yl)
  215 + .tickValues(getLeftTicks(maxemissions));
  216 + svg.append("g")
  217 + .attr("class", "yl axis")
  218 + .call(ylAxis);
233 219  
234   - svg.append("text")
235   - .attr("transform",
236   - "translate(" + (width / 2) + " ," +
237   - (height + margin.top + 12) + ")")
238   - .style("text-anchor", "middle")
239   - .text("Distance travelled (km)");
  220 + // Y axis Right
  221 + let yr = d3.scaleLinear()
  222 + .domain([0, maxemissionsPercent])
  223 + .range([height, 0]);
  224 + let yrAxis = d3.axisRight(yr)
  225 + .tickValues(getRightTicks(maxemissionsPercent));
  226 + svg.append("g")
  227 + .attr("transform", "translate(" + width + ", 0)")
  228 + .attr("class", "yr axis")
  229 + .call(yrAxis);
240 230  
241   - svg.append("text")
242   - .attr("transform",
243   - "translate(" + (width) + ", 0), rotate(-90)")
244   - .attr("x", 0 - (height / 2))
245   - .attr("y", 42)
246   - .style("text-anchor", "middle")
247   - .text("Share of emission [%]");
  231 + svg.append("text")
  232 + .attr("transform",
  233 + "translate(" + (width / 2) + " ," +
  234 + (height + margin.top + 12) + ")")
  235 + .style("text-anchor", "middle")
  236 + .text("Distance travelled (km)");
248 237  
249   - svg.append("text")
250   - .attr("transform", "rotate(-90)")
251   - .attr("y", 0 - margin.left)
252   - .attr("x", 0 - (height / 2))
253   - .attr("dy", "1em")
254   - .style("text-anchor", "middle")
255   - .text("Emission (tCO2e)");
256   - // set the parameters for the histogram
257   - var histogram = d3.histogram()
258   - .domain(x.domain()) // then the domain of the graphic
259   - .thresholds(x.ticks(Math.floor(maxDistance / 500))); // then the numbers of bins
  238 + svg.append("text")
  239 + .attr("transform",
  240 + "translate(" + (width) + ", 0), rotate(-90)")
  241 + .attr("x", 0 - (height / 2))
  242 + .attr("y", 42)
  243 + .style("text-anchor", "middle")
  244 + .text("Share of emission [%]");
260 245  
261   - let histolol = histogram(0);
262   - // console.log(histolol);
263   - let barSettings = [];
264   - emissionsPerGroup.forEach((element, index) => {
265   - barSettings[index] =
266   - {
267   - height: element,
268   - leftBorder: histolol[index].x0,
269   - rightBorder: histolol[index].x1,
270   - };
271   - // console.log(index);
272   - // console.log(barSettings[index]);
273   - });
274   - svg.selectAll("rect")
275   - .data(barSettings)
276   - .enter()
277   - .append("rect")
278   - .attr("x", 1)
279   - .attr("transform", function (d) {
280   - return "translate(" + x(d.leftBorder) + "," + yl(d.height) + ")";
281   - })
282   - .attr("width", function (d) {
283   - return x(d.rightBorder) - x(d.leftBorder) - 1;
284   - })
285   - .attr("height", function (d) {
286   - return (height - yl(d.height));
287   - })
288   - .style("z-index", "500")
289   - .style("fill", "#4444E5");
290   - addVerticalLineAndListenCursor(x, attendeeNumberPerGroup, attendeeSum);
291   - };
  246 + svg.append("text")
  247 + .attr("transform", "rotate(-90)")
  248 + .attr("y", 0 - margin.left)
  249 + .attr("x", 0 - (height / 2))
  250 + .attr("dy", "1em")
  251 + .style("text-anchor", "middle")
  252 + .text("Emission (tCO2e)");
  253 + // set the parameters for the histogram
  254 + var histogram = d3.histogram()
  255 + .domain(x.domain()) // then the domain of the graphic
  256 + .thresholds(x.ticks(Math.floor(maxDistance / 500))); // then the numbers of bins
292 257  
293   - d3.csv(csvUrl, on_csv_datum)
294   - .then(on_csv_ready);
295   - }
  258 + let histolol = histogram(0);
  259 + // console.log(histolol);
  260 + let barSettings = [];
  261 + emissionsPerGroup.forEach((element, index) => {
  262 + barSettings[index] =
  263 + {
  264 + height: element,
  265 + leftBorder: histolol[index].x0,
  266 + rightBorder: histolol[index].x1,
  267 + };
  268 + // console.log(index);
  269 + // console.log(barSettings[index]);
  270 + });
  271 + svg.selectAll("rect")
  272 + .data(barSettings)
  273 + .enter()
  274 + .append("rect")
  275 + .attr("x", 1)
  276 + .attr("transform", function (d) {
  277 + return "translate(" + x(d.leftBorder) + "," + yl(d.height) + ")";
  278 + })
  279 + .attr("width", function (d) {
  280 + return x(d.rightBorder) - x(d.leftBorder) - 1;
  281 + })
  282 + .attr("height", function (d) {
  283 + return (height - yl(d.height));
  284 + })
  285 + .style("z-index", "500")
  286 + .style("fill", "#4444E5");
  287 + addVerticalLineAndListenCursor(x, attendeeNumberPerGroup, attendeeSum);
296 288 };
297 289  
298   - })();
  290 + d3.csv(csvUrl, on_csv_datum)
  291 + .then(on_csv_ready);
  292 + });
299 293  
300 294 }
301 295 \ No newline at end of file
... ...