Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions js/src/50_chartview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3031,6 +3031,18 @@ export class ChartView {
// Per-mark GPU setup is dispatched through MARK_KINDS (55_marks.js) so a
// new chart kind is an entry in that registry, not another branch here.
markOf(t.kind).build(this, g, t, buffer);
if (t.tier === "decimated") {
// T1 covering representation for M4 traces. Density has its texture
// cache; decimated line/area traces need the same guarantee while a
// window-specific re-decimation is in flight. Keep the initial (home)
// buffers in a separate drawable so refined replies never overwrite
// the only geometry that covers the full initial domain.
g._homeDecimated = {
...g, _vaos: null, _homeDecimated: null, _decimatedWindow: null,
};
g._decimatedWindow = [...this._axisRange(g.xAxis)];
g._decimatedRefined = false;
}
if (t.keys && Number.isInteger(t.keys.lo) && Number.isInteger(t.keys.hi)) {
const lo = this._columnView(buffer, this.spec.columns[t.keys.lo]);
const hi = this._columnView(buffer, this.spec.columns[t.keys.hi]);
Expand Down Expand Up @@ -4046,6 +4058,23 @@ export class ChartView {
lodDrawDensityTier(this, g, gx0, gx1, gy0, gy1);
return;
}
if (g.tier === "decimated" && g._decimatedRefined && g._homeDecimated) {
const r = g._decimatedWindow;
// _axisRange preserves axis direction (a reversed axis yields
// hi-before-lo) while the served window arrives normalized, so the
// coverage compare runs on normalized bounds.
const [vx0, vx1] = this._axisRange(g.xAxis);
const viewLo = Math.min(vx0, vx1);
const viewHi = Math.max(vx0, vx1);
const eps = r ? Math.abs(r[1] - r[0]) * 1e-9 + 1e-300 : 0;
// A prior refined window is not a truthful covering representation
// after a pan/zoom leaves it. Draw the retained overview until the
// pending reply installs a buffer covering this view (T1/T8).
if (!r || viewLo < r[0] - eps || viewHi > r[1] + eps) {
markOf(g.trace.kind).draw(this, g._homeDecimated, x0, x1, y0, y1);
return;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
markOf(g.trace.kind).draw(this, g, x0, x1, y0, y1);
};
for (const g of this._transitionOldTraces || []) drawTrace(g);
Expand Down Expand Up @@ -6194,6 +6223,7 @@ export class ChartView {
this._destroyDensitySample(g);
lodDropPointCache(this, g); // retired point windows die with the trace (T13)
this._deleteVaos(g);
this._deleteVaos(g._homeDecimated);
this._deleteVaos(g.drill);
this._deleteBuffers(g, [
"xBuf", "yBuf", "cBuf", "sBuf", "selBuf", "baseBuf",
Expand All @@ -6202,6 +6232,12 @@ export class ChartView {
"_transitionPrevXBuf", "_transitionPrevYBuf",
"_transitionPrevPosBuf", "_transitionPrevValue1Buf", "_transitionPrevValue0Buf",
]);
// Only geometry is owned independently by the retained M4 overview;
// style/channel buffers are shared with the live trace and were deleted
// above exactly once.
if (g._decimatedRefined) {
this._deleteBuffers(g._homeDecimated, ["xBuf", "yBuf", "baseBuf"]);
}
this._deleteBuffers(g.drill, [
"xBuf", "yBuf", "cBuf", "rgbaBuf", "sBuf", "styleBuf", "strokeBuf", "selBuf", "dBuf",
]);
Expand All @@ -6228,6 +6264,7 @@ export class ChartView {
g.densityCache = [];
g.heatmap = null;
g._cpu = null;
g._homeDecimated = null;
}

_destroyGlResources() {
Expand Down
13 changes: 13 additions & 0 deletions js/src/54_kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,16 @@ Object.assign(ChartView.prototype, {
const sm = this._smoothArrays(g.trace, xArr, yArr, bArr, n);
const src = sm || { x: xArr, y: yArr, n };
const st = this._stepArrays(g.trace, src.x, src.y, src.n);
if (g.tier === "decimated" && !g._decimatedRefined) {
// The initial buffers belong to the retained home drawable (T1).
// Give refinements their own stores rather than destroying that
// covering representation with the first window reply.
this._deleteVaos(g);
g.xBuf = gl.createBuffer();
g.yBuf = gl.createBuffer();
if (bArr && g.baseBuf) g.baseBuf = gl.createBuffer();
g._decimatedRefined = true;
}
gl.bindBuffer(gl.ARRAY_BUFFER, g.xBuf);
gl.bufferData(gl.ARRAY_BUFFER, st ? st.x : src.x, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, g.yBuf);
Expand All @@ -766,6 +776,9 @@ Object.assign(ChartView.prototype, {
g.baseMeta = { ...g.baseMeta, offset: upd.base.offset, scale: upd.base.scale };
}
g.n = st ? st.n : src.n;
if (Array.isArray(upd.x_range) && upd.x_range.length === 2) {
g._decimatedWindow = [Number(upd.x_range[0]), Number(upd.x_range[1])];
}
}
this.draw();
} else if (msg.type === "density_update") {
Expand Down
4 changes: 4 additions & 0 deletions python/xy/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ def decimate_view(
)
update = {
"id": t.id,
# The client retains the home M4 buffer as T1's covering
# representation. Record the exact window served by this reply
# so it can fall back to home while a later pan is in flight.
"x_range": [lo_x, hi_x],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"x": writer.add_encoded(x_col),
"y": writer.add_encoded(y_col),
}
Expand Down
11 changes: 10 additions & 1 deletion spec/design/lod-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,16 @@ The transition system was built and debugged in this repo; codifying the
invariants so future kinds don't regress them:

- **T1 — never blank:** a coarser covering representation draws until the
finer one arrives (density-under-points; broadest-cache fallback).
finer one arrives. Density uses density-under-points and its broadest-cache
fallback; an M4-decimated line/area retains its initial home/overview buffer
and draws that whenever the current refined window does not cover the view.
Each tier reply records its served `x_range` (small JSON metadata per the
Transport Matrix — the served window is a scalar control descriptor, not
point data, and stays f64 so deep-zoom coverage compares stay exact); the
overview hold ends only when that reply has installed covering geometry
(T8). Coverage compares normalized `[lo, hi]` bounds on both sides, so a
direction-preserving (reversed) axis range still resolves coverage
correctly.
- **T2 — never hard-cut:** representation changes crossfade (entry fade on
the aggregate→marks transition only — restarting per refresh reads as
flashing; exit fade with the "dying" state so buffers outlive the fade).
Expand Down
1 change: 1 addition & 0 deletions tests/test_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_valid_view_returns_tier_update():
assert reply is not None
msg, buffers = reply
assert msg["type"] == "tier_update"
assert msg["traces"][0]["x_range"] == [100.0, 5_000.0]
assert msg["seq"] == 3
assert msg["traces"]
assert buffers
Expand Down
Loading