import h337 from "heatmap.js" type ClickEvent = { x: number y: number tag: string } type UrlEvent = { url: string } type ClicksBaseEvent = ClickEvent | UrlEvent interface Format { format: string version: string width: number height: number brands: string events: ClicksBaseEvent[] } class PageData { readonly url: string readonly width: number readonly data: h337.HeatmapData constructor(url: string, width: number, data: h337.DataPoint[]) { this.url = url this.width = width let maxValue = 0 for (const d of data) { if (d.value > maxValue) maxValue = d.value } this.data = { data: data, min: 0, max: maxValue, } } } class Display { private status: HTMLElement private frame_container: HTMLElement private frame: HTMLIFrameElement private url: HTMLSelectElement private heatmap: HTMLElement private data: PageData[] | undefined private map: h337.Heatmap<"value", "x", "y"> private current: PageData | undefined constructor() { this.status = document.getElementById("status")! this.frame_container = document.getElementById("frame_container")! this.frame = document.getElementById("frame")! as HTMLIFrameElement this.heatmap = document.getElementById("heatmap")! this.url = document.getElementById("url")! as HTMLSelectElement this.map = h337.create({ container: this.heatmap, }) this.url.addEventListener("change", () => { if (this.data === undefined) return this.show(this.data[this.url.selectedIndex]) }) } loadFile(file: File) { this.setStatus("Loading...") const reader = new FileReader() reader.addEventListener("load", () => { const data = JSON.parse(reader.result as string) as Format if (data.format !== "clicks" || data.version !== "1.0") { this.setStatus("Unknown format") return } this.data = [] const scaleX = data.width const scaleY = data.width type TempPage = { url: string added: boolean map: Map clicks: h337.DataPoint[] } let current: TempPage | undefined const pages: TempPage[] = [] for (const event of data.events) { if ("url" in event) { if (current !== undefined && !current.added) { current.added = true pages.push(current) } current = undefined for (const page of pages) { if (page.url === event.url) { current = page break } } if (current === undefined) { current = { url: event.url, added: false, map: new Map(), clicks: [], } } } else if (current !== undefined) { const x = Math.round(event.x * scaleX) const y = Math.round(event.y * scaleY) const pos = `${x}x${y}` const index = current.map.get(pos) if (index === undefined) { current.map.set(pos, current.clicks.length) current.clicks.push({ x: x, y: y, value: 1 }) } else { current.clicks[index].value++ } } } if (current !== undefined && !current.added) { current.added = true pages.push(current) } for (const page of pages) { this.data.push(new PageData(page.url, data.width, page.clicks)) } while (this.url.options.length > 0) this.url.options.remove(0) for (let i = 0; i < this.data.length; i++) { const option = document.createElement("option") option.value = `${i}` option.innerText = this.data[i].url this.url.options.add(option) } this.setStatus("Loaded") if (this.data.length > 0) { this.url.selectedIndex = 0 this.show(this.data[0]) } }) reader.readAsText(file) } private setStatus(newStatus: string) { this.status.innerText = newStatus } private show(data: PageData) { if (this.current === data) return this.current = data this.frame_container.style.width = `${data.width}px` this.frame.src = data.url this.heatmap.style.transform = "translateY(0px)" this.frame.addEventListener("load", () => { this.frame.contentDocument?.addEventListener("scroll", () => { const y = this.frame.contentDocument!.documentElement.scrollTop this.heatmap.style.transform = `translateY(${-y}px)` }) }) this.map.setData(data.data) } } document.addEventListener("DOMContentLoaded", () => { const file = document.getElementById("file") const display = new Display() file?.addEventListener("change", (event: Event) => { const files = (event.target as HTMLInputElement | null)!.files if (files === null) return if (files.length > 0) { display.loadFile(files[0]) } }) })