From 436f88919284f16daf1fe17ecfe5682630d59173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20=C5=A0pa=C4=8Dek?= Date: Sun, 28 Apr 2024 04:10:30 +0200 Subject: [PATCH] Table editing --- public/scripts/chart_loader.js | 255 +++++++++++++------ public/scripts/charts/area_chart.js | 2 +- public/scripts/charts/bar_chart.js | 47 ++-- public/scripts/charts/chart.js | 103 +++++--- public/scripts/charts/line_chart.js | 2 +- public/scripts/charts/point_chart.js | 13 +- public/scripts/charts/primitives.js | 49 +++- public/scripts/charts/smooth_area_chart.js | 2 +- public/scripts/charts/smooth_line_chart.js | 2 +- public/scripts/charts/stacked_chart.js | 53 ++-- public/scripts/edit_chart.js | 231 ++++++----------- public/scripts/table.js | 273 ++++++++++++++++++++- public/scripts/zoom_manager.js | 71 ++++-- public/styles/chart_style.css | 57 +++-- public/styles/edit_chart.css | 69 ++++-- public/styles/style.css | 61 +---- public/styles/web_style.css | 2 - src/Controller/ChartController.php | 2 +- src/Form/Type/CellType.php | 14 -- src/Form/Type/ColumnType.php | 10 +- src/Form/Type/FontType.php | 10 +- src/Form/Type/MetadataType.php | 31 ++- templates/chart.html.twig | 14 +- templates/edit.html.twig | 93 +++---- 24 files changed, 931 insertions(+), 535 deletions(-) delete mode 100644 src/Form/Type/CellType.php diff --git a/public/scripts/chart_loader.js b/public/scripts/chart_loader.js index 41fc144..363709d 100644 --- a/public/scripts/chart_loader.js +++ b/public/scripts/chart_loader.js @@ -1,38 +1,102 @@ +/** + * Represents a loader for chart data and settings. + */ class ChartLoader { - constructor(canvas, effectCanvas, detectionCanvas, parent, legend, dataDiv) { + /** + * Creates an instance of ChartLoader. + * @param {HTMLCanvasElement} canvas - The canvas element for rendering the chart. + * @param {HTMLCanvasElement} effectCanvas - The canvas element for rendering visual effects. + * @param {HTMLCanvasElement} detectionCanvas - The canvas element for detecting mouse interactions. + * @param {HTMLElement} parent - The parent element that contains the chart. + * @param {HTMLElement} legend - The legend element associated with the chart. + * @param {HTMLElement} tooltip - The tooltip element used for displaying additional information. + */ + constructor(canvas, effectCanvas, detectionCanvas, parent, legend, tooltip) { this.canvas = canvas this.effectCanvas = effectCanvas this.detectionCanvas = detectionCanvas this.parent = parent this.legend = legend - this.dataDiv = dataDiv + this.tooltip = tooltip this.clickedShapeIndex = null this.selectedShapeIndex = null } - //Načtení dat + /** Function for loading chart data from the server + * @param {String} code - The code used to identify the chart data to load + **/ loadData(code) { + // Fetch chart data from the server fetch("/api/charts/" + code, { method: 'GET', headers: { 'Content-Type': 'json' } }) + // Parse the JSON response .then(response => response.json()) + // Process the retrieved data .then(data => { + // Extract metadata and table data from the response let metadata = data.metadata - metadata.custom_x_values = "" + //metadata.custom_x_values = "" let table = data.table + // Draw the chart it contains data if (data !== null) { this.drawChart(metadata, table) this.parent.style.backgroundColor = metadata.backgroundColor } + // Display an error message if chart data is not found else this.parent.innerHTML = "graph not found." }) } + /** + * Calculates the mode (most common value) in an array. + * @param {Array} array - The array of values. + * @returns {any} The mode of the array. + */ + mode(array) { + // Check if the array is empty + if (array.length == 0) + return null + + // Create a map to store the count of each element + let modeMap = {} + + // Initialize variables to track the most common element and its count + let maxEl = array[0], maxCount = 1 + + // Loop through the array to count occurrences of each element + for (let i = 0; i < array.length; i++) { + let el = array[i] + + // Increment the count for the current element in the map + if (modeMap[el] == null) + modeMap[el] = 1 + else + modeMap[el]++ + + // Update the most common element and its count if needed + if (modeMap[el] > maxCount) { + maxEl = el + maxCount = modeMap[el] + } + } + + // Return the most common element (mode) + return maxEl + } + + + /** + * Retrieves the index of the shape at a given position on the canvas. + * @param {HTMLCanvasElement} canvas - The canvas element containing the shapes. + * @param {Object} pos - The position on the canvas with x and y coordinates. + * @returns {number} The index of the shape at the specified position. + */ getShapeIndex(canvas, pos) { let ctx = canvas.getContext('2d') @@ -44,34 +108,15 @@ class ChartLoader { while (imageData.length > 0) { let pixel = imageData.splice(0, 4) - // only if alpha is 100% + // Only consider pixels with full opacity (alpha = 255) if (pixel[3] === 255) { let index = (pixel[0] * 256 + pixel[1]) * 256 + pixel[2] indexes.push(index) } } - function mode(array) { - if (array.length == 0) - return null - let modeMap = {} - let maxEl = array[0], maxCount = 1 - for (let i = 0; i < array.length; i++) { - let el = array[i] - if (modeMap[el] == null) - modeMap[el] = 1 - else - modeMap[el]++ - if (modeMap[el] > maxCount) { - maxEl = el - maxCount = modeMap[el] - } - } - return maxEl - } - // To avoid edge cases because of anti aliasing - let mostCommonIndex = mode(indexes) + let mostCommonIndex = this.mode(indexes) return mostCommonIndex } @@ -80,113 +125,175 @@ class ChartLoader { } - 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 = chart.objects[shapeIndex] + /** + * Handles mouse movement events for interactivity. + * @param {MouseEvent} e - The mouse event object. + * @param {Chart} chart - The chart object associated with the mouse movement. + */ + mouseMoveFunc(e, chart) { + // Get mouse position + const pos = { + x: e.clientX, + y: e.clientY + } - // Effect - let effectCtx = this.effectCanvas.getContext("2d") - effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) - this.effectCanvas.style.opacity = 1 - chart.drawEffect(effectCtx, [obj]) + let obj = [] + // Add selected shape to array if it exists + if (this.selectedShapeIndex !== null) + obj.push(chart.objects[this.selectedShapeIndex]) + + // Get index of hovered shape and add it to the array if it exists + let hoverShapeIndex = this.getShapeIndex(this.detectionCanvas, pos) + if (hoverShapeIndex !== null) + obj.push(chart.objects[hoverShapeIndex]) + + // Display tooltip and effect if a shape is selected + if (obj[0] !== undefined) { + // Get the drawing context for the effect canvas + let effectCtx = this.effectCanvas.getContext("2d") + // Clear the effect canvas + effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) + // Show the effect + this.effectCanvas.style.opacity = 1 + chart.drawEffect(effectCtx, obj) + + // Show tooltip + this.tooltip.style.display = "block" + // Set tooltip content + let name = chart.data[obj[0].colId.toString()].col_name + this.tooltip.innerHTML = "" + name + "

" + obj[0].value + "

" + + let tooltipPos = obj[0].getTooltipPos() + + // Adjust tooltip position to fit within the chart div + if (tooltipPos.right.x + this.tooltip.clientWidth <= this.parent.clientWidth - 2) + this.tooltip.style.left = tooltipPos.right.x + 5 + "px" + else + this.tooltip.style.left = tooltipPos.left.x - this.tooltip.clientWidth + "px" - this.dataDiv.style.display = "block" + // Check if the tooltip extends beyond the bottom edge of the chart div + if (tooltipPos.right.y + this.tooltip.clientHeight <= parent.clientHeight - 2) + this.tooltip.style.top = tooltipPos.right.y + "px" + else + this.tooltip.style.top = tooltipPos.right.y - this.tooltip.clientHeight / 2 + "px" + } + else { + // Hide tooltip and effect if no shape is selected + this.tooltip.style.display = "none" + this.effectCanvas.style.opacity = 0 + } + } - // make the info fit into the chart div - if (pos.x + this.dataDiv.clientWidth <= this.parent.clientWidth - 2) - this.dataDiv.style.left = pos.x + "px" - else - this.dataDiv.style.left = pos.x - this.dataDiv.clientWidth + "px" + /** + * Adds event listeners to enable interactivity on the chart. + * @param {Chart} chart - The chart object to which listeners will be added. + */ + addListeners(chart) { + // Mousemove event listener for tracking mouse movement + document.addEventListener('mousemove', (e) => { + this.mouseMoveFunc(e, chart) - if (pos.y + this.dataDiv.clientHeight <= parent.clientHeight - 2) - this.dataDiv.style.top = pos.y + "px" - else - this.dataDiv.style.top = pos.y - this.dataDiv.clientHeight + "px" + if (this.panningEnabled === true) { + this.chart.zoom.recalculate(e, true) - this.dataDiv.style.display = "block" - let name = chart.data[obj.colId.toString()].col_name - this.dataDiv.innerHTML = "" + name + "

" + obj.value + "

" - } - else { - this.dataDiv.style.display = "none" - this.effectCanvas.style.opacity = 0 + // Update chart and redraw + chart.updateBounds() + chart.draw() + this.addInteractivity() } }) + // Mousedown event listener for tracking mouse clicks document.addEventListener("mousedown", (e) => { const pos = { x: e.clientX, y: e.clientY } + // Store the index of the latest clicked shape this.clickedShapeIndex = this.getShapeIndex(this.detectionCanvas, pos) + + this.chart.zoom.lastPos = pos + this.panningEnabled = true }) + // Mouseup event listener for tracking mouse releases document.addEventListener("mouseup", (e) => { const pos = { x: e.clientX, y: e.clientY } + // Determine if the selected shape has changed after mouse release if (this.clickedShapeIndex === null || this.clickedShapeIndex !== this.getShapeIndex(this.detectionCanvas, pos)) this.selectedShapeIndex = null else this.selectedShapeIndex = this.clickedShapeIndex - /*console.log(this.clickedShapeIndex + " " + - this.getShapeIndex(this.detectionCanvas, pos) + " " + - this.shapeSelected)*/ + // Perform mouse move action after mouse release to show the change immediately + this.mouseMoveFunc(e, chart) + + this.panningEnabled = false }) + // Window resize event listener for resizing the chart canvas window.addEventListener("resize", e => { - //chart.updateLegend(chartSettings.displayLegend, data) + // Resize the chart canvas and redraw the chart chart.resizeCanvas(this.parent, this.legend.offsetHeight) chart.draw() + // Reapply interactivity this.addInteractivity() }) + // Wheel event listener for zooming the chart (if enabled) if (chart.settings.horizontalZoom || chart.settings.verticalZoom) window.addEventListener("wheel", e => { - e.stopImmediatePropagation() + // Prevent default scrolling behavior e.stopPropagation() e.preventDefault() - chart.zoom.recalculate(e, true, true) + // Recalculate zoom based on the mouse wheel event + chart.zoom.recalculate(e) + + // Update chart and redraw chart.updateBounds() chart.draw() this.addInteractivity() - }, { passive: false }) + + this.mouseMoveFunc(e, chart) + }, + // Has to be active to stop default scroll behaviour + { passive: false }) } + /* Asynchronously adds interactivity to the chart. + */ async addInteractivity() { setTimeout(() => { - console.time("2") + //console.time("2") + // Set dimensions of effect canvas this.effectCanvas.width = this.canvas.width this.effectCanvas.height = this.canvas.height + // Set dimensions of detection canvas this.detectionCanvas.width = this.canvas.width this.detectionCanvas.height = this.canvas.height + // Draw detection map on the detection canvas this.chart.drawDetectionMap(this.detectionCanvas.getContext("2d")) - console.timeEnd("2") + //console.timeEnd("2") }, 0) } + /** + * Draws the chart based on the provided settings and data. + * @param {Object} chartSettings - The settings for the chart. + * @param {Array} data - The data to visualize on the chart. + */ drawChart(chartSettings, data) { let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom) - //Choose the correct graph + // Choose the correct type of chart switch (chartSettings.type) { case "point": this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager) @@ -217,13 +324,15 @@ class ChartLoader { break } + // Asynchronously update the legend setTimeout(this.chart.updateLegend(chartSettings.displayLegend, this.legend, this), 0) + + // Resize the canvas and chart based on the parent container and legend size this.chart.resizeCanvas(this.parent, this.legend.offsetHeight) this.chart.draw() this.addInteractivity() - - this.addListener(this.chart) + this.addListeners(this.chart) } } diff --git a/public/scripts/charts/area_chart.js b/public/scripts/charts/area_chart.js index 8fa96cd..a1ce38a 100644 --- a/public/scripts/charts/area_chart.js +++ b/public/scripts/charts/area_chart.js @@ -11,7 +11,7 @@ class AreaChart extends PointChart { * @param {ZoomManager} zoom - The zoom manager for the chart. */ constructor(canvas, data, settings, zoom) { - // Call the constructor of the parent class (Chart) + // Call the constructor of the parent class (PointChart) super(canvas, data, settings, zoom) } diff --git a/public/scripts/charts/bar_chart.js b/public/scripts/charts/bar_chart.js index 72ed8e8..91d1652 100644 --- a/public/scripts/charts/bar_chart.js +++ b/public/scripts/charts/bar_chart.js @@ -28,7 +28,7 @@ class BarChart extends Chart { // Divide the space into equal section // Calculate section size - let size = this.bounds.width / this.dataLen + let size = this.zoomBounds.width / this.dataLen // Callculate inner size with margin let innerSize = size * 0.8 // Width of each chart @@ -43,27 +43,13 @@ class BarChart extends Chart { // Value of the bar let value = categ.values[i] // The left position of the bar in section - let left = this.zoomBounds.left + (size * (i + 0.15) + (innerSize * num / barCount)) * this.zoom.scaleX + let left = this.zoomBounds.left + (size * (i + 0.15) + (innerSize * num / barCount)) // The height of the bar relative to the chart scale let bar_height = value / this.extreme * this.scale * this.zoom.scaleY // The top position of the bar 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 - if (num === 0 && this.settings.displayAxisValues) { - let text = (i + 1).toString() - - this.ctx.beginPath() - this.ctx.font = "16px Arial" - this.ctx.fillStyle = "black" - this.ctx.textAlign = "center" - 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() - } - - // Increment the count of bars + // Increment the bar count num++ // Create a new Rectangle object representing the current bar @@ -82,5 +68,32 @@ class BarChart extends Chart { } }) } + + // Draw x-axis labels if enabled + if (this.settings.displayAxisValues) { + // Restore canvas state to undo region clipping + this.ctx.restore() + // Loop through each data point to draw the labels + for (let i = 0; i < this.dataLen; i++) { + let text = (i + 1).toString() + + // Begin drawing the text + this.ctx.beginPath() + this.ctx.font = "16px Arial" + this.ctx.fillStyle = "black" + this.ctx.textAlign = "center" + + // Calculate the position of the label + let x = this.zoomBounds.left + this.zoomBounds.width / this.dataLen * i + size / 2 + let y = this.bounds.bottom + 15 + + // Draw the label text + this.ctx.fillText(text, x, y) + // Stroke the text + this.ctx.stroke() + // Close the path + this.ctx.closePath() + } + } } } diff --git a/public/scripts/charts/chart.js b/public/scripts/charts/chart.js index 4df09bc..9953955 100644 --- a/public/scripts/charts/chart.js +++ b/public/scripts/charts/chart.js @@ -97,12 +97,12 @@ class Chart { // Calculate the bounds using the canvas and margin settings this.bounds = this.getBounds(this.canvas, this.settings.margin) + this.zoomBounds = this.getZoomBounds() + // 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.scale = this.zoomBounds.height + - (this.largest >= 0 ? (this.zoomBounds.bottom - this.zoomBounds.xAxis) : 0) this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest) - - this.zoomBounds = this.getZoomBounds() } /** @@ -138,7 +138,7 @@ 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 } @@ -159,12 +159,33 @@ class Chart { result.right = pos.x result.bottom = pos.y + // Prevent the possiblity of leaving the chart bounds + if (result.left > this.bounds.left) + result.left = this.bounds.left + if (result.right < this.bounds.right) + result.right = this.bounds.right + if (result.top > this.bounds.top) + result.top = this.bounds.top + if (result.bottom < this.bounds.bottom) + result.bottom = this.bounds.bottom + // Calculate width and height of the zoomed bounds - result.width = this.bounds.width * this.zoom.scaleX - result.height = this.bounds.height * this.zoom.scaleY + result.width = result.right - result.left + result.height = result.bottom - result.top - // Get the screen coordinate of the x-axis - result.xAxis = this.zoom.worldToScreen(null, this.bounds.xAxis).y + // 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 } @@ -271,6 +292,10 @@ class Chart { * Clear the canvas and remove all stored objects. */ clear() { + // Set the clip region to the whole canvas + this.ctx.restore() + this.ctx.save() + // Check if a custom background color is set if (this.settings.backgroundColor == null) { // Clear the canvas using the default clear method @@ -295,8 +320,8 @@ class Chart { drawDetectionMap(ctx) { // Move the drawing point to (0.5, 0.5) to avoid anti-aliasing issues ctx.moveTo(0.5, 0.5) - // Set the line width to make the map slightly bigger than visible shapes - //ctx.lineWidth = 3 + // Set the line width to make the map bigger than visible shapes + ctx.lineWidth = 50 // Clear the entire canvas to prepare for redrawing ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) @@ -309,9 +334,10 @@ class Chart { let color = "#" + object.index.toString(16).padStart(6, '0') ctx.fillStyle = color ctx.strokeStyle = color - + // Draw the shape object.draw(ctx) + //ctx.stroke() } }) } @@ -328,23 +354,22 @@ class Chart { ctx.fill() ctx.shadowBlur = 15 + ctx.shadowColor = 'rgba(0,0,0,1)' 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 - // 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)' + ctx.fillStyle = lighterColor + ctx.strokeStyle = lighterColor + // Draw the object object.draw(ctx) - ctx.shadowColor = 'rgba(0,0,0,0)' // Stroke to draw the border ctx.stroke() }) + ctx.shadowColor = 'rgba(0,0,0,0)' } /** @@ -361,7 +386,7 @@ class Chart { this.settings.yStep = 1 // Draw ticks and labels for the Y-axis - this.drawYAxisTicks() + this.drawYAxisTicks(displayAxisValues) // Draw ticks and labels for the X-axis this.drawXAxisTicks(displayAxisValues) @@ -376,28 +401,43 @@ class Chart { if (this.settings.displayTitle) this.drawTitle() + // Set the clip region to the inside bounds + this.setClipRegion(this.bounds.left, this.bounds.top, this.bounds.width, this.bounds.height) + } + + /** + * Sets the clipping region for the canvas context. + * @param {number} x - The x-coordinate of the top-left corner of the clipping region. + * @param {number} y - The y-coordinate of the top-left corner of the clipping region. + * @param {number} w - The width of the clipping region. + * @param {number} h - The height of the clipping region. + */ + setClipRegion(x, y, w, h) { this.ctx.beginPath() - this.ctx.rect(this.bounds.left, this.bounds.top, this.bounds.width, this.bounds.height) + // Create a rectangular path for the clipping region + this.ctx.rect(x, y, w, h) + // Clip the canvas context to the specified region this.ctx.clip() this.ctx.closePath() } /** - * Draws the ticks and labels on the Y-axis + * Draws the ticks and labels on the Y-axis. */ drawYAxisTicks() { this.ctx.beginPath() + // Set stroke style for the ticks + this.ctx.strokeStyle = "#BBB" + this.ctx.lineWidth = 1 + this.ctx.fillStyle = "black" + this.ctx.textAlign = "end" + // 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 y = this.bounds.xAxis - i * this.scale / this.extreme - y = this.zoom.worldToScreen(null, y).y + let y = this.zoomBounds.xAxis - i * this.scale / this.extreme // Skip text and lines if out of bounds if (y < this.bounds.top || y > this.bounds.bottom) @@ -410,9 +450,8 @@ class Chart { } // 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, y) + if (this.settings.displayAxisValues) + this.ctx.fillText(i, this.bounds.left - 3, y) // Draw the tick this.ctx.stroke() @@ -422,9 +461,9 @@ class Chart { /** * Draws the ticks and labels on the X-axis * - * @param {boolean} displayAxisValues - Whether to display axis values + * @param {boolean} [displayAxisValues=true] - Whether to display axis values */ - drawXAxisTicks(displayAxisValues) { + drawXAxisTicks(displayAxisValues = true) { // Check if axis values should be displayed if (displayAxisValues) { // Set text style and alignment for the X-axis values diff --git a/public/scripts/charts/line_chart.js b/public/scripts/charts/line_chart.js index b811cab..232f7a7 100644 --- a/public/scripts/charts/line_chart.js +++ b/public/scripts/charts/line_chart.js @@ -11,7 +11,7 @@ class LineChart extends PointChart { * @param {ZoomManager} zoom - The zoom manager for the chart. */ constructor(canvas, data, settings, zoom) { - // Call the constructor of the parent class (Chart) + // Call the constructor of the parent class (PointChart) super(canvas, data, settings, zoom) } diff --git a/public/scripts/charts/point_chart.js b/public/scripts/charts/point_chart.js index 2fc4ff8..8d947cc 100644 --- a/public/scripts/charts/point_chart.js +++ b/public/scripts/charts/point_chart.js @@ -14,10 +14,17 @@ class PointChart extends Chart { super(canvas, data, settings, zoom) } + /** + * Calculates the position of a point on the chart. + * + * @param {number} i - The index of the point. + * @param {number} value - The value of the point. + * @returns {Object} The position of the point {x, y}. + */ 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) + y: Math.round(this.zoomBounds.xAxis - value / this.extreme * this.scale)// * this.zoom.scaleY) } } @@ -46,7 +53,7 @@ class PointChart extends Chart { // Skip empty points if (values[i] === null || values[i] === undefined) continue - + // Calculate the x and y coordinates for the data point let pos = this.getPointPos(i, values[i]) @@ -79,7 +86,7 @@ class PointChart extends Chart { if (this.settings.displayPoints) setTimeout(() => { 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") }, 0) diff --git a/public/scripts/charts/primitives.js b/public/scripts/charts/primitives.js index d70bad8..c543e20 100644 --- a/public/scripts/charts/primitives.js +++ b/public/scripts/charts/primitives.js @@ -77,6 +77,23 @@ class Rectangle extends Shape { y: 50 } } + + /** + * Retrieves the position for displaying a tooltip. + * @returns {Object} An object containing x and y coordinates for the tooltip position. + */ + getTooltipPos() { + return { + right: { + x: this.x + this.w, + y: this.y + }, + left: { + x: this.x, + y: this.y + } + } + } } /** @@ -103,7 +120,7 @@ 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) @@ -122,6 +139,23 @@ class Circle extends Shape { y: this.y } } + + /** + * Retrieves the position for displaying a tooltip. + * @returns {Object} An object containing x and y coordinates for the tooltip position. + */ + getTooltipPos() { + return { + right: { + x: this.x + this.r / 2, + y: this.y + }, + left: { + x: this.x - this.r / 2, + y: this.y + } + } + } } /** @@ -174,6 +208,17 @@ class PieSlice extends Circle { y: this.y } } + + /** + * Retrieves the position for displaying a tooltip. + * @returns {Object} An object containing x and y coordinates for the tooltip position. + */ + getTooltipPos() { + return { + right: this.getCenter(), + left: this.getCenter() + } + } } /** @@ -214,7 +259,7 @@ class DonutSlice extends PieSlice { // Fill the donut slice with the current fill style ctx.fill() } - + /** * Returns the center coordinates of the donut slice. * diff --git a/public/scripts/charts/smooth_area_chart.js b/public/scripts/charts/smooth_area_chart.js index 758ab68..061bd23 100644 --- a/public/scripts/charts/smooth_area_chart.js +++ b/public/scripts/charts/smooth_area_chart.js @@ -11,7 +11,7 @@ class SmoothAreaChart extends PointChart { * @param {ZoomManager} zoom - The zoom manager for the chart. */ constructor(canvas, data, settings, zoom) { - // Call the constructor of the parent class (Chart) + // Call the constructor of the parent class (PointChart) super(canvas, data, settings, zoom) } diff --git a/public/scripts/charts/smooth_line_chart.js b/public/scripts/charts/smooth_line_chart.js index 10df393..2d4456c 100644 --- a/public/scripts/charts/smooth_line_chart.js +++ b/public/scripts/charts/smooth_line_chart.js @@ -11,7 +11,7 @@ class SmoothLineChart extends PointChart { * @param {ZoomManager} zoom - The zoom manager for the chart. */ constructor(canvas, data, settings, zoom) { - // Call the constructor of the parent class (Chart) + // Call the constructor of the parent class (PointChart) super(canvas, data, settings, zoom) } diff --git a/public/scripts/charts/stacked_chart.js b/public/scripts/charts/stacked_chart.js index 5b3db4f..8363b0f 100644 --- a/public/scripts/charts/stacked_chart.js +++ b/public/scripts/charts/stacked_chart.js @@ -19,10 +19,6 @@ class StackedChart extends Chart { * Draws the stacked bar chart on the canvas. */ draw() { - /*this.ctx.shadowOffsetX = 15 - this.ctx.shadowOffsetY = 15 - this.ctx.shadowBlur = 4*/ - // Calculate the largest total value across all categories for normalization let largest = 0 for (let i = 0; i < this.dataLen; i++) { @@ -48,8 +44,6 @@ class StackedChart extends Chart { for (let i = 0; i < this.dataLen; i++) { // The top position of the last stacked bar segment let last_top = this.zoomBounds.xAxis - // Counter to determine the first category in each data point - let num = 0 this.data.forEach((categ, colId) => { // Value of the bar segment @@ -64,26 +58,6 @@ class StackedChart extends Chart { // Update the last top position for the next iteration last_top = top - // Draw x-axis labels - // Only for first category to avoid repeated drawings - if (num === 0) { - let text = (i + 1).toString() - /*if (this.settings.custom_x_values !== "") - text = this.settings.custom_x_values.split(';')[i]*/ - - this.ctx.beginPath() - this.ctx.font = "16px Arial" - this.ctx.fillStyle = "black" - this.ctx.textAlign = "center" - 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++ - - // 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 @@ -100,5 +74,32 @@ class StackedChart extends Chart { } }) } + + // Draw x-axis labels if enabled + if (this.settings.displayAxisValues) { + // Restore canvas state to undo region clipping + this.ctx.restore() + // Loop through each data point to draw the labels + for (let i = 0; i < this.dataLen; i++) { + let text = (i + 1).toString() + + // Begin drawing the text + this.ctx.beginPath() + this.ctx.font = "16px Arial" + this.ctx.fillStyle = "black" + this.ctx.textAlign = "center" + + // Calculate the position of the label + let x = this.zoomBounds.left + this.zoomBounds.width / this.dataLen * i + size / 2 + let y = this.bounds.bottom + 15 + + // Draw the label text + this.ctx.fillText(text, x, y) + // Stroke the text + this.ctx.stroke() + // Close the path + this.ctx.closePath() + } + } } } diff --git a/public/scripts/edit_chart.js b/public/scripts/edit_chart.js index 89ed17f..e5c58ef 100755 --- a/public/scripts/edit_chart.js +++ b/public/scripts/edit_chart.js @@ -1,127 +1,121 @@ -let canvas, parent, legend, dataDiv, table; -const urlParams = new URLSearchParams(window.location.search); -const graph_code = urlParams.get('code'); -validateUser(graph_code); -let rcTarget = {}; +/** + * Initializes variables and event listeners after the DOM content is loaded. + */ +let canvas, parent, legend, dataDiv, table + +let rcTarget = {} $(document).ready( function () { - canvas = document.getElementById("graphCanvas"); - parent = document.getElementById("graphDiv"); - legend = document.getElementById("graphLegend"); - dataDiv = document.getElementById("dataDiv"); - table = new Table(document.getElementById("dataTable")); + canvas = document.getElementById("graphCanvas") + parent = document.getElementById("graphDiv") + legend = document.getElementById("graphLegend") + dataDiv = document.getElementById("dataDiv") + table = new Table(document.getElementById("dataTable")) - document.getElementById('upload').addEventListener('change', handleFileSelect, false); + document.getElementById('upload').addEventListener('change', handleFileSelect, false) - load_data(); - table.reloadEvLi(); - reloadShares(); + load_data() + table.reloadEvLi() + reloadShares() //Click document.addEventListener('mousemove', (e) => { const pos = { x: e.clientX - canvas.offsetLeft, y: e.clientY - canvas.offsetTop - }; - let obj = checkHit(pos); + } + let obj = checkHit(pos) //show point value if (obj !== null) { - dataDiv.style.left = pos.x + canvas.offsetLeft + "px"; - dataDiv.style.top = pos.y + canvas.offsetTop + "px"; - dataDiv.style.display = "block"; - dataDiv.innerHTML = "" + obj.name + "

" + obj.value + "

"; + dataDiv.style.left = pos.x + canvas.offsetLeft + "px" + dataDiv.style.top = pos.y + canvas.offsetTop + "px" + dataDiv.style.display = "block" + dataDiv.innerHTML = "" + obj.name + "

" + obj.value + "

" } else { - dataDiv.style.display = "none"; + dataDiv.style.display = "none" } - }); + }) - $("#exportBtn").on('click', function (e) { - table.reloadData(); - exportData('tableData'); - }); + /*$("#exportBtn").on('click', function (e) { + table.reloadData() + exportData('tableData') + }) $("#saveBtn").on('click', function (e) { - table.reloadData(); - save_data(); - }); + table.reloadData() + save_data() + }) $("#drawBtn").on('click', function (e) { - table.reloadData(); - submitData(); - }); + table.reloadData() + submitData() + }) //RIGHT CLICK menu $(document).bind("click", function(event) { - document.getElementById("rcMenu").style.display = "none"; - }); + document.getElementById("rcMenu").style.display = "none" + }) //odebere řádek $("#rcDelRow").on('click', function (e) { - e.preventDefault(); + e.preventDefault() if (rcTarget.parentElement.parentElement.tagName === "THEAD") - return; - table.removeRow(rcTarget.parentElement); - }); + return + table.removeRow(rcTarget.parentElement) + }) //přidá řádek $("#rcAddRow").on('click', function (e) { - e.preventDefault(); - table.addRow(table, rcTarget); - }); + e.preventDefault() + table.addRow(table, rcTarget) + }) //odebere sloupec $('#rcDelCol').on('click', function (e) { - e.preventDefault(); + e.preventDefault() - table.removeCol(getCellIndex(rcTarget)); - }); + table.removeCol(getCellIndex(rcTarget)) + }) //přidá sloupec $('#rcAddCol').on('click', function (e) { - e.preventDefault(); + e.preventDefault() - table.addCol(getCellIndex(rcTarget)); - }); + table.addCol(getCellIndex(rcTarget)) + }) //Sharing $('#shareBtn').on('click', function (e) { - e.preventDefault(); - - let username = document.getElementById("shareUsername").value; - addShare(username); - }); -}); - -//Resize -$(window).on('resize', function () { - resizeCanvas(canvas, parent); - table.reloadData(); - drawChart(getSettings(), table.data); -}); - -function handleFileSelect(evt) { - let files = evt.target.files; - table.importData(files[0], table); + e.preventDefault() + + let username = document.getElementById("shareUsername").value + addShare(username) + })*/ +}) + +/*function handleFileSelect(evt) { + let files = evt.target.files + table.importData(files[0], table) } function submitData() { - table.reloadData(); - drawChart(getSettings(), table.data); + table.reloadData() + drawChart(getSettings(), table.data) } function save_data() { - table.reloadData(); - let settings = getSettings(); + table.reloadData() + let settings = getSettings() $.ajax({ url: "php/save_data.php", type: "post", dataType: "text", data: {code: graph_code, data: JSON.stringify(table.data), settings: JSON.stringify(settings), name:settings.title}, success: function (result) { - //alert("Saved successfully " + result); + //alert("Saved successfully " + result) } - }); + }) } function load_data() { @@ -132,103 +126,28 @@ function load_data() { data: {code: graph_code}, success: function (result) { if (result.data == null) { - alert("Error: no data found"); - return; + alert("Error: no data found") + return } - table.data = JSON.parse(result.data); - table.updateTable(); + table.data = JSON.parse(result.data) + table.updateTable() if (result.settings == null) { - alert("Error: no graph settings found"); + alert("Error: no graph settings found") } else { - loadSettings(JSON.parse(result.settings)); + loadSettings(JSON.parse(result.settings)) } - drawChart(getSettings(), table.data); + drawChart(getSettings(), table.data) } }) } function getCellIndex(cell) { - let parent = cell.parentElement; + let parent = cell.parentElement - let children = Array.from(parent.children); + let children = Array.from(parent.children) for (let i = 0; i < children.length; i++){ if (children[i] === cell) { - return i; + return i } } -} - -//Nastavení grafu -function getSettings() { - return { - type: document.getElementById('graph_types').value, - y_step: parseFloat(document.getElementById('y_step').value), - b_color: ((document.getElementById('enableBgColor').checked) ? document.getElementById('graphBgColor').value : null), - display_legend: document.getElementById("displayLegend").checked, - display_points: document.getElementById("displayPoints").checked, - display_support_lines: document.getElementById("displaySupportLines").checked, - title: document.getElementById("graphTitle").value, - display_title: document.getElementById("displayTitle").checked, - margin: parseFloat(document.getElementById("graphMargin").value), - custom_x_values: document.getElementById('customXValues').value, - x_label: document.getElementById('xLabel').value, - y_label: document.getElementById('yLabel').value, - }; -} - -function loadSettings(new_settings) { - document.getElementById('graph_types').value = new_settings.type; - document.getElementById('y_step').value = new_settings.y_step; - if (new_settings.b_color !== null) { - document.getElementById('graphBgColor').value = new_settings.b_color; - document.getElementById('enableBgColor').checked = true; - } - document.getElementById("displayLegend").checked = new_settings.display_legend; - document.getElementById("displayPoints").checked = new_settings.display_points; - document.getElementById("displaySupportLines").checked = new_settings.display_support_lines; - document.getElementById("graphTitle").value = new_settings.title; - document.getElementById("displayTitle").checked = new_settings.display_title; - document.getElementById("graphMargin").value = new_settings.margin; - document.getElementById('customXValues').value = new_settings.custom_x_values; - document.getElementById('xLabel').value = new_settings.x_label; - document.getElementById('yLabel').value = new_settings.y_label; -} - -//Sdílení grafu -function reloadShares() { - $.ajax({ - url: "php/load_shares.php", - type: "post", - dataType: "text", - data: {code: graph_code}, - success: function(result) { - document.getElementById("shareList").innerHTML = result; - } - }); -} - -function removeShare(username) { - $.ajax({ - url: "php/remove_share.php", - type: "post", - dataType: "text", - data: {username: username, code: graph_code}, - success: function(result) { - reloadShares(); - } - }); -} - -function addShare(username) { - $.ajax({ - url: "php/add_share.php", - type: "post", - dataType: "text", - data: {username: username, code: graph_code}, - success: function(result) { - //alert(result); - validateUser(graph_code); - reloadShares(); - } - }); -} \ No newline at end of file +}*/ diff --git a/public/scripts/table.js b/public/scripts/table.js index 85c4609..f3751cd 100644 --- a/public/scripts/table.js +++ b/public/scripts/table.js @@ -1,8 +1,273 @@ +/** + * Represents a table object. + */ class Table { - constructor(table_element) { - this.table_element = table_element - this.data = [] + /** + * Initializes a new instance of the Table class. + * @param {HTMLElement} tableElement - The HTML table element. + * @param {HTMLElement} rcMenu - The right-click menu element. + */ + constructor(tableElement, rcMenu) { + this.tableElement = tableElement + this.tableBody = tableElement.querySelector("tbody") + this.tableHead = tableElement.querySelector("thead") + this.rcMenu = rcMenu + + // Initialize right-click menu options + this.rcAddRow = rcMenu.querySelector("#rcAddRow") + this.rcDelRow = rcMenu.querySelector("#rcDelRow") + this.rcAddCol = rcMenu.querySelector("#rcAddCol") + this.rcDelCol = rcMenu.querySelector("#rcDelCol") + + this.selectedCell = null + + this.addEventListeners(this.rcMenu, this.tableElement) + } + + addEventListeners() { + // Hide context menu when mouse button is clicked anywhere on the window + window.addEventListener('mousedown', (e) => { + this.rcMenu.style.display = "none" + }) + + // Show context menu when right-clicking on the data table + this.tableElement.addEventListener('contextmenu', (e) => { + const pos = { + x: e.clientX, + y: e.clientY + } + + this.handleContextMenu(this.rcMenu, this.tableElement, pos) + + this.selectedCell = e.target + + // Prevent default context menu from appearing + e.preventDefault() + e.stopPropagation() + + }) + + // Add click event listeners to toggle display of settings submenus + let labels = [...document.getElementsByClassName("submenuLabel")] + labels.forEach(label => { + label.addEventListener('click', e => { + let submenuDiv = e.target.nextElementSibling + + // Toggle display of submenu + if (getComputedStyle(submenuDiv).display == "block") + submenuDiv.style.display = "none" + else + submenuDiv.style.display = "block" + }) + }) + + this.rcAddRow.addEventListener("mousedown", (e) => { this.addRow() }) + this.rcDelRow.addEventListener("mousedown", (e) => { this.delRow() }) + this.rcAddCol.addEventListener("mousedown", (e) => { this.addCol() }) + this.rcDelCol.addEventListener("mousedown", (e) => { this.delCol() }) + } + + handleContextMenu(rcMenu, tableElement, pos) { + rcMenu.style.display = "block" + + // Position the context menu relative to the mouse pointer + if (pos.x + rcMenu.clientWidth <= tableElement.clientWidth) + rcMenu.style.left = pos.x + "px" + else + rcMenu.style.left = pos.x - rcMenu.clientWidth + "px" + + if (pos.y + rcMenu.clientHeight <= window.innerHeight + document.documentElement.scrollTop) + rcMenu.style.top = pos.y + "px" + else + rcMenu.style.top = pos.y - rcMenu.clientHeight + "px" + + } + + /** + * Gets the position of the cell in the table. + * @param {HTMLElement} cell - The HTML cell element. + * @returns {Object} An object containing the column and row indexes. + */ + getCellPos(cell) { + // Name contains the column and row indexes. + let name = cell.name + + let match = name.match(/\d+/g) + let col = +match[0] + let row = +match[1] + row = isNaN(row) ? -1 : row + + return { + col: col, + row: row + } + } + + /** + * Adds a new row to the table below the selected cell. + */ + addRow() { + // Clone the last row to create a new row. + let lastRow = this.tableBody.lastElementChild + let newRow = lastRow.cloneNode(true) + + // Get all input cells in the new row. + let cells = Array.from(newRow.children).map(cell => cell.querySelector("input")) + // Determine the row index of the last row. + let lastRowIndex = this.getCellPos(cells[0]).row + + // Update IDs and names of input cells in the new row. + cells.forEach(cell => { + let pos = this.getCellPos(cell) + cell.id = `chart_table_${pos.col}_values_${pos.row + 1}` + cell.name = `chart[table][${pos.col}][values][${pos.row + 1}]` + }) + + // Shift existing row data down to make room for a new row. + let pos = this.getCellPos(this.selectedCell) + //pos.row = pos.row === -1 ? 0 : pos.row + pos.row++ + for (let i = lastRowIndex; i > pos.row; i--) { + for (let j = 0; j < cells.length; j++) { + let currentCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i}`) + let previousCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i - 1}`) + currentCell.value = previousCell.value + } + } + + this.tableBody.appendChild(newRow) + + // Clear input cells in the new row. + for (let j = 0; j < cells.length; j++) { + let currentCell = this.tableBody.querySelector(`#chart_table_${j}_values_${pos.row}`) + currentCell.value = "" + console.log(currentCell) + } } - + /** + * Deletes the row containing the selected cell. + */ + delRow() { + // Get the number of rows and columns in the table. + let rowCount = this.tableBody.children.length + let colCount = this.tableBody.lastElementChild.children.length + + // Get the position of the selected cell. + let pos = this.getCellPos(this.selectedCell) + + // Don't delete heading row. + if (pos.row === -1) + return + + // Shift row data + for (let i = pos.row; i < rowCount - 1; i++) { + for (let j = 0; j < colCount; j++) { + let currentCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i}`) + let previousCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i + 1}`) + currentCell.value = previousCell.value + } + } + + // Remove the last row from the table. + this.tableBody.removeChild(this.tableBody.lastElementChild) + } + + /** + * Adds a new column to the table. + */ + addCol() { + // Get the rows and column count of the table. + let rows = Array.from(this.tableBody.children) + let rowCount = rows.length + let colCount = this.tableBody.lastElementChild.children.length + // Clone the last cell in the heading row to create a new header cell for the new column. + let newHeadCell = this.tableHead.lastElementChild.lastElementChild.cloneNode(true) + let newHeadCellInput = newHeadCell.querySelector("input[type='text']") + newHeadCellInput.id = `chart_table_${colCount}_col_name` + newHeadCellInput.name = `chart[table][${colCount}][col_name]` + let newHeadCellColor = newHeadCell.querySelector("input[type='color']") + newHeadCellColor.id = `chart_table_${colCount}_color` + newHeadCellColor.name = `chart[table][${colCount}][color]` + this.tableHead.lastElementChild.appendChild(newHeadCell) + + // Clone the last cell in each row to create a new cell for the new column in each row. + for (let i = 0; i < rowCount; i++) { + let newCell = rows[i].lastElementChild.cloneNode(true) + let newCellInput = newCell.querySelector("input") + newCellInput.id = `chart_table_${colCount}_values_${i}` + newCellInput.name = `chart[table][${colCount}][values][${i}]` + + rows[i].appendChild(newCell) + } + + // Shift existing column data to the right to make room for a new column. + let pos = this.getCellPos(this.selectedCell) + pos.col++ + for (let i = 0; i < rowCount; i++) { + for (let j = colCount; j > pos.col; j--) { + let currentCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i}`) + let previousCell = this.tableBody.querySelector(`#chart_table_${j - 1}_values_${i}`) + currentCell.value = previousCell.value + } + } + + // Shift existing column headings to the right to make room for a new column heading. + for (let j = colCount; j > pos.col; j--) { + let currentCell = this.tableHead.querySelector(`#chart_table_${j}_col_name`) + let previousCell = this.tableHead.querySelector(`#chart_table_${j - 1}_col_name`) + currentCell.value = previousCell.value + } + + // Shift existing column colors to the right to make room for a new column color. + for (let j = colCount; j > pos.col; j--) { + let currentCell = this.tableHead.querySelector(`#chart_table_${j}_color`) + let previousCell = this.tableHead.querySelector(`#chart_table_${j - 1}_color`) + currentCell.value = previousCell.value + } + + // Clear input cells in the new column. + for (let i = 0; i < rowCount; i++) { + let currentCell = this.tableBody.querySelector(`#chart_table_${pos.col}_values_${i}`) + currentCell.value = "" + } + + // Clear the input and color of the new column heading. + let headCell = this.tableHead.lastElementChild.children[pos.col] + headCell.querySelector("input[type='text']").value = "" + headCell.querySelector("input[type='color']").value = "#FFFFFF" + } + + delCol() { + let rows = Array.from(this.tableBody.children) + let rowCount = rows.length + let colCount = this.tableBody.lastElementChild.children.length + + let pos = this.getCellPos(this.selectedCell) + for (let i = 0; i < rowCount; i++) { + for (let j = pos.col; j < colCount - 1; j++) { + let currentCell = this.tableBody.querySelector(`#chart_table_${j}_values_${i}`) + let previousCell = this.tableBody.querySelector(`#chart_table_${j + 1}_values_${i}`) + currentCell.value = previousCell.value + } + } + + for (let i = pos.col; i < colCount - 1; i++) { + let currentCell = this.tableHead.querySelector(`#chart_table_${i}_col_name`) + let previousCell = this.tableHead.querySelector(`#chart_table_${i + 1}_col_name`) + currentCell.value = previousCell.value + } + + for (let i = pos.col; i < colCount - 1; i++) { + let currentCell = this.tableHead.querySelector(`#chart_table_${i}_color`) + let previousCell = this.tableHead.querySelector(`#chart_table_${i + 1}_color`) + currentCell.value = previousCell.value + } + + rows.forEach(row => { + row.removeChild(row.lastElementChild) + }) + + this.tableHead.lastElementChild.removeChild(this.tableHead.lastElementChild.lastElementChild) + } } diff --git a/public/scripts/zoom_manager.js b/public/scripts/zoom_manager.js index 651c3cc..eb84716 100644 --- a/public/scripts/zoom_manager.js +++ b/public/scripts/zoom_manager.js @@ -1,11 +1,24 @@ +/** + * Class for handling zoom operations. + */ class ZoomManager { + /** + * Creates a new instance of the ZoomManager class. + * @param {boolean} horizontalZoom - Indicates if horizontal zoom is enabled. + * @param {boolean} verticalZoom - Indicates if vertical zoom is enabled. + */ constructor(horizontalZoom, verticalZoom) { - this.x = 0 - this.y = 0 - this.scaleX = 1 - this.scaleY = 1 - this.horizontalZoom = horizontalZoom - this.verticalZoom = verticalZoom + this.x = 0 // Horizontal position + this.y = 0 // Vertical position + this.scaleX = 1 // Horizontal scale + this.scaleY = 1 // Vertical scale + this.horizontalZoom = horizontalZoom // Flag for horizontal zoom + this.verticalZoom = verticalZoom // Flag for vertical zoom + + this.lastPos = { + x: 0, + y: 0 + } } /** @@ -41,32 +54,48 @@ class ZoomManager { } /** - * Recalculates zoom based on mouse wheel event. + * Recalculates zoom based on mouse wheel event or panning. * @param {MouseEvent} event - The mouse wheel event object. + * @param {boolean} [panning=false] - Whether panning is enabled. */ - recalculate(event) { + recalculate(event, panning = false) { // 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) + // Zooming + if (event.deltaY) { + // 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; + // 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) + // 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 + // Adjust zoom position to keep zoom centered around mouse position + this.x += beforeZoom.x - afterZoom.x + this.y += beforeZoom.y - afterZoom.y + } + + // Panning + if (panning) { + // Only pan if the mouse movement exceeds minimum distance + if ((this.lastPos.x - pos.x) ** 2 + (this.lastPos.y - pos.y) ** 2 > 10) { + if (this.horizontalZoom) + this.x += (this.lastPos.x - pos.x) / this.scaleX + if (this.verticalZoom) + this.y += (this.lastPos.y - pos.y) / this.scaleY + this.lastPos = pos + } + } // Reset zoom if it goes below 1 if (this.scaleX < 1 || this.scaleY < 1) { diff --git a/public/styles/chart_style.css b/public/styles/chart_style.css index 4bf7b17..44291f8 100644 --- a/public/styles/chart_style.css +++ b/public/styles/chart_style.css @@ -1,7 +1,9 @@ +/* Define custom properties for easy reuse */ :root { - --legend-height: 30px + --legend-height: 30px; /* Height of the legend */ } +/* Ensure full height coverage for HTML and body */ html { height: 100%; } @@ -10,58 +12,77 @@ body { height: 100%; margin: 0; display: grid; - grid-template-rows: auto 2em; + grid-template-rows: auto 2em; /* Grid layout with two rows: chart area and legend */ } +/* Styles for canvas elements */ #chartCanvas, #effectCanvas, #detectionCanvas { - grid-column: 1; + grid-column: 1; /* Position within the grid */ grid-row: 1; width: 100%; height: 100%; } +/* Transition effect for the effect canvas opacity */ #effectCanvas { transition: opacity 0.3s; } +/* Set detection canvas to invisible */ #detectionCanvas { opacity: 0; } -#graphLegend { - grid-column: 1; +/* Styles for the chart legend */ +#chartLegend { + grid-column: 1; /* Position within the grid */ grid-row: 2; - display: none; + display: none; /* Hidden by default */ justify-content: center; align-items: center; text-align: center; } -#graphLegend div { +/* Styles for legend items */ +#chartLegend div { display: inline; - margin: 0 10px 0 10px; + margin: 0 10px 0 10px; /* Spacing between legend items */ width: 50px; } -#dataDiv { - display: none; +/* Styles for the tooltip */ +#tooltip { + display: none; /* Initially hidden */ position: absolute; - height: 35px; - width: 70px; background-color: white; - border: 1px solid lightgrey; + border: 1px solid grey; + text-align: center; padding: 5px; - border-radius: 5px; + border-radius: 3px; + z-index: 1; /* Ensure tooltip appears above other elements */ } -#dataDiv p { +/* Styles for text within the tooltip */ +#tooltip p { margin: 0; - font-size: 13px; + font-size: 1em; } -#dataDiv b { +#tooltip b { position: center; - font-size: 13px; + font-size: 1em; +} + +/* Styles for tooltip arrow */ +#tooltip::after { + content: ""; /* Create a pseudo-element */ + position: absolute; /* Position relative to its containing block */ + top: 50%; /* Position vertically centered */ + right: 100%; /* Position to the right of the tooltip */ + margin-top: -0.5rem; /* Adjust vertical position */ + border-width: 0.5rem; /* Size of the arrow */ + border-style: solid; /* Solid border */ + border-color: transparent white transparent transparent; /* Arrow color */ } \ No newline at end of file diff --git a/public/styles/edit_chart.css b/public/styles/edit_chart.css index c8359ff..5740964 100755 --- a/public/styles/edit_chart.css +++ b/public/styles/edit_chart.css @@ -1,50 +1,52 @@ +/* Styles for the main content area */ main { display: block; } +/* Styles for the main container div */ #mainDiv { width: 100%; display: flex; padding: 10px; } -#graphDiv { +/* Styles for the chart container div */ +#chartDiv { flex-basis: 75%; height: 400px; } - +/* Styles for the settings container div */ #settings_div { text-align: center; flex-basis: 25%; } +/* Styles for submenu label */ .submenuLabel + div { display: none; } +/* Styles for chart metadata groups */ div[id^="chart_metadata_group"] { - padding: 0.5em; - margin-bottom: 0.5em; - background-color: var(--side); + padding: 0.5em; /* Padding around metadata groups */ + margin-bottom: 0.5em; /* Margin below metadata groups */ + background-color: var(--side); /* Background color */ } +/* Styles for the table container div */ #tableDiv { - padding: 0; - overflow: auto; - flex-basis: 100%; -} - -#shareDiv { - padding: 5px; - flex-basis: 25%; - margin-left: 5px; + padding: 0; /* Remove padding */ + overflow: auto; /* Add scrollbars if content overflows */ + flex-basis: 100%; /* Set the size of the table area */ } +/* Styles for secondary div */ #secondaryDiv { - display: flex; + display: flex; /* Use flexbox layout */ } +/* Styles for table elements */ #dataTable tr, #dataTable td, #dataTable th { @@ -53,13 +55,15 @@ div[id^="chart_metadata_group"] { #dataTable td, #dataTable th { - height: 2em; + height: 2em; /* Set height of table cells */ } +/* Styles for table input elements */ #dataTable th input { font-weight: bold; } +/* Styles for table header input */ #dataTable input, #dataTable div { margin: 0; @@ -75,11 +79,13 @@ div[id^="chart_metadata_group"] { vertical-align: middle; } +/* Styles for table header input */ #dataTable th div:has(input[type=text]) { display: inline-block; width: 85%; } +/* Styles for table color input */ #dataTable th div:has(input[type=color]) { display: inline-block; width: 15%; @@ -89,22 +95,41 @@ div[id^="chart_metadata_group"] { border-width: 0; } +#rcMenu { + z-index: 1000; + position: absolute; + background-color: white; + border: 1px solid lightgrey; + box-shadow: 2px 2px 2px gray; + display: none; + margin: 0; + width: 180px; +} -#shareUsername { - width: 100px; - height: 35px; +#rcMenu ul{ + list-style-type: none; + padding-left: 10px; + width: 180px; } -#shareHeader { - text-align: center; +#rcMenu a { + text-decoration: none; + border-bottom: 1px solid lightgrey; + width: 160px; + display: block; +} + +#rcMenu a:hover{ + background-color: lightgrey; } +/* Media query for smaller screens */ @media (max-width: 950px) { #mainDiv { display: block } - #graphDiv { + #chartDiv { width: 100%; } diff --git a/public/styles/style.css b/public/styles/style.css index 9233766..9d0638b 100755 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -1,9 +1,9 @@ - - +/* Styles for graph container */ #graphDiv { height: 400px; } +/* Styles for settings container */ #settings_div{ width: 300px; padding: 5px; @@ -40,6 +40,7 @@ th, td { text-align: center; } +/* Styles for even rows of table */ tr:nth-child(even) td:not(.UI_remove_row){ background-color: var(--main); } @@ -60,59 +61,3 @@ th input { [contenteditable] { outline: 0 solid transparent; } - -#rcMenu { - z-index: 1000; - position: absolute; - background-color: white; - border: 1px solid lightgrey; - box-shadow: 2px 2px 2px gray; - display: none; - margin: 0; - width: 180px; -} - -#rcMenu ul{ - list-style-type: none; - padding-left: 10px; - width: 180px; -} - -#rcMenu a { - text-decoration: none; - border-bottom: 1px solid lightgrey; - width: 160px; - display: block; -} - -#rcMenu a:hover{ - background-color: lightgrey; -} - -#shareDiv { - background-color: var(--main); -} - -#shareList { - border: 1px solid var(--main-dark); -} - -#shareList div{ - padding: 5px; -} - -#shareList div button{ - color: red; - display: inline-block; - float: right; - height: 25px; - margin: 0; - padding: 2px; -} - -#shareList div button:hover{ - background-color: var(--side); - color: red; - display: flex; - transition: 300ms; -} \ No newline at end of file diff --git a/public/styles/web_style.css b/public/styles/web_style.css index 6e6ad6a..0cdf98c 100755 --- a/public/styles/web_style.css +++ b/public/styles/web_style.css @@ -1,5 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700;800&display=swap'); - :root { --dark: #1E1B18; --light: #E3E7F1; diff --git a/src/Controller/ChartController.php b/src/Controller/ChartController.php index 658ab72..05d4a93 100644 --- a/src/Controller/ChartController.php +++ b/src/Controller/ChartController.php @@ -39,7 +39,7 @@ class ChartController extends AbstractController } return $this->render('edit.html.twig', [ - 'form' => $form->createView() + 'chartForm' => $form ]); } diff --git a/src/Form/Type/CellType.php b/src/Form/Type/CellType.php deleted file mode 100644 index 145c5c9..0000000 --- a/src/Form/Type/CellType.php +++ /dev/null @@ -1,14 +0,0 @@ -add('col_name', TextType::class, [ 'label' => false, + 'attr'=>['autocomplete' => 'off'] ]) ->add('color', ColorType::class, [ 'label' => false, @@ -23,19 +24,18 @@ class ColumnType extends AbstractType ->add('values', CollectionType::class, [ 'entry_type' => NumberType::class, 'allow_add' => true, - 'prototype' => true, - 'prototype_data' => 0, 'entry_options' => [ 'label' => false, + 'attr'=>['autocomplete' => 'off'] ], 'required' => false ]); } - /*public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - 'data_class' => Chart::class, + 'data_class' => null, ]); - }*/ + } } \ No newline at end of file diff --git a/src/Form/Type/FontType.php b/src/Form/Type/FontType.php index 01f1fd3..7ff4352 100644 --- a/src/Form/Type/FontType.php +++ b/src/Form/Type/FontType.php @@ -2,10 +2,11 @@ namespace App\Form\Type; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints as Assert; class FontType extends AbstractType { @@ -28,8 +29,11 @@ class FontType extends AbstractType 'Brush Script MT' => 'Brush Script MT', ], ]) - ->add('size', NumberType::class, [ - 'label' => false + ->add('size', IntegerType::class, [ + 'label' => false, + 'constraints' => [ + new Assert\PositiveOrZero() + ], ]); } diff --git a/src/Form/Type/MetadataType.php b/src/Form/Type/MetadataType.php index ee467d0..5238874 100644 --- a/src/Form/Type/MetadataType.php +++ b/src/Form/Type/MetadataType.php @@ -6,11 +6,13 @@ use App\Form\Type\FontType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -36,9 +38,12 @@ class MetadataType extends AbstractType ], 'label' => 'Chart type', ]) - ->add('margin', NumberType::class, [ + ->add('margin', IntegerType::class, [ 'label' => 'Margin', - 'required' => false + 'required' => false, + 'constraints' => [ + new Assert\PositiveOrZero() + ], ]) // Title settings ->add( @@ -79,6 +84,9 @@ class MetadataType extends AbstractType ->add('yStep', NumberType::class, [ 'label' => 'Y step', 'required' => false, + 'constraints' => [ + new Assert\PositiveOrZero() + ], ]) ->add('displayAxisValues', CheckboxType::class, [ 'label' => 'Display axis values', @@ -120,13 +128,19 @@ class MetadataType extends AbstractType 'label' => 'Display points', 'required' => false, ]) - ->add('pointSize', NumberType::class, [ + ->add('pointSize', IntegerType::class, [ 'label' => 'Point size', 'required' => false, + 'constraints' => [ + new Assert\PositiveOrZero() + ], ]) - ->add('pointBorderSize', NumberType::class, [ + ->add('pointBorderSize', IntegerType::class, [ 'label' => 'Point border size', 'required' => false, + 'constraints' => [ + new Assert\PositiveOrZero() + ], ]) ->add('pointBorderColor', ColorType::class, [ 'label' => 'Point border color', @@ -158,6 +172,15 @@ class MetadataType extends AbstractType public function configureOptions(OptionsResolver $resolver) { + $resolver->setDefaults(array( + 'constraints' => array( + new Assert\Callback(function($data){ + // $data is instance of object (or array) with all properties + // you can compare Count1, Count2 and Count 3 + // and raise validation errors + }) + ) + )); /*$resolver->setDefaults([ 'type' => 'point', 'margin'=> 5, diff --git a/templates/chart.html.twig b/templates/chart.html.twig index b349c1c..4a39ece 100644 --- a/templates/chart.html.twig +++ b/templates/chart.html.twig @@ -26,16 +26,18 @@ @@ -45,6 +47,6 @@ -
-
+
+
{% endblock %} \ No newline at end of file diff --git a/templates/edit.html.twig b/templates/edit.html.twig index 361c16a..2017d51 100644 --- a/templates/edit.html.twig +++ b/templates/edit.html.twig @@ -8,6 +8,17 @@ {% block javascripts %} {{ parent() }} + + {% endblock %} {% block title %} @@ -17,13 +28,13 @@ {% block body %} {{ parent() }}
- {{ form_start(form) }} + {{ form_start(chartForm) }}
- +
- {{ form_row(form.name) }} - {{ form_row(form.code) }} - {{ form_row(form.metadata) }} + {{ form_row(chartForm.name) }} + {{ form_row(chartForm.code) }} + {{ form_row(chartForm.metadata) }}
@@ -31,14 +42,16 @@
- - {% for col in form.table %} - - {% endfor %} - - {% for i in 0..form.table[0].values|length-1 %} + + + {% for col in chartForm.table %} + + {% endfor %} + + + {% for i in 0..chartForm.table[0].values|length-1 %} - {% for col in form.table %} + {% for col in chartForm.table %} {% endfor %} @@ -46,62 +59,14 @@
{{ form_row(col.col_name) }}{{ form_row(col.color) }}
{{ form_row(col.col_name) }}{{ form_row(col.color) }}
{{ form_row(col.values[i]) }}
- {{ form_end(form) }} + {{ form_end(chartForm) }} -
{% endblock %}