function renderSVGtoPNGBlob(svgObject, callback) { try { const svg = svgObject.contentDocument.querySelector('svg'); // Make a copy (clone) of the SVG object so we can manipulate it const clonedSvg = svg.cloneNode(true); // Remove any foreignObject elements from the clone. These are seen by the browser as cross-origin, and cause conversion (toBlob) to fail. clonedSvg.querySelectorAll('foreignObject').forEach(fo => fo.remove()); // Create a canvas element (bitmap) const canvas = document.createElement('canvas'); // To improve antialiasing in the final render, first convert to raster at double the desired resolution before later scaling down again const scale = 2; canvas.width = 1852 * scale; // Maintain aspect ratio const svgRect = svg.getBoundingClientRect(); canvas.height = Math.round(svgRect.height / svgRect.width * canvas.width); const ctx = canvas.getContext('2d'); // Convert SVG to a data URL const svgData = new XMLSerializer().serializeToString(clonedSvg); const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); const DOMURL = window.URL || window.webkitURL || window; const svgUrl = DOMURL.createObjectURL(svgBlob); // Create a bitmap from the SVG const img = new Image(); img.onload = function () { // First draw a white background ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Then draw the image on the canvas ctx.drawImage(img, 0, 0, canvas.width, canvas.height); DOMURL.revokeObjectURL(svgUrl); // Create a smaller canvas for the final output const finalCanvas = document.createElement('canvas'); finalCanvas.width = canvas.width / scale; finalCanvas.height = canvas.height / scale; const finalCtx = finalCanvas.getContext('2d'); // Draw the high-resolution canvas onto the smaller canvas finalCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, finalCanvas.width, finalCanvas.height); finalCanvas.toBlob(blob => { callback(blob); }, 'image/png'); }; img.src = svgUrl; } catch (error) { console.warn("Error rendering SVG to PNG", error); } } function copySVGasPNG(svgObject, anchorElement) { renderSVGtoPNGBlob(svgObject, async (blob) => { try { await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); showToastBelowElement(anchorElement,'Copied to clipboard as PNG!'); } catch (err) { console.error('Clipboard write failed', err); showToastBelowElement(anchorElement,'Failed to copy image to clipboard.'); } }); } function downloadSVGasPNG(svgObject) { renderSVGtoPNGBlob(svgObject, (blob) => { const DOMURL = window.URL || window.webkitURL || window; const url = DOMURL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${svgObject.getAttribute('data').split('/').pop().replace('.svg', '')}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); DOMURL.revokeObjectURL(url); }); } function isIOSorIPadOS() { return ( /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.userAgent.includes("Macintosh") && 'ontouchend' in document) ); } function showToastBelowElement(anchorElement, message, duration = 2000) { const rect = anchorElement.getBoundingClientRect(); const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollLeft = window.scrollX || document.documentElement.scrollLeft; const toast = document.createElement('div'); toast.textContent = message; toast.style.position = 'absolute'; toast.style.top = `${rect.bottom + scrollTop + 4}px`; // 4px spacing below the element toast.style.background = 'rgba(0,0,0,0.85)'; toast.style.color = 'white'; toast.style.padding = '0.4rem 0.8rem'; toast.style.borderRadius = '6px'; toast.style.fontSize = '0.85rem'; toast.style.boxShadow = '0 2px 6px rgba(0,0,0,0.25)'; toast.style.zIndex = 10000; toast.style.opacity = '0'; toast.style.transition = 'opacity 0.2s ease'; // toast.style.left = `${rect.left + scrollLeft}px`; // left-aligned with element above const anchorCenter = rect.left + scrollLeft + (rect.width / 2); toast.style.left = `${anchorCenter}px`; toast.style.transform = 'translateX(-50%)'; toast.style.textAlign = 'center'; document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => document.body.removeChild(toast), 300); }, duration); } function createDownloadLink(svgObject) { const downloadLink = document.createElement('a'); downloadLink.href = '#'; downloadLink.textContent = 'Download as PNG'; downloadLink.className = 'simple-button'; downloadLink.addEventListener('click', (e) => { e.preventDefault(); // prevent browser trying to navigate to https://.../# downloadSVGasPNG(svgObject); }); return downloadLink; } function createEnlargeLink(svgObject) { const enlargeLink = document.createElement('a'); const imageURL = svgObject.data; enlargeLink.href = '#' ; enlargeLink.textContent = 'Enlarge this figure'; enlargeLink.className = 'simple-button'; enlargeLink.addEventListener('click', (e) => { e.preventDefault(); const currentURL = svgObject.data; if (currentURL) { window.open(currentURL, '_blank'); } else { console.warn('SVG object has no data URL to open.'); } }); return enlargeLink; } function createCopyLink(svgObject) { const copyLink = document.createElement('a'); copyLink.href = '#'; copyLink.textContent = 'Copy to clipboard'; copyLink.className = 'simple-button'; copyLink.addEventListener('click', (e) => { e.preventDefault(); copySVGasPNG(svgObject, copyLink); }); return copyLink; } function createAltTextLink(svgObject) { const altLink = document.createElement('a'); const svgDoc = svgObject.contentDocument; const svgURL = svgObject.data; if (!svgDoc) return null; const titleEl = svgDoc.querySelector("title"); if (!titleEl) { console.error("SVG object has no 'title' element: ",svgURL.split('/').pop()); return null; } altLink.href = '#'; altLink.textContent = 'Alt'; altLink.className = 'simple-button'; altLink.addEventListener('click', (e) => { e.preventDefault(); const titleText = titleEl.textContent.trim() || "Untitled graph"; const altText = `Graph showing: ${titleText}`; navigator.clipboard.writeText(altText) .then(() => { showToastBelowElement(altLink,'Copied simple alt text to clipboard!'); console.log("Copied to clipboard:", altText); }) .catch(err => { showToastBelowElement(altLink,'Failed to alt text to clipboard.'); console.error("Failed to copy:", err); }); }); return altLink; } // Only implemented for country/index.html function createDataDownloadLink(svgObject) { const svgURL = svgObject.data; if (!svgURL) { console.log("SVG object has no data URL."); return null; } const urlParts = svgURL.split('/'); const imgIndex = urlParts.indexOf('img'); if (imgIndex === -1 || imgIndex + 1 >= urlParts.length) { console.log("URL does not contain the correct '/img/' path or isoCode is missing."); return null; } // if the svg file is located at, e.g. img/POL/POL_monthlyCO2.svg, then extract 'POL/' as the isoCode let isoCode ; if (urlParts.length-imgIndex === 3) { isoCode = urlParts[imgIndex + 1] + "/" ; // The element immediately following 'img' is the isoCode } else { isoCode = "" ; } if (typeof availableDataFiles === 'undefined' || !Array.isArray(availableDataFiles)) { // No data file list available. Skipping data link. return null; } const baseName = svgURL.split('/').pop().replace('.svg', ''); const candidateFilenames = [`${baseName}.csv`, `${baseName}_data.csv`]; if (availableDataFiles.length === 0) { console.log("createDataDownloadLink: availableDataFiles is empty!"); return null; } for (const name of candidateFilenames) { if (availableDataFiles.includes(name)) { const dataFilePath = `data/${isoCode}${name}`; console.log(`Data file found: ${dataFilePath}`); const link = document.createElement('a'); link.href = "#"; link.className = 'simple-button'; link.textContent = 'Download data'; link.addEventListener('click', (event) => { event.preventDefault(); const hiddenLink = document.createElement('a'); hiddenLink.href = dataFilePath; hiddenLink.download = name; document.body.appendChild(hiddenLink); hiddenLink.click(); document.body.removeChild(hiddenLink); }); return link; } } // console.log(`No matching data file found for: ${baseName}`); return null; } /* Given an SVG object, 1. Create a link that will generate a PNG file and download it 2. Add this link into the parent of the SVG object as: a. If there is already a link "View as PNG", replace that link with the new one b. If there is no such link, append the new link within the existing
tag c. If there is no
tag following the SVG object, simply append the new download link within the parent of the SVG object. */ function addSVGbuttons(svgObject) { const container = svgObject.parentNode; // let linkContainer = svgObject.nextElementSibling; let linkContainer = container.querySelector('.svg-button-group'); if (!linkContainer || !linkContainer.matches('p') || !linkContainer.classList.contains('svg-button-group')) { // If it doesn't exist, create a new one linkContainer = document.createElement('p'); linkContainer.classList.add("svg-button-group"); container.insertBefore(linkContainer, svgObject.nextSibling); } /* // Ensure the link container (p or div) exists if (!linkContainer || (!linkContainer.matches('p') && !linkContainer.matches('div'))) { linkContainer = document.createElement('p'); container.insertBefore(linkContainer, svgObject.nextSibling); } // Add a class to the button group to allow styling linkContainer.classList.add("svg-button-group");*/ // Create the download and copy links const downloadLink = createDownloadLink(svgObject); const copyLink = createCopyLink(svgObject); const enlargeLink = createEnlargeLink(svgObject); const alttextLink = createAltTextLink(svgObject); const dataLink = createDataDownloadLink(svgObject); // Add buttons, but only if they're not already there let hasDownload = false; let hasCopy = false; let hasEnlarge = false; let hasALT = false ; let hasData = false ; const links = linkContainer.querySelectorAll('a'); // Check for existing links for (const link of links) { const linktext = link.textContent.trim(); if (linktext === 'Download as PNG') hasDownload = true; else if (linktext === 'Copy to clipboard') hasCopy = true; else if (linktext === 'Enlarge this figure') hasEnlarge = true; else if (linktext === 'View as PNG') { linkContainer.replaceChild(downloadLink, link); hasDownload = true; } else if (linktext === 'Alt') hasALT = true; else if (linktext === 'Download data') hasData = true; } if (!hasData && dataLink) linkContainer.appendChild(dataLink); if (!hasEnlarge && enlargeLink) linkContainer.appendChild(enlargeLink); if (!hasDownload && downloadLink) linkContainer.appendChild(downloadLink); if (!hasCopy && !isIOSorIPadOS() && copyLink) linkContainer.appendChild(copyLink); if (!hasALT && alttextLink) linkContainer.appendChild(alttextLink); } function reloadSVGs() { // Get all object tags with the class "fig" const svgObjects = document.querySelectorAll('object.fig'); svgObjects.forEach(obj => { // Get the original URL const originalData = obj.getAttribute('data'); // Check if the URL already has a cache-busting parameter const url = new URL(originalData, window.location.href); // Append a new timestamp parameter to the URL url.searchParams.set('v', new Date().getTime()); // Update the data attribute, which forces a reload obj.setAttribute('data', url.toString()); }); }