/** * Represents a loader for chart data and settings. */ class ChartLoader { /** * Creates an instance of ChartLoader. * @param {HTMLCanvasElement} canvas - The canvas element for rendering the chart. * @param {HTMLCanvasElement} effectCanvas - The canvas element for rendering visual effects. * @param {HTMLCanvasElement} detectionCanvas - The canvas element for detecting mouse interactions. * @param {HTMLElement} parent - The parent element that contains the chart. * @param {HTMLElement} legend - The legend element associated with the chart. * @param {HTMLElement} tooltip - The tooltip element used for displaying additional information. */ constructor(canvas, effectCanvas, detectionCanvas, parent, legend, tooltip) { this.canvas = canvas this.effectCanvas = effectCanvas this.detectionCanvas = detectionCanvas this.parent = parent this.legend = legend this.tooltip = tooltip this.clickedShapeIndex = null this.selectedShapeIndex = null } /** Function for loading chart data from the server * @param {String} id - The id used to identify the chart **/ loadData(id) { // Fetch chart data from the server fetch("/api/charts/" + id, { method: 'GET', headers: { 'Content-Type': 'json' } }) // Parse the JSON response .then(response => response.json()) // Process the retrieved data .then(data => { // Extract metadata and table data from the response let metadata = data.metadata //metadata.custom_x_values = "" let table = data.table // Draw the chart it contains data if (data !== null) { this.drawChart(metadata, table) this.parent.style.backgroundColor = metadata.backgroundColor } // Display an error message if chart data is not found else this.parent.innerHTML = "graph not found." }) } /** * Calculates the mode (most common value) in an array. * @param {Array} array - The array of values. * @returns {any} The mode of the array. */ mode(array) { // Check if the array is empty if (array.length == 0) return null // Create a map to store the count of each element let modeMap = {} // Initialize variables to track the most common element and its count let maxEl = array[0], maxCount = 1 // Loop through the array to count occurrences of each element for (let i = 0; i < array.length; i++) { let el = array[i] // Increment the count for the current element in the map if (modeMap[el] == null) modeMap[el] = 1 else modeMap[el]++ // Update the most common element and its count if needed if (modeMap[el] > maxCount) { maxEl = el maxCount = modeMap[el] } } // Return the most common element (mode) return maxEl } /** * Retrieves the index of the shape at a given position on the canvas. * @param {HTMLCanvasElement} canvas - The canvas element containing the shapes. * @param {Object} pos - The position on the canvas with x and y coordinates. * @returns {number} The index of the shape at the specified position. */ getShapeIndex(canvas, pos) { let ctx = canvas.getContext('2d') // Get neigboring pixels let imageData = Array.from(ctx.getImageData(pos.x - 2, pos.y - 2, 5, 5).data) // Convert into indexes let indexes = [] while (imageData.length > 0) { let pixel = imageData.splice(0, 4) // Only consider pixels with full opacity (alpha = 255) if (pixel[3] === 255) { let index = (pixel[0] * 256 + pixel[1]) * 256 + pixel[2] indexes.push(index) } } // To avoid edge cases because of anti aliasing let mostCommonIndex = this.mode(indexes) return mostCommonIndex } getEffectObjects() { } /** * Handles mouse movement events for interactivity. * @param {MouseEvent} e - The mouse event object. * @param {Chart} chart - The chart object associated with the mouse movement. */ mouseMoveFunc(e, chart) { // Get mouse position const pos = { x: e.clientX, y: e.clientY } let obj = [] // Add selected shape to array if it exists if (this.selectedShapeIndex !== null) obj.push(chart.objects[this.selectedShapeIndex]) // Get index of hovered shape and add it to the array if it exists let hoverShapeIndex = this.getShapeIndex(this.detectionCanvas, pos) if (hoverShapeIndex !== null) obj.push(chart.objects[hoverShapeIndex]) // Display tooltip and effect if a shape is selected if (obj[0] !== undefined) { // Get the drawing context for the effect canvas let effectCtx = this.effectCanvas.getContext("2d") // Clear the effect canvas effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) // Show the effect this.effectCanvas.style.opacity = 1 chart.drawEffect(effectCtx, obj) // Show tooltip this.tooltip.style.display = "block" // Set tooltip content let name = chart.data[obj[0].colId.toString()].col_name this.tooltip.innerHTML = "" + name + "

" + obj[0].value + "

" let tooltipPos = obj[0].getTooltipPos() // Adjust tooltip position to fit within the chart div if (tooltipPos.right.x + this.tooltip.clientWidth <= this.parent.clientWidth - 2) { this.tooltip.className = "tooltip leftArrow" this.tooltip.style.left = tooltipPos.right.x + 5 + "px" } else { this.tooltip.className = "tooltip rightArrow" this.tooltip.style.left = tooltipPos.left.x - this.tooltip.clientWidth + "px" } // Check if the tooltip extends beyond the bottom edge of the chart div if (tooltipPos.right.y + this.tooltip.clientHeight <= parent.clientHeight - 2) this.tooltip.style.top = tooltipPos.right.y + "px" else this.tooltip.style.top = tooltipPos.right.y - this.tooltip.clientHeight / 2 + "px" } else { // Hide tooltip and effect if no shape is selected this.tooltip.style.display = "none" this.effectCanvas.style.opacity = 0 } } /** * Adds event listeners to enable interactivity on the chart. * @param {Chart} chart - The chart object to which listeners will be added. */ addListeners(chart) { // Mousemove event listener for tracking mouse movement document.addEventListener('mousemove', (e) => { this.mouseMoveFunc(e, chart) if (this.panningEnabled === true) { this.chart.zoom.recalculate(e, true) // Update chart and redraw chart.updateBounds() chart.draw(false) this.addInteractivity() } }) // Mousedown event listener for tracking mouse clicks document.addEventListener("mousedown", (e) => { const pos = { x: e.clientX, y: e.clientY } // Store the index of the latest clicked shape this.clickedShapeIndex = this.getShapeIndex(this.detectionCanvas, pos) this.chart.zoom.lastPos = pos this.panningEnabled = true }) // Mouseup event listener for tracking mouse releases document.addEventListener("mouseup", (e) => { const pos = { x: e.clientX, y: e.clientY } // Determine if the selected shape has changed after mouse release if (this.clickedShapeIndex === null || this.clickedShapeIndex !== this.getShapeIndex(this.detectionCanvas, pos)) this.selectedShapeIndex = null else this.selectedShapeIndex = this.clickedShapeIndex // Perform mouse move action after mouse release to show the change immediately this.mouseMoveFunc(e, chart) this.panningEnabled = false }) // Window resize event listener for resizing the chart canvas window.addEventListener("resize", e => { // Resize the chart canvas and redraw the chart chart.resizeCanvas(this.parent, this.legend.offsetHeight) chart.draw(false) // Reapply interactivity this.addInteractivity() }) // Wheel event listener for zooming the chart (if enabled) if (chart.settings.horizontalZoom || chart.settings.verticalZoom) window.addEventListener("wheel", e => { // Prevent default scrolling behavior e.stopPropagation() e.preventDefault() // Recalculate zoom based on the mouse wheel event chart.zoom.recalculate(e) // Update chart and redraw chart.updateBounds() chart.draw(false) this.addInteractivity() this.mouseMoveFunc(e, chart) }, // Has to be active to stop default scroll behaviour { passive: false }) } /* Asynchronously adds interactivity to the chart. */ async addInteractivity() { setTimeout(() => { console.time("2") // Set dimensions of effect canvas this.effectCanvas.width = this.canvas.width this.effectCanvas.height = this.canvas.height // Set dimensions of detection canvas this.detectionCanvas.width = this.canvas.width this.detectionCanvas.height = this.canvas.height // Draw detection map on the detection canvas this.chart.drawDetectionMap(this.detectionCanvas.getContext("2d")) console.timeEnd("2") }, 0) } /** * Draws the chart based on the provided settings and data. * @param {Object} chartSettings - The settings for the chart. * @param {Array} data - The data to visualize on the chart. */ drawChart(chartSettings, data) { console.time("1") let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom) // Choose the correct type of chart switch (chartSettings.type) { case "point": this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager) break case "line": this.chart = new LineChart(this.canvas, data, chartSettings, zoomManager) break case "smoothline": this.chart = new SmoothLineChart(this.canvas, data, chartSettings, zoomManager) break case "pie": this.chart = new PieChart(this.canvas, data, chartSettings, zoomManager) break case "donut": this.chart = new DonutChart(this.canvas, data, chartSettings, zoomManager) break case "bar": this.chart = new BarChart(this.canvas, data, chartSettings, zoomManager) break case "area": this.chart = new AreaChart(this.canvas, data, chartSettings, zoomManager) break case "smootharea": this.chart = new SmoothAreaChart(this.canvas, data, chartSettings, zoomManager) break case "stacked": this.chart = new StackedChart(this.canvas, data, chartSettings, zoomManager) break } // Asynchronously update the legend setTimeout(this.chart.updateLegend(chartSettings.displayLegend, this.legend, this), 0) // Resize the canvas and chart based on the parent container and legend size this.chart.resizeCanvas(this.parent, this.legend.offsetHeight) this.chart.draw() console.timeEnd("1") this.addInteractivity() this.addListeners(this.chart) } }