UNPKG

apexcharts

Version:

A JavaScript Chart Library

145 lines (133 loc) 6.44 kB
// @ts-check /** * Strata (#2): the `Renderer` interface and shared selection helpers. * * The interface abstracts emit, identity, state, and export for the SERIES * layer. Both the SVG path (today) and the opt-in Canvas path implement it; * Prism (#20, WebGPU) will too. Keeping this contract right is the whole point * of sequencing Strata into the foundation: Marks (#11) emit through it and * Weave (#1) `api.layer` targets it. * * P1 ships the interface + selection + the SVG adapter (core, ~zero weight: it * wraps the Graphics/Fill/Filters that already ship). The Canvas renderer is a * separate tree-shakeable feature (`apexcharts/features/renderer-canvas`); when * it is not bundled, selection falls back to SVG. * * @module renderers/Renderer * * @typedef {'svg'|'canvas'|'gpu'} RendererKind * * @typedef {object} Renderer * @property {RendererKind} kind * @property {() => void} beginSeries Start a fresh series layer for a render pass. * @property {() => any} present Finalize; returns what mount()/fastUpdate() insert. * @property {() => void} clear Wipe the series layer (fast path). * @property {(attrs: any) => any} group * @property {(opts: any) => any} drawPath * @property {(...args: any[]) => any} drawLine * @property {(...args: any[]) => any} drawRect * @property {(x: number, y: number, w: number, h: number, opts: any) => any} [drawRectCell] Columnar dense rect (heatmap cell); optional. * @property {(r: number, attrs: any) => any} drawCircle * @property {(x: number, y: number, opts: any) => any} drawMarker * @property {(opts: any) => any} renderPaths High-level series path (animation-aware). * @property {(opts: any) => any} drawText Series-attached text only (axis/label text stays SVG). * @property {(feature: string) => boolean} supports * @property {(px: number, py: number) => ({seriesIndex:number,dataPointIndex:number,x?:number,y?:number,width?:number,height?:number}|null)} hitTest * @property {(target: any, style: any) => void} restyle * @property {() => ({dataURL:string,x:number,y:number,w:number,h:number}|null)} toBitmap * @property {() => void} destroy */ /** Canvas cannot reproduce these; their presence forces SVG. */ const OK_FILTER_TYPES = ['none', 'lighten', 'darken'] /** * The active series emit target. In canvas (and, later, gpu) mode series marks * are RECORDED through `ctx.renderer`; in SVG mode we return the caller's own * `Graphics` instance untouched, so the SVG render path is byte-for-byte the * same as before Strata (the P1 promise). Only leaf mark calls (`renderPaths`, * `drawMarker`) are diverted: structural groups, string builders, and chrome * stay on `Graphics`. * * @param {any} ctx * @param {any} graphics The SVG-path fallback (a `new Graphics(w)` the caller already holds). * @returns {any} */ export function seriesEmitter(ctx, graphics) { const r = ctx && ctx.renderer return r && r.kind && r.kind !== 'svg' ? r : graphics } /** * Count the marks a render will emit: markers, data labels, and scatter/bubble * points: NOT the raw data length. The spike showed the SVG node-count killer * is per-point decorations, not the series line: a bare line/area path is O(1) * nodes. A separate large-`d` trigger still routes a giant plain line to canvas. * * @param {any} w * @returns {number} */ export function computeMarkCount(w) { const series = w.config.series || [] const type = w.config.chart.type const scatterish = type === 'scatter' || type === 'bubble' const markerSize = w.config.markers && w.config.markers.size const markersOn = Array.isArray(markerSize) ? markerSize.some((s) => s > 0) : (markerSize || 0) > 0 const labelsOn = !!(w.config.dataLabels && w.config.dataLabels.enabled) // Heatmap paints one rect per data point, so every cell is a mark: the raw // cell count is exactly the canvas-vs-svg lever (the SVG node killer here is // the per-cell <rect>). Count cells so 'auto' promotes dense heatmaps. const isHeatmap = type === 'heatmap' let total = 0 let maxLen = 0 series.forEach((/** @type {any} */ s) => { const n = Array.isArray(s.data) ? s.data.length : 0 if (n > maxLen) maxLen = n if (scatterish || markersOn || isHeatmap) total += n if (labelsOn) total += n }) // Marks (#11): custom series are NOT auto-promoted. Their per-datum primitives // are object commands (no columnar fast path), so canvas paint is only fast at // low/moderate density; auto-promoting at the threshold would regress. Users // opt in explicitly with renderer:'canvas' when their renderItem is cheap. // Even a bare line/area re-rasterizes slowly with a huge `d` on zoom. const LARGE_D = 50000 if (maxLen >= LARGE_D) total = Math.max(total, maxLen) return total } /** * Whether the config uses a feature the canvas renderer cannot reproduce * (pattern/image fill, or a state color-matrix filter beyond lighten/darken). * Their presence makes `'auto'` decline canvas and `'canvas'` warn + fall back. * * @param {any} w * @returns {boolean} */ export function hasCanvasUnsupportedFeature(w) { const fillType = w.config.fill && w.config.fill.type // 'gradient' is included as a P2 gate, not a hard limit: canvas CAN paint // gradients, but the P2 canvas renderer ships solid fills only (gradient // reconstruction via Fill.getFillDescriptor is the P2.1 fast-follow). Until // then a gradient chart routes to SVG. pattern/image are genuine v1 non-goals. const isUnsupportedFill = (/** @type {any} */ t) => t === 'pattern' || t === 'image' || t === 'gradient' if ( Array.isArray(fillType) ? fillType.some(isUnsupportedFill) : isUnsupportedFill(fillType) ) { return true } // Multi-color line (colorAboveThreshold/BelowThreshold) is drawn via a // gradient internally: same P2 gate as above. const lineColors = w.config.plotOptions?.line?.colors if (lineColors && lineColors.colorAboveThreshold && lineColors.colorBelowThreshold) { return true } const states = w.config.states || {} const hoverFilter = states.hover && states.hover.filter && states.hover.filter.type const activeFilter = states.active && states.active.filter && states.active.filter.type if (hoverFilter && !OK_FILTER_TYPES.includes(hoverFilter)) return true if (activeFilter && !OK_FILTER_TYPES.includes(activeFilter)) return true return false }