// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import type { WebGLContextManager } from "../../../webgl/context-manager"; import type { SeriesChart } from "../series"; import type { SeriesInfo } from "../series-build"; import type { InterpolateMode } from "../series-type"; import { compileProgram } from "../../../webgl/program-cache"; import areaVert from "../../../shaders/area.vert.glsl"; import areaFrag from "../../../shaders/area.frag.glsl"; type GL = WebGL2RenderingContext | WebGLRenderingContext; interface AreaProgramCache { program: WebGLProgram; u_projection: WebGLUniformLocation | null; u_color: WebGLUniformLocation | null; u_opacity: WebGLUniformLocation | null; a_position: number; } interface AreaStrip { /** * Byte offset of the strip start within the per-series GPU buffer. */ offsetBytes: number; /** * Vertex count (= 2 × number of categories in this run). */ vertexCount: number; } interface AreaSeriesEntry { seriesId: number; axis: 0 | 1; color: [number, number, number]; gpuBuffer: WebGLBuffer; strips: AreaStrip[]; } /** * Persistent area glyph state. Built in `rebuildBuffers`. Each series * owns one GPU buffer holding all of its strip vertices in * `[x,y_bot, x,y_top, ...]` layout; draws rebind without uploading. */ interface AreaBuffers { series: AreaSeriesEntry[]; } /** * Reusable Float32 strip scratch. Sized to `N * 4` (two vertices per * category: bottom + top). Grown on demand. */ let _stripScratch: Float32Array = new Float32Array(0); function ensureStripScratch(n: number): Float32Array { if (_stripScratch.length >= n) { return _stripScratch; } _stripScratch = new Float32Array(Math.max(n, _stripScratch.length * 2)); return _stripScratch; } /** * Area glyph for {@link SeriesChart}. Owns its program + per-series * strip buffers privately. */ export class AreaGlyph { private _program: AreaProgramCache | null = null; private _buffers: AreaBuffers | null = null; private ensureProgram(glManager: WebGLContextManager): AreaProgramCache { if (this._program) { return this._program; } this._program = compileProgram( glManager, "bar-area", areaVert, areaFrag, ["u_projection", "u_color", "u_opacity"], ["a_position"], ); return this._program; } /** * Drop persistent area buffers. Subsequent draws no-op until rebuild. */ invalidateBuffers(chart: SeriesChart): void { const buf = this._buffers; if (!buf || !chart._glManager) { this._buffers = null; return; } const gl = chart._glManager.gl; for (const s of buf.series) { gl.deleteBuffer(s.gpuBuffer); } this._buffers = null; } /** * Build per-series strip buffers for area glyphs. Reads stacked * y0/y1 from `chart._areaBarIndex` (cached at data load) and * unstacked values from `_samples`. Single GPU upload per series; * subsequent frames just rebind. */ rebuildBuffers(chart: SeriesChart, glManager: WebGLContextManager): void { const areaSeries = chart._areaSeries; if (areaSeries.length === 0) { this._buffers = null; return; } const N = chart._numCategories; const S = chart._series.length; if (N === 0 || S === 0) { this._buffers = null; return; } this.ensureProgram(glManager); const gl = glManager.gl; const samples = chart._samples; const valid = chart._sampleValid; const positions = chart._categoryPositions; const xOrigin = chart._categoryOrigin; const barIndex = chart._areaBarIndex; const bars = chart._bars; const entries: AreaSeriesEntry[] = []; for (const s of areaSeries) { const seriesInfo = chart._series[s.seriesId]; const strips = collectAreaStrips( s, N, S, samples, valid, barIndex, bars.y0, bars.y1, positions, xOrigin, seriesInfo.start, seriesInfo.end, seriesInfo.interpolateMode, ); if (strips.totalVertices === 0) { continue; } const buf = gl.createBuffer()!; gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData( gl.ARRAY_BUFFER, strips.scratch.subarray(0, strips.totalVertices * 2), gl.STATIC_DRAW, ); entries.push({ seriesId: s.seriesId, axis: s.axis, color: [s.color[0], s.color[1], s.color[2]], gpuBuffer: buf, strips: strips.descriptors, }); } this._buffers = { series: entries }; } /** * Bind persistent strip buffers and dispatch one TRIANGLE_STRIP per * series-run. Skips hidden series. */ draw( chart: SeriesChart, gl: GL, _glManager: WebGLContextManager, projLeft: Float32Array, projRight: Float32Array, opacity: number, ): void { const buf = this._buffers; const cache = this._program; if (!buf || !cache || buf.series.length === 0) { return; } gl.useProgram(cache.program); gl.uniform1f(cache.u_opacity, opacity); const hidden = chart._hiddenSeries; for (const s of buf.series) { if (hidden.has(s.seriesId)) { continue; } gl.uniformMatrix4fv( cache.u_projection, false, s.axis === 1 ? projRight : projLeft, ); const color = chart._series[s.seriesId].color; gl.uniform3f(cache.u_color, color[0], color[1], color[2]); gl.bindBuffer(gl.ARRAY_BUFFER, s.gpuBuffer); gl.enableVertexAttribArray(cache.a_position); gl.vertexAttribPointer(cache.a_position, 2, gl.FLOAT, false, 0, 0); for (const strip of s.strips) { gl.bindBuffer(gl.ARRAY_BUFFER, s.gpuBuffer); gl.vertexAttribPointer( cache.a_position, 2, gl.FLOAT, false, 0, strip.offsetBytes, ); gl.drawArrays(gl.TRIANGLE_STRIP, 0, strip.vertexCount); } } } destroy(chart: SeriesChart): void { this.invalidateBuffers(chart); this._program = null; } } interface CollectedStrips { descriptors: AreaStrip[]; totalVertices: number; scratch: Float32Array; } /** * Walk the per-category sample grid for one series and emit strip * descriptors. Each contiguous run of present cells becomes one * `TRIANGLE_STRIP` with `[x,bot, x,top, ...]` layout. * * Reads stacked y0/y1 from the pre-built `barIndex` (cached on the * chart at data load) so this hot path doesn't rebuild the map each * call. * * The "present" predicate is mode-aware for the unstacked branch: * * - `mode = "solid"` (also coerced from `"transparent"` for area): * Pass 2 has populated every cell in `[start, end]`, including * leading/trailing zero-fills. Treat `c in [start, end]` as * present and ignore `sampleValid` (the bit is still 0 at * synthesized cells). * - `mode = "skip"`: Pass 2 didn't run for this series. Interior * nulls inside `[start, end]` remain and the strip must break at * them — use `sampleValid` as the "is present" check, matching * the pre-feature behavior. * * The stacked branch is unchanged: the `barIndex` lookup already * encodes presence post-stacking. */ function collectAreaStrips( s: SeriesInfo, N: number, S: number, samples: Float32Array, valid: Uint8Array, barIndex: Map | null, barY0: Float64Array, barY1: Float64Array, positions: Float64Array | null, xOrigin: number, seriesStart: number, seriesEnd: number, interpolateMode: InterpolateMode, ): CollectedStrips { const scratch = ensureStripScratch(N * 4); const descriptors: AreaStrip[] = []; const seriesBase = s.seriesId * 1_000_000_000; const trustRange = interpolateMode !== "skip"; let write = 0; let runStart = 0; for (let c = 0; c < N; c++) { let bot = 0; let top = 0; let present = false; if (s.stack) { const idx = barIndex?.get(seriesBase + c); if (idx !== undefined) { bot = barY0[idx]; top = barY1[idx]; present = true; } } else { const idx = c * S + s.seriesId; if (trustRange) { if (c >= seriesStart && c <= seriesEnd) { top = samples[idx]; present = true; } } else if ((valid[idx >> 3] >> (idx & 7)) & 1) { top = samples[idx]; present = true; } } if (present) { const x = positions ? positions[c] - xOrigin : c; scratch[write++] = x; scratch[write++] = bot; scratch[write++] = x; scratch[write++] = top; } else if (write > runStart) { const vertexCount = (write - runStart) / 2; if (vertexCount >= 4) { descriptors.push({ offsetBytes: runStart * 4, vertexCount, }); } runStart = write; } } if (write > runStart) { const vertexCount = (write - runStart) / 2; if (vertexCount >= 4) { descriptors.push({ offsetBytes: runStart * 4, vertexCount, }); } } return { descriptors, totalVertices: write / 2, scratch, }; }