init
49ca40414f3ed9a04bc06760f25173f1543166fc
4 files changed
src/balanced.csssrc/balanced.htmlsrc/balanced.jssrc/example.js
diff --git a/src/balanced.css b/src/balanced.css
new file mode 100644
index 0000000..10dfc0a
--- /dev/null
+++ b/src/balanced.css
@@ -0,0 +1,57 @@
+body
+{
+ background-color: #1a1a1a;
+ margin: 0;
+ padding: 10px;
+}
+
+.meters
+{
+ font-family: Arial;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.meter
+{
+ position: relative;
+ text-align: center;
+ margin: 4px;
+ background: #2c2c2c;
+ padding: 10px;
+ border-radius: 10px;
+ width: 200px;
+ height: 180px;
+}
+
+canvas
+{
+ aborder: solid 1px red;
+ display: block;
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ width: 200px;
+ height: 180px;
+}
+
+.meter canvas:first-child
+{
+ z-index: 1;
+}
+
+.meter canvas:nth-child(2)
+{
+ z-index: 2;
+}
+
+.meter p
+{
+ font-size: 16px;
+ font-weight: 700;
+ color: #ffffff;
+ margin: 160px 0 0 0;
+ position: relative;
+ z-index: 3;
+}
diff --git a/src/balanced.html b/src/balanced.html
new file mode 100644
index 0000000..ccb99c4
--- /dev/null
+++ b/src/balanced.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="nl">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Balanced Scorecard Meters</title>
+ <link href="balanced.css" rel="stylesheet" />
+ <script src="balanced.js" defer></script>
+ <script src="example.js" defer></script>
+</head>
+<body>
+ <div class="meters" id="metersContainer"></div>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/balanced.js b/src/balanced.js
new file mode 100644
index 0000000..fd11459
--- /dev/null
+++ b/src/balanced.js
@@ -0,0 +1,226 @@
+// Cache for background canvases based on minValue and maxValue
+const backgroundCanvasCache = new Map();
+
+function updateMeter(canvasId, newTargetValue, label = null)
+{
+ const canvas = document.getElementById(canvasId);
+ if (!canvas)
+ {
+ addMeter(newTargetValue, label || 'Meter', -1, 1);
+ return;
+ }
+ const minValue = parseFloat(canvas.dataset.minValue);
+ const maxValue = parseFloat(canvas.dataset.maxValue);
+ canvas.dataset.currentValue = newTargetValue;
+ const meterDiv = canvas.parentElement;
+ const currentLabel = label || meterDiv.querySelector('p').textContent;
+ const needleCanvas = document.getElementById(`${canvasId}-needle`);
+ drawMeter(canvasId, `${canvasId}-needle`, newTargetValue, currentLabel, minValue, maxValue);
+}
+
+function addMeter(value, label, minValue = -1, maxValue = 1)
+{
+ const metersContainer = document.getElementById('metersContainer');
+ const meterId = `meter${metersContainer.children.length + 1}`;
+ const meterDiv = document.createElement('div');
+ meterDiv.className = 'meter';
+ meterDiv.style.position = 'relative';
+
+ // Background canvas
+ const bgCanvas = document.createElement('canvas');
+ bgCanvas.id = meterId;
+ bgCanvas.width = 200;
+ bgCanvas.height = 180;
+ bgCanvas.dataset.currentValue = value;
+ bgCanvas.dataset.minValue = minValue;
+ bgCanvas.dataset.maxValue = maxValue;
+ bgCanvas.style.position = 'absolute';
+ bgCanvas.style.zIndex = '1';
+
+ // Needle canvas
+ const needleCanvas = document.createElement('canvas');
+ needleCanvas.id = `${meterId}-needle`;
+ needleCanvas.width = 200;
+ needleCanvas.height = 180;
+ needleCanvas.style.position = 'absolute';
+ needleCanvas.style.zIndex = '2';
+
+ const labelP = document.createElement('p');
+ labelP.textContent = label;
+ meterDiv.appendChild(bgCanvas);
+ meterDiv.appendChild(needleCanvas);
+ meterDiv.appendChild(labelP);
+ metersContainer.appendChild(meterDiv);
+
+ drawMeter(meterId, `${meterId}-needle`, value, label, minValue, maxValue);
+}
+
+function createBackgroundCanvas(width, height, minValue, maxValue)
+{
+ //console.log('createBackgroundCanvas ' + minValue + ' ' + maxValue);
+ const bgCanvas = document.createElement('canvas');
+ bgCanvas.width = width;
+ bgCanvas.height = height;
+ const ctx = bgCanvas.getContext('2d');
+ const centerX = width / 2;
+ const centerY = height * 0.65;
+ const radius = width * 0.4;
+ const startAngle = Math.PI;
+ const endAngle = 2 * Math.PI;
+
+ // Draw background arc
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, startAngle, endAngle);
+ ctx.lineWidth = 20;
+ const gradient = ctx.createLinearGradient(0, 0, width, 0);
+ gradient.addColorStop(0, '#ff4d4d');
+ gradient.addColorStop(0.5, '#ffa500');
+ gradient.addColorStop(1, '#00cc00');
+ ctx.strokeStyle = gradient;
+ ctx.stroke();
+
+ // Draw radial gradient overlay
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, startAngle, endAngle);
+ const radialGradient = ctx.createRadialGradient(centerX, centerY, radius * 0.8, centerX, centerY, radius * 1.2);
+ radialGradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)');
+ radialGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
+ ctx.strokeStyle = radialGradient;
+ ctx.lineWidth = 10;
+ ctx.stroke();
+
+ // Draw ticks
+ const tickCount = 10;
+ const tickLength = 8;
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
+ for (let i = 0; i <= tickCount; i++)
+ {
+ const tickAngle = startAngle + (i / tickCount) * (endAngle - startAngle);
+ ctx.beginPath();
+ ctx.moveTo(centerX + radius * Math.cos(tickAngle), centerY + radius * Math.sin(tickAngle));
+ ctx.lineTo(centerX + (radius + tickLength) * Math.cos(tickAngle), centerY + (radius + tickLength) * Math.sin(tickAngle));
+ ctx.stroke();
+ }
+
+ // Draw labels
+ ctx.font = '14px Arial';
+ ctx.fillStyle = 'white';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const midValue = (maxValue + minValue) / 2;
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+ ctx.fillRect(centerX - radius - 20, centerY + 10, 40, 24);
+ ctx.fillStyle = 'white';
+ ctx.fillText(minValue.toFixed(0), centerX - radius, centerY + 22);
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+ ctx.fillRect(centerX - 20, centerY + 10, 40, 24);
+ ctx.fillStyle = 'white';
+ ctx.fillText(midValue.toFixed(0), centerX, centerY + 22);
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+ ctx.fillRect(centerX + radius - 20, centerY + 10, 40, 24);
+ ctx.fillStyle = 'white';
+ ctx.fillText(maxValue.toFixed(0), centerX + radius, centerY + 22);
+
+ // Draw screws
+ const screwRadius = 10;
+ const screwPositions = [
+ { x: screwRadius, y: screwRadius }, // top-left
+ { x: width - screwRadius, y: screwRadius }, // top-right
+ { x: screwRadius, y: height - screwRadius }, // bottom-left
+ { x: width - screwRadius, y: height - screwRadius } // bottom-right
+ ];
+ ctx.fillStyle = '#666666';
+ ctx.strokeStyle = '#333333';
+ ctx.lineWidth = 2;
+ screwPositions.forEach(pos =>
+ {
+ // Draw screw circle
+ ctx.beginPath();
+ ctx.arc(pos.x, pos.y, screwRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+ // Draw cross
+ ctx.beginPath();
+ ctx.moveTo(pos.x - screwRadius * 0.6, pos.y);
+ ctx.lineTo(pos.x + screwRadius * 0.6, pos.y);
+ ctx.moveTo(pos.x, pos.y - screwRadius * 0.6);
+ ctx.lineTo(pos.x, pos.y + screwRadius * 0.6);
+ ctx.stroke();
+ });
+
+ return bgCanvas;
+}
+
+function drawMeter(bgCanvasId, needleCanvasId, value, label, minValue, maxValue)
+{
+ const bgCanvas = document.getElementById(bgCanvasId);
+ const needleCanvas = document.getElementById(needleCanvasId);
+ const bgCtx = bgCanvas.getContext('2d');
+ const needleCtx = needleCanvas.getContext('2d');
+ const width = bgCanvas.width;
+ const height = bgCanvas.height;
+ const centerX = width / 2;
+ const centerY = height * 0.65;
+ const radius = width * 0.4;
+ const startAngle = Math.PI;
+ const endAngle = 2 * Math.PI;
+
+ if (!bgCanvas.hasBackground)
+ {
+ const cacheKey = `${minValue}_${maxValue}`;
+ if (!backgroundCanvasCache.has(cacheKey))
+ {
+ backgroundCanvasCache.set(cacheKey, createBackgroundCanvas(width, height, minValue, maxValue));
+ }
+ //console.log('draw cached(' + cacheKey + ') ' + bgCanvasId);
+ bgCtx.drawImage(backgroundCanvasCache.get(cacheKey), 0, 0);
+ bgCanvas.hasBackground = true; // Mark as drawn
+ }
+
+ needleCtx.clearRect(0, 0, width, height);
+
+ // Draw needle base
+ needleCtx.beginPath();
+ needleCtx.arc(centerX, centerY, 6, 0, 2 * Math.PI);
+ const baseGradient = needleCtx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 6);
+ baseGradient.addColorStop(0, '#ffffff');
+ baseGradient.addColorStop(1, '#666666');
+ needleCtx.fillStyle = baseGradient;
+ needleCtx.fill();
+
+ // Draw needle
+ const normalizedValue = (value - minValue) / (maxValue - minValue);
+ const angle = startAngle + normalizedValue * (endAngle - startAngle);
+ const needleLength = radius * 1.0;
+ const needleBaseWidth = 8;
+ const needleTipWidth = 1;
+ needleCtx.save();
+ needleCtx.shadowColor = 'rgba(0, 0, 0, 0.6)';
+ needleCtx.shadowBlur = 8;
+ needleCtx.shadowOffsetX = 3;
+ needleCtx.shadowOffsetY = 3;
+ needleCtx.beginPath();
+ needleCtx.translate(centerX, centerY);
+ needleCtx.rotate(angle);
+ needleCtx.moveTo(0, -needleBaseWidth / 2);
+ needleCtx.lineTo(needleLength, -needleTipWidth / 2);
+ needleCtx.lineTo(needleLength, needleTipWidth / 2);
+ needleCtx.lineTo(0, needleBaseWidth / 2);
+ needleCtx.closePath();
+ const needleGradient = needleCtx.createLinearGradient(0, -needleBaseWidth / 2, 0, needleBaseWidth / 2);
+ needleGradient.addColorStop(0, '#cccccc');
+ needleGradient.addColorStop(1, '#666666');
+ needleCtx.fillStyle = needleGradient;
+ needleCtx.fill();
+ needleCtx.restore();
+
+ // Draw value label on needle canvas
+ needleCtx.font = '14px Arial';
+ needleCtx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+ needleCtx.fillRect(centerX - 30, 0, 60, 24);
+ needleCtx.fillStyle = 'white';
+ needleCtx.textAlign = 'center';
+ needleCtx.textBaseline = 'middle';
+ needleCtx.fillText(`${value.toFixed(2)}`, centerX, 12);
+}
diff --git a/src/example.js b/src/example.js
new file mode 100644
index 0000000..26c71f2
--- /dev/null
+++ b/src/example.js
@@ -0,0 +1,75 @@
+
+var arr = [
+ "algorithm",
+ "bandwidth",
+ "cache",
+ "database",
+ "encryption",
+ "firewall",
+ "gateway",
+ "hash",
+ "interface",
+ "kernel",
+ "latency",
+ "metadata",
+ "network",
+ "packet",
+ "protocol",
+ "query",
+ "runtime",
+ "server",
+ "throughput",
+ "virtualization",
+ "authentication",
+ "bitrate",
+ "cloud",
+ "compression",
+ "container",
+ "cpu",
+ "datagram",
+ "endpoint",
+ "firmware",
+ "frame",
+ "hashrate",
+ "hypervisor",
+ "index",
+ "jitter",
+ "load",
+ "middleware",
+ "port",
+ "queue",
+ "socket",
+ "thread"
+];
+
+arr.forEach((woord, index) =>
+{
+ if (Math.random() < 0.5)
+ addMeter(50.0, woord, 0.0, 100.0);
+ else
+ addMeter(0.0, woord, -1.0, 1.0);
+});
+
+function randomUpdateMeter(meterId)
+{
+ if (Math.random() < 0.01)
+ { // 10% chance
+ const canvas = document.getElementById(meterId);
+ if (!canvas) return;
+ const currentValue = parseFloat(canvas.dataset.currentValue) || 0;
+ const minValue = parseFloat(canvas.dataset.minValue);
+ const maxValue = parseFloat(canvas.dataset.maxValue);
+ const change = (Math.random() - 0.5) * (maxValue / 2.0);
+ const newValue = Math.max(minValue, Math.min(maxValue, currentValue + change));
+ updateMeter(meterId, newValue);
+ }
+}
+
+function startRandomUpdateTimers()
+{
+ for (let i = 0; i < arr.length; i++)
+ {
+ setInterval(() => { randomUpdateMeter(`meter${1 + i}`); }, 100);
+ }
+}
+startRandomUpdateTimers();