1import { VizDisplayMode, ColorStrategy, CandleStyle } from './models.gen'; 2import uPlot from 'uplot'; 3import { colorManipulator } from '@grafana/data'; 4 5const { alpha } = colorManipulator; 6 7export type FieldIndices = Record<string, number>; 8 9interface RendererOpts { 10 mode: VizDisplayMode; 11 candleStyle: CandleStyle; 12 fields: FieldIndices; 13 colorStrategy: ColorStrategy; 14 upColor: string; 15 downColor: string; 16 flatColor: string; 17 volumeAlpha: number; 18 flatAsUp: boolean; 19} 20 21export function drawMarkers(opts: RendererOpts) { 22 let { mode, candleStyle, fields, colorStrategy, upColor, downColor, flatColor, volumeAlpha, flatAsUp = true } = opts; 23 24 const drawPrice = mode !== VizDisplayMode.Volume && fields.high != null && fields.low != null; 25 const asCandles = drawPrice && candleStyle === CandleStyle.Candles; 26 const drawVolume = mode !== VizDisplayMode.Candles && fields.volume != null; 27 28 function selectPath(priceDir: number, flatPath: Path2D, upPath: Path2D, downPath: Path2D, flatAsUp: boolean) { 29 return priceDir > 0 ? upPath : priceDir < 0 ? downPath : flatAsUp ? upPath : flatPath; 30 } 31 32 let tIdx = 0, 33 oIdx = fields.open, 34 hIdx = fields.high, 35 lIdx = fields.low, 36 cIdx = fields.close, 37 vIdx = fields.volume; 38 39 return (u: uPlot) => { 40 // split by discrete color to reduce draw calls 41 let downPath, upPath, flatPath; 42 // with adjusted reduced 43 let downPathVol, upPathVol, flatPathVol; 44 45 if (drawPrice) { 46 flatPath = new Path2D(); 47 upPath = new Path2D(); 48 downPath = new Path2D(); 49 } 50 51 if (drawVolume) { 52 downPathVol = new Path2D(); 53 upPathVol = new Path2D(); 54 flatPathVol = new Path2D(); 55 } 56 57 let hollowPath = new Path2D(); 58 59 let ctx = u.ctx; 60 61 let tData = u.data[tIdx!]; 62 63 let oData = u.data[oIdx!]; 64 let cData = u.data[cIdx!]; 65 66 let hData = drawPrice ? u.data[hIdx!] : null; 67 let lData = drawPrice ? u.data[lIdx!] : null; 68 let vData = drawVolume ? u.data[vIdx!] : null; 69 70 let zeroPx = vIdx != null ? Math.round(u.valToPos(0, u.series[vIdx!].scale!, true)) : null; 71 72 let [idx0, idx1] = u.series[0].idxs!; 73 74 let dataX = u.data[0]; 75 let dataY = oData; 76 77 let colWidth = u.bbox.width; 78 79 if (dataX.length > 1) { 80 // prior index with non-undefined y data 81 let prevIdx = null; 82 83 // scan full dataset for smallest adjacent delta 84 // will not work properly for non-linear x scales, since does not do expensive valToPosX calcs till end 85 for (let i = 0, minDelta = Infinity; i < dataX.length; i++) { 86 if (dataY[i] !== undefined) { 87 if (prevIdx != null) { 88 let delta = Math.abs(dataX[i] - dataX[prevIdx]); 89 90 if (delta < minDelta) { 91 minDelta = delta; 92 colWidth = Math.abs(u.valToPos(dataX[i], 'x') - u.valToPos(dataX[prevIdx], 'x')); 93 } 94 } 95 96 prevIdx = i; 97 } 98 } 99 } 100 101 let barWidth = Math.round(0.6 * colWidth); 102 103 let stickWidth = 2; 104 let outlineWidth = 2; 105 106 if (barWidth <= 12) { 107 stickWidth = outlineWidth = 1; 108 } 109 110 let halfWidth = Math.floor(barWidth / 2); 111 112 for (let i = idx0; i <= idx1; i++) { 113 let tPx = Math.round(u.valToPos(tData[i]!, 'x', true)); 114 115 // current close vs prior close 116 let interDir = i === idx0 ? 0 : Math.sign(cData[i]! - cData[i - 1]!); 117 // current close vs current open 118 let intraDir = Math.sign(cData[i]! - oData[i]!); 119 120 // volume 121 if (drawVolume) { 122 let outerPath = selectPath( 123 colorStrategy === ColorStrategy.CloseClose ? interDir : intraDir, 124 flatPathVol as Path2D, 125 upPathVol as Path2D, 126 downPathVol as Path2D, 127 i === idx0 && ColorStrategy.CloseClose ? false : flatAsUp 128 ); 129 130 let vPx = Math.round(u.valToPos(vData![i]!, u.series[vIdx!].scale!, true)); 131 outerPath.rect(tPx - halfWidth, vPx, barWidth, zeroPx! - vPx); 132 } 133 134 if (drawPrice) { 135 let outerPath = selectPath( 136 colorStrategy === ColorStrategy.CloseClose ? interDir : intraDir, 137 flatPath as Path2D, 138 upPath as Path2D, 139 downPath as Path2D, 140 i === idx0 && ColorStrategy.CloseClose ? false : flatAsUp 141 ); 142 143 // stick 144 let hPx = Math.round(u.valToPos(hData![i]!, u.series[hIdx!].scale!, true)); 145 let lPx = Math.round(u.valToPos(lData![i]!, u.series[lIdx!].scale!, true)); 146 outerPath.rect(tPx - Math.floor(stickWidth / 2), hPx, stickWidth, lPx - hPx); 147 148 let oPx = Math.round(u.valToPos(oData[i]!, u.series[oIdx!].scale!, true)); 149 let cPx = Math.round(u.valToPos(cData[i]!, u.series[cIdx!].scale!, true)); 150 151 if (asCandles) { 152 // rect 153 let top = Math.min(oPx, cPx); 154 let btm = Math.max(oPx, cPx); 155 let hgt = Math.max(1, btm - top); 156 outerPath.rect(tPx - halfWidth, top, barWidth, hgt); 157 158 if (colorStrategy === ColorStrategy.CloseClose) { 159 if (intraDir >= 0 && hgt > outlineWidth * 2) { 160 hollowPath.rect( 161 tPx - halfWidth + outlineWidth, 162 top + outlineWidth, 163 barWidth - outlineWidth * 2, 164 hgt - outlineWidth * 2 165 ); 166 } 167 } 168 } else { 169 outerPath.rect(tPx - halfWidth, oPx, halfWidth, stickWidth); 170 outerPath.rect(tPx, cPx, halfWidth, stickWidth); 171 } 172 } 173 } 174 175 ctx.save(); 176 177 ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height); 178 ctx.clip(); 179 180 if (drawVolume) { 181 ctx.fillStyle = alpha(upColor, volumeAlpha); 182 ctx.fill(upPathVol as Path2D); 183 184 ctx.fillStyle = alpha(downColor, volumeAlpha); 185 ctx.fill(downPathVol as Path2D); 186 187 ctx.fillStyle = alpha(flatColor, volumeAlpha); 188 ctx.fill(flatPathVol as Path2D); 189 } 190 191 if (drawPrice) { 192 ctx.fillStyle = upColor; 193 ctx.fill(upPath as Path2D); 194 195 ctx.fillStyle = downColor; 196 ctx.fill(downPath as Path2D); 197 198 ctx.fillStyle = flatColor; 199 ctx.fill(flatPath as Path2D); 200 201 ctx.globalCompositeOperation = 'destination-out'; 202 ctx.fill(hollowPath); 203 } 204 205 ctx.restore(); 206 }; 207} 208