utils.ts 6.1 KB

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