From 6492022eaddc89521695db85c7c05c4f66cefd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20=C5=A0pa=C4=8Dek?= Date: Sat, 27 Apr 2024 00:50:15 +0200 Subject: [PATCH] Working Zoom --- public/scripts/chart_loader.js | 61 ++++++----- public/scripts/charts/area_chart.js | 19 ++-- public/scripts/charts/bar_chart.js | 39 ++++--- public/scripts/charts/chart.js | 115 +++++++++++++++++---- public/scripts/charts/donut_chart.js | 8 +- public/scripts/charts/line_chart.js | 13 +-- public/scripts/charts/pie_chart.js | 8 +- public/scripts/charts/point_chart.js | 36 +++++-- public/scripts/charts/primitives.js | 50 ++++++++- public/scripts/charts/smooth_area_chart.js | 36 +++---- public/scripts/charts/smooth_line_chart.js | 29 +++--- public/scripts/charts/stacked_chart.js | 38 ++++--- public/scripts/zoom_manager.js | 79 ++++++++++++++ src/Form/Type/MetadataType.php | 16 +++ templates/chart.html.twig | 1 + templates/edit.html.twig | 2 +- 16 files changed, 412 insertions(+), 138 deletions(-) create mode 100644 public/scripts/zoom_manager.js diff --git a/public/scripts/chart_loader.js b/public/scripts/chart_loader.js index 9e8edac..41fc144 100644 --- a/public/scripts/chart_loader.js +++ b/public/scripts/chart_loader.js @@ -77,29 +77,29 @@ class ChartLoader { } getEffectObjects() { - + } - addListener(newChart) { + addListener(chart) { //Click document.addEventListener('mousemove', (e) => { const pos = { x: e.clientX, y: e.clientY } - + let shapeIndex = this.selectedShapeIndex if (shapeIndex === null) shapeIndex = this.getShapeIndex(this.detectionCanvas, pos) if (shapeIndex !== null) { - let obj = newChart.objects[shapeIndex] + let obj = chart.objects[shapeIndex] // Effect let effectCtx = this.effectCanvas.getContext("2d") effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) this.effectCanvas.style.opacity = 1 - newChart.drawEffect(effectCtx, [obj]) + chart.drawEffect(effectCtx, [obj]) this.dataDiv.style.display = "block" @@ -115,7 +115,7 @@ class ChartLoader { this.dataDiv.style.top = pos.y - this.dataDiv.clientHeight + "px" 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 = "" + name + "

" + obj.value + "

" } else { @@ -145,18 +145,30 @@ class ChartLoader { else this.selectedShapeIndex = this.clickedShapeIndex - console.log(this.clickedShapeIndex + " " + - this.getShapeIndex(this.detectionCanvas, pos) + " " + - this.shapeSelected) + /*console.log(this.clickedShapeIndex + " " + + this.getShapeIndex(this.detectionCanvas, pos) + " " + + this.shapeSelected)*/ }) window.addEventListener("resize", e => { - //newChart.updateLegend(graphSettings.displayLegend, data) - newChart.resizeCanvas(this.parent, this.legend.offsetHeight) - newChart.draw() + //chart.updateLegend(chartSettings.displayLegend, data) + chart.resizeCanvas(this.parent, this.legend.offsetHeight) + chart.draw() 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() { @@ -171,40 +183,41 @@ class ChartLoader { }, 0) } - drawChart(graphSettings, data) { + drawChart(chartSettings, data) { + let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom) //Choose the correct graph - switch (graphSettings.type) { + switch (chartSettings.type) { case "point": - this.chart = new PointChart(this.canvas, data, graphSettings) + this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager) break case "line": - this.chart = new LineChart(this.canvas, data, graphSettings) + this.chart = new LineChart(this.canvas, data, chartSettings, zoomManager) break case "smoothline": - this.chart = new SmoothLineChart(this.canvas, data, graphSettings) + this.chart = new SmoothLineChart(this.canvas, data, chartSettings, zoomManager) break case "pie": - this.chart = new PieChart(this.canvas, data, graphSettings) + this.chart = new PieChart(this.canvas, data, chartSettings, zoomManager) break case "donut": - this.chart = new DonutChart(this.canvas, data, graphSettings) + this.chart = new DonutChart(this.canvas, data, chartSettings, zoomManager) break case "bar": - this.chart = new BarChart(this.canvas, data, graphSettings) + this.chart = new BarChart(this.canvas, data, chartSettings, zoomManager) break case "area": - this.chart = new AreaChart(this.canvas, data, graphSettings) + this.chart = new AreaChart(this.canvas, data, chartSettings, zoomManager) break case "smootharea": - this.chart = new SmoothAreaChart(this.canvas, data, graphSettings) + this.chart = new SmoothAreaChart(this.canvas, data, chartSettings, zoomManager) break case "stacked": - this.chart = new StackedChart(this.canvas, data, graphSettings) + this.chart = new StackedChart(this.canvas, data, chartSettings, zoomManager) 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.draw() diff --git a/public/scripts/charts/area_chart.js b/public/scripts/charts/area_chart.js index 7e81a35..8fa96cd 100644 --- a/public/scripts/charts/area_chart.js +++ b/public/scripts/charts/area_chart.js @@ -8,9 +8,11 @@ class AreaChart extends PointChart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on the chart * @param {Object} settings - The settings for the chart - */ - constructor(canvas, data, settings) { - super(canvas, data, settings) + * @param {ZoomManager} zoom - The zoom manager for the chart. + */ + 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 // Calculate the x and y coordinates for the current value - let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i - let y = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale - xmax = x + let pos = this.getPointPos(i, categ.values[i]) + xmax = pos.x - this.ctx.lineTo(x, y) + this.ctx.lineTo(pos.x, pos.y) } this.ctx.stroke() // 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(this.bounds.left, this.bounds.xAxis) + this.ctx.lineTo(xmax, this.zoomBounds.xAxis) + this.ctx.lineTo(this.zoomBounds.left, this.zoomBounds.xAxis) this.ctx.globalAlpha = 0.5 this.ctx.fillStyle = categ.color this.ctx.closePath() diff --git a/public/scripts/charts/bar_chart.js b/public/scripts/charts/bar_chart.js index ef6f13a..72ed8e8 100644 --- a/public/scripts/charts/bar_chart.js +++ b/public/scripts/charts/bar_chart.js @@ -8,9 +8,11 @@ class BarChart extends Chart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on the chart * @param {Object} settings - The settings for the chart - */ - constructor(canvas, data, settings) { - super(canvas, data, settings) + * @param {ZoomManager} zoom - The zoom manager for the chart. + */ + 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() { // Calculate the number of bars/categories in the chart + this.clear() let barCount = this.data.length // Draw the axis without displaying axis values @@ -40,11 +43,11 @@ class BarChart extends Chart { // Value of the bar let value = categ.values[i] // 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 - 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 - 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 // Only for first category to avoid repeated drawings @@ -55,18 +58,28 @@ class BarChart extends Chart { this.ctx.font = "16px Arial" this.ctx.fillStyle = "black" 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.closePath() } - // Set the fill color and draw the rectangle for the current bar + // Increment the count of bars num++ - this.ctx.fillStyle = categ.color - this.ctx.lineWidth = 0 - let new_object = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height) - new_object.draw() - this.objects.push(new_object) + + // Create a new Rectangle object representing the current bar + 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() + } }) } } diff --git a/public/scripts/charts/chart.js b/public/scripts/charts/chart.js index f127bd3..4df09bc 100644 --- a/public/scripts/charts/chart.js +++ b/public/scripts/charts/chart.js @@ -44,7 +44,13 @@ function getSmallest(data) { 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) { 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 {Array} data - The data to visualize on 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.settings = settings // The settings for the chart 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 // Calculate the largest and smallest values from the data @@ -74,7 +82,7 @@ class Chart { 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 + )) + 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() @@ -93,6 +101,8 @@ class Chart { 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) + + this.zoomBounds = this.getZoomBounds() } /** @@ -128,10 +138,51 @@ class Chart { // 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 + } + + /** + * 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. */ @@ -252,13 +303,16 @@ class Chart { ctx.fill() 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) + // Only draw objects on screen + if (this.isInBounds(object.getCenter())) { + // 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) + } }) } @@ -272,8 +326,6 @@ class Chart { // Clear the entire canvas to prepare for redrawing ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) ctx.fill() - - ctx.scale(2, 2) ctx.shadowBlur = 15 objects.forEach(object => { @@ -293,8 +345,6 @@ class Chart { // Stroke to draw the border ctx.stroke() }) - - ctx.scale(0.5, 0.5) } /** @@ -325,6 +375,11 @@ class Chart { // Draw the title if enabled if (this.settings.displayTitle) 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 // 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 if (this.settings.displaySupportLines) { - this.ctx.moveTo(this.bounds.left, yPos) - this.ctx.lineTo(this.bounds.right, yPos) + this.ctx.moveTo(this.bounds.left, y) + this.ctx.lineTo(this.bounds.right, y) } // Set text style and alignment for the Y-axis values this.ctx.fillStyle = "black" 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 this.ctx.stroke() @@ -374,13 +434,18 @@ class Chart { // 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 + 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 let text = (i + 1).toString() // Draw the X-axis label - this.ctx.fillText(text, x, this.bounds.bottom + 18) + this.ctx.fillText(text, x, y) } this.ctx.closePath() } @@ -401,8 +466,14 @@ class Chart { 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) + let y = this.zoomBounds.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() } diff --git a/public/scripts/charts/donut_chart.js b/public/scripts/charts/donut_chart.js index 910d8cb..3b98148 100644 --- a/public/scripts/charts/donut_chart.js +++ b/public/scripts/charts/donut_chart.js @@ -8,9 +8,11 @@ class DonutChart extends Chart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on the chart * @param {Object} settings - The settings for the chart - */ - constructor(canvas, data, settings) { - super(canvas, data, settings) + * @param {ZoomManager} zoom - The zoom manager for the chart. + */ + constructor(canvas, data, settings, zoom) { + // Call the constructor of the parent class (Chart) + super(canvas, data, settings, zoom) } /** diff --git a/public/scripts/charts/line_chart.js b/public/scripts/charts/line_chart.js index fed8b51..b811cab 100644 --- a/public/scripts/charts/line_chart.js +++ b/public/scripts/charts/line_chart.js @@ -8,9 +8,11 @@ class LineChart extends PointChart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on the chart * @param {Object} settings - The settings for the chart - */ - constructor(canvas, data, settings) { - super(canvas, data, settings) + * @param {ZoomManager} zoom - The zoom manager for the chart. + */ + 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 // Calculate the x and y coordinates for the current value - let x = this.bounds.left + this.bounds.width / (this.dataLen - 1) * i - let y = this.bounds.xAxis - categ.values[i] / this.extreme * this.scale + let pos = this.getPointPos(i, categ.values[i]) - this.ctx.lineTo(x, y) + this.ctx.lineTo(pos.x, pos.y) } this.ctx.stroke() this.ctx.closePath() diff --git a/public/scripts/charts/pie_chart.js b/public/scripts/charts/pie_chart.js index 2219006..c36d28b 100644 --- a/public/scripts/charts/pie_chart.js +++ b/public/scripts/charts/pie_chart.js @@ -8,9 +8,11 @@ class PieChart extends Chart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on the chart * @param {Object} settings - The settings for the chart - */ - constructor(canvas, data, settings) { - super(canvas, data, settings) + * @param {ZoomManager} zoom - The zoom manager for the chart. + */ + constructor(canvas, data, settings, zoom) { + // Call the constructor of the parent class (Chart) + super(canvas, data, settings, zoom) } /** diff --git a/public/scripts/charts/point_chart.js b/public/scripts/charts/point_chart.js index 0ccd146..2fc4ff8 100644 --- a/public/scripts/charts/point_chart.js +++ b/public/scripts/charts/point_chart.js @@ -7,10 +7,18 @@ class PointChart extends Chart { * @param {HTMLElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data array containing values for 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) - 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 // 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 y = Math.round(this.bounds.xAxis - values[i] / this.extreme * this.scale) + let pos = this.getPointPos(i, values[i]) // Create a new Circle object for the data point - let new_object = new Circle(this.ctx, values[i], name, x, y, this.settings.pointSize) - new_object.draw() - this.objects.push(new_object) + let shape = new Circle(this.ctx, values[i], name, pos.x, pos.y, this.settings.pointSize) - // Draw a border if enabled - if (this.settings.pointBorderSize > 0) - this.ctx.stroke() + // Don't draw objects outside of screen (zoom or panning) + if (this.isInBounds(shape.getCenter())) { + 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") this.data.forEach((categ, colId) => {this.drawPoints(categ.values, colId, categ.color)}) console.timeEnd("1") - }, 0); + }, 0) + } } diff --git a/public/scripts/charts/primitives.js b/public/scripts/charts/primitives.js index 779301e..d70bad8 100644 --- a/public/scripts/charts/primitives.js +++ b/public/scripts/charts/primitives.js @@ -65,6 +65,18 @@ class Rectangle extends Shape { // Fill the rectangle with the current fill style 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 + } + } } /** @@ -91,13 +103,25 @@ class Circle extends Shape { * * @param {CanvasRenderingContext2D} [ctx=this.ctx] - The 2D drawing context for the canvas */ - draw(ctx = this.ctx) { + draw(ctx = this.ctx) { ctx.beginPath() // Define the circle ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI) // Fill the circle with the current fill style 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 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 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 + } + } } diff --git a/public/scripts/charts/smooth_area_chart.js b/public/scripts/charts/smooth_area_chart.js index 8ffd078..758ab68 100644 --- a/public/scripts/charts/smooth_area_chart.js +++ b/public/scripts/charts/smooth_area_chart.js @@ -8,9 +8,11 @@ class SmoothAreaChart extends PointChart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on 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) } /** @@ -29,36 +31,34 @@ class SmoothAreaChart extends PointChart { this.ctx.strokeStyle = categ.color // Calculate the starting point for the line - let x = this.bounds.left - let y = (this.bounds.xAxis - categ.values[0] / this.extreme * this.scale) - this.ctx.moveTo(x, y) + let pos1 = this.getPointPos(0, categ.values[0]) + this.ctx.moveTo(pos1.x, pos1.y) let xmax = 0 // rightmost point (some lines can be shorter) // Draw the Bezier curve for (let i = 0; i < this.dataLen - 1; i++) { - // Calculate left point coodrinates - 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 left point coordinates - // Calculate right point coodrinates - 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 + let leftPos = this.getPointPos(i, categ.values[i]) + + // Calculate right point coordinates + let rightPos = this.getPointPos(i + 1, categ.values[i + 1]) // Find middle point - let xm = (x1 + x2) / 2 + let xm = (leftPos.x + rightPos.x) / 2 // Find quarter points - let xl = (x1 + xm) / 2 - let xr = (x2 + xm) / 2 + let xl = (leftPos.x + xm) / 2 + let xr = (rightPos.x + xm) / 2 // Draw a curve that smoothly connects the points - this.ctx.bezierCurveTo(xl, y1, xr, y2, x2, y2); - xmax = x2 + this.ctx.bezierCurveTo(xl, leftPos.y, xr, rightPos.y, rightPos.x, rightPos.y); + xmax = rightPos.x } // Complete the area by connecting the last point to the x-axis - this.ctx.lineTo(xmax, this.bounds.xAxis) - this.ctx.lineTo(this.bounds.left, this.bounds.xAxis) + this.ctx.lineTo(xmax, this.zoomBounds.xAxis) + this.ctx.lineTo(this.zoomBounds.left, this.zoomBounds.xAxis) // Set transparency and fill the area this.ctx.globalAlpha = 0.5 diff --git a/public/scripts/charts/smooth_line_chart.js b/public/scripts/charts/smooth_line_chart.js index 6027c56..10df393 100644 --- a/public/scripts/charts/smooth_line_chart.js +++ b/public/scripts/charts/smooth_line_chart.js @@ -8,9 +8,11 @@ class SmoothLineChart extends PointChart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on 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) } /** @@ -29,28 +31,25 @@ class SmoothLineChart extends PointChart { this.ctx.strokeStyle = categ.color // Calculate the starting point for the line - let x = this.bounds.left - let y = (this.bounds.xAxis - categ.values[0] / this.extreme * this.scale) - this.ctx.moveTo(x, y) + let pos1 = this.getPointPos(0, categ.values[0]) + this.ctx.moveTo(pos1.x, pos1.y) // Draw the Bezier curve for the smooth line for (let i = 0; i < this.dataLen - 1; i++) { - // Calculate left point coodrinates - 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 left point coordinates + let leftPos = this.getPointPos(i, categ.values[i]) - // Calculate right point coodrinates - 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 - let xm = (x1 + x2) / 2 + let xm = (leftPos.x + rightPos.x) / 2 // Find quarter points - let xl = (x1 + xm) / 2 - let xr = (x2 + xm) / 2 + let xl = (leftPos.x + xm) / 2 + let xr = (rightPos.x + xm) / 2 // 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.closePath() diff --git a/public/scripts/charts/stacked_chart.js b/public/scripts/charts/stacked_chart.js index 6dae326..5b3db4f 100644 --- a/public/scripts/charts/stacked_chart.js +++ b/public/scripts/charts/stacked_chart.js @@ -8,9 +8,11 @@ class StackedChart extends Chart { * @param {HTMLCanvasElement} canvas - The canvas element to draw the chart on * @param {Array} data - The data to visualize on 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) } /** @@ -38,14 +40,14 @@ class StackedChart extends Chart { this.drawAxis(false) // 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 let bar_width = size * 0.7 // Iterate over each data point to draw the stacked bars for (let i = 0; i < this.dataLen; i++) { // 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 let num = 0 @@ -53,9 +55,9 @@ class StackedChart extends Chart { // Value of the bar segment let value = categ.values[i] // 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 - 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 let top = last_top - bar_height @@ -73,17 +75,29 @@ class StackedChart extends Chart { this.ctx.font = "16px Arial" this.ctx.fillStyle = "black" 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.closePath() } num++ - // Set the fill color and draw the rectangle for the current bar segment - this.ctx.fillStyle = categ.color - let new_object = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height) - new_object.draw() - this.objects.push(new_object) + // Increment the count of bars + num++ + + // Create a new Rectangle object representing the current bar + 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() + } }) } } diff --git a/public/scripts/zoom_manager.js b/public/scripts/zoom_manager.js new file mode 100644 index 0000000..651c3cc --- /dev/null +++ b/public/scripts/zoom_manager.js @@ -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 + } + } +} \ No newline at end of file diff --git a/src/Form/Type/MetadataType.php b/src/Form/Type/MetadataType.php index 988170a..ee467d0 100644 --- a/src/Form/Type/MetadataType.php +++ b/src/Form/Type/MetadataType.php @@ -133,6 +133,22 @@ class MetadataType extends AbstractType '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, [ 'label' => 'Background color', 'required' => false, diff --git a/templates/chart.html.twig b/templates/chart.html.twig index ab3933f..b349c1c 100644 --- a/templates/chart.html.twig +++ b/templates/chart.html.twig @@ -11,6 +11,7 @@ {% block javascripts %} {{ parent() }} + diff --git a/templates/edit.html.twig b/templates/edit.html.twig index a790c0a..361c16a 100644 --- a/templates/edit.html.twig +++ b/templates/edit.html.twig @@ -19,7 +19,7 @@
{{ form_start(form) }}
- +
{{ form_row(form.name) }} {{ form_row(form.code) }}