PageSourceSearch

https://cityscannerteck2.vercel.app/scripts/hotspot_graph.js

js cityscannerteck2.vercel.app collected 2026-10-03 05:16:01 UTC 13,410 bytes, 320 lines download raw bytes

1HotspotGraph = function(container) {
2
3  let self = this;
4
5  let hotspotModule = document.querySelector(container);
6
7  let hotspotContainer = hotspotModule.querySelector("#hotspot-container"),
8    currentTime = hotspotModule.querySelector("#current-time"),
9    leftArrow = hotspotModule.querySelector("#left-arrow"),
10    rightArrow = hotspotModule.querySelector("#right-arrow");
11
12  // get the radios/labels for the modes of the hotspot module
13  let weekRadio = hotspotModule.querySelector("#week"),
14      monthRadio = hotspotModule.querySelector("#month"),
15      yearRadio = hotspotModule.querySelector("#year");
16
17  let weekLabel = hotspotModule.querySelector("#week-label"),
18      monthLabel = hotspotModule.querySelector("#month-label"),
19      yearLabel = hotspotModule.querySelector("#year-label");
20
21  let menuRadios = [weekRadio, monthRadio, yearRadio];
22  let menuLabels = [weekLabel, monthLabel, yearLabel];
23  let menuNames = ["week", "month", "year"];
24  let hotspot_graph_mode_index = 1;
25
26  let FIRST_DATE_WITH_DATA = null; // will be set to the first date with data
27  let LAST_DATE_WITH_DATA = null; // will be set to the last date with data
28
29  // set up the radios for switching the mode of the hotspot graph
30  menuRadios.forEach(function (radio, i) {
31    radio.addEventListener("change", function () {
32      menuLabels[hotspot_graph_mode_index].classList.remove("active");
33      hotspot_graph_mode_index = i;
34      menuLabels[hotspot_graph_mode_index].classList.add("active");
35      self.resetDatesBasedOnMode(getAllMapFeatures(), menuNames[hotspot_graph_mode_index]);
36      self.update(getAllMapFeatures(), new Date(currentTime.getAttribute("data-start")), new Date(currentTime.getAttribute("data-end")), menuNames[hotspot_graph_mode_index]);
37    });
38  });
39
40  // sets start and end dates for when the mode of the graph is switched
41  // i.e. will calculate the proper start and end dates for a month when
42  // the start and end dates used to be a week long
43  self.resetDatesBasedOnMode = function(data, mode) {
44    let startDate = new Date(currentTime.getAttribute("data-start"));
45    let endDate = new Date(currentTime.getAttribute("data-end"));
46
47    let newStartDate = null;
48    let newEndDate = null;
49
50    if(mode == "week") {
51      newStartDate = startDate; //start at the same start date
52      newEndDate = new Date(startDate.getTime() + 1000 * 60 * 60 * 24 * 6); // add 6 days to get week
53    } else if (mode == "month") {
54      newStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1); //get first day of current month
55      newEndDate = new Date(startDate.getFullYear(), startDate.getMonth()+1, 0); //get the last day of current month
56    } else if (mode == "year") {
57      newStartDate = new Date(startDate.getFullYear(), 0, 1); //get first day of current year
58      newEndDate = new Date(startDate.getFullYear()+1, 0, 0); //get the last day of current year
59    }
60
61
62    // if the new date range will be out of the range of the data, reinitialize the dates
63    if(newEndDate < FIRST_DATE_WITH_DATA || newStartDate > LAST_DATE_WITH_DATA) {
64      if(mode == "week") {
65        newStartDate = FIRST_DATE_WITH_DATA; //start at first date with data
66        newEndDate = new Date(newStartDate.getTime() + 1000 * 60 * 60 * 24 * 6); // plus 6 days
67      } else if (mode == "month") {
68        newStartDate = new Date(FIRST_DATE_WITH_DATA.getFullYear(), FIRST_DATE_WITH_DATA.getMonth(), 1); //get the first day of the month
69        newEndDate = new Date(FIRST_DATE_WITH_DATA.getFullYear(), FIRST_DATE_WITH_DATA.getMonth()+1, 0); //get the last day of the month
70      } else if (mode == "year") {
71        newStartDate = new Date(FIRST_DATE_WITH_DATA.getFullYear(), 0, 1); //get the first day of the year
72        newEndDate = new Date(FIRST_DATE_WITH_DATA.getFullYear()+1, 0, 0); //get the last day of the year
73      }
74    }
75
76    // set the dates in the attributes of the page
77    currentTime.setAttribute("data-start", newStartDate);
78    currentTime.setAttribute("data-end", newEndDate);
79  }
80
81
82  leftArrow.addEventListener("click", function () {
83    let mode = menuNames[hotspot_graph_mode_index];
84    // get current start date
85    currentStartDate = new Date(currentTime.getAttribute("data-start"));
86
87    // compute new date range based on mode
88    if(mode == "week") {
89      newEndDate = new Date(currentStartDate.getTime() - 1000 * 60 * 60 * 24); // minus 1 day from current start
90      newStartDate = new Date(newEndDate.getTime() - 1000 * 60 * 60 * 24 * 6); // minus 6 days
91    } else if (mode == "month") {
92      newStartDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth()-1, 1); //get the first day of last month
93      newEndDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 0); //get the last day of the last month
94    } else if (mode == "year") {
95      newStartDate = new Date(currentStartDate.getFullYear()-1, 0, 1); //get the first day of the last year
96      newEndDate = new Date(currentStartDate.getFullYear(), 0, 0); //get the last day of the last year
97    }
98    // pass new date range to generateHotspotGraph
99    self.update(getAllMapFeatures(), newStartDate, newEndDate, menuNames[hotspot_graph_mode_index]);
100  });
101
102  rightArrow.addEventListener("click", function () {
103    let mode = menuNames[hotspot_graph_mode_index];
104    // get current end date
105    currentEndDate = new Date(currentTime.getAttribute("data-end"));
106
107    // compute new date range based on mode
108    if(mode == "week") {
109      newStartDate = new Date(currentEndDate.getTime()+ 1000 * 60 * 60 * 24 * 1); // start at old last day + 1 day
110      newEndDate = new Date(newStartDate.getTime() + 1000 * 60 * 60 * 24 * 6); // add 6 days
111    } else if (mode == "month") {
112      newStartDate = new Date(currentEndDate.getFullYear(), currentEndDate.getMonth()+1, 1); //get first day of the next month
113      newEndDate = new Date(currentEndDate.getFullYear(), currentEndDate.getMonth()+2, 0); //get the last day of the next month
114    } else if (mode == "year") {
115      newStartDate = new Date(currentEndDate.getFullYear()+1, 0, 1); //get first day of the next year
116      newEndDate = new Date(currentEndDate.getFullYear()+2, 0, 0
116); //get the last day of the next year
117    }
118
119    // pass new date range to generateHotspotGraph
120    self.update(getAllMapFeatures(), newStartDate, newEndDate, menuNames[hotspot_graph_mode_index]);
121  });
122
123  self.generateHotspotData = function(allData) {
124    // temporary proxy for hotspots: filter data points with PM 2.5 <= 40
125    let highPmData = allData.filter((datum) => datum.properties["pm25"] > 40);
126
127    // construct an array of arrays to be used for hotspot line graph
128    // 1st value of each child array is date, 2nd value is # of hotspots for that date
129    let dates = highPmData.map((datum) => datum.properties["time"]);
130    let minDate = d3.min(dates);
131    let maxDate = d3.max(dates);
132    let dateObjToString = d3.timeFormat("%Y-%m-%d");
133
134
135    // initialize global data about first date with data and last date with data
136    if(FIRST_DATE_WITH_DATA == null) {
137      FIRST_DATE_WITH_DATA = minDate;
138    }
139
140    if(LAST_DATE_WITH_DATA == null) {
141      LAST_DATE_WITH_DATA = maxDate;
142    }
143
144    let hotspotData = {};
145    dates.forEach(function (dateObj) {
146      let date = dateObjToString(dateObj);
147      if (hotspotData[date]) {
148        hotspotData[date] += 1;
149        return;
150      }
151      hotspotData[date] = 1;
152    });
153
154    return hotspotData;
155  }
156
157  self.update = function(data, startDate, endDate, mode = "month") {
158    let hotspotData = self.generateHotspotData(data);
159
160    // if startDate and endDate args not provided, determine it from the data
161    // generally, args would be provided when *updating* the graph, but not for initial load
162    if (typeof startDate === "undefined") {
163      startDate = new Date(FIRST_DATE_WITH_DATA.getFullYear(), FIRST_DATE_WITH_DATA.getMonth(), 1); //get the first day of the month
164    }
165    if (typeof endDate === "undefined") {
166      endDate = new Date(FIRST_DATE_WITH_DATA.getFullYear(), FIRST_DATE_WITH_DATA.getMonth()+1, 0); //get the last day of the month
167    }
168
169    // only keep values for the one time range specified
170    filteredHotspotData = Object.entries(hotspotData); // turn into array
171    filteredHotspotData = filteredHotspotData.filter(
172      (datum) => new Date(datum[0]) >= startDate && new Date(datum[0]) <= endDate
173    );
174
175    // remove previous graph if one exists
176    d3.select(hotspotContainer).selectAll("svg").remove();
177    // remove previous "no data" notice if it exists
178    d3.select(hotspotContainer).selectAll("p").remove();
179
180    // set up SVG
181    let width = hotspotContainer.offsetWidth,
182      height = 200,
183      MARGINS = {left: 50, right: 0};
184    let svg = d3
185      .select(hotspotContainer)
186      .append("svg")
187      .attr("viewBox", `0 0 ${width} ${height}`);
188
189    // x scale function
190    // initialize x-axis as showing the time period, starting from the earliest date in the data
191    var x = d3
192      .scaleTime()
193      .domain([startDate, endDate])
194      .range([MARGINS.left, width - MARGINS.right - MARGINS.left]);
195
196    // y scale function
197    hotspotYValues = Array.from(filteredHotspotData, x => x[1]);
198    var y = d3
199      .scaleLinear()
200      .domain([0, d3.max(hotspotYValues) + 10])
201      .range([height, 0]);
202
203    // axes
204    let xAxis = null;
205
206    // set x axis tick numbers and labels based on the mode of the hotspot graph
207    if(mode == "week") {
208      xAxis = (g) =>
209        g.attr("transform", `translate(0, ${height})`).call(
210          d3
211            .axisBottom(x)
212            .ticks(7)
213            .tickFormat((date) => date.getDate()) // labeled by day of month
214        );
215    } else if (mode == "month") {
216      xAxis = (g) =>
217        g.attr("transform", `translate(0, ${height})`).call(
218          d3
219            .axisBottom(x)
220            .ticks()
221            .tickFormat((date) => date.getDate()) // labeled by day of month
222        );
223    } else if (mode == "year") {
224      xAxis = (g) =>
225        g.attr("transform", `translate(0, ${height})`).call(
226          d3
227            .axisBottom(x)
228            .ticks(12)
229            .tickFormat((date) => date.getMonth()+1) // labeled by numbered month
230        );
231    }
232    svg.append("g").call(xAxis);
233
234    let yAxis = (g) => g.call(d3.axisLeft(y).ticks(4));
235    svg.append("g").call(yAxis)
236      .attr("transform", "translate(" + (MARGINS.left) + ",0)")
237
238
239    svg.append("text")
240      .attr("transform", "rotate(-90)")
241      .attr("y", 0)
242      .attr("x",0 - (height / 2))
243      .attr("dy", "1em")
244      .attr("fill", "white")
245      .style("text-anchor", "middle")
246      .text("Number of Hotspots");
247
248    // line graph
249    let lineGenerator = d3
250      .line()
251      .defined((d) => d !== null) // leave gap in line for missing data
252      .x((d) => x(new Date(d[0]))) // date
253      .y((d) => y(d[1])); // number of hotspots for that date
254    svg
255      .append("path")
256      .datum(filteredHotspotData) // turn object into array
257      .attr("class", "line") // assign a class for styling
258      .attr("stroke", "#7d919e")
259      .attr("d", lineGenerator); // calls the line generator
260
261    // draw circles as well
262    svg
263      .selectAll("circle")
264      .data(filteredHotspotData)
265      .enter()
266      .append("circle")
267      .attr("cx", (d) => x(new Date(d[0])))
268      .attr("cy", (d) => y(d[1]))
269      .attr("fill", "#7d919e") // gray color
270      .attr("r", 2);
271
272    // if no hotspot data, show message on graph
273    if (hotspotContainer.querySelector(".line").getAttribute("d") == null) {
274      let p = document.createElement("p");
275      p.setAttribute("class", "notice");
276      let notice = document.createTextNode(
277        "No hotspot data for this time period"
278      );
279      p.appendChild(notice);
280      hotspotContainer.appendChild(p);
281    }
282
283    // populate current time in the temporal navigation below the graph
284    populateCurrentTime(startDate, endDate, mode);
285  }
286
287  // adds/updates the string in the hotspot panel time navigation
288  // currently only implemented for weekly time periods
289  function populateCurrentTime(startDate, endDate, mode) {
290
291    // populate attributes so other graph re-drawing processes can pull from this
292    currentTime.setAttribute("data-start", startDate);
293    currentTime.setAttribute("data-end", endDate);
294
295    if(mode == "week") {
296      startMonth = startDate.toLocaleString("default", { month: "long" });
297      endMonth = endDate.toLocaleString("default", { month: "long" });
298
299      let year = startDate.toLocaleString("default", { year: "numeric" });
300      if (startMonth == endMonth) {
301        // e.g. "March 5–11 2020"
302        currentTime.innerText = `${startMonth} ${startDate.getDate()} – ${endDate.getDate()} ${year}`;
303        return;
304      }
305      // e.g. "March 29 2020 – April 4 2020"
306      currentTime.innerText = `${startMonth} ${startDate.getDate()} ${year} –${endMonth} ${endDate.getDate()} ${year}`;
307    } else if (mode == "month"){
308      let year = startDate.toLocaleString("default", { year: "numeric" });
309      let month = startDate.toLocaleString("default", { month: "long" });
310      currentTime.innerText = `${month} ${year}`; // display the month in view
311    } else if (mode == "year"){
312      let year = startDate.toLocaleString("default", { year: "numeric" });
313      currentTime.innerText = `${year}`; //display the year in view
314    }
315  }
316
317  self.getCurrentTime = function() {
318    return currentTime;
319  }
320}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.