Working Zoom

main
František Špaček 2 years ago
parent 2b3b9e3e3e
commit 6492022ead

@ -80,7 +80,7 @@ class ChartLoader {
} }
addListener(newChart) { addListener(chart) {
//Click //Click
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
const pos = { const pos = {
@ -93,13 +93,13 @@ class ChartLoader {
shapeIndex = this.getShapeIndex(this.detectionCanvas, pos) shapeIndex = this.getShapeIndex(this.detectionCanvas, pos)
if (shapeIndex !== null) { if (shapeIndex !== null) {
let obj = newChart.objects[shapeIndex] let obj = chart.objects[shapeIndex]
// Effect // Effect
let effectCtx = this.effectCanvas.getContext("2d") let effectCtx = this.effectCanvas.getContext("2d")
effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height)
this.effectCanvas.style.opacity = 1 this.effectCanvas.style.opacity = 1
newChart.drawEffect(effectCtx, [obj]) chart.drawEffect(effectCtx, [obj])
this.dataDiv.style.display = "block" this.dataDiv.style.display = "block"
@ -115,7 +115,7 @@ class ChartLoader {
this.dataDiv.style.top = pos.y - this.dataDiv.clientHeight + "px" this.dataDiv.style.top = pos.y - this.dataDiv.clientHeight + "px"
this.dataDiv.style.display = "block" this.dataDiv.style.display = "block"
let name = newChart.data[obj.colId.toString()].col_name let name = chart.data[obj.colId.toString()].col_name
this.dataDiv.innerHTML = "<b>" + name + "</b><br><p>" + obj.value + "</p>" this.dataDiv.innerHTML = "<b>" + name + "</b><br><p>" + obj.value + "</p>"
} }
else { else {
@ -145,18 +145,30 @@ class ChartLoader {
else else
this.selectedShapeIndex = this.clickedShapeIndex this.selectedShapeIndex = this.clickedShapeIndex
console.log(this.clickedShapeIndex + " " + /*console.log(this.clickedShapeIndex + " " +
this.getShapeIndex(this.detectionCanvas, pos) + " " + this.getShapeIndex(this.detectionCanvas, pos) + " " +
this.shapeSelected) this.shapeSelected)*/
}) })
window.addEventListener("resize", e => { window.addEventListener("resize", e => {
//newChart.updateLegend(graphSettings.displayLegend, data) //chart.updateLegend(chartSettings.displayLegend, data)
newChart.resizeCanvas(this.parent, this.legend.offsetHeight) chart.resizeCanvas(this.parent, this.legend.offsetHeight)
newChart.draw() chart.draw()
this.addInteractivity() this.addInteractivity()
}) })
if (chart.settings.horizontalZoom || chart.settings.verticalZoom)
window.addEventListener("wheel", e => {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
chart.zoom.recalculate(e, true, true)
chart.updateBounds()
chart.draw()
this.addInteractivity()
}, { passive: false })
} }
async addInteractivity() { async addInteractivity() {
@ -171,40 +183,41 @@ class ChartLoader {
}, 0) }, 0)
} }
drawChart(graphSettings, data) { drawChart(chartSettings, data) {
let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom)
//Choose the correct graph //Choose the correct graph
switch (graphSettings.type) { switch (chartSettings.type) {
case "point": case "point":
this.chart = new PointChart(this.canvas, data, graphSettings) this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager)
break break
case "line": case "line":
this.chart = new LineChart(this.canvas, data, graphSettings) this.chart = new LineChart(this.canvas, data, chartSettings, zoomManager)
break break
case "smoothline": case "smoothline":
this.chart = new SmoothLineChart(this.canvas, data, graphSettings) this.chart = new SmoothLineChart(this.canvas, data, chartSettings, zoomManager)
break break
case "pie": case "pie":
this.chart = new PieChart(this.canvas, data, graphSettings) this.chart = new PieChart(this.canvas, data, chartSettings, zoomManager)
break break
case "donut": case "donut":
this.chart = new DonutChart(this.canvas, data, graphSettings) this.chart = new DonutChart(this.canvas, data, chartSettings, zoomManager)
break break
case "bar": case "bar":
this.chart = new BarChart(this.canvas, data, graphSettings) this.chart = new BarChart(this.canvas, data, chartSettings, zoomManager)
break break
case "area": case "area":
this.chart = new AreaChart(this.canvas, data, graphSettings) this.chart = new AreaChart(this.canvas, data, chartSettings, zoomManager)
break break
case "smootharea": case "smootharea":
this.chart = new SmoothAreaChart(this.canvas, data, graphSettings) this.chart = new SmoothAreaChart(this.canvas, data, chartSettings, zoomManager)
break break
case "stacked": case "stacked":
this.chart = new StackedChart(this.canvas, data, graphSettings) this.chart = new StackedChart(this.canvas, data, chartSettings, zoomManager)
break break
} }
setTimeout(this.chart.updateLegend(graphSettings.displayLegend, this.legend, this), 0) setTimeout(this.chart.updateLegend(chartSettings.displayLegend, this.legend, this), 0)
this.chart.resizeCanvas(this.parent, this.legend.offsetHeight) this.chart.resizeCanvas(this.parent, this.legend.offsetHeight)
this.chart.draw() this.chart.draw()

@ -8,9 +8,11 @@ class AreaChart extends PointChart {
* @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) { */
super(canvas, data, settings) constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -35,17 +37,16 @@ class AreaChart extends PointChart {
if (categ.values[i] === null) continue if (categ.values[i] === null) continue
// Calculate the x and y coordinates for the current value // Calculate the x and y coordinates for the current value
let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i let pos = this.getPointPos(i, categ.values[i])
let y = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale xmax = pos.x
xmax = x
this.ctx.lineTo(x, y) this.ctx.lineTo(pos.x, pos.y)
} }
this.ctx.stroke() this.ctx.stroke()
// Complete the area by drawing lines to the x-axis and back to the starting point // Complete the area by drawing lines to the x-axis and back to the starting point
this.ctx.lineTo(xmax, this.bounds.xAxis) this.ctx.lineTo(xmax, this.zoomBounds.xAxis)
this.ctx.lineTo(this.bounds.left, this.bounds.xAxis) this.ctx.lineTo(this.zoomBounds.left, this.zoomBounds.xAxis)
this.ctx.globalAlpha = 0.5 this.ctx.globalAlpha = 0.5
this.ctx.fillStyle = categ.color this.ctx.fillStyle = categ.color
this.ctx.closePath() this.ctx.closePath()

@ -8,9 +8,11 @@ class BarChart extends 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) { */
super(canvas, data, settings) constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -18,6 +20,7 @@ class BarChart extends Chart {
*/ */
draw() { draw() {
// Calculate the number of bars/categories in the chart // Calculate the number of bars/categories in the chart
this.clear()
let barCount = this.data.length let barCount = this.data.length
// Draw the axis without displaying axis values // Draw the axis without displaying axis values
@ -40,11 +43,11 @@ class BarChart extends Chart {
// Value of the bar // Value of the bar
let value = categ.values[i] let value = categ.values[i]
// The left position of the bar in section // The left position of the bar in section
let left = this.bounds.left + (size * (i + 0.15) + (innerSize * num / barCount)) let left = this.zoomBounds.left + (size * (i + 0.15) + (innerSize * num / barCount)) * this.zoom.scaleX
// The height of the bar relative to the chart scale // The height of the bar relative to the chart scale
let bar_height = value / this.extreme * this.scale let bar_height = value / this.extreme * this.scale * this.zoom.scaleY
// The top position of the bar // The top position of the bar
let top = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale let top = this.zoomBounds.xAxis - categ.values[i] / this.extreme * this.scale * this.zoom.scaleY
// Draw x-axis labels // Draw x-axis labels
// Only for first category to avoid repeated drawings // Only for first category to avoid repeated drawings
@ -55,18 +58,28 @@ class BarChart extends Chart {
this.ctx.font = "16px Arial" this.ctx.font = "16px Arial"
this.ctx.fillStyle = "black" this.ctx.fillStyle = "black"
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText(text, this.bounds.width / this.dataLen * i + size / 2 + this.bounds.left, this.bounds.bottom + 15) this.ctx.fillText(text, this.zoomBounds.width / this.dataLen * i + size / 2 + this.zoomBounds.left, this.bounds.bottom + 15)
this.ctx.stroke() this.ctx.stroke()
this.ctx.closePath() this.ctx.closePath()
} }
// Set the fill color and draw the rectangle for the current bar // Increment the count of bars
num++ num++
this.ctx.fillStyle = categ.color
this.ctx.lineWidth = 0 // Create a new Rectangle object representing the current bar
let new_object = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height) let newObject = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height)
new_object.draw() // Add the new Rectangle object to the list of objects
this.objects.push(new_object) this.objects.push(newObject)
// Check if the center of the new bar is within the bounds
if (this.isInBounds(newObject.getCenter())) {
// Set the fill color to the category color
this.ctx.fillStyle = categ.color
// Set the line width to 0 to prevent drawing the border
this.ctx.lineWidth = 0
// Draw the filled rectangle representing the current bar
newObject.draw()
}
}) })
} }
} }

@ -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()
} }
/** /**
@ -132,6 +142,47 @@ class Chart {
return result 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
}
/**
* 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)
}
}) })
} }
@ -273,8 +327,6 @@ class Chart {
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 => {
// Get the color of the object based on its column ID // Get the color of the object based on its column ID
@ -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()
} }

@ -8,9 +8,11 @@ class DonutChart extends 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) { */
super(canvas, data, settings) constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**

@ -8,9 +8,11 @@ class LineChart extends PointChart {
* @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) { */
super(canvas, data, settings) constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -33,10 +35,9 @@ class LineChart extends PointChart {
if (categ.values[i] === null) continue if (categ.values[i] === null) continue
// Calculate the x and y coordinates for the current value // Calculate the x and y coordinates for the current value
let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i let pos = this.getPointPos(i, categ.values[i])
let y = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale
this.ctx.lineTo(x, y) this.ctx.lineTo(pos.x, pos.y)
} }
this.ctx.stroke() this.ctx.stroke()
this.ctx.closePath() this.ctx.closePath()

@ -8,9 +8,11 @@ class PieChart extends 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) { */
super(canvas, data, settings) constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**

@ -7,10 +7,18 @@ class PointChart extends Chart {
* @param {HTMLElement} canvas - The canvas element to draw the chart on * @param {HTMLElement} canvas - The canvas element to draw the chart on
* @param {Array} data - The data array containing values for the chart * @param {Array} data - The data array containing values for the chart
* @param {Object} settings - The settings object for configuring the chart * @param {Object} settings - The settings object for configuring the chart
* @param {ZoomManager} zoom - The zoom manager for the chart.
*/ */
constructor(canvas, data, settings) { constructor(canvas, data, settings, zoom) {
// Call the constructor of the parent class (Chart) // Call the constructor of the parent class (Chart)
super(canvas, data, settings) super(canvas, data, settings, zoom)
}
getPointPos(i, value) {
return {
x: Math.round(this.zoomBounds.left + this.zoomBounds.width / (this.dataLen - 1) * i),
y: Math.round(this.zoomBounds.xAxis - value / this.extreme * this.scale * this.zoom.scaleY)
}
} }
/** /**
@ -40,17 +48,22 @@ class PointChart extends Chart {
continue continue
// Calculate the x and y coordinates for the data point // Calculate the x and y coordinates for the data point
let x = Math.round(this.bounds.left + this.bounds.width / (this.dataLen - 1) * i) let pos = this.getPointPos(i, values[i])
let y = Math.round(this.bounds.xAxis - values[i] / this.extreme * this.scale)
// Create a new Circle object for the data point // Create a new Circle object for the data point
let new_object = new Circle(this.ctx, values[i], name, x, y, this.settings.pointSize) let shape = new Circle(this.ctx, values[i], name, pos.x, pos.y, this.settings.pointSize)
new_object.draw()
this.objects.push(new_object)
// Draw a border if enabled // Don't draw objects outside of screen (zoom or panning)
if (this.settings.pointBorderSize > 0) if (this.isInBounds(shape.getCenter())) {
this.ctx.stroke() shape.draw(this.ctx)
// Draw a border if enabled
if (this.settings.pointBorderSize > 0)
this.ctx.stroke()
}
// Add the shape
this.objects.push(shape)
} }
} }
@ -68,6 +81,7 @@ class PointChart extends Chart {
console.time("1") console.time("1")
this.data.forEach((categ, colId) => {this.drawPoints(categ.values, colId, categ.color)}) this.data.forEach((categ, colId) => {this.drawPoints(categ.values, colId, categ.color)})
console.timeEnd("1") console.timeEnd("1")
}, 0); }, 0)
} }
} }

@ -65,6 +65,18 @@ class Rectangle extends Shape {
// Fill the rectangle with the current fill style // Fill the rectangle with the current fill style
ctx.fill() ctx.fill()
} }
/**
* Returns the center coordinates of the rectangle.
*
* @returns {Object} Object containing x and y coordinates of the center.
*/
getCenter() {
return {
x: this.x + this.w / 2,
y: 50
}
}
} }
/** /**
@ -98,6 +110,18 @@ class Circle extends Shape {
// Fill the circle with the current fill style // Fill the circle with the current fill style
ctx.fill() ctx.fill()
} }
/**
* Returns the center coordinates of the circle.
*
* @returns {Object} Object containing x and y coordinates of the center.
*/
getCenter() {
return {
x: this.x,
y: this.y
}
}
} }
/** /**
@ -138,6 +162,18 @@ class PieSlice extends Circle {
// Fill the pie slice with the current fill style // Fill the pie slice with the current fill style
ctx.fill() ctx.fill()
} }
/**
* Returns the center coordinates of the pie slice.
*
* @returns {Object} Object containing x and y coordinates of the center.
*/
getCenter() {
return {
x: this.x,
y: this.y
}
}
} }
/** /**
@ -178,4 +214,16 @@ class DonutSlice extends PieSlice {
// Fill the donut slice with the current fill style // Fill the donut slice with the current fill style
ctx.fill() ctx.fill()
} }
/**
* Returns the center coordinates of the donut slice.
*
* @returns {Object} Object containing x and y coordinates of the center.
*/
getCenter() {
return {
x: this.x,
y: this.y
}
}
} }

@ -8,9 +8,11 @@ class SmoothAreaChart extends PointChart {
* @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) {
super(canvas, data, settings) // Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -29,36 +31,34 @@ class SmoothAreaChart extends PointChart {
this.ctx.strokeStyle = categ.color this.ctx.strokeStyle = categ.color
// Calculate the starting point for the line // Calculate the starting point for the line
let x = this.bounds.left let pos1 = this.getPointPos(0, categ.values[0])
let y = (this.bounds.xAxis - categ.values[0] / this.extreme * this.scale) this.ctx.moveTo(pos1.x, pos1.y)
this.ctx.moveTo(x, y)
let xmax = 0 // rightmost point (some lines can be shorter) let xmax = 0 // rightmost point (some lines can be shorter)
// Draw the Bezier curve // Draw the Bezier curve
for (let i = 0; i < this.dataLen - 1; i++) { for (let i = 0; i < this.dataLen - 1; i++) {
// Calculate left point coodrinates // Calculate left point coordinates
let x1 = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i
let y1 = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale
// Calculate right point coodrinates let leftPos = this.getPointPos(i, categ.values[i])
let x2 = this.bounds.left + this.bounds.width / (this.dataLen - 1) * (i + 1)
let y2 = this.bounds.xAxis - categ.values[i + 1] / this.extreme * this.scale // Calculate right point coordinates
let rightPos = this.getPointPos(i + 1, categ.values[i + 1])
// Find middle point // Find middle point
let xm = (x1 + x2) / 2 let xm = (leftPos.x + rightPos.x) / 2
// Find quarter points // Find quarter points
let xl = (x1 + xm) / 2 let xl = (leftPos.x + xm) / 2
let xr = (x2 + xm) / 2 let xr = (rightPos.x + xm) / 2
// Draw a curve that smoothly connects the points // Draw a curve that smoothly connects the points
this.ctx.bezierCurveTo(xl, y1, xr, y2, x2, y2); this.ctx.bezierCurveTo(xl, leftPos.y, xr, rightPos.y, rightPos.x, rightPos.y);
xmax = x2 xmax = rightPos.x
} }
// Complete the area by connecting the last point to the x-axis // Complete the area by connecting the last point to the x-axis
this.ctx.lineTo(xmax, this.bounds.xAxis) this.ctx.lineTo(xmax, this.zoomBounds.xAxis)
this.ctx.lineTo(this.bounds.left, this.bounds.xAxis) this.ctx.lineTo(this.zoomBounds.left, this.zoomBounds.xAxis)
// Set transparency and fill the area // Set transparency and fill the area
this.ctx.globalAlpha = 0.5 this.ctx.globalAlpha = 0.5

@ -8,9 +8,11 @@ class SmoothLineChart extends PointChart {
* @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) {
super(canvas, data, settings) // Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -29,28 +31,25 @@ class SmoothLineChart extends PointChart {
this.ctx.strokeStyle = categ.color this.ctx.strokeStyle = categ.color
// Calculate the starting point for the line // Calculate the starting point for the line
let x = this.bounds.left let pos1 = this.getPointPos(0, categ.values[0])
let y = (this.bounds.xAxis - categ.values[0] / this.extreme * this.scale) this.ctx.moveTo(pos1.x, pos1.y)
this.ctx.moveTo(x, y)
// Draw the Bezier curve for the smooth line // Draw the Bezier curve for the smooth line
for (let i = 0; i < this.dataLen - 1; i++) { for (let i = 0; i < this.dataLen - 1; i++) {
// Calculate left point coodrinates // Calculate left point coordinates
let x1 = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i let leftPos = this.getPointPos(i, categ.values[i])
let y1 = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale
// Calculate right point coodrinates // Calculate right point coordinates
let x2 = this.bounds.left + this.bounds.width / (this.dataLen - 1) * (i + 1) let rightPos = this.getPointPos(i + 1, categ.values[i + 1])
let y2 = this.bounds.xAxis - categ.values[i + 1] / this.extreme * this.scale
// Find middle point // Find middle point
let xm = (x1 + x2) / 2 let xm = (leftPos.x + rightPos.x) / 2
// Find quarter points // Find quarter points
let xl = (x1 + xm) / 2 let xl = (leftPos.x + xm) / 2
let xr = (x2 + xm) / 2 let xr = (rightPos.x + xm) / 2
// Draw a curve that smoothly connects the points // Draw a curve that smoothly connects the points
this.ctx.bezierCurveTo(xl, y1, xr, y2, x2, y2); this.ctx.bezierCurveTo(xl, leftPos.y, xr, rightPos.y, rightPos.x, rightPos.y);
} }
this.ctx.stroke() this.ctx.stroke()
this.ctx.closePath() this.ctx.closePath()

@ -8,9 +8,11 @@ class StackedChart extends 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) {
super(canvas, data, settings) // Call the constructor of the parent class (Chart)
super(canvas, data, settings, zoom)
} }
/** /**
@ -38,14 +40,14 @@ class StackedChart extends Chart {
this.drawAxis(false) this.drawAxis(false)
// Calculate the size of each bar segment based on the canvas width and number of data points // Calculate the size of each bar segment based on the canvas width and number of data points
let size = this.bounds.width / this.dataLen let size = this.zoomBounds.width / this.dataLen
// Calculate the actual width of each bar, making it 70% of the calculated size // Calculate the actual width of each bar, making it 70% of the calculated size
let bar_width = size * 0.7 let bar_width = size * 0.7
// Iterate over each data point to draw the stacked bars // Iterate over each data point to draw the stacked bars
for (let i = 0; i < this.dataLen; i++) { for (let i = 0; i < this.dataLen; i++) {
// The top position of the last stacked bar segment // The top position of the last stacked bar segment
let last_top = this.bounds.xAxis let last_top = this.zoomBounds.xAxis
// Counter to determine the first category in each data point // Counter to determine the first category in each data point
let num = 0 let num = 0
@ -53,9 +55,9 @@ class StackedChart extends Chart {
// Value of the bar segment // Value of the bar segment
let value = categ.values[i] let value = categ.values[i]
// The height of the bar segment relative to the largest total value // The height of the bar segment relative to the largest total value
let bar_height = value / largest * this.bounds.height let bar_height = value / largest * this.zoomBounds.height
// The left position of the bar segment // The left position of the bar segment
let left = this.bounds.left + size * (i + 0.15) let left = this.zoomBounds.left + size * (i + 0.15)
// The top position of the bar segment // The top position of the bar segment
let top = last_top - bar_height let top = last_top - bar_height
@ -73,17 +75,29 @@ class StackedChart extends Chart {
this.ctx.font = "16px Arial" this.ctx.font = "16px Arial"
this.ctx.fillStyle = "black" this.ctx.fillStyle = "black"
this.ctx.textAlign = "center" this.ctx.textAlign = "center"
this.ctx.fillText(text, this.bounds.width / this.dataLen * i + size / 2 + this.bounds.left, this.bounds.bottom + 15) this.ctx.fillText(text, this.zoomBounds.width / this.dataLen * i + size / 2 + this.zoomBounds.left, this.zoomBounds.bottom + 15)
this.ctx.stroke() this.ctx.stroke()
this.ctx.closePath() this.ctx.closePath()
} }
num++ num++
// Set the fill color and draw the rectangle for the current bar segment // Increment the count of bars
this.ctx.fillStyle = categ.color num++
let new_object = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height)
new_object.draw() // Create a new Rectangle object representing the current bar
this.objects.push(new_object) let newObject = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height)
// Add the new Rectangle object to the list of objects
this.objects.push(newObject)
// Check if the center of the new bar is within the bounds
if (this.isInBounds(newObject.getCenter())) {
// Set the fill color to the category color
this.ctx.fillStyle = categ.color
// Set the line width to 0 to prevent drawing the border
this.ctx.lineWidth = 0
// Draw the filled rectangle representing the current bar
newObject.draw()
}
}) })
} }
} }

@ -0,0 +1,79 @@
class ZoomManager {
constructor(horizontalZoom, verticalZoom) {
this.x = 0
this.y = 0
this.scaleX = 1
this.scaleY = 1
this.horizontalZoom = horizontalZoom
this.verticalZoom = verticalZoom
}
/**
* Convert world coordinates to screen coordinates.
*
* @param {number} worldX - The x-coordinate in world space.
* @param {number} worldY - The y-coordinate in world space.
* @returns {Array} The screen coordinates [screenX, screenY].
*/
worldToScreen(worldX, worldY) {
let screenX = (worldX - this.x) * this.scaleX
let screenY = (worldY - this.y) * this.scaleY
return {
x: screenX,
y: screenY
}
}
/**
* Convert screen coordinates to world coordinates.
*
* @param {number} screenX - The x-coordinate on the screen.
* @param {number} screenY - The y-coordinate on the screen.
* @returns {Array} The world coordinates [worldX, worldY].
*/
screenToWorld(screenX, screenY) {
let worldX = screenX / this.scaleX + this.x
let worldY = screenY / this.scaleY + this.y
return {
x: worldX,
y: worldY
}
}
/**
* Recalculates zoom based on mouse wheel event.
* @param {MouseEvent} event - The mouse wheel event object.
*/
recalculate(event) {
// Get mouse position
const pos = {
x: event.clientX,
y: event.clientY
}
// Calculate world coordinates before zoom
let beforeZoom, afterZoom
beforeZoom = this.screenToWorld(pos.x, pos.y)
// Adjust zoom scale based on mouse wheel delta
if (this.horizontalZoom)
this.scaleX -= (10 * this.scaleX) / event.deltaY;
if (this.verticalZoom)
this.scaleY -= (10 * this.scaleY) / event.deltaY;
// Calculate world coordinates after zoom
afterZoom = this.screenToWorld(pos.x, pos.y)
// Adjust zoom position to keep zoom centered around mouse position
this.x += beforeZoom.x - afterZoom.x
this.y += beforeZoom.y - afterZoom.y
// Reset zoom if it goes below 1
if (this.scaleX < 1 || this.scaleY < 1) {
this.x = 0
this.y = 0
this.scaleX = 1
this.scaleY = 1
}
}
}

@ -133,6 +133,22 @@ class MetadataType extends AbstractType
'required' => false, 'required' => false,
]) ])
) )
// Zoom settings
->add(
$builder->create('group5', FormType::class, [
'inherit_data' => true,
'label' => 'Zoom Settings',
'label_attr' => ['class' => 'submenuLabel']
])
->add('horizontalZoom', CheckboxType::class, [
'label' => 'Enable horizontal zoom',
'required' => false,
])
->add('verticalZoom', CheckboxType::class, [
'label' => 'Enable vertical zoom',
'required' => false,
])
)
->add('backgroundColor', ColorType::class, [ ->add('backgroundColor', ColorType::class, [
'label' => 'Background color', 'label' => 'Background color',
'required' => false, 'required' => false,

@ -11,6 +11,7 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script type="text/javascript" src={{ asset('/scripts/zoom_manager.js') }}></script>
<script type="text/javascript" src={{ asset('/scripts/charts/primitives.js') }}></script> <script type="text/javascript" src={{ asset('/scripts/charts/primitives.js') }}></script>
<script type="text/javascript" src={{ asset('/scripts/charts/chart.js') }}></script> <script type="text/javascript" src={{ asset('/scripts/charts/chart.js') }}></script>
<script type="text/javascript" src={{ asset('/scripts/charts/point_chart.js') }}></script> <script type="text/javascript" src={{ asset('/scripts/charts/point_chart.js') }}></script>

@ -19,7 +19,7 @@
<main> <main>
{{ form_start(form) }} {{ form_start(form) }}
<div id="mainDiv"> <div id="mainDiv">
<object id="graphDiv" data={{ "https://spacek.blue/charts/" ~ field_value(form.code) }}></object> <iframe id="graphDiv" src={{ "https://spacek.blue/charts/" ~ field_value(form.code) }}></iframe>
<div id="settings_div"> <div id="settings_div">
{{ form_row(form.name) }} {{ form_row(form.name) }}
{{ form_row(form.code) }} {{ form_row(form.code) }}

Loading…
Cancel
Save

Powered by TurnKey Linux.