|
| 1 | +{% block content %} |
| 2 | +<html> |
| 3 | +<body> |
| 4 | + <h2>Eye diagram</h2> |
| 5 | + |
| 6 | + <div class="opts"> |
| 7 | + <div>Lane</div> |
| 8 | + <div>Bit Number</div> |
| 9 | + <select onchange="updateEyeDiagram()" name="lanes" id="lanes"></select> |
| 10 | + <select onchange="updateEyeDiagram()" name="bits" id="bits"> |
| 11 | + <option>Average All Bits</option> |
| 12 | + </select> |
| 13 | + </div> |
| 14 | + |
| 15 | + <div> |
| 16 | + <canvas height="570px" width="680px" id="eye_diagram"></canvas> |
| 17 | + </div> |
| 18 | + <div class="stats"> |
| 19 | + <div>Eye width:</div> <span id="width">0</span> |
| 20 | + <div>Eye height:</div> <span id="height">0</span> |
| 21 | + </div> |
| 22 | +</body> |
| 23 | + |
| 24 | +<style> |
| 25 | +canvas { |
| 26 | + margin-top: 15px; |
| 27 | +} |
| 28 | + |
| 29 | +.opts { |
| 30 | + display: grid; |
| 31 | + width: 10em; |
| 32 | + grid-template-columns: 1fr 1fr; |
| 33 | + gap: 0.3em; |
| 34 | +} |
| 35 | + |
| 36 | +.stats { |
| 37 | + display: grid; |
| 38 | + width: 15em; |
| 39 | + grid-template-columns: 1fr 1fr; |
| 40 | + margin-left: 3em |
| 41 | +} |
| 42 | +</style> |
| 43 | + |
| 44 | +<script> |
| 45 | +const chartData = {{ samples|tojson }}; |
| 46 | + |
| 47 | +const ctx = document.getElementById("eye_diagram").getContext("2d"); |
| 48 | + |
| 49 | +// Configuration |
| 50 | +const nLanes = 8; |
| 51 | +const nBits = 20; |
| 52 | +const avgAmplitude = 2048; |
| 53 | +const axisPadding = { left: 55, right: 90, top: 10, bottom: 35 }; |
| 54 | +const gridSize = { x: 32, y: 64 }; |
| 55 | +const translateOrigin = { x: gridSize.x / 2, y: gridSize.y / 2 }; |
| 56 | +const ticksCommon = { |
| 57 | + length: 9, |
| 58 | + padding: { grid: 1, text: 2 }, |
| 59 | + step: 2, |
| 60 | +}; |
| 61 | +const ticks = { x: { ...ticksCommon }, y: { ...ticksCommon } }; |
| 62 | +const axisMultiplier = {{ axis_multiplier|tojson }}; |
| 63 | + |
| 64 | +const cell = { |
| 65 | + width: Math.floor((ctx.canvas.width - axisPadding.left - axisPadding.right) / gridSize.x), |
| 66 | + height: Math.floor((ctx.canvas.height - axisPadding.top - axisPadding.bottom) / gridSize.y), |
| 67 | +}; |
| 68 | + |
| 69 | +// Styles |
| 70 | +ctx.strokeStyle = "black"; |
| 71 | +ctx.font = "12px Arial"; |
| 72 | + |
| 73 | +// Y Axis label |
| 74 | +ctx.textBaseline = "top"; |
| 75 | +ctx.rotate(-Math.PI / 2); |
| 76 | +ctx.fillText("Voltage Offset (mV)", - ctx.canvas.width / 2, 10); |
| 77 | +ctx.rotate(Math.PI / 2); |
| 78 | + |
| 79 | +// X Axis label |
| 80 | +ctx.textAlign = "center"; |
| 81 | +ctx.textBaseline = "bottom"; |
| 82 | +ctx.fillText("Phase offset (1/32 UI)", axisPadding.left + cell.width * gridSize.x / 2, ctx.canvas.height - 10); |
| 83 | + |
| 84 | +// Legend |
| 85 | +{ |
| 86 | + const label = "Amplitude"; |
| 87 | + const width = 30; |
| 88 | + const height = 120; |
| 89 | + const padding = { |
| 90 | + grid: 10, |
| 91 | + label: 15, |
| 92 | + }; |
| 93 | + |
| 94 | + const x = axisPadding.left + (gridSize.x + 1) * cell.width + padding.grid; |
| 95 | + const y = axisPadding.top; |
| 96 | + |
| 97 | + // Border |
| 98 | + const borderWidth = 1; |
| 99 | + ctx.fillStyle = "black"; |
| 100 | + ctx.fillRect(x - borderWidth, y - borderWidth, width + borderWidth * 2, height + borderWidth * 2); |
| 101 | + |
| 102 | + // Gradient |
| 103 | + const gradient = ctx.createLinearGradient(0, y, 0, y + height); |
| 104 | + gradient.addColorStop(0, "black"); |
| 105 | + gradient.addColorStop(0.5, "red"); |
| 106 | + gradient.addColorStop(1, "blue"); |
| 107 | + ctx.fillStyle = gradient; |
| 108 | + ctx.fillRect(x, y, width, height); |
| 109 | + |
| 110 | + // Ticks |
| 111 | + const font = ctx.font; // save font |
| 112 | + ctx.font = "10px Arial"; |
| 113 | + ctx.textAlign = "left"; |
| 114 | + ctx.textBaseline = "middle"; |
| 115 | + ctx.fillStyle = "black"; |
| 116 | + const ticksLength = 6; |
| 117 | + const maxValue = chartData |
| 118 | + .flat(1) |
| 119 | + .flatMap((pixel) => pixel.amp) |
| 120 | + .reduce((a, b) => Math.max(a, b), -Infinity); // Math.max(...values) causes stack overflow |
| 121 | + const ticksValues = [0, avgAmplitude, maxValue]; |
| 122 | + ticksValues.forEach((value) => { |
| 123 | + const xPos = x + width + borderWidth |
| 124 | + const yPos = y + height - value / maxValue * height; |
| 125 | + ctx.moveTo(xPos + 1, yPos); |
| 126 | + ctx.lineTo(xPos + 1 + ticksLength, yPos); |
| 127 | + ctx.fillText(value, xPos + ticksLength + 3, yPos + ctx.lineWidth / 4); |
| 128 | + }); |
| 129 | + const maxTicksWidth = Math.max(...ticksValues.map((value) => ctx.measureText(value).width)); |
| 130 | + ctx.font = font; // restore font |
| 131 | + |
| 132 | + // Label |
| 133 | + ctx.textAlign = "center"; |
| 134 | + ctx.textBaseline = "bottom"; |
| 135 | + ctx.rotate(Math.PI / 2); |
| 136 | + ctx.fillText(label, axisPadding.top + height / 2, - (x + width + padding.label + maxTicksWidth)); |
| 137 | + ctx.rotate(-Math.PI / 2); |
| 138 | +} |
| 139 | + |
| 140 | +function updateEyeDiagram() { |
| 141 | + const laneNumber = document.getElementById("lanes").selectedIndex; |
| 142 | + const bitNumber = document.getElementById("bits").selectedIndex - 1; |
| 143 | + |
| 144 | + // Calculate current amplitude grid |
| 145 | + const gridData = chartData[laneNumber] |
| 146 | + .map((pixel) => { |
| 147 | + const amp = bitNumber == -1 |
| 148 | + ? pixel.amp.reduce((a, b) => a + b, 0) / pixel.amp.length |
| 149 | + : pixel.amp[bitNumber]; |
| 150 | + return { ...pixel, amp }; |
| 151 | + }); |
| 152 | + |
| 153 | + // Calculate width and height |
| 154 | + const maxValue = Math.max(...gridData.map(({ amp }) => amp)); |
| 155 | + const eyePixels = gridData.filter(({ amp }) => amp !== maxValue); |
| 156 | + const width = (Math.max(...eyePixels.map(({ x }) => x)) - Math.min(...eyePixels.map(({ x }) => x))) * axisMultiplier.x; |
| 157 | + const height = (Math.max(...eyePixels.map(({ y }) => y)) - Math.min(...eyePixels.map(({ y }) => y))) * axisMultiplier.y; |
| 158 | + document.getElementById("width").innerText = width; |
| 159 | + document.getElementById("height").innerText = height; |
| 160 | + |
| 161 | + // Fill grid |
| 162 | + for (const pixel of gridData) { |
| 163 | + const xPos = (translateOrigin.x + pixel.x) * cell.width + axisPadding.left; |
| 164 | + const yPos = (translateOrigin.y - pixel.y - 1) * cell.height + axisPadding.top; |
| 165 | + |
| 166 | + const fillStyle = pixel.amp >= 0 && pixel.amp <= avgAmplitude |
| 167 | + ? `rgb(${(pixel.amp / avgAmplitude) * 255}, 0, ${(1 - pixel.amp / avgAmplitude) * 255})` |
| 168 | + : `rgb(${(1 - ((pixel.amp - avgAmplitude) / avgAmplitude)) * 255}, 0, 0)`; |
| 169 | + |
| 170 | + ctx.fillStyle = fillStyle; |
| 171 | + ctx.fillRect(xPos, yPos, cell.width, cell.height); |
| 172 | + } |
| 173 | + |
| 174 | + // X Axis ticks |
| 175 | + ctx.textAlign = "right"; |
| 176 | + ctx.textBaseline = "middle"; |
| 177 | + const xTicksBase = axisPadding.left - ticks.y.padding.grid - ticks.y.length; |
| 178 | + for (let i = 0; i <= gridSize.y; i += ticks.y.step) { |
| 179 | + const y = axisPadding.top + i * cell.height; |
| 180 | + ctx.moveTo(xTicksBase, y); |
| 181 | + ctx.lineTo(xTicksBase + ticks.y.length, y); |
| 182 | + ctx.stroke(); |
| 183 | + |
| 184 | + const text = ((translateOrigin.y - i) * axisMultiplier.y).toString(); |
| 185 | + ctx.fillText(text, xTicksBase - ticks.y.padding.text, y + ctx.lineWidth / 4); |
| 186 | + } |
| 187 | + |
| 188 | + // Y Axis ticks |
| 189 | + ctx.textAlign = "center"; |
| 190 | + ctx.textBaseline = "top"; |
| 191 | + const yTicksBase = axisPadding.top + gridSize.y * cell.height + ticks.x.padding.grid + ticks.x.length; |
| 192 | + for (let i = 0; i <= gridSize.x; i = i += ticks.x.step) { |
| 193 | + const x = axisPadding.left + i * cell.width; |
| 194 | + ctx.moveTo(x, yTicksBase - ticks.x.length); |
| 195 | + ctx.lineTo(x, yTicksBase); |
| 196 | + ctx.stroke(); |
| 197 | + const text = ((i - translateOrigin.x) * axisMultiplier.x).toString(); |
| 198 | + ctx.fillText(text, x, yTicksBase + ticks.x.padding.text); |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | +window.addEventListener("DOMContentLoaded", () => { |
| 203 | + const selectLanes = document.getElementById("lanes"); |
| 204 | + for (var i = 0; i < nLanes; i++) { |
| 205 | + const option = document.createElement("option"); |
| 206 | + option.text = i; |
| 207 | + selectLanes.add(option); |
| 208 | + } |
| 209 | + |
| 210 | + const selectBits = document.getElementById("bits"); |
| 211 | + for (var i = 0; i < nBits; i++) { |
| 212 | + const option = document.createElement("option"); |
| 213 | + option.text = i; |
| 214 | + selectBits.add(new Option(i)); |
| 215 | + } |
| 216 | + |
| 217 | + updateEyeDiagram(); |
| 218 | +}) |
| 219 | + |
| 220 | +</script> |
| 221 | + |
| 222 | +</html> |
| 223 | +{% endblock %} |
0 commit comments