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 {
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 = "<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)
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 = "<b>" + name + "</b><br><p>" + obj.value + "</p>"
}
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)
}
}

@ -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)
}

@ -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()
}
}
}
}

@ -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()
}
/**
@ -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)
@ -312,6 +337,7 @@ class Chart {
// 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
ctx.strokeStyle = 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)'
// 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

@ -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)
}

@ -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)
}
}
@ -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)

@ -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
}
}
}
}
/**
@ -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()
}
}
}
/**

@ -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)
}

@ -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)
}

@ -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()
}
}
}
}

@ -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 = "<b>" + obj.name + "</b><br><p>" + obj.value + "</p>";
dataDiv.style.left = pos.x + canvas.offsetLeft + "px"
dataDiv.style.top = pos.y + canvas.offsetTop + "px"
dataDiv.style.display = "block"
dataDiv.innerHTML = "<b>" + obj.name + "</b><br><p>" + obj.value + "</p>"
} 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();
}
});
}
}*/

@ -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)
}
}

@ -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) {

@ -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 */
}

@ -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%;
}

@ -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;
}

@ -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;

@ -39,7 +39,7 @@ class ChartController extends AbstractController
}
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
->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,
]);
}*/
}
}

@ -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()
],
]);
}

@ -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,

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

@ -8,6 +8,17 @@
{% block javascripts %}
{{ 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 %}
{% block title %}
@ -17,13 +28,13 @@
{% block body %}
{{ parent() }}
<main>
{{ form_start(form) }}
{{ form_start(chartForm) }}
<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">
{{ form_row(form.name) }}
{{ form_row(form.code) }}
{{ form_row(form.metadata) }}
{{ form_row(chartForm.name) }}
{{ form_row(chartForm.code) }}
{{ form_row(chartForm.metadata) }}
<!--<button id="saveBtn">Save</button>
<button id="drawBtn">Draw</button>-->
</div>
@ -31,14 +42,16 @@
<div id="secondaryDiv">
<div id="tableDiv">
<table id="dataTable">
<tr>
{% for col in form.table %}
<th>{{ form_row(col.col_name) }}{{ form_row(col.color) }}</th>
{% endfor %}
</tr>
{% for i in 0..form.table[0].values|length-1 %}
<thead>
<tr>
{% for col in chartForm.table %}
<th>{{ form_row(col.col_name) }}{{ form_row(col.color) }}</th>
{% endfor %}
</tr>
</thead>
{% for i in 0..chartForm.table[0].values|length-1 %}
<tr>
{% for col in form.table %}
{% for col in chartForm.table %}
<td>{{ form_row(col.values[i]) }}</td>
{% endfor %}
</tr>
@ -46,62 +59,14 @@
</table>
</div>
</div>
{{ form_end(form) }}
{{ form_end(chartForm) }}
<div id="rcMenu">
<ul>
<li><a id="rcDelRow" href="">delete row</a></li>
<li><a id="rcAddRow" href="">add row</a></li>
<li><a id="rcDelCol" href="">remove column</a></li>
<li><a id="rcAddCol" href="">copy column</a></li>
<li><a id="rcDelRow" href="">delete row</a></li>
<li><a id="rcAddCol" href="">add column</a></li>
<li><a id="rcDelCol" href="">delete column</a></li>
</ul>
</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>
{% endblock %}

Loading…
Cancel
Save

Powered by TurnKey Linux.