UNPKG

gazeplotter

Version:

Gazeplotter is a Svelte application for visualizing eye-tracking data.

183 lines (182 loc) 7.93 kB
import { AbstractDownloader } from './AbstractDownloader.ts'; export class ScarfDownloader extends AbstractDownloader { minimalWidth = 300; width; height; fileType; fileName; staticSvg; constructor(fileName, fileType, width, el) { super(); if (Number(width) < this.minimalWidth) throw new Error(`Minimal width is ${this.minimalWidth}`); if (fileType !== '.svg' && fileType !== '.png' && fileType !== '.jpg' && fileType !== '.webp') throw new Error('File type not supported'); this.width = width; this.fileType = fileType; this.fileName = fileName; el.style.width = `${width}px`; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.height = el.offsetHeight; svg.setAttribute('width', el.offsetWidth.toString()); svg.setAttribute('height', this.height.toString()); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg.id = this.getSvgId(el); /** See getSvgId() why this is crucial */ svg.innerHTML = this.#createInnerHtml(el); this.staticSvg = svg; el.style.width = ''; } async download() { return await new Promise(resolve => { void this.buildContent().then(content => { super.triggerDownload(content, this.fileName, this.fileType); resolve(); }); }); } async buildContent() { return await new Promise(resolve => { const svgUrl = this.#getSvgUrl(this.staticSvg); if (this.fileType !== '.svg') { void this.#getRasterCanvasUrl(svgUrl).then(rasterUrl => { resolve(rasterUrl); }); } else { resolve(svgUrl); } }); } #getSvgUrl(svg) { const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml;charset=utf-8', }); const pageURL = window.URL; // || window.webkitURL || window return pageURL.createObjectURL(blob); } async #getRasterCanvasUrl(svgUrl) { const width = this.width; const height = this.height; // prepare canvas const canvas = document.createElement('canvas'); // set display size canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; // set resolution size const scale = 2; canvas.width = width * scale; canvas.height = height * scale; const ctx = canvas.getContext('2d'); if (ctx == null) throw new Error('Canvas context is null'); // adjust coordinates to resolution size ctx.scale(scale, scale); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); const chartAreaImg = new Image(); chartAreaImg.src = svgUrl; return await new Promise(resolve => { chartAreaImg.onload = () => { ctx.imageSmoothingEnabled = false; ctx.drawImage(chartAreaImg, 0, 0, width, height); resolve(canvas.toDataURL()); }; }); } #createInnerHtml(el) { const styleInnerHtml = el.querySelector('style')?.innerHTML; if (styleInnerHtml == null) throw new Error('No style found'); return ` <style><![CDATA[${styleInnerHtml}text{alignment-baseline:hanging;font-size:12px;font-family:Arial,Helvetica,sans-serif}.participant-label{alignment-baseline:middle}.chlitems text{alignment-baseline:middle}]]></style> ${this.#createParticipantLabelsHtml(el)} ${this.#createSvgChartArea(el)} ${this.#createXAxisLabels(el)} ${this.#createLegendTitles(el)} ${this.#createLegendItems(el)}`; } #createParticipantLabelsHtml(el) { const htmlCollection = el.querySelector('.chylabs')?.children; if (htmlCollection == null) throw new Error('No participant labels found'); const gap = Number(el.querySelector('.chylabs')?.getAttribute('data-gap')); let yPos = gap / 2; let result = ''; const clipPathId = 'clip-' + Date.now(); // Unique ID for the clipPath // Define your clipPath dimensions and position here const clipWidth = 130; const clipHeight = yPos + gap * htmlCollection.length; // Adjusted height based on content const clipPath = `<clipPath id="${clipPathId}"><rect width="${clipWidth}" height="${clipHeight}" x="0" y="0"/></clipPath>`; for (let i = 0; i < htmlCollection.length; i++) { const content = htmlCollection[i].innerHTML; result += `<text class="participant-label" y="${yPos}" clip-path="url(#${clipPathId})">${content}</text>`; yPos += gap; } return `<defs>${clipPath}</defs>${result}`; } #createSvgChartArea(el) { const svgArea = el.querySelector('.charea-holder'); const leftOffset = svgArea.offsetLeft - el.offsetLeft; const svgAreaClone = svgArea.cloneNode(true); const animateTags = svgAreaClone.getElementsByTagName('animate'); while (animateTags.length > 0) { animateTags[0].remove(); } return `<svg x="${leftOffset}" width="${el.offsetWidth - leftOffset}">${svgAreaClone.innerHTML}</svg>`; } #createXAxisLabels(el) { const htmlLab = el.querySelector('.chxlab'); return `<text x="100%" y="${htmlLab.offsetTop - el.offsetTop}" text-anchor="end">${htmlLab.innerText}</text>`; } #createLegendTitles(el) { const htmlTitles = el.getElementsByClassName('chlegendtitle'); let result = ''; for (let i = 0; i < htmlTitles.length; i++) { const titleElement = htmlTitles[i]; const calculatedY = titleElement.offsetTop - el.offsetTop; const calculatedX = titleElement.offsetLeft - el.offsetLeft; result += `<text text-anchor="start" x="${calculatedX}" y="${calculatedY}">${titleElement.innerHTML}</text>`; } return `${result}`; } #createLegendItems(el) { const htmlItems = el.getElementsByClassName('legendItem'); let result = ''; for (let i = 0; i < htmlItems.length; i++) { const legendElement = htmlItems[i]; const symbol = legendElement.children[0]; const text = legendElement.children[1]; const y = legendElement.offsetTop - el.offsetTop; const x = legendElement.offsetLeft - el.offsetLeft; const rectX = symbol.getBoundingClientRect().left - legendElement.getBoundingClientRect().left; const rectY = symbol.getBoundingClientRect().top - legendElement.getBoundingClientRect().top; const textX = text.getBoundingClientRect().left - legendElement.getBoundingClientRect().left; result += ` <svg x="${x}" y="${y}" width="${legendElement.offsetWidth}" height="${legendElement.offsetHeight}"> <svg x="${rectX}" y="${rectY - 2}" width="${symbol.width.baseVal.valueInSpecifiedUnits}" height="${symbol.height.baseVal.valueInSpecifiedUnits}">${symbol.innerHTML}</svg> <text x="${textX}" y="50%" alignment-baseline="middle">${text.innerHTML}</text> </svg>`; } return `<g class="chlitems">${result}</g>`; } /** * Returns the id of the chartwrap element. * * This is needed for correct styling of the svg, * because colors are applied to rect elements which are children of the given id. * * Example: #id-of-chartwrap rect { fill: red; } * * @param el Whole scarf plot element * @returns id of the chartwrap element */ getSvgId(el) { return el.id; } }