|
| 1 | +{% extends "base.html" %} |
| 2 | + |
| 3 | +{% block content %} |
| 4 | +<html> |
| 5 | +<body> |
| 6 | + <h2>Eye diagram</h2> |
| 7 | + |
| 8 | + <div> |
| 9 | + <canvas height="570px" width="680px" id="eye_diagram"></canvas> |
| 10 | + </div> |
| 11 | + <div class="stats"> |
| 12 | + <div>Eye width:</div> <span id="width">0</span> |
| 13 | + <div>Eye height:</div> <span id="height">0</span> |
| 14 | + </div> |
| 15 | +</body> |
| 16 | + |
| 17 | +<style> |
| 18 | +canvas { |
| 19 | + margin-top: 15px; |
| 20 | +} |
| 21 | + |
| 22 | +.stats { |
| 23 | + display: grid; |
| 24 | + width: 15em; |
| 25 | + grid-template-columns: 1fr 1fr; |
| 26 | + margin-left: 3em |
| 27 | +} |
| 28 | +</style> |
| 29 | + |
| 30 | +<script> |
| 31 | +const chartData = {{ samples|tojson }}; |
| 32 | + |
| 33 | +const ctx = document.getElementById("eye_diagram").getContext("2d"); |
| 34 | + |
| 35 | +// Configuration |
| 36 | +const xValues = [...new Set(chartData.map(({ x }) => x))].sort((a, b) => a - b); |
| 37 | +const yValues = [...new Set(chartData.map(({ y }) => y))].sort((a, b) => a - b); |
| 38 | +const stepSize = { |
| 39 | + x: xValues[1] - xValues[0], |
| 40 | + y: yValues[1] - yValues[0], |
| 41 | +}; |
| 42 | + |
| 43 | +const avgAmplitude = 0.27; |
| 44 | +const axisPadding = { left: 55, right: 120, top: 10, bottom: 35 }; |
| 45 | +const gridSize = { x: xValues.length, y: yValues.length }; |
| 46 | +const translateOrigin = { x: gridSize.x / 2, y: gridSize.y / 2 }; |
| 47 | +const ticksCommon = { |
| 48 | + length: 9, |
| 49 | + padding: { grid: 1, text: 2 }, |
| 50 | + step: 2, |
| 51 | +}; |
| 52 | +const ticks = { x: { ...ticksCommon }, y: { ...ticksCommon } }; |
| 53 | + |
| 54 | +const cell = { |
| 55 | + width: Math.floor((ctx.canvas.width - axisPadding.left - axisPadding.right) / gridSize.x), |
| 56 | + height: Math.floor((ctx.canvas.height - axisPadding.top - axisPadding.bottom) / gridSize.y), |
| 57 | +}; |
| 58 | + |
| 59 | +// Styles |
| 60 | +ctx.strokeStyle = "black"; |
| 61 | +ctx.font = "12px Arial"; |
| 62 | + |
| 63 | +// Y Axis label |
| 64 | +ctx.textBaseline = "top"; |
| 65 | +ctx.rotate(-Math.PI / 2); |
| 66 | +ctx.fillText("Voltage", -ctx.canvas.height / 2, 10); |
| 67 | +ctx.rotate(Math.PI / 2); |
| 68 | + |
| 69 | +// X Axis label |
| 70 | +ctx.textAlign = "center"; |
| 71 | +ctx.textBaseline = "bottom"; |
| 72 | +ctx.fillText("Unit Interval", axisPadding.left + cell.width * gridSize.x / 2, ctx.canvas.height - 10); |
| 73 | + |
| 74 | +// Legend |
| 75 | +{ |
| 76 | + const label = "Amplitude"; |
| 77 | + const width = 30; |
| 78 | + const height = 120; |
| 79 | + const padding = { |
| 80 | + grid: 10, |
| 81 | + label: 10, |
| 82 | + }; |
| 83 | + |
| 84 | + const x = axisPadding.left + (gridSize.x + 1) * cell.width + padding.grid; |
| 85 | + const y = axisPadding.top; |
| 86 | + |
| 87 | + // Border |
| 88 | + const borderWidth = 1; |
| 89 | + ctx.fillStyle = "black"; |
| 90 | + ctx.fillRect(x - borderWidth, y - borderWidth, width + borderWidth * 2, height + borderWidth * 2); |
| 91 | + |
| 92 | + // Gradient |
| 93 | + const gradient = ctx.createLinearGradient(0, y, 0, y + height); |
| 94 | + gradient.addColorStop(0, "black"); |
| 95 | + gradient.addColorStop(0.5, "red"); |
| 96 | + gradient.addColorStop(1, "blue"); |
| 97 | + ctx.fillStyle = gradient; |
| 98 | + ctx.fillRect(x, y, width, height); |
| 99 | + |
| 100 | + // Ticks |
| 101 | + const font = ctx.font; // save font |
| 102 | + ctx.font = "10px Arial"; |
| 103 | + ctx.textAlign = "left"; |
| 104 | + ctx.textBaseline = "middle"; |
| 105 | + ctx.fillStyle = "black"; |
| 106 | + const ticksLength = 6; |
| 107 | + const maxValue = chartData |
| 108 | + .flat(1) |
| 109 | + .flatMap((pixel) => pixel.amp) |
| 110 | + .reduce((a, b) => Math.max(a, b), -Infinity); // Math.max(...values) causes stack overflow |
| 111 | + const ticksValues = [0, avgAmplitude, maxValue]; |
| 112 | + ticksValues.forEach((value) => { |
| 113 | + const xPos = x + width + borderWidth |
| 114 | + const yPos = y + height - value / maxValue * height; |
| 115 | + ctx.moveTo(xPos + 1, yPos); |
| 116 | + ctx.lineTo(xPos + 1 + ticksLength, yPos); |
| 117 | + ctx.fillText(value, xPos + ticksLength + 3, yPos + ctx.lineWidth / 4); |
| 118 | + }); |
| 119 | + const maxTicksWidth = Math.max(...ticksValues.map((value) => ctx.measureText(value).width)); |
| 120 | + ctx.font = font; // restore font |
| 121 | + |
| 122 | + // Label |
| 123 | + ctx.textAlign = "center"; |
| 124 | + ctx.textBaseline = "bottom"; |
| 125 | + ctx.rotate(Math.PI / 2); |
| 126 | + ctx.fillText(label, axisPadding.top + height / 2, - (x + width + padding.label + maxTicksWidth)); |
| 127 | + ctx.rotate(-Math.PI / 2); |
| 128 | +} |
| 129 | + |
| 130 | +function updateEyeDiagram() { |
| 131 | + // Calculate eye width and height |
| 132 | + const minValue = Math.min(...chartData.map(({ amp }) => amp)); |
| 133 | + const eyePixels = chartData.filter(({ amp }) => amp === minValue); |
| 134 | + const width = (Math.max(...eyePixels.map(({ x }) => x)) - Math.min(...eyePixels.map(({ x }) => x))); |
| 135 | + const height = (Math.max(...eyePixels.map(({ y }) => y)) - Math.min(...eyePixels.map(({ y }) => y))); |
| 136 | + document.getElementById("width").innerText = width; |
| 137 | + document.getElementById("height").innerText = height; |
| 138 | + |
| 139 | + // Fill grid |
| 140 | + for (const pixel of chartData) { |
| 141 | + const xIndex = pixel.x / stepSize.x; |
| 142 | + const yIndex = pixel.y / stepSize.y; |
| 143 | + const xPos = (translateOrigin.x + xIndex) * cell.width + axisPadding.left; |
| 144 | + const yPos = (translateOrigin.y - yIndex - 1) * cell.height + axisPadding.top; |
| 145 | + |
| 146 | + const fillStyle = pixel.amp >= 0 && pixel.amp <= avgAmplitude |
| 147 | + ? `rgb(${(pixel.amp / avgAmplitude) * 255}, 0, ${(1 - pixel.amp / avgAmplitude) * 255})` |
| 148 | + : `rgb(${(1 - ((pixel.amp - avgAmplitude) / avgAmplitude)) * 255}, 0, 0)`; |
| 149 | + |
| 150 | + ctx.fillStyle = fillStyle; |
| 151 | + ctx.fillRect(xPos, yPos, cell.width, cell.height); |
| 152 | + } |
| 153 | + |
| 154 | + // X Axis ticks |
| 155 | + ctx.textAlign = "right"; |
| 156 | + ctx.textBaseline = "middle"; |
| 157 | + const xTicksBase = axisPadding.left - ticks.y.padding.grid - ticks.y.length; |
| 158 | + for (let i = 0; i <= gridSize.y; i += ticks.y.step) { |
| 159 | + const y = axisPadding.top + i * cell.height; |
| 160 | + ctx.moveTo(xTicksBase, y); |
| 161 | + ctx.lineTo(xTicksBase + ticks.y.length, y); |
| 162 | + ctx.stroke(); |
| 163 | + |
| 164 | + const text = ((translateOrigin.y - i) * stepSize.y).toString(); |
| 165 | + ctx.fillText(text, xTicksBase - ticks.y.padding.text, y + ctx.lineWidth / 4); |
| 166 | + } |
| 167 | + |
| 168 | + // Y Axis ticks |
| 169 | + ctx.textAlign = "center"; |
| 170 | + ctx.textBaseline = "top"; |
| 171 | + const yTicksBase = axisPadding.top + gridSize.y * cell.height + ticks.x.padding.grid + ticks.x.length; |
| 172 | + for (let i = 0; i <= gridSize.x; i = i += ticks.x.step) { |
| 173 | + const x = axisPadding.left + i * cell.width; |
| 174 | + ctx.moveTo(x, yTicksBase - ticks.x.length); |
| 175 | + ctx.lineTo(x, yTicksBase); |
| 176 | + ctx.stroke(); |
| 177 | + const text = ((i - translateOrigin.x) * stepSize.x).toString(); |
| 178 | + ctx.fillText(text, x, yTicksBase + ticks.x.padding.text); |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +window.addEventListener("DOMContentLoaded", updateEyeDiagram); |
| 183 | + |
| 184 | +</script> |
| 185 | + |
| 186 | +</html> |
| 187 | +{% endblock %} |
0 commit comments