Table editing

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

@ -1,38 +1,102 @@
/**
* Represents a loader for chart data and settings.
*/
class ChartLoader { 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.canvas = canvas
this.effectCanvas = effectCanvas this.effectCanvas = effectCanvas
this.detectionCanvas = detectionCanvas this.detectionCanvas = detectionCanvas
this.parent = parent this.parent = parent
this.legend = legend this.legend = legend
this.dataDiv = dataDiv this.tooltip = tooltip
this.clickedShapeIndex = null this.clickedShapeIndex = null
this.selectedShapeIndex = 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) { loadData(code) {
// Fetch chart data from the server
fetch("/api/charts/" + code, { fetch("/api/charts/" + code, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'json' 'Content-Type': 'json'
} }
}) })
// Parse the JSON response
.then(response => response.json()) .then(response => response.json())
// Process the retrieved data
.then(data => { .then(data => {
// Extract metadata and table data from the response
let metadata = data.metadata let metadata = data.metadata
metadata.custom_x_values = "" //metadata.custom_x_values = ""
let table = data.table let table = data.table
// Draw the chart it contains data
if (data !== null) { if (data !== null) {
this.drawChart(metadata, table) this.drawChart(metadata, table)
this.parent.style.backgroundColor = metadata.backgroundColor this.parent.style.backgroundColor = metadata.backgroundColor
} }
// Display an error message if chart data is not found
else else
this.parent.innerHTML = "graph not found." 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) { getShapeIndex(canvas, pos) {
let ctx = canvas.getContext('2d') let ctx = canvas.getContext('2d')
@ -44,34 +108,15 @@ class ChartLoader {
while (imageData.length > 0) { while (imageData.length > 0) {
let pixel = imageData.splice(0, 4) let pixel = imageData.splice(0, 4)
// only if alpha is 100% // Only consider pixels with full opacity (alpha = 255)
if (pixel[3] === 255) { if (pixel[3] === 255) {
let index = (pixel[0] * 256 + pixel[1]) * 256 + pixel[2] let index = (pixel[0] * 256 + pixel[1]) * 256 + pixel[2]
indexes.push(index) 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 // To avoid edge cases because of anti aliasing
let mostCommonIndex = mode(indexes) let mostCommonIndex = this.mode(indexes)
return mostCommonIndex return mostCommonIndex
} }
@ -80,113 +125,175 @@ class ChartLoader {
} }
addListener(chart) { /**
//Click * Handles mouse movement events for interactivity.
document.addEventListener('mousemove', (e) => { * @param {MouseEvent} e - The mouse event object.
const pos = { * @param {Chart} chart - The chart object associated with the mouse movement.
x: e.clientX, */
y: e.clientY mouseMoveFunc(e, chart) {
} // Get mouse position
const pos = {
let shapeIndex = this.selectedShapeIndex x: e.clientX,
if (shapeIndex === null) y: e.clientY
shapeIndex = this.getShapeIndex(this.detectionCanvas, pos) }
if (shapeIndex !== null) {
let obj = chart.objects[shapeIndex]
// Effect let obj = []
let effectCtx = this.effectCanvas.getContext("2d") // Add selected shape to array if it exists
effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height) if (this.selectedShapeIndex !== null)
this.effectCanvas.style.opacity = 1 obj.push(chart.objects[this.selectedShapeIndex])
chart.drawEffect(effectCtx, [obj])
// 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 = "<b>" + name + "</b><br><p>" + obj[0].value + "</p>"
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) * Adds event listeners to enable interactivity on the chart.
this.dataDiv.style.left = pos.x + "px" * @param {Chart} chart - The chart object to which listeners will be added.
else */
this.dataDiv.style.left = pos.x - this.dataDiv.clientWidth + "px" 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) if (this.panningEnabled === true) {
this.dataDiv.style.top = pos.y + "px" this.chart.zoom.recalculate(e, true)
else
this.dataDiv.style.top = pos.y - this.dataDiv.clientHeight + "px"
this.dataDiv.style.display = "block" // Update chart and redraw
let name = chart.data[obj.colId.toString()].col_name chart.updateBounds()
this.dataDiv.innerHTML = "<b>" + name + "</b><br><p>" + obj.value + "</p>" chart.draw()
} this.addInteractivity()
else {
this.dataDiv.style.display = "none"
this.effectCanvas.style.opacity = 0
} }
}) })
// Mousedown event listener for tracking mouse clicks
document.addEventListener("mousedown", (e) => { document.addEventListener("mousedown", (e) => {
const pos = { const pos = {
x: e.clientX, x: e.clientX,
y: e.clientY y: e.clientY
} }
// Store the index of the latest clicked shape
this.clickedShapeIndex = this.getShapeIndex(this.detectionCanvas, pos) 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) => { document.addEventListener("mouseup", (e) => {
const pos = { const pos = {
x: e.clientX, x: e.clientX,
y: e.clientY y: e.clientY
} }
// Determine if the selected shape has changed after mouse release
if (this.clickedShapeIndex === null || if (this.clickedShapeIndex === null ||
this.clickedShapeIndex !== this.getShapeIndex(this.detectionCanvas, pos)) this.clickedShapeIndex !== this.getShapeIndex(this.detectionCanvas, pos))
this.selectedShapeIndex = null this.selectedShapeIndex = null
else else
this.selectedShapeIndex = this.clickedShapeIndex this.selectedShapeIndex = this.clickedShapeIndex
/*console.log(this.clickedShapeIndex + " " + // Perform mouse move action after mouse release to show the change immediately
this.getShapeIndex(this.detectionCanvas, pos) + " " + this.mouseMoveFunc(e, chart)
this.shapeSelected)*/
this.panningEnabled = false
}) })
// Window resize event listener for resizing the chart canvas
window.addEventListener("resize", e => { 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.resizeCanvas(this.parent, this.legend.offsetHeight)
chart.draw() chart.draw()
// Reapply interactivity
this.addInteractivity() this.addInteractivity()
}) })
// Wheel event listener for zooming the chart (if enabled)
if (chart.settings.horizontalZoom || chart.settings.verticalZoom) if (chart.settings.horizontalZoom || chart.settings.verticalZoom)
window.addEventListener("wheel", e => { window.addEventListener("wheel", e => {
e.stopImmediatePropagation() // Prevent default scrolling behavior
e.stopPropagation() e.stopPropagation()
e.preventDefault() 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.updateBounds()
chart.draw() chart.draw()
this.addInteractivity() 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() { async addInteractivity() {
setTimeout(() => { setTimeout(() => {
console.time("2") //console.time("2")
// Set dimensions of effect canvas
this.effectCanvas.width = this.canvas.width this.effectCanvas.width = this.canvas.width
this.effectCanvas.height = this.canvas.height this.effectCanvas.height = this.canvas.height
// Set dimensions of detection canvas
this.detectionCanvas.width = this.canvas.width this.detectionCanvas.width = this.canvas.width
this.detectionCanvas.height = this.canvas.height this.detectionCanvas.height = this.canvas.height
// Draw detection map on the detection canvas
this.chart.drawDetectionMap(this.detectionCanvas.getContext("2d")) this.chart.drawDetectionMap(this.detectionCanvas.getContext("2d"))
console.timeEnd("2") //console.timeEnd("2")
}, 0) }, 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) { drawChart(chartSettings, data) {
let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom) let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom)
//Choose the correct graph // Choose the correct type of chart
switch (chartSettings.type) { switch (chartSettings.type) {
case "point": case "point":
this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager) this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager)
@ -217,13 +324,15 @@ class ChartLoader {
break break
} }
// Asynchronously update the legend
setTimeout(this.chart.updateLegend(chartSettings.displayLegend, this.legend, this), 0) 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.resizeCanvas(this.parent, this.legend.offsetHeight)
this.chart.draw() this.chart.draw()
this.addInteractivity() this.addInteractivity()
this.addListeners(this.chart)
this.addListener(this.chart)
} }
} }

@ -11,7 +11,7 @@ class AreaChart extends PointChart {
* @param {ZoomManager} zoom - The zoom manager for the chart. * @param {ZoomManager} zoom - The zoom manager for the chart.
*/ */
constructor(canvas, data, settings, zoom) { 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) super(canvas, data, settings, zoom)
} }

@ -28,7 +28,7 @@ class BarChart extends Chart {
// Divide the space into equal section // Divide the space into equal section
// Calculate section size // Calculate section size
let size = this.bounds.width / this.dataLen let size = this.zoomBounds.width / this.dataLen
// Callculate inner size with margin // Callculate inner size with margin
let innerSize = size * 0.8 let innerSize = size * 0.8
// Width of each chart // Width of each chart
@ -43,27 +43,13 @@ class BarChart extends Chart {
// Value of the bar // Value of the bar
let value = categ.values[i] let value = categ.values[i]
// The left position of the bar in section // The left position of the bar in section
let left = this.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 // The height of the bar relative to the chart scale
let bar_height = value / this.extreme * this.scale * this.zoom.scaleY let bar_height = value / this.extreme * this.scale * this.zoom.scaleY
// The top position of the bar // The top position of the bar
let top = this.zoomBounds.xAxis - categ.values[i] / this.extreme * this.scale * this.zoom.scaleY let top = this.zoomBounds.xAxis - categ.values[i] / this.extreme * this.scale * this.zoom.scaleY
// Draw x-axis labels // Increment the bar count
// 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
num++ num++
// Create a new Rectangle object representing the current bar // 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()
}
}
} }
} }

@ -97,12 +97,12 @@ class Chart {
// Calculate the bounds using the canvas and margin settings // Calculate the bounds using the canvas and margin settings
this.bounds = this.getBounds(this.canvas, this.settings.margin) 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 // Calculate the scale and extreme values based on the largest and smallest data points
this.scale = this.bounds.height this.scale = this.zoomBounds.height
- (this.largest >= 0 ? (this.bounds.bottom - this.bounds.xAxis) : 0) - (this.largest >= 0 ? (this.zoomBounds.bottom - this.zoomBounds.xAxis) : 0)
this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest) this.extreme = this.largest <= 0 ? Math.abs(this.smallest) : Math.abs(this.largest)
this.zoomBounds = this.getZoomBounds()
} }
/** /**
@ -138,7 +138,7 @@ class Chart {
// between the two points on the graph area. // between the two points on the graph area.
result.xAxis = result.bottom result.xAxis = result.bottom
- result.height / ((Math.abs(this.largest)) + Math.abs(this.smallest)) * Math.abs(this.smallest) - result.height / ((Math.abs(this.largest)) + Math.abs(this.smallest)) * Math.abs(this.smallest)
return result return result
} }
@ -159,12 +159,33 @@ class Chart {
result.right = pos.x result.right = pos.x
result.bottom = pos.y 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 // Calculate width and height of the zoomed bounds
result.width = this.bounds.width * this.zoom.scaleX result.width = result.right - result.left
result.height = this.bounds.height * this.zoom.scaleY result.height = result.bottom - result.top
// Get the screen coordinate of the x-axis // Calculate the xAxis position based on the smallest and largest data points
result.xAxis = this.zoom.worldToScreen(null, this.bounds.xAxis).y 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 return result
} }
@ -271,6 +292,10 @@ class Chart {
* Clear the canvas and remove all stored objects. * Clear the canvas and remove all stored objects.
*/ */
clear() { clear() {
// Set the clip region to the whole canvas
this.ctx.restore()
this.ctx.save()
// Check if a custom background color is set // Check if a custom background color is set
if (this.settings.backgroundColor == null) { if (this.settings.backgroundColor == null) {
// Clear the canvas using the default clear method // Clear the canvas using the default clear method
@ -295,8 +320,8 @@ class Chart {
drawDetectionMap(ctx) { drawDetectionMap(ctx) {
// Move the drawing point to (0.5, 0.5) to avoid anti-aliasing issues // Move the drawing point to (0.5, 0.5) to avoid anti-aliasing issues
ctx.moveTo(0.5, 0.5) ctx.moveTo(0.5, 0.5)
// Set the line width to make the map slightly bigger than visible shapes // Set the line width to make the map bigger than visible shapes
//ctx.lineWidth = 3 ctx.lineWidth = 50
// Clear the entire canvas to prepare for redrawing // Clear the entire canvas to prepare for redrawing
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
@ -309,9 +334,10 @@ class Chart {
let color = "#" + object.index.toString(16).padStart(6, '0') let color = "#" + object.index.toString(16).padStart(6, '0')
ctx.fillStyle = color ctx.fillStyle = color
ctx.strokeStyle = color ctx.strokeStyle = color
// Draw the shape // Draw the shape
object.draw(ctx) object.draw(ctx)
//ctx.stroke()
} }
}) })
} }
@ -328,23 +354,22 @@ class Chart {
ctx.fill() ctx.fill()
ctx.shadowBlur = 15 ctx.shadowBlur = 15
ctx.shadowColor = 'rgba(0,0,0,1)'
objects.forEach(object => { objects.forEach(object => {
// Get the color of the object based on its column ID // Get the color of the object based on its column ID
let color = this.data[object.colId].color let color = this.data[object.colId].color
// Adjust the color to make it lighter by 20 units // Adjust the color to make it lighter by 20 units
let lighterColor = adjustColor(color, 20) let lighterColor = adjustColor(color, 20)
ctx.fillStyle = lighterColor
// Set the stroke style to a semi-transparent black ctx.fillStyle = lighterColor
ctx.strokeStyle = lighterColor//'rgba(0,0,0,0.3)' ctx.strokeStyle = lighterColor
//ctx.lineWidth = 0
ctx.shadowColor = 'rgba(0,0,0,1)'
// Draw the object // Draw the object
object.draw(ctx) object.draw(ctx)
ctx.shadowColor = 'rgba(0,0,0,0)'
// Stroke to draw the border // Stroke to draw the border
ctx.stroke() ctx.stroke()
}) })
ctx.shadowColor = 'rgba(0,0,0,0)'
} }
/** /**
@ -361,7 +386,7 @@ class Chart {
this.settings.yStep = 1 this.settings.yStep = 1
// Draw ticks and labels for the Y-axis // Draw ticks and labels for the Y-axis
this.drawYAxisTicks() this.drawYAxisTicks(displayAxisValues)
// Draw ticks and labels for the X-axis // Draw ticks and labels for the X-axis
this.drawXAxisTicks(displayAxisValues) this.drawXAxisTicks(displayAxisValues)
@ -376,28 +401,43 @@ class Chart {
if (this.settings.displayTitle) if (this.settings.displayTitle)
this.drawTitle() 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.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.clip()
this.ctx.closePath() this.ctx.closePath()
} }
/** /**
* Draws the ticks and labels on the Y-axis * Draws the ticks and labels on the Y-axis.
*/ */
drawYAxisTicks() { drawYAxisTicks() {
this.ctx.beginPath() 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 // 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)) { 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 // Calculate the Y-position for the tick
let y = this.bounds.xAxis - i * this.scale / this.extreme let y = this.zoomBounds.xAxis - i * this.scale / this.extreme
y = this.zoom.worldToScreen(null, y).y
// Skip text and lines if out of bounds // Skip text and lines if out of bounds
if (y < this.bounds.top || y > this.bounds.bottom) 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 // Set text style and alignment for the Y-axis values
this.ctx.fillStyle = "black" if (this.settings.displayAxisValues)
this.ctx.textAlign = "end" this.ctx.fillText(i, this.bounds.left - 3, y)
this.ctx.fillText(i, this.bounds.left - 3, y)
// Draw the tick // Draw the tick
this.ctx.stroke() this.ctx.stroke()
@ -422,9 +461,9 @@ class Chart {
/** /**
* Draws the ticks and labels on the X-axis * 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 // Check if axis values should be displayed
if (displayAxisValues) { if (displayAxisValues) {
// Set text style and alignment for the X-axis values // Set text style and alignment for the X-axis values

@ -11,7 +11,7 @@ class LineChart extends PointChart {
* @param {ZoomManager} zoom - The zoom manager for the chart. * @param {ZoomManager} zoom - The zoom manager for the chart.
*/ */
constructor(canvas, data, settings, zoom) { 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) super(canvas, data, settings, zoom)
} }

@ -14,10 +14,17 @@ class PointChart extends Chart {
super(canvas, data, settings, zoom) 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) { getPointPos(i, value) {
return { return {
x: Math.round(this.zoomBounds.left + this.zoomBounds.width / (this.dataLen - 1) * i), 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 // Skip empty points
if (values[i] === null || values[i] === undefined) if (values[i] === null || values[i] === undefined)
continue continue
// Calculate the x and y coordinates for the data point // Calculate the x and y coordinates for the data point
let pos = this.getPointPos(i, values[i]) let pos = this.getPointPos(i, values[i])
@ -79,7 +86,7 @@ class PointChart extends Chart {
if (this.settings.displayPoints) if (this.settings.displayPoints)
setTimeout(() => { setTimeout(() => {
console.time("1") console.time("1")
this.data.forEach((categ, colId) => {this.drawPoints(categ.values, colId, categ.color)}) this.data.forEach((categ, colId) => { this.drawPoints(categ.values, colId, categ.color) })
console.timeEnd("1") console.timeEnd("1")
}, 0) }, 0)

@ -77,6 +77,23 @@ class Rectangle extends Shape {
y: 50 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 * @param {CanvasRenderingContext2D} [ctx=this.ctx] - The 2D drawing context for the canvas
*/ */
draw(ctx = this.ctx) { draw(ctx = this.ctx) {
ctx.beginPath() ctx.beginPath()
// Define the circle // Define the circle
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI) ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI)
@ -122,6 +139,23 @@ class Circle extends Shape {
y: this.y 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 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 // Fill the donut slice with the current fill style
ctx.fill() ctx.fill()
} }
/** /**
* Returns the center coordinates of the donut slice. * Returns the center coordinates of the donut slice.
* *

@ -11,7 +11,7 @@ class SmoothAreaChart extends PointChart {
* @param {ZoomManager} zoom - The zoom manager for the chart. * @param {ZoomManager} zoom - The zoom manager for the chart.
*/ */
constructor(canvas, data, settings, zoom) { 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) super(canvas, data, settings, zoom)
} }

@ -11,7 +11,7 @@ class SmoothLineChart extends PointChart {
* @param {ZoomManager} zoom - The zoom manager for the chart. * @param {ZoomManager} zoom - The zoom manager for the chart.
*/ */
constructor(canvas, data, settings, zoom) { 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) super(canvas, data, settings, zoom)
} }

@ -19,10 +19,6 @@ class StackedChart extends Chart {
* Draws the stacked bar chart on the canvas. * Draws the stacked bar chart on the canvas.
*/ */
draw() { draw() {
/*this.ctx.shadowOffsetX = 15
this.ctx.shadowOffsetY = 15
this.ctx.shadowBlur = 4*/
// Calculate the largest total value across all categories for normalization // Calculate the largest total value across all categories for normalization
let largest = 0 let largest = 0
for (let i = 0; i < this.dataLen; i++) { for (let i = 0; i < this.dataLen; i++) {
@ -48,8 +44,6 @@ class StackedChart extends Chart {
for (let i = 0; i < this.dataLen; i++) { for (let i = 0; i < this.dataLen; i++) {
// The top position of the last stacked bar segment // The top position of the last stacked bar segment
let last_top = this.zoomBounds.xAxis let last_top = this.zoomBounds.xAxis
// Counter to determine the first category in each data point
let num = 0
this.data.forEach((categ, colId) => { this.data.forEach((categ, colId) => {
// Value of the bar segment // Value of the bar segment
@ -64,26 +58,6 @@ class StackedChart extends Chart {
// Update the last top position for the next iteration // Update the last top position for the next iteration
last_top = top 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 // Create a new Rectangle object representing the current bar
let newObject = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height) let newObject = new Rectangle(this.ctx, value, colId, left, top, bar_width, bar_height)
// Add the new Rectangle object to the list of objects // 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()
}
}
} }
} }

@ -1,127 +1,121 @@
let canvas, parent, legend, dataDiv, table; /**
const urlParams = new URLSearchParams(window.location.search); * Initializes variables and event listeners after the DOM content is loaded.
const graph_code = urlParams.get('code'); */
validateUser(graph_code); let canvas, parent, legend, dataDiv, table
let rcTarget = {};
let rcTarget = {}
$(document).ready( function () { $(document).ready( function () {
canvas = document.getElementById("graphCanvas"); canvas = document.getElementById("graphCanvas")
parent = document.getElementById("graphDiv"); parent = document.getElementById("graphDiv")
legend = document.getElementById("graphLegend"); legend = document.getElementById("graphLegend")
dataDiv = document.getElementById("dataDiv"); dataDiv = document.getElementById("dataDiv")
table = new Table(document.getElementById("dataTable")); table = new Table(document.getElementById("dataTable"))
document.getElementById('upload').addEventListener('change', handleFileSelect, false); document.getElementById('upload').addEventListener('change', handleFileSelect, false)
load_data(); load_data()
table.reloadEvLi(); table.reloadEvLi()
reloadShares(); reloadShares()
//Click //Click
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
const pos = { const pos = {
x: e.clientX - canvas.offsetLeft, x: e.clientX - canvas.offsetLeft,
y: e.clientY - canvas.offsetTop y: e.clientY - canvas.offsetTop
}; }
let obj = checkHit(pos); let obj = checkHit(pos)
//show point value //show point value
if (obj !== null) { if (obj !== null) {
dataDiv.style.left = pos.x + canvas.offsetLeft + "px"; dataDiv.style.left = pos.x + canvas.offsetLeft + "px"
dataDiv.style.top = pos.y + canvas.offsetTop + "px"; dataDiv.style.top = pos.y + canvas.offsetTop + "px"
dataDiv.style.display = "block"; dataDiv.style.display = "block"
dataDiv.innerHTML = "<b>" + obj.name + "</b><br><p>" + obj.value + "</p>"; dataDiv.innerHTML = "<b>" + obj.name + "</b><br><p>" + obj.value + "</p>"
} else { } else {
dataDiv.style.display = "none"; dataDiv.style.display = "none"
} }
}); })
$("#exportBtn").on('click', function (e) { /*$("#exportBtn").on('click', function (e) {
table.reloadData(); table.reloadData()
exportData('tableData'); exportData('tableData')
}); })
$("#saveBtn").on('click', function (e) { $("#saveBtn").on('click', function (e) {
table.reloadData(); table.reloadData()
save_data(); save_data()
}); })
$("#drawBtn").on('click', function (e) { $("#drawBtn").on('click', function (e) {
table.reloadData(); table.reloadData()
submitData(); submitData()
}); })
//RIGHT CLICK menu //RIGHT CLICK menu
$(document).bind("click", function(event) { $(document).bind("click", function(event) {
document.getElementById("rcMenu").style.display = "none"; document.getElementById("rcMenu").style.display = "none"
}); })
//odebere řádek //odebere řádek
$("#rcDelRow").on('click', function (e) { $("#rcDelRow").on('click', function (e) {
e.preventDefault(); e.preventDefault()
if (rcTarget.parentElement.parentElement.tagName === "THEAD") if (rcTarget.parentElement.parentElement.tagName === "THEAD")
return; return
table.removeRow(rcTarget.parentElement); table.removeRow(rcTarget.parentElement)
}); })
//přidá řádek //přidá řádek
$("#rcAddRow").on('click', function (e) { $("#rcAddRow").on('click', function (e) {
e.preventDefault(); e.preventDefault()
table.addRow(table, rcTarget); table.addRow(table, rcTarget)
}); })
//odebere sloupec //odebere sloupec
$('#rcDelCol').on('click', function (e) { $('#rcDelCol').on('click', function (e) {
e.preventDefault(); e.preventDefault()
table.removeCol(getCellIndex(rcTarget)); table.removeCol(getCellIndex(rcTarget))
}); })
//přidá sloupec //přidá sloupec
$('#rcAddCol').on('click', function (e) { $('#rcAddCol').on('click', function (e) {
e.preventDefault(); e.preventDefault()
table.addCol(getCellIndex(rcTarget)); table.addCol(getCellIndex(rcTarget))
}); })
//Sharing //Sharing
$('#shareBtn').on('click', function (e) { $('#shareBtn').on('click', function (e) {
e.preventDefault(); e.preventDefault()
let username = document.getElementById("shareUsername").value; let username = document.getElementById("shareUsername").value
addShare(username); addShare(username)
}); })*/
}); })
//Resize /*function handleFileSelect(evt) {
$(window).on('resize', function () { let files = evt.target.files
resizeCanvas(canvas, parent); table.importData(files[0], table)
table.reloadData();
drawChart(getSettings(), table.data);
});
function handleFileSelect(evt) {
let files = evt.target.files;
table.importData(files[0], table);
} }
function submitData() { function submitData() {
table.reloadData(); table.reloadData()
drawChart(getSettings(), table.data); drawChart(getSettings(), table.data)
} }
function save_data() { function save_data() {
table.reloadData(); table.reloadData()
let settings = getSettings(); let settings = getSettings()
$.ajax({ $.ajax({
url: "php/save_data.php", url: "php/save_data.php",
type: "post", type: "post",
dataType: "text", dataType: "text",
data: {code: graph_code, data: JSON.stringify(table.data), settings: JSON.stringify(settings), name:settings.title}, data: {code: graph_code, data: JSON.stringify(table.data), settings: JSON.stringify(settings), name:settings.title},
success: function (result) { success: function (result) {
//alert("Saved successfully " + result); //alert("Saved successfully " + result)
} }
}); })
} }
function load_data() { function load_data() {
@ -132,103 +126,28 @@ function load_data() {
data: {code: graph_code}, data: {code: graph_code},
success: function (result) { success: function (result) {
if (result.data == null) { if (result.data == null) {
alert("Error: no data found"); alert("Error: no data found")
return; return
} }
table.data = JSON.parse(result.data); table.data = JSON.parse(result.data)
table.updateTable(); table.updateTable()
if (result.settings == null) { if (result.settings == null) {
alert("Error: no graph settings found"); alert("Error: no graph settings found")
} else { } else {
loadSettings(JSON.parse(result.settings)); loadSettings(JSON.parse(result.settings))
} }
drawChart(getSettings(), table.data); drawChart(getSettings(), table.data)
} }
}) })
} }
function getCellIndex(cell) { 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++){ for (let i = 0; i < children.length; i++){
if (children[i] === cell) { 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();
}
});
}

@ -1,8 +1,273 @@
/**
* Represents a table object.
*/
class Table { class Table {
constructor(table_element) { /**
this.table_element = table_element * Initializes a new instance of the Table class.
this.data = [] * @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)
}
} }

@ -1,11 +1,24 @@
/**
* Class for handling zoom operations.
*/
class ZoomManager { 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) { constructor(horizontalZoom, verticalZoom) {
this.x = 0 this.x = 0 // Horizontal position
this.y = 0 this.y = 0 // Vertical position
this.scaleX = 1 this.scaleX = 1 // Horizontal scale
this.scaleY = 1 this.scaleY = 1 // Vertical scale
this.horizontalZoom = horizontalZoom this.horizontalZoom = horizontalZoom // Flag for horizontal zoom
this.verticalZoom = verticalZoom 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 {MouseEvent} event - The mouse wheel event object.
* @param {boolean} [panning=false] - Whether panning is enabled.
*/ */
recalculate(event) { recalculate(event, panning = false) {
// Get mouse position // Get mouse position
const pos = { const pos = {
x: event.clientX, x: event.clientX,
y: event.clientY y: event.clientY
} }
// Calculate world coordinates before zoom // Zooming
let beforeZoom, afterZoom if (event.deltaY) {
beforeZoom = this.screenToWorld(pos.x, pos.y) // Calculate world coordinates before zoom
let beforeZoom, afterZoom
beforeZoom = this.screenToWorld(pos.x, pos.y)
// Adjust zoom scale based on mouse wheel delta // Adjust zoom scale based on mouse wheel delta
if (this.horizontalZoom) if (this.horizontalZoom)
this.scaleX -= (10 * this.scaleX) / event.deltaY; this.scaleX -= (10 * this.scaleX) / event.deltaY
if (this.verticalZoom) if (this.verticalZoom)
this.scaleY -= (10 * this.scaleY) / event.deltaY; this.scaleY -= (10 * this.scaleY) / event.deltaY
// Calculate world coordinates after zoom // Calculate world coordinates after zoom
afterZoom = this.screenToWorld(pos.x, pos.y) afterZoom = this.screenToWorld(pos.x, pos.y)
// Adjust zoom position to keep zoom centered around mouse position // Adjust zoom position to keep zoom centered around mouse position
this.x += beforeZoom.x - afterZoom.x this.x += beforeZoom.x - afterZoom.x
this.y += beforeZoom.y - afterZoom.y 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 // Reset zoom if it goes below 1
if (this.scaleX < 1 || this.scaleY < 1) { if (this.scaleX < 1 || this.scaleY < 1) {

@ -1,7 +1,9 @@
/* Define custom properties for easy reuse */
:root { :root {
--legend-height: 30px --legend-height: 30px; /* Height of the legend */
} }
/* Ensure full height coverage for HTML and body */
html { html {
height: 100%; height: 100%;
} }
@ -10,58 +12,77 @@ body {
height: 100%; height: 100%;
margin: 0; margin: 0;
display: grid; 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, #chartCanvas,
#effectCanvas, #effectCanvas,
#detectionCanvas { #detectionCanvas {
grid-column: 1; grid-column: 1; /* Position within the grid */
grid-row: 1; grid-row: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
/* Transition effect for the effect canvas opacity */
#effectCanvas { #effectCanvas {
transition: opacity 0.3s; transition: opacity 0.3s;
} }
/* Set detection canvas to invisible */
#detectionCanvas { #detectionCanvas {
opacity: 0; opacity: 0;
} }
#graphLegend { /* Styles for the chart legend */
grid-column: 1; #chartLegend {
grid-column: 1; /* Position within the grid */
grid-row: 2; grid-row: 2;
display: none; display: none; /* Hidden by default */
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
} }
#graphLegend div { /* Styles for legend items */
#chartLegend div {
display: inline; display: inline;
margin: 0 10px 0 10px; margin: 0 10px 0 10px; /* Spacing between legend items */
width: 50px; width: 50px;
} }
#dataDiv { /* Styles for the tooltip */
display: none; #tooltip {
display: none; /* Initially hidden */
position: absolute; position: absolute;
height: 35px;
width: 70px;
background-color: white; background-color: white;
border: 1px solid lightgrey; border: 1px solid grey;
text-align: center;
padding: 5px; 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; margin: 0;
font-size: 13px; font-size: 1em;
} }
#dataDiv b { #tooltip b {
position: center; 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 */
} }

@ -1,50 +1,52 @@
/* Styles for the main content area */
main { main {
display: block; display: block;
} }
/* Styles for the main container div */
#mainDiv { #mainDiv {
width: 100%; width: 100%;
display: flex; display: flex;
padding: 10px; padding: 10px;
} }
#graphDiv { /* Styles for the chart container div */
#chartDiv {
flex-basis: 75%; flex-basis: 75%;
height: 400px; height: 400px;
} }
/* Styles for the settings container div */
#settings_div { #settings_div {
text-align: center; text-align: center;
flex-basis: 25%; flex-basis: 25%;
} }
/* Styles for submenu label */
.submenuLabel + div { .submenuLabel + div {
display: none; display: none;
} }
/* Styles for chart metadata groups */
div[id^="chart_metadata_group"] { div[id^="chart_metadata_group"] {
padding: 0.5em; padding: 0.5em; /* Padding around metadata groups */
margin-bottom: 0.5em; margin-bottom: 0.5em; /* Margin below metadata groups */
background-color: var(--side); background-color: var(--side); /* Background color */
} }
/* Styles for the table container div */
#tableDiv { #tableDiv {
padding: 0; padding: 0; /* Remove padding */
overflow: auto; overflow: auto; /* Add scrollbars if content overflows */
flex-basis: 100%; flex-basis: 100%; /* Set the size of the table area */
}
#shareDiv {
padding: 5px;
flex-basis: 25%;
margin-left: 5px;
} }
/* Styles for secondary div */
#secondaryDiv { #secondaryDiv {
display: flex; display: flex; /* Use flexbox layout */
} }
/* Styles for table elements */
#dataTable tr, #dataTable tr,
#dataTable td, #dataTable td,
#dataTable th { #dataTable th {
@ -53,13 +55,15 @@ div[id^="chart_metadata_group"] {
#dataTable td, #dataTable td,
#dataTable th { #dataTable th {
height: 2em; height: 2em; /* Set height of table cells */
} }
/* Styles for table input elements */
#dataTable th input { #dataTable th input {
font-weight: bold; font-weight: bold;
} }
/* Styles for table header input */
#dataTable input, #dataTable input,
#dataTable div { #dataTable div {
margin: 0; margin: 0;
@ -75,11 +79,13 @@ div[id^="chart_metadata_group"] {
vertical-align: middle; vertical-align: middle;
} }
/* Styles for table header input */
#dataTable th div:has(input[type=text]) { #dataTable th div:has(input[type=text]) {
display: inline-block; display: inline-block;
width: 85%; width: 85%;
} }
/* Styles for table color input */
#dataTable th div:has(input[type=color]) { #dataTable th div:has(input[type=color]) {
display: inline-block; display: inline-block;
width: 15%; width: 15%;
@ -89,22 +95,41 @@ div[id^="chart_metadata_group"] {
border-width: 0; 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 { #rcMenu ul{
width: 100px; list-style-type: none;
height: 35px; padding-left: 10px;
width: 180px;
} }
#shareHeader { #rcMenu a {
text-align: center; 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) { @media (max-width: 950px) {
#mainDiv { #mainDiv {
display: block display: block
} }
#graphDiv { #chartDiv {
width: 100%; width: 100%;
} }

@ -1,9 +1,9 @@
/* Styles for graph container */
#graphDiv { #graphDiv {
height: 400px; height: 400px;
} }
/* Styles for settings container */
#settings_div{ #settings_div{
width: 300px; width: 300px;
padding: 5px; padding: 5px;
@ -40,6 +40,7 @@ th, td {
text-align: center; text-align: center;
} }
/* Styles for even rows of table */
tr:nth-child(even) td:not(.UI_remove_row){ tr:nth-child(even) td:not(.UI_remove_row){
background-color: var(--main); background-color: var(--main);
} }
@ -60,59 +61,3 @@ th input {
[contenteditable] { [contenteditable] {
outline: 0 solid transparent; 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;
}

@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700;800&display=swap');
:root { :root {
--dark: #1E1B18; --dark: #1E1B18;
--light: #E3E7F1; --light: #E3E7F1;

@ -39,7 +39,7 @@ class ChartController extends AbstractController
} }
return $this->render('edit.html.twig', [ return $this->render('edit.html.twig', [
'form' => $form->createView() 'chartForm' => $form
]); ]);
} }

@ -1,14 +0,0 @@
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType as BaseType;
class CellType extends AbstractType
{
public function getParent()
{
return BaseType::class;
}
}

@ -16,6 +16,7 @@ class ColumnType extends AbstractType
$builder $builder
->add('col_name', TextType::class, [ ->add('col_name', TextType::class, [
'label' => false, 'label' => false,
'attr'=>['autocomplete' => 'off']
]) ])
->add('color', ColorType::class, [ ->add('color', ColorType::class, [
'label' => false, 'label' => false,
@ -23,19 +24,18 @@ class ColumnType extends AbstractType
->add('values', CollectionType::class, [ ->add('values', CollectionType::class, [
'entry_type' => NumberType::class, 'entry_type' => NumberType::class,
'allow_add' => true, 'allow_add' => true,
'prototype' => true,
'prototype_data' => 0,
'entry_options' => [ 'entry_options' => [
'label' => false, 'label' => false,
'attr'=>['autocomplete' => 'off']
], ],
'required' => false 'required' => false
]); ]);
} }
/*public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'data_class' => Chart::class, 'data_class' => null,
]); ]);
}*/ }
} }

@ -2,10 +2,11 @@
namespace App\Form\Type; namespace App\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; 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\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
class FontType extends AbstractType class FontType extends AbstractType
{ {
@ -28,8 +29,11 @@ class FontType extends AbstractType
'Brush Script MT' => 'Brush Script MT', 'Brush Script MT' => 'Brush Script MT',
], ],
]) ])
->add('size', NumberType::class, [ ->add('size', IntegerType::class, [
'label' => false 'label' => false,
'constraints' => [
new Assert\PositiveOrZero()
],
]); ]);
} }

@ -6,11 +6,13 @@ use App\Form\Type\FontType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; 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\TextType;
use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -36,9 +38,12 @@ class MetadataType extends AbstractType
], ],
'label' => 'Chart type', 'label' => 'Chart type',
]) ])
->add('margin', NumberType::class, [ ->add('margin', IntegerType::class, [
'label' => 'Margin', 'label' => 'Margin',
'required' => false 'required' => false,
'constraints' => [
new Assert\PositiveOrZero()
],
]) ])
// Title settings // Title settings
->add( ->add(
@ -79,6 +84,9 @@ class MetadataType extends AbstractType
->add('yStep', NumberType::class, [ ->add('yStep', NumberType::class, [
'label' => 'Y step', 'label' => 'Y step',
'required' => false, 'required' => false,
'constraints' => [
new Assert\PositiveOrZero()
],
]) ])
->add('displayAxisValues', CheckboxType::class, [ ->add('displayAxisValues', CheckboxType::class, [
'label' => 'Display axis values', 'label' => 'Display axis values',
@ -120,13 +128,19 @@ class MetadataType extends AbstractType
'label' => 'Display points', 'label' => 'Display points',
'required' => false, 'required' => false,
]) ])
->add('pointSize', NumberType::class, [ ->add('pointSize', IntegerType::class, [
'label' => 'Point size', 'label' => 'Point size',
'required' => false, 'required' => false,
'constraints' => [
new Assert\PositiveOrZero()
],
]) ])
->add('pointBorderSize', NumberType::class, [ ->add('pointBorderSize', IntegerType::class, [
'label' => 'Point border size', 'label' => 'Point border size',
'required' => false, 'required' => false,
'constraints' => [
new Assert\PositiveOrZero()
],
]) ])
->add('pointBorderColor', ColorType::class, [ ->add('pointBorderColor', ColorType::class, [
'label' => 'Point border color', 'label' => 'Point border color',
@ -158,6 +172,15 @@ class MetadataType extends AbstractType
public function configureOptions(OptionsResolver $resolver) 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([ /*$resolver->setDefaults([
'type' => 'point', 'type' => 'point',
'margin'=> 5, 'margin'=> 5,

@ -26,16 +26,18 @@
<script type="text/javascript" src={{ asset('/scripts/chart_loader.js') }}></script> <script type="text/javascript" src={{ asset('/scripts/chart_loader.js') }}></script>
<script> <script>
// Execute when DOM content is fully loaded
document.addEventListener("DOMContentLoaded", function(event){ document.addEventListener("DOMContentLoaded", function(event){
// Get DOM elements
let canvas = document.getElementById("chartCanvas") let canvas = document.getElementById("chartCanvas")
let effectCanvas = document.getElementById("effectCanvas") let effectCanvas = document.getElementById("effectCanvas")
let detectionCanvas = document.getElementById("detectionCanvas") let detectionCanvas = document.getElementById("detectionCanvas")
let parent = document.body let parent = document.body
let legend = document.getElementById("graphLegend") let legend = document.getElementById("chartLegend")
let dataDiv = document.getElementById("dataDiv") let tooltip = document.getElementById("tooltip")
let chartLoader = new ChartLoader(canvas, effectCanvas, detectionCanvas, parent, legend, dataDiv) // Create ChartLoader instance
let chartLoader = new ChartLoader(canvas, effectCanvas, detectionCanvas, parent, legend, tooltip)
chartLoader.loadData({{ code }}) chartLoader.loadData({{ code }})
}) })
</script> </script>
@ -45,6 +47,6 @@
<canvas id="chartCanvas"></canvas> <canvas id="chartCanvas"></canvas>
<canvas id="effectCanvas"></canvas> <canvas id="effectCanvas"></canvas>
<canvas id="detectionCanvas"></canvas> <canvas id="detectionCanvas"></canvas>
<div id="graphLegend"></div> <div id="chartLegend"></div>
<div id="dataDiv"></div> <div id="tooltip"></div>
{% endblock %} {% endblock %}

@ -8,6 +8,17 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script type="text/javascript" src={{ asset('/scripts/table.js') }}></script>
<script>
// Execute when DOM content is fully loaded
document.addEventListener("DOMContentLoaded", function(event){
// Get DOM elements
let tableElement = document.getElementById("dataTable")
let rcMenu = document.getElementById("rcMenu")
let table = new Table(tableElement, rcMenu)
})
</script>
{% endblock %} {% endblock %}
{% block title %} {% block title %}
@ -17,13 +28,13 @@
{% block body %} {% block body %}
{{ parent() }} {{ parent() }}
<main> <main>
{{ form_start(form) }} {{ form_start(chartForm) }}
<div id="mainDiv"> <div id="mainDiv">
<iframe id="graphDiv" src={{ "https://spacek.blue/charts/" ~ field_value(form.code) }}></iframe> <iframe id="chartDiv" src={{ "https://spacek.blue/charts/" ~ field_value(chartForm.code) }}></iframe>
<div id="settings_div"> <div id="settings_div">
{{ form_row(form.name) }} {{ form_row(chartForm.name) }}
{{ form_row(form.code) }} {{ form_row(chartForm.code) }}
{{ form_row(form.metadata) }} {{ form_row(chartForm.metadata) }}
<!--<button id="saveBtn">Save</button> <!--<button id="saveBtn">Save</button>
<button id="drawBtn">Draw</button>--> <button id="drawBtn">Draw</button>-->
</div> </div>
@ -31,14 +42,16 @@
<div id="secondaryDiv"> <div id="secondaryDiv">
<div id="tableDiv"> <div id="tableDiv">
<table id="dataTable"> <table id="dataTable">
<tr> <thead>
{% for col in form.table %} <tr>
<th>{{ form_row(col.col_name) }}{{ form_row(col.color) }}</th> {% for col in chartForm.table %}
{% endfor %} <th>{{ form_row(col.col_name) }}{{ form_row(col.color) }}</th>
</tr> {% endfor %}
{% for i in 0..form.table[0].values|length-1 %} </tr>
</thead>
{% for i in 0..chartForm.table[0].values|length-1 %}
<tr> <tr>
{% for col in form.table %} {% for col in chartForm.table %}
<td>{{ form_row(col.values[i]) }}</td> <td>{{ form_row(col.values[i]) }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
@ -46,62 +59,14 @@
</table> </table>
</div> </div>
</div> </div>
{{ form_end(form) }} {{ form_end(chartForm) }}
<div id="rcMenu"> <div id="rcMenu">
<ul> <ul>
<li><a id="rcDelRow" href="">delete row</a></li>
<li><a id="rcAddRow" href="">add row</a></li> <li><a id="rcAddRow" href="">add row</a></li>
<li><a id="rcDelCol" href="">remove column</a></li> <li><a id="rcDelRow" href="">delete row</a></li>
<li><a id="rcAddCol" href="">copy column</a></li> <li><a id="rcAddCol" href="">add column</a></li>
<li><a id="rcDelCol" href="">delete column</a></li>
</ul> </ul>
</div> </div>
<script>
window.addEventListener('mousedown', function(e) {
let menu = document.getElementById("rcMenu")
menu.style.display = "none"
})
document.getElementById("dataTable").addEventListener('contextmenu', function(e) {
const pos = {
x: e.clientX,
y: e.clientY
}
let menu = document.getElementById("rcMenu")
let table = document.getElementById("dataTable")
menu.style.display = "block"
if (pos.x + menu.clientWidth <= table.clientWidth)
menu.style.left = pos.x + "px"
else
menu.style.left = pos.x - menu.clientWidth + "px"
if (pos.y + menu.clientHeight <= window.innerHeight + document.documentElement.scrollTop)
menu.style.top = pos.y + "px"
else
menu.style.top = pos.y - menu.clientHeight + "px"
//menu.style.top = e.clientY + document.documentElement.scrollTop + 'px'
//menu.style.left = e.clientX + document.documentElement.scrollLeft + 'px'
//rcTarget = e.currentTarget
e.preventDefault()
e.stopPropagation()
})
let labels = [...document.getElementsByClassName("submenuLabel")]
labels.forEach(label => {
label.addEventListener('click', e => {
let submenuDiv = e.target.nextElementSibling
if (getComputedStyle(submenuDiv).display == "block")
submenuDiv.style.display = "none"
else
submenuDiv.style.display = "block"
})
})
</script>
</main> </main>
{% endblock %} {% endblock %}

Loading…
Cancel
Save

Powered by TurnKey Linux.