|
|
|
|
@ -1,99 +1,43 @@
|
|
|
|
|
class Shape {
|
|
|
|
|
static globalIndex = 0
|
|
|
|
|
|
|
|
|
|
constructor(ctx, value, colId, x, y) {
|
|
|
|
|
this.ctx = ctx
|
|
|
|
|
this.value = value
|
|
|
|
|
this.colId = colId
|
|
|
|
|
this.x = x
|
|
|
|
|
this.y = y
|
|
|
|
|
this.index = Shape.globalIndex++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw() { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Rectangle extends Shape {
|
|
|
|
|
constructor(ctx, value, colId, x, y, w, h) {
|
|
|
|
|
super(ctx, value, colId, x, y)
|
|
|
|
|
this.w = w
|
|
|
|
|
this.h = h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(ctx = this.ctx) {
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.rect(this.x, this.y, this.w, this.h)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
//ctx.closePath()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Circle extends Shape {
|
|
|
|
|
constructor(ctx, value, colId, x, y, r) {
|
|
|
|
|
super(ctx, value, colId, x, y)
|
|
|
|
|
this.r = r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(ctx = this.ctx) {
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
//ctx.closePath()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PieSlice extends Circle {
|
|
|
|
|
constructor(ctx, value, colId, x, y, r, sAngle, eAngle) {
|
|
|
|
|
super(ctx, value, colId, x, y, r)
|
|
|
|
|
this.sAngle = sAngle
|
|
|
|
|
this.eAngle = eAngle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(ctx = this.ctx) {
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.moveTo(this.x, this.y)
|
|
|
|
|
ctx.arc(this.x, this.y, this.r, this.sAngle, this.eAngle)
|
|
|
|
|
ctx.lineTo(this.x, this.y)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
//ctx.closePath()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DonutSlice extends PieSlice {
|
|
|
|
|
constructor(ctx, value, colId, x, y, r, sAngle, eAngle, r2) {
|
|
|
|
|
super(ctx, value, colId, x, y, r, sAngle, eAngle)
|
|
|
|
|
this.r2 = r2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(ctx = this.ctx) {
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
// move to start of the slice using polar coordinates
|
|
|
|
|
ctx.moveTo(this.x + this.r2 * Math.cos(this.sAngle), this.y + this.r2 * Math.sin(this.sAngle))
|
|
|
|
|
ctx.arc(this.x, this.y, this.r, this.sAngle, this.eAngle, false)
|
|
|
|
|
ctx.arc(this.x, this.y, this.r2, this.eAngle, this.sAngle, true)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
//ctx.closePath()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find the largest value among the provided data points.
|
|
|
|
|
*
|
|
|
|
|
* @param {Array} data - An array of columns with associated values.
|
|
|
|
|
* @returns {number} - The largest value found in the data.
|
|
|
|
|
*/
|
|
|
|
|
function getLargest(data) {
|
|
|
|
|
// Initialize largest with the first value of the first category
|
|
|
|
|
let largest = +data[0].values[0]
|
|
|
|
|
|
|
|
|
|
// Iterate through each category(column)
|
|
|
|
|
data.forEach(categ => {
|
|
|
|
|
// Iterate through the values of each category
|
|
|
|
|
for (let i = 0; i < categ.values.length; i++)
|
|
|
|
|
// Convert the value to a number and compare with the current largest
|
|
|
|
|
if (+categ.values[i] > +largest)
|
|
|
|
|
// If the current value is larger, update the largest value
|
|
|
|
|
largest = +categ.values[i]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return largest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find the smallest value among the provided data points.
|
|
|
|
|
*
|
|
|
|
|
* @param {Array} data - An array of columns with associated values.
|
|
|
|
|
* @returns {number} - The smallest value found in the data.
|
|
|
|
|
*/
|
|
|
|
|
function getSmallest(data) {
|
|
|
|
|
// Initialize smallest with the first value of the first category
|
|
|
|
|
let smallest = +data[0].values[0]
|
|
|
|
|
|
|
|
|
|
// Iterate through each category(column)
|
|
|
|
|
data.forEach(categ => {
|
|
|
|
|
// Iterate through the values of each category
|
|
|
|
|
for (let i = 0; i < categ.values.length; i++)
|
|
|
|
|
// Convert the value to a number and compare with the current smallest
|
|
|
|
|
if (+categ.values[i] < +smallest)
|
|
|
|
|
// If the current value is smaller, update the smallest value
|
|
|
|
|
smallest = +categ.values[i]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -102,32 +46,66 @@ function getSmallest(data) {
|
|
|
|
|
|
|
|
|
|
// Lighten or darken a hex color
|
|
|
|
|
function adjustColor(color, amount) {
|
|
|
|
|
return '#' + color.replace(/^#/, '').replace(/../g, color => ('0'+Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16)).substr(-2));
|
|
|
|
|
return '#' + color.replace(/^#/, '').replace(/../g, color => ('0' + Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16)).substr(-2))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Class representing a generic chart
|
|
|
|
|
*/
|
|
|
|
|
class Chart {
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new instance of the Chart class
|
|
|
|
|
*
|
|
|
|
|
* @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on
|
|
|
|
|
* @param {Array<Object>} data - The data to visualize on the chart
|
|
|
|
|
* @param {Object} settings - The settings for the chart
|
|
|
|
|
*/
|
|
|
|
|
constructor(canvas, data, settings) {
|
|
|
|
|
this.data = data
|
|
|
|
|
this.settings = settings
|
|
|
|
|
this.canvas = canvas
|
|
|
|
|
this.ctx = canvas.getContext("2d")
|
|
|
|
|
this.largest = getLargest(data)
|
|
|
|
|
this.smallest = getSmallest(data)
|
|
|
|
|
this.dataLen = data[0].values.length
|
|
|
|
|
this.data = data // The data to visualize on the chart
|
|
|
|
|
this.settings = settings // The settings for the chart
|
|
|
|
|
this.canvas = canvas // The canvas element to draw the chart on
|
|
|
|
|
this.ctx = canvas.getContext("2d") // The 2D drawing context for the canvas
|
|
|
|
|
|
|
|
|
|
// Calculate the largest and smallest values from the data
|
|
|
|
|
this.largest = getLargest(data) // The largest value in the data
|
|
|
|
|
this.smallest = getSmallest(data) // The smallest value in the data
|
|
|
|
|
|
|
|
|
|
// Find the largest length of values from all categories
|
|
|
|
|
this.dataLen = Math.max( // get max index
|
|
|
|
|
...data.map(category => // from each category
|
|
|
|
|
Object.keys(category.values)[Object.keys(category.values).length - 1] // gets last(largest) key
|
|
|
|
|
)) + 1 // indexes start at zero, so the length is +1
|
|
|
|
|
|
|
|
|
|
// Update the bounds of the chart based on the canvas size and margins
|
|
|
|
|
this.updateBounds()
|
|
|
|
|
|
|
|
|
|
// Initialize an empty array to store chart objects (shapes)
|
|
|
|
|
this.objects = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the bounds based on the canvas size and margin settings.
|
|
|
|
|
*/
|
|
|
|
|
updateBounds() {
|
|
|
|
|
// Calculate the bounds using the canvas and margin settings
|
|
|
|
|
this.bounds = this.getBounds(this.canvas, this.settings.margin)
|
|
|
|
|
|
|
|
|
|
// Calculate the scale and extreme values based on the largest and smallest data points
|
|
|
|
|
this.scale = this.bounds.height
|
|
|
|
|
- (this.largest >= 0 ? (this.bounds.bottom - this.bounds.xAxis) : 0)
|
|
|
|
|
this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the bounds of the graph area based on the canvas size and margin.
|
|
|
|
|
* @param {HTMLCanvasElement} canvas - The canvas element.
|
|
|
|
|
* @param {number} graphMargin - The margin around the graph area.
|
|
|
|
|
* @returns {Object} - The calculated bounds.
|
|
|
|
|
*/
|
|
|
|
|
getBounds(canvas, graphMargin) {
|
|
|
|
|
// Check if settings are valid
|
|
|
|
|
if (isNaN(graphMargin))
|
|
|
|
|
throw new Error("Invalid margin: " + graphMargin)
|
|
|
|
|
|
|
|
|
|
let result = {
|
|
|
|
|
top: graphMargin,
|
|
|
|
|
bottom: canvas.height - graphMargin,
|
|
|
|
|
@ -137,176 +115,335 @@ class Chart {
|
|
|
|
|
width: canvas.width - 2 * graphMargin,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate the xAxis position based on the smallest and largest data points
|
|
|
|
|
if (this.smallest >= 0)
|
|
|
|
|
// If the smallest data point is non-negative, set the xAxis to the bottom border
|
|
|
|
|
result.xAxis = result.bottom
|
|
|
|
|
else if (this.largest <= 0)
|
|
|
|
|
// If the largest data point is non-positive, set the xAxis to the top margin
|
|
|
|
|
result.xAxis = result.top
|
|
|
|
|
else
|
|
|
|
|
// If the smallest and largest data points have different signs,
|
|
|
|
|
// calculate the position of the xAxis to ensure proportional distribution
|
|
|
|
|
// between the two points on the graph area.
|
|
|
|
|
result.xAxis = result.bottom
|
|
|
|
|
- result.height / ((Math.abs(this.largest)) + Math.abs(this.smallest)) * Math.abs(this.smallest)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draw the title on the canvas.
|
|
|
|
|
*/
|
|
|
|
|
drawTitle() {
|
|
|
|
|
// Check if settings are valid
|
|
|
|
|
if (typeof this.settings.title !== "string" && typeof this.settings.title !== "number")
|
|
|
|
|
throw new Error("Invalid title. Expected number or string, received: " + typeof this.settings.titleFont)
|
|
|
|
|
|
|
|
|
|
// Calculate the x and y coordinates for the title
|
|
|
|
|
// place in in the middle of the chart
|
|
|
|
|
let x = this.canvas.width / 2
|
|
|
|
|
let y = 25
|
|
|
|
|
|
|
|
|
|
// Set the font style and alignment
|
|
|
|
|
this.ctx.font = this.settings.titleFont.size + "px " + this.settings.titleFont.font
|
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
|
this.ctx.textAlign = "center"
|
|
|
|
|
|
|
|
|
|
// Draw the title text on the canvas
|
|
|
|
|
this.ctx.fillText(this.settings.title, x, y)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateLegend(displayLegend, legend) {
|
|
|
|
|
/**
|
|
|
|
|
* Update the legend based on the displayLegend flag and provided legend element.
|
|
|
|
|
* @param {boolean} displayLegend - Whether to display the legend.
|
|
|
|
|
* @param {HTMLElement} legend - The HTML element to display the legend.
|
|
|
|
|
*/
|
|
|
|
|
updateLegend(displayLegend, legend, chartLoader = null) {
|
|
|
|
|
// Check if chart legend is enabled
|
|
|
|
|
if (displayLegend) {
|
|
|
|
|
let legendHTML = ""
|
|
|
|
|
this.data.forEach(categ => {
|
|
|
|
|
legendHTML += "<div><span style='display:inline-block;width:20px;background-color:" + categ.color + ";'> </span> " + categ.col_name + "</div>"
|
|
|
|
|
// Generate HTML for each category/column
|
|
|
|
|
this.data.forEach((categ, index) => {
|
|
|
|
|
// Create a new div element for each category
|
|
|
|
|
let element = document.createElement("div")
|
|
|
|
|
|
|
|
|
|
// Set attribute to store column ID
|
|
|
|
|
//element.setAttribute("col-id", index)
|
|
|
|
|
|
|
|
|
|
// Add event listener to handle mouse down event
|
|
|
|
|
element.addEventListener("mousemove", e => {
|
|
|
|
|
// Retrieve the column ID from the clicked element
|
|
|
|
|
//let colId = e.target.getAttribute("col-id")
|
|
|
|
|
|
|
|
|
|
// Filter objects based on the column ID
|
|
|
|
|
let objects = chartLoader.chart.objects.filter(object => object.colId == index)
|
|
|
|
|
|
|
|
|
|
// Set effect canvas opacity and draw effect
|
|
|
|
|
chartLoader.effectCanvas.style.opacity = 1
|
|
|
|
|
chartLoader.chart.drawEffect(chartLoader.effectCanvas.getContext("2d"), objects)
|
|
|
|
|
|
|
|
|
|
e.stopImmediatePropagation()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
legend.innerHTML = legendHTML
|
|
|
|
|
// Set inner HTML for the legend element
|
|
|
|
|
element.innerHTML = "<span style='display:inline-block;width:20px;background-color:" + categ.color + ";'> </span> " + categ.col_name
|
|
|
|
|
// Append the element to the legend container
|
|
|
|
|
legend.appendChild(element)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Update the legend HTML and display it
|
|
|
|
|
//legend.innerHTML = legendHTML
|
|
|
|
|
legend.style.display = "block"
|
|
|
|
|
} else {
|
|
|
|
|
// Hide the legend
|
|
|
|
|
legend.style.display = "none"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resize the canvas to match its container's size.
|
|
|
|
|
* This function clears the canvas, updates its dimensions, and updates bounds.
|
|
|
|
|
*/
|
|
|
|
|
resizeCanvas() {
|
|
|
|
|
this.clear()
|
|
|
|
|
//set size
|
|
|
|
|
// Set the canvas width to match the client width of the container
|
|
|
|
|
this.canvas.width = this.canvas.clientWidth
|
|
|
|
|
// Set the canvas height to match the client height of the container
|
|
|
|
|
this.canvas.height = this.canvas.clientHeight
|
|
|
|
|
|
|
|
|
|
// Remove old content
|
|
|
|
|
this.clear()
|
|
|
|
|
// Update the charts bounds after resizing
|
|
|
|
|
this.updateBounds()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear the canvas and remove all objects
|
|
|
|
|
/**
|
|
|
|
|
* Clear the canvas and remove all stored objects.
|
|
|
|
|
*/
|
|
|
|
|
clear() {
|
|
|
|
|
//reset canvas color
|
|
|
|
|
// Check if a custom background color is set
|
|
|
|
|
if (this.settings.backgroundColor == null) {
|
|
|
|
|
// Clear the canvas using the default clear method
|
|
|
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
|
} else {
|
|
|
|
|
// Set the canvas background color to the custom color
|
|
|
|
|
this.ctx.fillStyle = this.settings.backgroundColor
|
|
|
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset the global index for Shapes
|
|
|
|
|
Shape.globalIndex = 0
|
|
|
|
|
// Clear the objects array
|
|
|
|
|
this.objects = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draw a detection map. Draws all shapes on new canvas and endcodes indexes into colors.
|
|
|
|
|
*
|
|
|
|
|
* @param {CanvasRenderingContext2D} ctx - The 2D drawing context for the canvas.
|
|
|
|
|
*/
|
|
|
|
|
drawDetectionMap(ctx) {
|
|
|
|
|
// Move the drawing point to (0.5, 0.5) to avoid anti-aliasing issues
|
|
|
|
|
ctx.moveTo(0.5, 0.5)
|
|
|
|
|
ctx.lineWidth = 3
|
|
|
|
|
// Set the line width to make the map slightly bigger than visible shapes
|
|
|
|
|
//ctx.lineWidth = 3
|
|
|
|
|
|
|
|
|
|
// Clear the entire canvas to prepare for redrawing
|
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
|
|
|
|
this.objects.forEach(object => {
|
|
|
|
|
// Encode shape index into color
|
|
|
|
|
let color = "#" + object.index.toString(16).padStart(6, '0')
|
|
|
|
|
ctx.fillStyle = color
|
|
|
|
|
ctx.strokeStyle = color
|
|
|
|
|
|
|
|
|
|
// Draw the shape
|
|
|
|
|
object.draw(ctx)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draw effects for given shape. Lightens the color and creates shadow border
|
|
|
|
|
*
|
|
|
|
|
* @param {CanvasRenderingContext2D} ctx - The 2D drawing context for the canvas.
|
|
|
|
|
* @param {Array} objects - An array of shapes on the canvas.
|
|
|
|
|
*/
|
|
|
|
|
drawEffect(ctx, objects) {
|
|
|
|
|
objects.forEach(object => {
|
|
|
|
|
/*ctx.globalCompositeOperation = "source-over"
|
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,1)'
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0)'
|
|
|
|
|
object.draw(ctx)
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
// Clear the entire canvas to prepare for redrawing
|
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
|
ctx.fill()
|
|
|
|
|
|
|
|
|
|
ctx.globalCompositeOperation = "source-out"
|
|
|
|
|
ctx.lineWidth = 3
|
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,0)'
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.7)'
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
ctx.scale(2, 2)
|
|
|
|
|
|
|
|
|
|
ctx.globalCompositeOperation = "source-over"
|
|
|
|
|
ctx.fillStyle = 'rgba(255,255,255,0.2)'
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0)'
|
|
|
|
|
object.draw(ctx)
|
|
|
|
|
ctx.stroke()*/
|
|
|
|
|
ctx.shadowBlur = 15
|
|
|
|
|
objects.forEach(object => {
|
|
|
|
|
// Get the color of the object based on its column ID
|
|
|
|
|
let color = this.data[object.colId].color
|
|
|
|
|
// Adjust the color to make it lighter by 20 units
|
|
|
|
|
let lighterColor = adjustColor(color, 20)
|
|
|
|
|
ctx.fillStyle = lighterColor
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.3)'
|
|
|
|
|
ctx.lineWidth = 3
|
|
|
|
|
|
|
|
|
|
// Set the stroke style to a semi-transparent black
|
|
|
|
|
ctx.strokeStyle = lighterColor//'rgba(0,0,0,0.3)'
|
|
|
|
|
//ctx.lineWidth = 0
|
|
|
|
|
ctx.shadowColor = 'rgba(0,0,0,1)'
|
|
|
|
|
// Draw the object
|
|
|
|
|
object.draw(ctx)
|
|
|
|
|
ctx.shadowColor = 'rgba(0,0,0,0)'
|
|
|
|
|
// Stroke to draw the border
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ctx.scale(0.5, 0.5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draws the axis on the chart
|
|
|
|
|
*
|
|
|
|
|
* @param {boolean} [displayAxisValues=this.settings.displayAxisValues] - If axis values should be displayed
|
|
|
|
|
*/
|
|
|
|
|
drawAxis(displayAxisValues = this.settings.displayAxisValues) {
|
|
|
|
|
// Set the font for the axis labels
|
|
|
|
|
this.ctx.font = this.settings.labelFont.size + "px " + this.settings.labelFont.font
|
|
|
|
|
if (this.settings.yStep <= 0 || !this.settings.yStep) this.settings.yStep = 1
|
|
|
|
|
|
|
|
|
|
// Ensure yStep is positive and defined
|
|
|
|
|
if (this.settings.yStep <= 0 || !this.settings.yStep)
|
|
|
|
|
this.settings.yStep = 1
|
|
|
|
|
|
|
|
|
|
// Draw ticks and labels for the Y-axis
|
|
|
|
|
this.drawYAxisTicks()
|
|
|
|
|
|
|
|
|
|
// Draw ticks and labels for the X-axis
|
|
|
|
|
this.drawXAxisTicks(displayAxisValues)
|
|
|
|
|
|
|
|
|
|
// Draw lines for both X and Y axis
|
|
|
|
|
this.drawAxisLines()
|
|
|
|
|
|
|
|
|
|
// Draw labels for both X and Y axis
|
|
|
|
|
this.drawAxisLabels()
|
|
|
|
|
|
|
|
|
|
// Draw the title if enabled
|
|
|
|
|
if (this.settings.displayTitle)
|
|
|
|
|
this.drawTitle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draws the ticks and labels on the Y-axis
|
|
|
|
|
*/
|
|
|
|
|
drawYAxisTicks() {
|
|
|
|
|
this.ctx.beginPath()
|
|
|
|
|
|
|
|
|
|
// Loop through the Y-axis values to draw ticks and labels
|
|
|
|
|
for (let i = (this.smallest < 0) ? this.smallest : 0; i <= (this.largest >= 0 ? this.largest : 0); i += parseFloat(this.settings.yStep)) {
|
|
|
|
|
|
|
|
|
|
// Set stroke style and line width for the ticks
|
|
|
|
|
this.ctx.strokeStyle = "#BBB"
|
|
|
|
|
this.ctx.lineWidth = 1
|
|
|
|
|
|
|
|
|
|
// Calculate the Y-position for the tick
|
|
|
|
|
let yPos = Math.round(this.bounds.xAxis - i * this.scale / this.extreme)
|
|
|
|
|
|
|
|
|
|
//support line
|
|
|
|
|
// Draw support lines if enabled
|
|
|
|
|
if (this.settings.displaySupportLines) {
|
|
|
|
|
this.ctx.moveTo(this.bounds.left, yPos)
|
|
|
|
|
this.ctx.lineTo(this.bounds.right, yPos)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Y axis value
|
|
|
|
|
// Set text style and alignment for the Y-axis values
|
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
|
this.ctx.textAlign = "center"
|
|
|
|
|
this.ctx.textAlign = "end"
|
|
|
|
|
this.ctx.fillText(i, this.bounds.left - 3, yPos)
|
|
|
|
|
|
|
|
|
|
// Draw the tick
|
|
|
|
|
this.ctx.stroke()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//X axis value
|
|
|
|
|
/**
|
|
|
|
|
* Draws the ticks and labels on the X-axis
|
|
|
|
|
*
|
|
|
|
|
* @param {boolean} displayAxisValues - Whether to display axis values
|
|
|
|
|
*/
|
|
|
|
|
drawXAxisTicks(displayAxisValues) {
|
|
|
|
|
// Check if axis values should be displayed
|
|
|
|
|
if (displayAxisValues) {
|
|
|
|
|
// Set text style and alignment for the X-axis values
|
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
|
this.ctx.textAlign = "center"
|
|
|
|
|
|
|
|
|
|
// Loop through the data to draw X-axis ticks and labels
|
|
|
|
|
for (let i = 0; i < this.dataLen; i++) {
|
|
|
|
|
// Calculate the X-position for the tick
|
|
|
|
|
let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i
|
|
|
|
|
|
|
|
|
|
// Set the text for the X-axis label
|
|
|
|
|
let text = (i + 1).toString()
|
|
|
|
|
if (this.settings.custom_x_values !== "")
|
|
|
|
|
text = this.settings.custom_x_values.split(';')[i]
|
|
|
|
|
|
|
|
|
|
// Draw the X-axis label
|
|
|
|
|
this.ctx.fillText(text, x, this.bounds.bottom + 18)
|
|
|
|
|
}
|
|
|
|
|
this.ctx.closePath()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//X and Y axis
|
|
|
|
|
/**
|
|
|
|
|
* Draws the lines for both X and Y axis
|
|
|
|
|
*/
|
|
|
|
|
drawAxisLines() {
|
|
|
|
|
// Set the stroke style and line width for the axis lines
|
|
|
|
|
this.ctx.strokeStyle = "black"
|
|
|
|
|
this.ctx.lineWidth = "2px"
|
|
|
|
|
|
|
|
|
|
// Begin the path for drawing the axis lines
|
|
|
|
|
this.ctx.beginPath()
|
|
|
|
|
// Draw the vertical Y-axis line
|
|
|
|
|
this.ctx.moveTo(this.bounds.left, this.bounds.top)
|
|
|
|
|
this.ctx.lineTo(this.bounds.left, this.bounds.bottom)
|
|
|
|
|
|
|
|
|
|
// Draw the horizontal X-axis line
|
|
|
|
|
this.ctx.moveTo(this.bounds.left, this.bounds.xAxis)
|
|
|
|
|
this.ctx.lineTo(this.bounds.right, this.bounds.xAxis)
|
|
|
|
|
|
|
|
|
|
this.ctx.stroke()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Axis labels
|
|
|
|
|
//X axis text
|
|
|
|
|
/**
|
|
|
|
|
* Draws the labels for both X and Y axis
|
|
|
|
|
*/
|
|
|
|
|
drawAxisLabels() {
|
|
|
|
|
// Begin the path for drawing the axis labels
|
|
|
|
|
this.ctx.beginPath()
|
|
|
|
|
|
|
|
|
|
// Set font and text style for the X-axis label
|
|
|
|
|
this.ctx.font = "20px Arial"
|
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
|
this.ctx.textAlign = "center"
|
|
|
|
|
this.ctx.fillText(this.settings.xLabel, this.bounds.width / 2 + this.bounds.left, this.bounds.height + 2 * this.bounds.top - 5)
|
|
|
|
|
|
|
|
|
|
//Y axis text
|
|
|
|
|
// Draw the X-axis label at the center of the axis
|
|
|
|
|
this.ctx.fillText(
|
|
|
|
|
this.settings.xLabel, // Text to display (X-axis label)
|
|
|
|
|
this.bounds.width / 2 + this.bounds.left, // X coordinate
|
|
|
|
|
this.bounds.height + 2 * this.bounds.top - 5 // Y coordinate
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Save the current context before rotating for the Y-axis label
|
|
|
|
|
this.ctx.save()
|
|
|
|
|
// Rotate the context to draw the Y-axis label vertically
|
|
|
|
|
this.ctx.rotate(-Math.PI / 2)
|
|
|
|
|
// Recenter on the rotated axis
|
|
|
|
|
this.ctx.textAlign = "center"
|
|
|
|
|
this.ctx.fillText(this.settings.yLabel, -(this.bounds.left + this.bounds.height / 2), 18)
|
|
|
|
|
|
|
|
|
|
// Draw the Y-axis label at the center of the axis
|
|
|
|
|
this.ctx.fillText(
|
|
|
|
|
this.settings.yLabel, // Text to display (Y-axis label)
|
|
|
|
|
-(this.bounds.left + this.bounds.height / 2), // Y coordinate
|
|
|
|
|
18 // X coordinate
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Restore the context to its previous state
|
|
|
|
|
this.ctx.restore()
|
|
|
|
|
this.ctx.stroke()
|
|
|
|
|
|
|
|
|
|
if (this.settings.displayTitle)
|
|
|
|
|
this.drawTitle()
|
|
|
|
|
this.ctx.stroke()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|