Skip to content

Commit

Permalink
Realtime heatmap
Browse files Browse the repository at this point in the history
  • Loading branch information
paolobrasolin committed Jan 19, 2024
1 parent a53ca2a commit a330f82
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 22 deletions.
8 changes: 7 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
"dependencies": {
"@hotwired/stimulus": "^3.2.1",
"d3": "^7.8.5"
"d3": "^7.8.5",
"simpleheat": "^0.4.0"
}
}
102 changes: 84 additions & 18 deletions src/controllers/map_controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Controller } from "@hotwired/stimulus";
import { assert } from "console";

import * as simpleheat from "simpleheat";

const MAP_URL = {
municipalities:
"https://raw.githubusercontent.com/openpolis/geojson-italy/master/geojson/limits_IT_municipalities.geojson",
Expand All @@ -14,16 +16,38 @@ import * as d3 from "d3";
import { buildTimeFilter, buildWordFilter } from "../utils";

export default class extends Controller {
static targets = ["container"];
static targets = ["container", "heatCanvas"];
declare readonly containerTarget: HTMLDivElement;
declare readonly heatCanvasTarget: HTMLCanvasElement;

projection!: d3.GeoProjection;
dots!: d3.Selection<SVGGElement, unknown, null, undefined>;
heat!: any;

dataset: {
timestamp: Date;
day: string;
latitude: number;
longitude: number;
word: string;
epoch: number;
x: number;
y: number;
}[] = [];

timeFilter = buildTimeFilter(null);
wordFilter = buildWordFilter(null);

async connect() {
this.heat = simpleheat(this.heatCanvasTarget);
this.heatCanvasTarget.style.width = "100%";
this.heatCanvasTarget.style.height = "100%";
// ...then set the internal size to match
this.heatCanvasTarget.width = this.heatCanvasTarget.offsetWidth;
this.heatCanvasTarget.height = this.heatCanvasTarget.offsetHeight;
this.heat.resize();
this.heat.radius(3, 6);

const bb = (await d3.json<d3.ExtendedFeatureCollection>(MAP_URL.regions))!;
this.projection = d3.geoEqualEarth();
this.projection.fitExtent(
Expand All @@ -46,7 +70,7 @@ export default class extends Controller {
.enter()
.append("path")
.attr("d", geoGenerator)
.attr("fill", "#ABF")
.attr("fill", "#BBB")
.attr("stroke", "#FFF")
.attr("stroke-width", ".5px");
this.dots = d3
Expand All @@ -56,9 +80,7 @@ export default class extends Controller {
.attr("class", "dots");
}

disconnect() {
console.log("asd");
}
disconnect() {}

reloadDataset({
detail: { data },
Expand All @@ -69,17 +91,28 @@ export default class extends Controller {
latitude: number;
longitude: number;
word: string;
epoch: number;
}[];
}>) {
this.dots
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", (d) => this.projection([d.longitude, d.latitude])![0])
.attr("cy", (d) => this.projection([d.longitude, d.latitude])![1])
.attr("r", 1) // Radius of the dots
.attr("fill", "red"); // Color of the dots
const bins: number[] = Array(10000).fill(0);
this.dataset = data.map((d) => {
const [x, y] = this.projection([d.longitude, d.latitude])!;
bins[Math.floor(x / 6) + Math.floor(y / 6) * 1000] += 1;
return { ...d, x, y };
});
const maximum = d3.max(bins);
this.heat.max(maximum);
this.redrawheat();
//this.dots
// .selectAll("circle")
// .data(data)
// .enter()
// .append("circle")
// .attr("cx", (d) => this.projection([d.longitude, d.latitude])![0])
// .attr("cy", (d) => this.projection([d.longitude, d.latitude])![1])
// .attr("r", 2) // Radius of the dots
// .attr("opacity", 0.1)
// .attr("fill", "red"); // Color of the dots
}

updateWordlist({
Expand All @@ -101,9 +134,42 @@ export default class extends Controller {
}

applyFilter() {
this.dots.selectAll("circle").attr("visibility", (d: any) => {
const visible = this.wordFilter(d) && this.timeFilter(d);
return visible ? "visible" : "hidden";
});
this.throttledRedrawHeat();
// this.dots.selectAll("circle").attr("visibility", (d: any) => {
// const visible = this.wordFilter(d) && this.timeFilter(d);
// return visible ? "visible" : "hidden";
// });
}

redrawheat() {
this.heat.clear();

const data = this.dataset
.filter(this.wordFilter)
.filter(this.timeFilter)
.map(({ x, y }) => [x, y, 1]);
this.heat.data(data);
// this.heat.max(12000);
this.heat.draw(0.05);
}

throttle(mainFunction: any, delay: number) {
let timerFlag: NodeJS.Timeout | null = null;

// Returning a throttled version
return (...args: any) => {
if (timerFlag === null) {
// If there is no timer currently running
mainFunction(...args); // Execute the main function
timerFlag = setTimeout(() => {
timerFlag = null;
}, delay);
}
};
}

throttledRedrawHeat = this.throttle(
this.redrawheat.bind(this),
17 // 60fps
);
}
5 changes: 3 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
<input class="w-full p-2" type="file" data-action="change->main#changeDataset" />
<select multiple data-lexicon-target="select" class="w-full h-full"></select>
</section>
<section class="bg-green-50">
<section class="bg-gray-50 relative">
<div class="w-full h-full" data-map-target="container"></div>
<canvas class="w-full h-full absolute top-0" data-map-target="heatCanvas"></canvas>
</section>
<section class="bg-blue-50">
<!-- <input class="w-full" type="range" id="minDate" value="20" min="0" max="100" /> -->
Expand All @@ -38,4 +39,4 @@

</body>

</html>
</html>
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3434,6 +3434,11 @@
"resolved" "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
"version" "3.0.7"

"simpleheat@^0.4.0":
"integrity" "sha512-tdg3I1NvzMdPKscWBrHbF0LBf+VWuBBazzGUPtFJjG5Q12VQX6gPY8jLy9hx5CbgAIVc5nfnePwJvAYK6z1rAA=="
"resolved" "https://registry.npmjs.org/simpleheat/-/simpleheat-0.4.0.tgz"
"version" "0.4.0"

"sisteransi@^1.0.5":
"integrity" "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
"resolved" "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
Expand Down

0 comments on commit a330f82

Please sign in to comment.