diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f235ed1
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "*.html.erb": "html"
+ }
+}
\ No newline at end of file
diff --git a/lib/measures/detailed_hvac_viewer/resources/report.html.erb b/lib/measures/detailed_hvac_viewer/resources/report.html.erb
index 9695214..263e97f 100644
--- a/lib/measures/detailed_hvac_viewer/resources/report.html.erb
+++ b/lib/measures/detailed_hvac_viewer/resources/report.html.erb
@@ -1,654 +1,1100 @@
-
-
-HVAC Detailed Viewer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ function mouseMove(e) {
+ let xdate = xScale.invert(d3.pointer(e)[0]);
+
+ // hack to get closest index from 8760 hour. probably a much better way to do this.
+
+ let [start, end] = xScale.domain();
+
+ let domain_fraction =
+ (xdate.getTime() - start.getTime()) /
+ (end.getTime() - start.getTime());
+
+ let selected_date = new Date(
+ startDate.getTime() +
+ (start.getTime() - startDate.getTime()) +
+ domain_fraction * (end.getTime() - start.getTime())
+ );
+
+ let annual_fraction =
+ (selected_date.getTime() - startDate.getTime()) /
+ (endDate.getTime() - startDate.getTime());
+
+ let closest_idx = Math.round(annual_fraction * 8760);
+
+ let closest_date = new Date(
+ startDate.getTime() + closest_idx * 3.6e6
+ );
+
+ let pointarray = [];
+
+ data_array.forEach((d, i) => {
+ pointarray.push(d.NodeValues[closest_idx]);
+ });
+
+ x_line
+ .style("opacity", 1)
+ .attr("x1", xScale(xdate))
+ .attr("x2", xScale(xdate));
+
+ tooltip_div
+ .style(
+ "left",
+ 20 + event.pageX / window.innerWidth + event.pageX + "px"
+ )
+ .style("top", 20 + event.pageY + "px")
+ .style("transition", "left 100ms, top 100ms")
+ .style("z-index", 999)
+ .style("opacity", 0.9).html(`
+
+
+ ${d3.timeFormat("%B %d %H:%M")(closest_date)}
+
+ ${pointarray
+ .map((d, i) => {
+ let node_name = data_array[i].nodeName;
+ let node_val = d;
+
+ return `
+
+
+
${node_name}: ${node_val}
+
`;
+ })
+ .join("")}
+
`);
+ }
+
+ hover_rect.on("mouseover", mouseOver);
+ hover_rect.on("mouseout", mouseOut);
+ hover_rect.on("mousemove", mouseMove);
+
+ /* HANDLE ZOOM & BRUSH */
+
+ const brush = d3
+ .brushX()
+ .extent([
+ [0, 0],
+ [plotWidth, contextHeight],
+ ])
+ .on("brush end", brushed);
+
+ const zoomFunc = d3
+ .zoom()
+ .scaleExtent([1, Infinity])
+ .translateExtent([
+ [0, 0],
+ [plotWidth, plotHeight],
+ ])
+ .extent([
+ [0, 0],
+ [plotWidth, plotHeight],
+ ])
+ .on("zoom", zoomed);
+
+ let idleTimeout;
+ let idleDelay = 350;
+
+ function idled() {
+ idleTimeout = null;
+ }
+
+ function brushed(event) {
+ if (event.sourceEvent && event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
+ var s = event.selection || contextXScale.range();
+
+ let xdomain = s.map(contextXScale.invert, contextXScale);
+ xScale.domain(xdomain);
+
+ createLine(xdomain);
+ x_axis_g.call(d3.axisBottom(xScale));
+ }
+
+ function zoomed(event) {
+ if (event.sourceEvent && event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
+ var t = event.transform;
+ let xdomain = t.rescaleX(contextXScale).domain();
+ xScale.domain(xdomain);
+ createLine(xdomain);
+ x_axis_g.call(d3.axisBottom(xScale));
+ context_plot_g
+ .select(".brush")
+ .call(brush.move, xScale.range().map(t.invertX, t));
+ }
+
+ context_brush_g.call(brush);
+
+ x_axis_g.call(d3.axisBottom(xScale));
+
+ hover_g.call(zoomFunc);
+ }
+
+ /* END D3 MULTILINE PLOT */
+
+ function getRandomColor() {
+ const letters = "0123456789ABCDEF";
+ let color = "#";
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+ }
+ // Call function to attach event listener when the DOM is fully loaded
+ document.addEventListener("DOMContentLoaded", selectLoopEvent);
+
+