Skip to content

feat(web): timing cone, chart types, ruler styles, and text labels#10966

Open
jorge-ferreira-pii wants to merge 2 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:feature-WebGUI-Timing-Charts-Rulers-Labels
Open

feat(web): timing cone, chart types, ruler styles, and text labels#10966
jorge-ferreira-pii wants to merge 2 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:feature-WebGUI-Timing-Charts-Rulers-Labels

Conversation

@jorge-ferreira-pii

Copy link
Copy Markdown
Contributor

Addresses a portion of #10619

2.9 Timing cone on layout (fanin/fanout)

Feature GUI Web Status
Fanin / fanout cone from the selected pin/instance Gui::timingCone(Term, fanin, fanout)TimingConeRenderer over STA findFaninPins / findFanoutPins new WebSocketRequest::kTimingCone + handleTimingCone; TimingReport::computeTimingCone seeds from the selected inst (backend also accepts pin_name), walks the STA graph with findFaninPins / findFanoutPins + Vertex{In,Out}EdgeIterator, and returns nodes with per-pin logic depth and slack. A cone panel in the timing widget drives it (Fanin / Fanout, depth limits, color mode) ❌ → ✅
Slack coloring of the cone TimingConeRenderer colors by slack via SpectrumGenerator (Turbo) overlay draws one colored rect per instance (worst-slack pin) + flight lines source→sink, colored by the same Turbo ramp (spectrumColor, ported to color.cpp so libweb needs no Qt link); per-pin logic-depth labels are drawn too ❌ → ✅
Depth limits on the cone walk levels-based expansion in the STA visitors fanin_depth / fanout_depth inputs (0 = unlimited, Qt parity); the BFS stops at the requested level ❌ → ✅
Web additions (1) a depth color mode in addition to slack (map |logic depth| onto the ramp); (2) optional schematic sync — mirror the same cone target/depths into the schematic SVG view via an openroad-cone-sync event ✅ (web-only)

2.11 Multiple chart types

Feature GUI Web Status
Chart-type selector gui::ChartsWidget (charts.cpp) with a QComboBox mode selector, largely a single endpoint-slack histogram charts-widget.js unified dropdown listing built-in charts and engine-provided line charts fetched on demand (debug_charts); switching type re-renders in place ❌ → ✅
Slack histogram by path group endpoint-slack histogram with path-group filtering TimingReport::getSlackHistogram returns one series per path group; _drawBars renders them stacked, with a unified path-group filter (_populatePathGroups). In-group endpoint counting fixed so stacked totals equal distinct endpoints ❌ → ✅
Net-length (HPWL) histogram not available new computeNetLengthHistogram over netHpwlDbu (half-perimeter of dbNet::getTermBBox), selectable from the same dropdown ✅ (web-only)
Export CSV (buildChartCsv) and PNG (canvas.toDataURL) export for the active chart ✅ (web-only)
Select-by-bin drilldown double-clicking a histogram bar selects the matching objects on the layout — endpoints via select_fanout_bin, nets via handleSelectNetLengthBin / select_net_length_bin ✅ (web-only)

2.12 Rulers & labels

Feature GUI Web Status
Ruler style (euclidian / manhattan) Ruler carries a per-ruler euclidian_ flag toggled while building (k); manhattan rulers draw an L-shaped segment global default in ruler.js via the or_ruler_style cookie + a Tools → Euclidian rulers toggle; manhattan rulers render the L polyline (_getManhattanJoinPt) ❌ → ✅
Ruler visibility toggle display-control checkbox for rulers already present as the rulers display-control flag; kept and wired through the overlay request ✅ (already)
Labels / custom text annotations gui::Label via Gui::addLabel(x, y, text, …) / Tcl add_label / delete_label, drawn by LayoutViewer::drawLabels and included in save_image server-side labels in TileGenerator (addLabel / updateLabel / deleteLabel / clearLabels under one mutex), Tcl add_label / delete_label / clear_labels, and WS add_label / update_label / delete_label / clear_labels / list_labels. Labels composite into the overlay tiles and into save_image (renderLabelTile), matching Qt ❌ → ✅
Web addition: interactive label editing Qt only offers Tcl / a dialog a client LabelManager: create-by-click, drag-to-reposition, and edit/delete via the Inspector; move/edit use a single atomic update_label (never delete+add), so a label can't be lost mid-edit ✅ (web-only)

Advance the Web GUI toward Qt parity for issue The-OpenROAD-Project#10619:

- Timing cone (2.9): fanin/fanout cone on the layout with slack/depth
  coloring, flight lines, per-pin depth labels, and optional schematic sync.
- Chart types (2.11): unified chart dropdown, net-length (HPWL) histogram,
  stacked slack-by-path-group, CSV/PNG export, and select-by-bin drilldown.
- Rulers & labels (2.12): global ruler-style default (euclidian/manhattan),
  server-side text labels with Tcl add_label/delete_label/clear_labels and an
  interactive client label manager (create-by-click, drag, edit/delete),
  labels composited into overlay tiles and save_image.

Adds C++ unit tests (TestTimingCone, TestCharts, TestLabels) and JS tests,
registered in both CMake and Bazel.

Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several major features to the OpenROAD web interface, including server-side user text labels with interactive client-side editing, a timing cone overlay (fanin/fanout) with logic-depth and slack coloring, a net-length (HPWL) histogram, and stacked slack histograms for multiple path groups. The review feedback highlights critical issues in the STA timing graph traversal where input pins are skipped due to only querying pinDrvrVertex instead of falling back to pinLoadVertex. Additionally, potential server crashes from Boost.JSON's as_double() on integer values, label name collisions during auto-generation, and duplicate DOM elements in the inspector toolbar should be addressed.

Comment on lines +636 to +639
auto* pin_vertex = graph->pinDrvrVertex(pin);
if (pin_vertex == nullptr) {
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In STA, input pins of instances only have a load vertex (pinLoadVertex), while output pins only have a driver vertex (pinDrvrVertex). By only querying graph->pinDrvrVertex(pin), walkConeLevels will get a nullptr for all input pins and skip them. This prematurely terminates the timing cone traversal at the first input pin, preventing it from walking deeper than one level. To fix this, fall back to pinLoadVertex if pinDrvrVertex is null.

      auto* pin_vertex = graph->pinDrvrVertex(pin);
      if (pin_vertex == nullptr) {
        pin_vertex = graph->pinLoadVertex(pin);
      }
      if (pin_vertex == nullptr) {
        continue;
      }

Comment on lines +783 to +784
sta::Vertex* vertex = graph->pinDrvrVertex(pin);
if (vertex != nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similarly, querying only pinDrvrVertex to retrieve the vertex for slack calculation means that input pins (which only have a pinLoadVertex) will return nullptr, and their slack values will not be retrieved. Fall back to pinLoadVertex if pinDrvrVertex is null to ensure slack is correctly annotated for input pins.

      sta::Vertex* vertex = graph->pinDrvrVertex(pin);
      if (vertex == nullptr) {
        vertex = graph->pinLoadVertex(pin);
      }
      if (vertex != nullptr) {

Comment thread src/web/src/request_handler.cpp Outdated
Comment on lines +1106 to +1107
const double lower = req.json.at("lower").as_double();
const double upper = req.json.at("upper").as_double();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Boost.JSON, calling as_double() on a boost::json::value that was parsed as an integer (e.g., if the client sends 5 instead of 5.0) will throw a boost::system::system_error (specifically error::not_double). Since bin edges can be integers (especially when use_dbu is true), this will cause the request to fail with a server error. Consider safely extracting the double value by checking if the value is a double, an int64, or a uint64.

Suggested change
const double lower = req.json.at("lower").as_double();
const double upper = req.json.at("upper").as_double();
auto get_double = [](const boost::json::value& v) {
if (v.is_double()) {
return v.get_double();
}
if (v.is_int64()) {
return static_cast<double>(v.get_int64());
}
if (v.is_uint64()) {
return static_cast<double>(v.get_uint64());
}
throw std::runtime_error("value is not a number");
};
const double lower = get_double(req.json.at("lower"));
const double upper = get_double(req.json.at("upper"));

Comment on lines +3872 to +3881
std::string label_name = name;
if (label_name.empty()) {
label_name = "label" + std::to_string(next_label_id_++);
}
// Reject a duplicate name (mirrors the Qt GUI, which warns GUI-44).
for (const auto& l : labels_) {
if (l.name == label_name) {
return "";
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If a user manually creates a label with a name like 'label0', any subsequent auto-generated label that gets assigned that same ID will fail the duplicate check and return an empty string. To prevent this, the auto-generation logic should loop and increment next_label_id_ until a unique name is found.

  std::string label_name = name;
  if (label_name.empty()) {
    bool duplicate = true;
    while (duplicate) {
      label_name = "label" + std::to_string(next_label_id_++);
      duplicate = false;
      for (const auto& l : labels_) {
        if (l.name == label_name) {
          duplicate = true;
          break;
        }
      }
    }
  } else {
    // Reject a duplicate name (mirrors the Qt GUI, which warns GUI-44).
    for (const auto& l : labels_) {
      if (l.name == label_name) {
        return "";
      }
    }
  }

Comment on lines +199 to +212
requestAnimationFrame(() => {
const toolbar
= this._app.inspectorEl?.querySelector('.inspector-toolbar');
if (!toolbar) return;
const btn = document.createElement('button');
btn.className = 'inspector-btn';
btn.title = 'Delete label';
btn.innerHTML =
'<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">'
+ '<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>'
+ '</svg>';
btn.addEventListener('click', () => this.deleteLabel(name));
toolbar.appendChild(btn);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When appending the 'Delete label' button to the inspector toolbar, there is no check to see if a delete button already exists. If _select is called multiple times or if the inspector toolbar is not completely cleared/recreated on update, multiple duplicate delete buttons can accumulate in the DOM. Consider checking if a delete button is already present before appending a new one.

        requestAnimationFrame(() => {
            const toolbar
                = this._app.inspectorEl?.querySelector('.inspector-toolbar');
            if (!toolbar) return;
            if (toolbar.querySelector('.delete-label-btn')) return;
            const btn = document.createElement('button');
            btn.className = 'inspector-btn delete-label-btn';
            btn.title = 'Delete label';
            btn.innerHTML =
                '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">'
                + '<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>'
                + '</svg>';
            btn.addEventListener('click', () => this.deleteLabel(name));
            toolbar.appendChild(btn);
        });

…abels

- timing cone: walk through input pins by falling back to pinLoadVertex when
  pinDrvrVertex is null (both in the level walk and in slack annotation), so
  the cone reaches beyond the first logic level and colors input pins by slack.
- net-length histogram: read bin edges via a jsonToDouble helper that accepts
  integer JSON kinds, since JS serializes whole numbers without a decimal
  point and Boost.JSON's as_double() throws on int64/uint64.
- labels: auto-generated names now skip ids already taken by a user-named
  label instead of returning an empty string on collision.
- label manager: guard against stacking duplicate "Delete label" buttons in
  the inspector toolbar across re-selections.

Add TestLabels.AutoNameSkipsUserTakenId.

Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
@jorge-ferreira-pii
jorge-ferreira-pii marked this pull request as ready for review July 21, 2026 20:23
@jorge-ferreira-pii
jorge-ferreira-pii requested a review from a team as a code owner July 21, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant