// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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 { CandlestickChart } from "../candlestick"; import { createLineCornerBuffer, getInstancing, } from "../../../webgl/instanced-attrs"; import { compileProgram } from "../../../webgl/program-cache"; import lineVert from "../../../shaders/line-uniform.vert.glsl"; import lineFrag from "../../../shaders/line-uniform.frag.glsl"; type GL = WebGL2RenderingContext | WebGLRenderingContext; interface OHLCCache { program: WebGLProgram; cornerBuffer: WebGLBuffer; segmentBuffer: WebGLBuffer; u_projection: WebGLUniformLocation | null; u_color: WebGLUniformLocation | null; u_resolution: WebGLUniformLocation | null; u_line_width: WebGLUniformLocation | null; /** * `line-uniform` was extended to support the Y-Line `interpolate` * feature with a per-segment alpha multiplier driven by * `a_real_start * a_real_end` and `u_interp_alpha`. The OHLC glyph * doesn't need that — every segment is "real" — so we hold these * locations to neutralize them at draw time (uniform = 1.0, * constant attribute values = 1.0). Without that, an unset uniform * defaults to 0 and the fragment alpha collapses to 0, rendering * the entire OHLC glyph invisible. */ u_interp_alpha: WebGLUniformLocation | null; a_corner: number; a_start: number; a_end: number; a_real_start: number; a_real_end: number; } /** * Persistent OHLC vertex buffer state — one buffer per color group * (up / down), each holding 3 line segments per candle (H–L vertical, * open tick, close tick). Built once per data load; pan/zoom redraws * rebind + dispatch with no uploads. */ interface OHLCBuffers { upBuffer: WebGLBuffer; downBuffer: WebGLBuffer; /** * Number of line-segment instances in the up buffer (= 3 × up candle count). */ upInstanceCount: number; downInstanceCount: number; } /** * OHLC bar glyph. Owns the OHLC program + per-color persistent segment * buffers built per data load. Co-tenanted with `BodyWickGlyph` on * `CandlestickChart`; only one of the two is active per frame depending * on `_defaultChartType`. */ export class OHLCGlyph { private _program: OHLCCache | null = null; private _buffers: OHLCBuffers | null = null; private ensureProgram(glManager: WebGLContextManager): OHLCCache { if (this._program) { return this._program; } const gl = glManager.gl; const cornerBuffer = createLineCornerBuffer(gl); const partial = compileProgram< Omit >( glManager, "line-uniform", lineVert, lineFrag, [ "u_projection", "u_color", "u_resolution", "u_line_width", "u_interp_alpha", ], ["a_corner", "a_start", "a_end", "a_real_start", "a_real_end"], ); this._program = { ...partial, cornerBuffer, segmentBuffer: gl.createBuffer()!, }; return this._program; } /** * Drop persistent OHLC vertex buffers. Called from data-load (before * `rebuildBuffers`) and from chart-destroy paths. */ invalidateBuffers(chart: CandlestickChart): void { const buf = this._buffers; if (!buf || !chart._glManager) { this._buffers = null; return; } const gl = chart._glManager.gl; gl.deleteBuffer(buf.upBuffer); gl.deleteBuffer(buf.downBuffer); this._buffers = null; } /** * Pre-build the per-group OHLC instance buffers. Each candle emits 3 * line segments (H–L, open tick, close tick); layout per instance is * `[start.x, start.y, end.x, end.y]`. Single GPU upload per group per * data load. */ rebuildBuffers( chart: CandlestickChart, glManager: WebGLContextManager, ): void { // Only rebuild when this chart actually paints OHLC. Cheap enough // to always rebuild but skipping avoids two empty GPU buffers on // candlestick instances. if (chart._defaultChartType !== "ohlc") { return; } const candles = chart._candles; const gl = glManager.gl; this.ensureProgram(glManager); const xOrigin = chart._categoryOrigin; const xC = candles.xCenter; const hw = candles.halfWidth; const open = candles.open; const close = candles.close; const high = candles.high; const low = candles.low; const isUp = candles.isUp; let upCount = 0; let downCount = 0; for (let i = 0; i < candles.count; i++) { if (isUp[i] !== 0) { upCount++; } else { downCount++; } } const upData = new Float32Array(upCount * 3 * 4); const downData = new Float32Array(downCount * 3 * 4); let upW = 0; let downW = 0; for (let i = 0; i < candles.count; i++) { const xc = xC[i] - xOrigin; const o = open[i]; const c = close[i]; const lo = low[i]; const hi = high[i]; const halfW = hw[i]; const target = isUp[i] !== 0 ? upData : downData; let w = isUp[i] !== 0 ? upW : downW; // H–L vertical line. target[w++] = xc; target[w++] = lo; target[w++] = xc; target[w++] = hi; // Open tick: left-facing horizontal stub at y=open. target[w++] = xc - halfW; target[w++] = o; target[w++] = xc; target[w++] = o; // Close tick: right-facing horizontal stub at y=close. target[w++] = xc; target[w++] = c; target[w++] = xc + halfW; target[w++] = c; if (isUp[i] !== 0) { upW = w; } else { downW = w; } } const prev = this._buffers; const upBuf = prev?.upBuffer ?? gl.createBuffer()!; const downBuf = prev?.downBuffer ?? gl.createBuffer()!; gl.bindBuffer(gl.ARRAY_BUFFER, upBuf); gl.bufferData(gl.ARRAY_BUFFER, upData, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, downBuf); gl.bufferData(gl.ARRAY_BUFFER, downData, gl.STATIC_DRAW); this._buffers = { upBuffer: upBuf, downBuffer: downBuf, upInstanceCount: upCount * 3, downInstanceCount: downCount * 3, }; } /** * Bind the persistent up/down OHLC buffers and dispatch one instanced * draw per color group. */ draw( chart: CandlestickChart, gl: GL, glManager: WebGLContextManager, projection: Float32Array, ): void { const buf = this._buffers; if ( !buf || (buf.upInstanceCount === 0 && buf.downInstanceCount === 0) ) { return; } const cache = this.ensureProgram(glManager); const dpr = glManager.dpr; gl.useProgram(cache.program); gl.uniformMatrix4fv(cache.u_projection, false, projection); gl.uniform2f(cache.u_resolution, gl.canvas.width, gl.canvas.height); gl.uniform1f( cache.u_line_width, chart._pluginConfig.ohlc_line_width_px * dpr, ); // `line-uniform` was extended for the Y-Line interpolate // feature with a per-segment alpha multiplier; neutralize it // here. Constant attribute values (used when the array is // disabled) and uniform are stable for every draw, so set // once after `useProgram`. Disabling the arrays first guards // against a prior Y-Line draw that left them enabled at the // same attribute index (locations are shared because both // programs link from the same source). gl.disableVertexAttribArray(cache.a_real_start); gl.disableVertexAttribArray(cache.a_real_end); gl.vertexAttrib1f(cache.a_real_start, 1.0); gl.vertexAttrib1f(cache.a_real_end, 1.0); gl.uniform1f(cache.u_interp_alpha, 1.0); const instancing = getInstancing(glManager); const { setDivisor } = instancing; gl.bindBuffer(gl.ARRAY_BUFFER, cache.cornerBuffer); gl.enableVertexAttribArray(cache.a_corner); gl.vertexAttribPointer(cache.a_corner, 1, gl.FLOAT, false, 0, 0); setDivisor(cache.a_corner, 0); drawGroup( gl, instancing, cache, buf.upBuffer, buf.upInstanceCount, chart._upColor, ); drawGroup( gl, instancing, cache, buf.downBuffer, buf.downInstanceCount, chart._downColor, ); setDivisor(cache.a_start, 0); setDivisor(cache.a_end, 0); } destroy(chart: CandlestickChart): void { const gl = chart._glManager?.gl; if (gl) { this.invalidateBuffers(chart); const cache = this._program; if (cache) { gl.deleteBuffer(cache.cornerBuffer); gl.deleteBuffer(cache.segmentBuffer); } } this._program = null; this._buffers = null; } } function drawGroup( gl: GL, instancing: ReturnType, cache: OHLCCache, buffer: WebGLBuffer, instanceCount: number, color: [number, number, number], ): void { if (instanceCount === 0) { return; } const instanceStride = 4 * Float32Array.BYTES_PER_ELEMENT; const pointSize = 2 * Float32Array.BYTES_PER_ELEMENT; const { setDivisor } = instancing; gl.uniform4f(cache.u_color, color[0], color[1], color[2], 1.0); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(cache.a_start); gl.vertexAttribPointer( cache.a_start, 2, gl.FLOAT, false, instanceStride, 0, ); setDivisor(cache.a_start, 1); gl.enableVertexAttribArray(cache.a_end); gl.vertexAttribPointer( cache.a_end, 2, gl.FLOAT, false, instanceStride, pointSize, ); setDivisor(cache.a_end, 1); instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount); }