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