-
Notifications
You must be signed in to change notification settings - Fork 865
Demo: static HTML viewer sharing code with live web viewer #9944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| #!/usr/bin/env bash | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # Copyright (c) 2026, The OpenROAD Authors | ||
| # | ||
| # Generate a demo static HTML report (histogram-only prototype) and open it. | ||
| # Usage: bazelisk run //test/orfs/gcd:gcd_route_demo_html | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| if [[ -n "${RUNFILES_DIR:-}" ]]; then | ||
| RUNFILES="$RUNFILES_DIR" | ||
| elif [[ -d "$0.runfiles" ]]; then | ||
| RUNFILES="$0.runfiles" | ||
| else | ||
| echo "Error: cannot find runfiles directory" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| RENDER_STATIC="$RUNFILES/_main/src/web/render_static_/render_static" | ||
| if [[ ! -x "$RENDER_STATIC" ]]; then | ||
| echo "Error: render_static not found at $RENDER_STATIC" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| JSON=$(find "$RUNFILES/_main" -name "*.json" -path "*/test/orfs/*" 2>/dev/null | head -1) | ||
| if [[ -z "$JSON" ]]; then | ||
| echo "Error: no JSON payload found in runfiles" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then | ||
| OUTPUT="$BUILD_WORKSPACE_DIRECTORY/gcd_demo_report.html" | ||
| else | ||
| OUTPUT="/tmp/gcd_demo_report.html" | ||
| fi | ||
|
|
||
| "$RENDER_STATIC" --label route "$JSON" -o "$OUTPUT" | ||
|
|
||
| echo "Opening $OUTPUT" | ||
| xdg-open "$OUTPUT" 2>/dev/null || open "$OUTPUT" 2>/dev/null || echo "Open $OUTPUT in your browser" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # Copyright (c) 2026, The OpenROAD Authors | ||
|
|
||
| # Export timing data as JSON for static HTML reports. | ||
| # Used by orfs_run targets to extract data from a built design. | ||
|
|
||
| source $::env(SCRIPTS_DIR)/open.tcl | ||
|
|
||
| set design "" | ||
| if { [info exists ::env(DESIGN_NAME)] } { | ||
| set design $::env(DESIGN_NAME) | ||
| } | ||
| set stage "" | ||
| if { [info exists ::env(STAGE_NAME)] } { | ||
| set stage $::env(STAGE_NAME) | ||
| } | ||
| set variant "" | ||
| if { [info exists ::env(VARIANT_NAME)] } { | ||
| set variant $::env(VARIANT_NAME) | ||
| } | ||
|
|
||
| web_export_json -output $::env(OUTPUT) \ | ||
| -design $design -stage $stage -variant $variant |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| // Copyright (c) 2026, The OpenROAD Authors | ||
|
|
||
| // Pure SVG renderer for slack histograms. | ||
| // Takes computeHistogramLayout() output and produces an SVG string. | ||
|
|
||
| import { computeHistogramLayout } from './charts-widget.js'; | ||
|
|
||
| // Colors — reuse from charts-widget.js when loaded as ES module, | ||
| // redeclare with unique names for bundled/concatenated mode. | ||
| const kSvgNegativeFill = '#f08080'; // lightcoral | ||
| const kSvgNegativeBorder = '#8b0000'; // darkred | ||
| const kSvgPositiveFill = '#90ee90'; // lightgreen | ||
| const kSvgPositiveBorder = '#006400'; // darkgreen | ||
|
|
||
| const kSvgAxisColor = '#888'; | ||
| const kSvgTextColor = '#ccc'; | ||
| const kSvgGridColor = '#333'; | ||
|
|
||
| function escapeXml(s) { | ||
| return String(s).replace(/&/g, '&').replace(/</g, '<') | ||
| .replace(/>/g, '>').replace(/"/g, '"'); | ||
| } | ||
|
|
||
| // Render a slack histogram as an SVG string. | ||
| // | ||
| // histogramData: { bins: [{lower, upper, count, negative},...], | ||
| // time_unit, total_endpoints, unconstrained_count } | ||
| // width, height: SVG dimensions in pixels | ||
| export function renderHistogramSVG(histogramData, width = 800, height = 400) { | ||
| const layout = computeHistogramLayout(histogramData, width, height); | ||
| if (!layout.chartArea) { | ||
| return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">` + | ||
| `<text x="${width / 2}" y="${height / 2}" text-anchor="middle" ` + | ||
| `fill="${kSvgTextColor}" font-family="monospace" font-size="14">No data</text></svg>`; | ||
| } | ||
|
|
||
| const { bars, yMax, yTicks, chartArea } = layout; | ||
| const parts = []; | ||
|
|
||
| parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` + | ||
| `font-family="monospace" font-size="11">`); | ||
|
|
||
| // Background | ||
| parts.push(`<rect width="${width}" height="${height}" fill="#1e1e1e"/>`); | ||
|
|
||
| // Title | ||
| const unit = histogramData.time_unit || 'ns'; | ||
| const total = histogramData.total_endpoints || 0; | ||
| parts.push(`<text x="${width / 2}" y="18" text-anchor="middle" ` + | ||
| `fill="${kSvgTextColor}" font-size="13">Endpoint Slack (${escapeXml(unit)}) — ` + | ||
| `${total} endpoints</text>`); | ||
|
|
||
| // Y-axis grid lines and labels | ||
| for (const tick of yTicks) { | ||
| const y = chartArea.bottom - (tick / yMax) * (chartArea.bottom - chartArea.top); | ||
| parts.push(`<line x1="${chartArea.left}" y1="${y}" ` + | ||
| `x2="${chartArea.right}" y2="${y}" stroke="${kSvgGridColor}" stroke-width="1"/>`); | ||
| parts.push(`<text x="${chartArea.left - 8}" y="${y + 4}" ` + | ||
| `text-anchor="end" fill="${kSvgTextColor}">${tick}</text>`); | ||
| } | ||
|
|
||
| // Bars | ||
| for (const bar of bars) { | ||
| if (bar.height <= 0) continue; | ||
| const fill = bar.negative ? kSvgNegativeFill : kSvgPositiveFill; | ||
| const stroke = bar.negative ? kSvgNegativeBorder : kSvgPositiveBorder; | ||
| parts.push(`<rect x="${bar.x.toFixed(1)}" y="${bar.y.toFixed(1)}" ` + | ||
| `width="${bar.width.toFixed(1)}" height="${bar.height.toFixed(1)}" ` + | ||
| `fill="${fill}" stroke="${stroke}" stroke-width="1">`); | ||
| parts.push(`<title>${bar.count} endpoints [${bar.lower.toFixed(4)}, ${bar.upper.toFixed(4)}] ${unit}</title>`); | ||
| parts.push(`</rect>`); | ||
| } | ||
|
|
||
| // X-axis labels (show a subset to avoid overlap) | ||
| const maxLabels = Math.min(bars.length, 10); | ||
| const step = Math.max(1, Math.floor(bars.length / maxLabels)); | ||
| for (let i = 0; i < bars.length; i += step) { | ||
| const bar = bars[i]; | ||
| const x = bar.x + bar.width / 2; | ||
| parts.push(`<text x="${x.toFixed(1)}" y="${chartArea.bottom + 15}" ` + | ||
| `text-anchor="middle" fill="${kSvgTextColor}" font-size="10">` + | ||
| `${bar.lower.toFixed(2)}</text>`); | ||
| } | ||
| // Always label the last bar's upper bound | ||
| if (bars.length > 0) { | ||
| const last = bars[bars.length - 1]; | ||
| parts.push(`<text x="${(last.x + last.width).toFixed(1)}" ` + | ||
| `y="${chartArea.bottom + 15}" text-anchor="middle" fill="${kSvgTextColor}" ` + | ||
| `font-size="10">${last.upper.toFixed(2)}</text>`); | ||
| } | ||
|
|
||
| // X-axis label | ||
| parts.push(`<text x="${(chartArea.left + chartArea.right) / 2}" ` + | ||
| `y="${height - 5}" text-anchor="middle" fill="${kSvgTextColor}" ` + | ||
| `font-size="12">Slack (${escapeXml(unit)})</text>`); | ||
|
|
||
| // Axes | ||
| parts.push(`<line x1="${chartArea.left}" y1="${chartArea.top}" ` + | ||
| `x2="${chartArea.left}" y2="${chartArea.bottom}" ` + | ||
| `stroke="${kSvgAxisColor}" stroke-width="1"/>`); | ||
| parts.push(`<line x1="${chartArea.left}" y1="${chartArea.bottom}" ` + | ||
| `x2="${chartArea.right}" y2="${chartArea.bottom}" ` + | ||
| `stroke="${kSvgAxisColor}" stroke-width="1"/>`); | ||
|
|
||
| parts.push('</svg>'); | ||
| return parts.join('\n'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #!/usr/bin/env bash | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # Copyright (c) 2026, The OpenROAD Authors | ||
| # | ||
| # Generate a self-contained static HTML viewer and open it. | ||
| # No server needed — single file, works from file:// directly. | ||
| # | ||
| # Usage: bazelisk run //test/orfs/gcd:gcd_route_html | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| if [[ -n "${RUNFILES_DIR:-}" ]]; then | ||
| RUNFILES="$RUNFILES_DIR" | ||
| elif [[ -d "$0.runfiles" ]]; then | ||
| RUNFILES="$0.runfiles" | ||
| else | ||
| echo "Error: cannot find runfiles directory" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| NODE=$(command -v node 2>/dev/null || true) | ||
| if [[ -z "$NODE" ]]; then | ||
| echo "Error: node not found in PATH" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| WEB_SRC="$RUNFILES/_main/src/web/src" | ||
| RENDER_PAGE="$WEB_SRC/render-static-page.js" | ||
| JSON=$(find "$RUNFILES/_main" -name "*.json" -path "*/test/orfs/*" 2>/dev/null | head -1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| if [[ ! -f "$RENDER_PAGE" ]]; then | ||
| echo "Error: render-static-page.js not found" >&2 | ||
| exit 1 | ||
| fi | ||
| if [[ -z "$JSON" ]]; then | ||
| echo "Error: no JSON payload found" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then | ||
| OUTPUT="$BUILD_WORKSPACE_DIRECTORY/gcd_report.html" | ||
| else | ||
| OUTPUT="/tmp/gcd_report.html" | ||
| fi | ||
|
|
||
| "$NODE" "$RENDER_PAGE" "$JSON" -o "$OUTPUT" | ||
|
|
||
| echo "Opening $OUTPUT" | ||
| xdg-open "$OUTPUT" 2>/dev/null || open "$OUTPUT" 2>/dev/null || echo "Open $OUTPUT in your browser" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| import { GoldenLayout, LayoutConfig } from 'https://esm.sh/golden-layout@2.6.0'; | ||
| import { latLngToDbu } from './coordinates.js'; | ||
| import { WebSocketManager } from './websocket-manager.js'; | ||
| import { StaticDataManager } from './static-data-manager.js'; | ||
| import { createWebSocketTileLayer } from './websocket-tile-layer.js'; | ||
| import { TimingWidget } from './timing-widget.js'; | ||
| import { ClockTreeWidget } from './clock-tree-widget.js'; | ||
|
|
@@ -362,6 +363,7 @@ function createBrowser(container) { | |
|
|
||
| function createTimingWidget(container) { | ||
| app.timingWidget = new TimingWidget(container, app, redrawAllLayers); | ||
| if (app.staticMode) setTimeout(() => app.timingWidget.update(), 100); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| } | ||
|
|
||
| function createDRCWidget(container) { | ||
|
|
@@ -375,6 +377,7 @@ function createClockWidget(container) { | |
|
|
||
| function createChartsWidget(container) { | ||
| app.chartsWidget = new ChartsWidget(container, app, redrawAllLayers); | ||
| if (app.staticMode) setTimeout(() => app.chartsWidget.update(), 100); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| } | ||
|
|
||
| function createHelpWidget(container) { | ||
|
|
@@ -443,6 +446,11 @@ const defaultLayoutConfig = { | |
| componentType: 'SchematicWidget', | ||
| title: 'Schematic', | ||
| }, | ||
| { | ||
| type: 'component', | ||
| componentType: 'ChartsWidget', | ||
| title: 'Charts', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
|
|
@@ -522,22 +530,31 @@ const LAYOUT_VERSION = 3; | |
| // Must be created before loadLayout so that components (e.g. SchematicWidget) | ||
| // constructed during layout initialisation can access app.websocketManager. | ||
|
|
||
| const websocketUrl = `ws://${window.location.host || 'localhost:8080'}/ws`; | ||
| app.websocketManager = new WebSocketManager(websocketUrl, updateStatus); | ||
| // Static mode: if embedded data is present, use StaticDataManager instead of WebSocket. | ||
| const embeddedEl = document.querySelector('script[type="application/json"][data-static]'); | ||
| if (embeddedEl) { | ||
| const data = JSON.parse(embeddedEl.textContent); | ||
| app.websocketManager = new StaticDataManager(data); | ||
| app.staticMode = true; | ||
| statusDiv.style.display = 'none'; | ||
| } else { | ||
| const websocketUrl = `ws://${window.location.host || 'localhost:8080'}/ws`; | ||
| app.websocketManager = new WebSocketManager(websocketUrl, updateStatus); | ||
| } | ||
|
|
||
| // Restore saved layout or use default | ||
| // Load the layout. In static mode (IIFE bundle), GoldenLayout needs | ||
| // init(resolvedConfig) instead of loadLayout() because loadLayout | ||
| // requires isInitialised. Detect by checking isInitialised. | ||
| const savedLayout = localStorage.getItem('gl-layout'); | ||
| const savedVersion = parseInt(localStorage.getItem('gl-layout-version'), 10); | ||
| let layoutConfig = defaultLayoutConfig; | ||
| if (savedLayout && savedVersion === LAYOUT_VERSION) { | ||
| try { | ||
| const resolved = JSON.parse(savedLayout); | ||
| app.goldenLayout.loadLayout(LayoutConfig.fromResolved(resolved)); | ||
| } catch (e) { | ||
| app.goldenLayout.loadLayout(defaultLayoutConfig); | ||
| } | ||
| } else { | ||
| app.goldenLayout.loadLayout(defaultLayoutConfig); | ||
| layoutConfig = LayoutConfig.fromResolved(JSON.parse(savedLayout)); | ||
| } catch (e) { /* use default */ } | ||
| } | ||
|
|
||
| app.goldenLayout.loadLayout(layoutConfig); | ||
| localStorage.setItem('gl-layout-version', LAYOUT_VERSION); | ||
|
|
||
| // Persist layout on changes (drag, resize, close, etc.) | ||
|
|
@@ -644,7 +661,19 @@ app.websocketManager.readyPromise.then(async () => { | |
| [-minY * scale, minX * scale], | ||
| [-maxY * scale, maxX * scale] | ||
| ]; | ||
| app.map.fitBounds(app.fitBounds); | ||
| if (app.map) app.map.fitBounds(app.fitBounds); | ||
| } | ||
|
|
||
| // Skip interactive map features in static mode (no tiles/server) | ||
| if (!app.map) { | ||
| populateDisplayControls(app, visibility, null, techData, redrawAllLayers, null); | ||
| updateHeatMaps(heatMapData); | ||
|
|
||
| // Auto-populate widgets with pre-embedded data | ||
| if (app.chartsWidget) app.chartsWidget.update(); | ||
| if (app.timingWidget) app.timingWidget.update(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // Click-to-select: convert click position to DBU and query server | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
find ... | head -1to locate the JSON file is fragile and can lead to unpredictable behavior if multiple JSON files are present. For a more robust solution, consider accepting the JSON file path as a command-line argument.