feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:17:01 +02:00
1 parent cae672bc99
commit eee7c34dd5
12 files changed
+166 -12

No files matched your search

+4
View File
@@ -23,6 +23,10 @@ void LineProgram::set_viewport(Vec2 px) const noexcept { program_.set_uniform("u
void LineProgram::set_width(f32 px) const noexcept { program_.set_uniform("u_width", px); }
void LineProgram::set_dash(Vec2 on_off) const noexcept {
program_.set_uniform("u_dash", on_off);
}
void LineProgram::set_color(Color color) const noexcept { program_.set_uniform("u_color", color); }
bool TextProgram::create(Status* st) noexcept {
+13 -4
View File
@@ -568,7 +568,7 @@ void Renderer::upload_text_vertices(std::span<const f32> interleaved) noexcept {
text_vao_.upload(interleaved.data(), static_cast<u32>(interleaved.size() / 6u));
}
void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept {
void Renderer::flush_lines(const Mapping& map, Color color, f32 width, Vec2 dash) noexcept {
const u32 segments = lines_.segment_count();
if (segments != 0 && ready_) {
upload_vertices(lines_.data());
@@ -577,6 +577,9 @@ void Renderer::flush_lines(const Mapping& map, Color color, f32 width) noexcept
line_program_.set_offset(map.offset);
line_program_.set_viewport({viewport_.w, viewport_.h});
line_program_.set_width(width);
// Set on *every* flush, solid included, so the crosshair's dash cannot leak into the
// grid, a trace or a cursor drawn after it (see draw_crosshair_lines()).
line_program_.set_dash(dash);
line_program_.set_color(color);
line_vao_.bind();
// One triangle strip per instance, four corners from gl_VertexID.
@@ -1192,25 +1195,31 @@ void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout)
const CrosshairLayout& crosshair = layout.crosshair;
if (crosshair.empty()) return;
const f32 stroke = style_.device(style_.line_width_px);
// The stroke and dash the layout already resolved from the crosshair's own override and the
// global style, in device pixels -- one source, so the boxes the layout held inside `axes` and
// the strokes drawn here cannot disagree (spec §17).
const f32 stroke = crosshair.stroke;
// The two lines. Each is staged as one data-space segment at the *centre* of the stroke box
// the layout placed, spanning that box's own extent, so the pixels this draws are the box
// rather than a second placement of the same rule: the layout held the box inside `axes`,
// and the clip keeps the half pixel the rasteriser puts outside the box's fractional edges.
//
// The dash rides on the same segment: `v_along` is measured from the axes edge the line is
// staged from, so no geometry is added and the phase does not depend on the transform.
const Mapping data = mapping(plot, 0u, 0u);
if (crosshair.vertical.w > 0.0f) {
const f64 x = plot.x_transform(0u).to_data_x(
static_cast<f64>(crosshair.vertical.x + 0.5f * crosshair.vertical.w));
const Range y_range = plot.y_range(0u);
stage_segment(x, y_range.min, x, y_range.max, plot.x_axis(0u).log, plot.y_axis(0u).log);
flush_lines(data, style_.crosshair, stroke);
flush_lines(data, style_.crosshair, stroke, crosshair.dash);
}
if (crosshair.horizontal.h > 0.0f) {
const f64 y = plot.y_transform(0u).to_data_y(
static_cast<f64>(crosshair.horizontal.y + 0.5f * crosshair.horizontal.h));
const Range x_range = plot.x_range(0u);
stage_segment(x_range.min, y, x_range.max, y, plot.x_axis(0u).log, plot.y_axis(0u).log);
flush_lines(data, style_.crosshair, stroke);
flush_lines(data, style_.crosshair, stroke, crosshair.dash);
}
// A small cross at each trace's intersection, in the trace's own colour -- opaque, like a
+22 -2
View File
@@ -18,7 +18,12 @@ uniform vec2 u_viewport; // render target size, pixels
uniform float u_width; // stroke width, pixels
out float v_dist; // signed distance to the segment axis, pixels
out float v_along; // distance travelled from a_p0, pixels
// Distance travelled from a_p0, pixels -- and the dash's phase anchor. Both of the
// crosshair's lines are staged from an *axes edge* (its vertical from `y_range.min`,
// the axes' bottom; its horizontal from `x_range.min`, the axes' left), so a dash measured
// from here is measured from a fixed edge of the rect: it does not crawl as the view pans
// or zooms, and a stationary crosshair renders identically frame to frame.
out float v_along;
void main() {
vec2 p0 = a_p0 * u_scale + u_offset;
@@ -52,14 +57,29 @@ void main() {
)";
const char* const kLineFS = R"(#version 330 core
in float v_dist; // signed distance to the segment axis, pixels
in float v_dist; // signed distance to the segment axis, pixels
in float v_along; // distance travelled from a_p0, pixels
uniform float u_width; // stroke width, pixels
uniform vec4 u_color;
uniform vec2 u_dash; // dash pattern in pixels: x = ink, y = gap; y <= 0 is solid
out vec4 o_color;
void main() {
// The dash (spec §17): a fragment-shader discard, never new geometry -- the vertices
// stay in data space and the pattern is one per-draw uniform. `v_along` interpolates the
// distance from `a_p0` across the quad in pixels, so `mod` measures the pattern along the
// line rather than in screen space, which is what keeps it anchored to the segment while
// the view pans. `y <= 0` is solid, so every other stroke in the library -- grid, traces,
// axis lines, regions, cursors, the legend border -- draws unchanged by this uniform's
// default.
if (u_dash.y > 0.0) {
float period = u_dash.x + u_dash.y;
if (period > 0.0 && mod(v_along, period) >= u_dash.x) {
discard; // in the gap of the pattern
}
}
float half_w = u_width * 0.5;
float d = abs(v_dist);
if (d > half_w + 1.0) {