|
|
|
@ -44,7 +44,13 @@ function getSmallest(data) {
|
|
|
|
return smallest
|
|
|
|
return smallest
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Lighten or darken a hex color
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Lighten or darken a hex color.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @param {string} color - The hex color code.
|
|
|
|
|
|
|
|
* @param {number} amount - The amount to lighten or darken the color by.
|
|
|
|
|
|
|
|
* @returns {string} The adjusted hex color.
|
|
|
|
|
|
|
|
*/
|
|
|
|
function adjustColor(color, amount) {
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -59,11 +65,13 @@ class Chart {
|
|
|
|
* @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on
|
|
|
|
* @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on
|
|
|
|
* @param {Array<Object>} data - The data to visualize on the chart
|
|
|
|
* @param {Array<Object>} data - The data to visualize on the chart
|
|
|
|
* @param {Object} settings - The settings for the chart
|
|
|
|
* @param {Object} settings - The settings for the chart
|
|
|
|
|
|
|
|
* @param {ZoomManager} zoom - The zoom manager for the chart.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
constructor(canvas, data, settings) {
|
|
|
|
constructor(canvas, data, settings, zoom) {
|
|
|
|
this.data = data // The data to visualize on the chart
|
|
|
|
this.data = data // The data to visualize on the chart
|
|
|
|
this.settings = settings // The settings for the chart
|
|
|
|
this.settings = settings // The settings for the chart
|
|
|
|
this.canvas = canvas // The canvas element to draw the chart on
|
|
|
|
this.canvas = canvas // The canvas element to draw the chart on
|
|
|
|
|
|
|
|
this.zoom = zoom // The zoom manager for the chart.
|
|
|
|
this.ctx = canvas.getContext("2d") // The 2D drawing context for the canvas
|
|
|
|
this.ctx = canvas.getContext("2d") // The 2D drawing context for the canvas
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the largest and smallest values from the data
|
|
|
|
// Calculate the largest and smallest values from the data
|
|
|
|
@ -74,7 +82,7 @@ class Chart {
|
|
|
|
this.dataLen = Math.max( // get max index
|
|
|
|
this.dataLen = Math.max( // get max index
|
|
|
|
...data.map(category => // from each category
|
|
|
|
...data.map(category => // from each category
|
|
|
|
Object.keys(category.values)[Object.keys(category.values).length - 1] // gets last(largest) key
|
|
|
|
Object.keys(category.values)[Object.keys(category.values).length - 1] // gets last(largest) key
|
|
|
|
)) + 1 // indexes start at zero, so the length is +1
|
|
|
|
)) + 1 // indexes start at zero, so the length is +1
|
|
|
|
|
|
|
|
|
|
|
|
// Update the bounds of the chart based on the canvas size and margins
|
|
|
|
// Update the bounds of the chart based on the canvas size and margins
|
|
|
|
this.updateBounds()
|
|
|
|
this.updateBounds()
|
|
|
|
@ -93,6 +101,8 @@ class Chart {
|
|
|
|
this.scale = this.bounds.height
|
|
|
|
this.scale = this.bounds.height
|
|
|
|
- (this.largest >= 0 ? (this.bounds.bottom - this.bounds.xAxis) : 0)
|
|
|
|
- (this.largest >= 0 ? (this.bounds.bottom - this.bounds.xAxis) : 0)
|
|
|
|
this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest)
|
|
|
|
this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.zoomBounds = this.getZoomBounds()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -128,10 +138,51 @@ class Chart {
|
|
|
|
// between the two points on the graph area.
|
|
|
|
// between the two points on the graph area.
|
|
|
|
result.xAxis = result.bottom
|
|
|
|
result.xAxis = result.bottom
|
|
|
|
- result.height / ((Math.abs(this.largest)) + Math.abs(this.smallest)) * Math.abs(this.smallest)
|
|
|
|
- result.height / ((Math.abs(this.largest)) + Math.abs(this.smallest)) * Math.abs(this.smallest)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Retrieves the zoomed bounds of the chart.
|
|
|
|
|
|
|
|
* @returns {Object} An object containing zoomed bounds information.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
getZoomBounds() {
|
|
|
|
|
|
|
|
let result = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get screen coordinates of the top-left corner of the bounds
|
|
|
|
|
|
|
|
let pos = this.zoom.worldToScreen(this.bounds.left, this.bounds.top)
|
|
|
|
|
|
|
|
result.left = pos.x
|
|
|
|
|
|
|
|
result.top = pos.y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get screen coordinates of the bottom-right corner of the bounds
|
|
|
|
|
|
|
|
pos = this.zoom.worldToScreen(this.bounds.right, this.bounds.bottom)
|
|
|
|
|
|
|
|
result.right = pos.x
|
|
|
|
|
|
|
|
result.bottom = pos.y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate width and height of the zoomed bounds
|
|
|
|
|
|
|
|
result.width = this.bounds.width * this.zoom.scaleX
|
|
|
|
|
|
|
|
result.height = this.bounds.height * this.zoom.scaleY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get the screen coordinate of the x-axis
|
|
|
|
|
|
|
|
result.xAxis = this.zoom.worldToScreen(null, this.bounds.xAxis).y
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Checks if a given position is within the bounds.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @param {Object} pos - The position to check with x and y coordinates.
|
|
|
|
|
|
|
|
* @returns {boolean} Returns true if the position is within the bounds, false otherwise.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
isInBounds(pos) {
|
|
|
|
|
|
|
|
return !(
|
|
|
|
|
|
|
|
pos.x < this.bounds.left ||
|
|
|
|
|
|
|
|
pos.x > this.bounds.right ||
|
|
|
|
|
|
|
|
pos.y < this.bounds.top ||
|
|
|
|
|
|
|
|
pos.y > this.bounds.bottom)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Draw the title on the canvas.
|
|
|
|
* Draw the title on the canvas.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@ -252,13 +303,16 @@ class Chart {
|
|
|
|
ctx.fill()
|
|
|
|
ctx.fill()
|
|
|
|
|
|
|
|
|
|
|
|
this.objects.forEach(object => {
|
|
|
|
this.objects.forEach(object => {
|
|
|
|
// Encode shape index into color
|
|
|
|
// Only draw objects on screen
|
|
|
|
let color = "#" + object.index.toString(16).padStart(6, '0')
|
|
|
|
if (this.isInBounds(object.getCenter())) {
|
|
|
|
ctx.fillStyle = color
|
|
|
|
// Encode shape index into color
|
|
|
|
ctx.strokeStyle = color
|
|
|
|
let color = "#" + object.index.toString(16).padStart(6, '0')
|
|
|
|
|
|
|
|
ctx.fillStyle = color
|
|
|
|
// Draw the shape
|
|
|
|
ctx.strokeStyle = color
|
|
|
|
object.draw(ctx)
|
|
|
|
|
|
|
|
|
|
|
|
// Draw the shape
|
|
|
|
|
|
|
|
object.draw(ctx)
|
|
|
|
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -272,8 +326,6 @@ class Chart {
|
|
|
|
// Clear the entire canvas to prepare for redrawing
|
|
|
|
// Clear the entire canvas to prepare for redrawing
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
|
|
ctx.fill()
|
|
|
|
ctx.fill()
|
|
|
|
|
|
|
|
|
|
|
|
ctx.scale(2, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.shadowBlur = 15
|
|
|
|
ctx.shadowBlur = 15
|
|
|
|
objects.forEach(object => {
|
|
|
|
objects.forEach(object => {
|
|
|
|
@ -293,8 +345,6 @@ class Chart {
|
|
|
|
// Stroke to draw the border
|
|
|
|
// Stroke to draw the border
|
|
|
|
ctx.stroke()
|
|
|
|
ctx.stroke()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
ctx.scale(0.5, 0.5)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -325,6 +375,11 @@ class Chart {
|
|
|
|
// Draw the title if enabled
|
|
|
|
// Draw the title if enabled
|
|
|
|
if (this.settings.displayTitle)
|
|
|
|
if (this.settings.displayTitle)
|
|
|
|
this.drawTitle()
|
|
|
|
this.drawTitle()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.ctx.beginPath()
|
|
|
|
|
|
|
|
this.ctx.rect(this.bounds.left, this.bounds.top, this.bounds.width, this.bounds.height)
|
|
|
|
|
|
|
|
this.ctx.clip()
|
|
|
|
|
|
|
|
this.ctx.closePath()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -341,18 +396,23 @@ class Chart {
|
|
|
|
this.ctx.lineWidth = 1
|
|
|
|
this.ctx.lineWidth = 1
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the Y-position for the tick
|
|
|
|
// Calculate the Y-position for the tick
|
|
|
|
let yPos = Math.round(this.bounds.xAxis - i * this.scale / this.extreme)
|
|
|
|
let y = this.bounds.xAxis - i * this.scale / this.extreme
|
|
|
|
|
|
|
|
y = this.zoom.worldToScreen(null, y).y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Skip text and lines if out of bounds
|
|
|
|
|
|
|
|
if (y < this.bounds.top || y > this.bounds.bottom)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
// Draw support lines if enabled
|
|
|
|
// Draw support lines if enabled
|
|
|
|
if (this.settings.displaySupportLines) {
|
|
|
|
if (this.settings.displaySupportLines) {
|
|
|
|
this.ctx.moveTo(this.bounds.left, yPos)
|
|
|
|
this.ctx.moveTo(this.bounds.left, y)
|
|
|
|
this.ctx.lineTo(this.bounds.right, yPos)
|
|
|
|
this.ctx.lineTo(this.bounds.right, y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set text style and alignment for the Y-axis values
|
|
|
|
// Set text style and alignment for the Y-axis values
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
this.ctx.fillStyle = "black"
|
|
|
|
this.ctx.textAlign = "end"
|
|
|
|
this.ctx.textAlign = "end"
|
|
|
|
this.ctx.fillText(i, this.bounds.left - 3, yPos)
|
|
|
|
this.ctx.fillText(i, this.bounds.left - 3, y)
|
|
|
|
|
|
|
|
|
|
|
|
// Draw the tick
|
|
|
|
// Draw the tick
|
|
|
|
this.ctx.stroke()
|
|
|
|
this.ctx.stroke()
|
|
|
|
@ -374,13 +434,18 @@ class Chart {
|
|
|
|
// Loop through the data to draw X-axis ticks and labels
|
|
|
|
// Loop through the data to draw X-axis ticks and labels
|
|
|
|
for (let i = 0; i < this.dataLen; i++) {
|
|
|
|
for (let i = 0; i < this.dataLen; i++) {
|
|
|
|
// Calculate the X-position for the tick
|
|
|
|
// Calculate the X-position for the tick
|
|
|
|
let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i
|
|
|
|
let x = this.zoomBounds.left + this.zoomBounds.width / (this.dataLen - 1) * i
|
|
|
|
|
|
|
|
let y = this.bounds.bottom + 18
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Skip text if out of bounds
|
|
|
|
|
|
|
|
if (x < this.bounds.left || x > this.bounds.right)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
// Set the text for the X-axis label
|
|
|
|
// Set the text for the X-axis label
|
|
|
|
let text = (i + 1).toString()
|
|
|
|
let text = (i + 1).toString()
|
|
|
|
|
|
|
|
|
|
|
|
// Draw the X-axis label
|
|
|
|
// Draw the X-axis label
|
|
|
|
this.ctx.fillText(text, x, this.bounds.bottom + 18)
|
|
|
|
this.ctx.fillText(text, x, y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.ctx.closePath()
|
|
|
|
this.ctx.closePath()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -401,8 +466,14 @@ class Chart {
|
|
|
|
this.ctx.lineTo(this.bounds.left, this.bounds.bottom)
|
|
|
|
this.ctx.lineTo(this.bounds.left, this.bounds.bottom)
|
|
|
|
|
|
|
|
|
|
|
|
// Draw the horizontal X-axis line
|
|
|
|
// Draw the horizontal X-axis line
|
|
|
|
this.ctx.moveTo(this.bounds.left, this.bounds.xAxis)
|
|
|
|
let y = this.zoomBounds.xAxis
|
|
|
|
this.ctx.lineTo(this.bounds.right, this.bounds.xAxis)
|
|
|
|
if (y < this.bounds.top)
|
|
|
|
|
|
|
|
y = this.bounds.top
|
|
|
|
|
|
|
|
if (y > this.bounds.bottom)
|
|
|
|
|
|
|
|
y = this.bounds.bottom
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.ctx.moveTo(this.bounds.left, y)
|
|
|
|
|
|
|
|
this.ctx.lineTo(this.bounds.right, y)
|
|
|
|
|
|
|
|
|
|
|
|
this.ctx.stroke()
|
|
|
|
this.ctx.stroke()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|