You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
12 KiB
342 lines
12 KiB
/**
|
|
* 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 = "<b>" + name + "</b><br><p>" + obj[0].value + "</p>"
|
|
|
|
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.style.left = tooltipPos.right.x + 5 + "px"
|
|
else
|
|
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)
|
|
}
|
|
}
|