chartjs-horizontal-bar.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import Chart from 'chart.js';
  2. // Modify horizontalBar so that each dataset (fragments, timeRanges) draws on the same row (level, track or buffer)
  3. Chart.controllers.horizontalBar.prototype.calculateBarValuePixels = function (
  4. datasetIndex,
  5. index,
  6. options
  7. ) {
  8. const chart = this.chart;
  9. const scale = this._getValueScale();
  10. const datasets = chart.data.datasets;
  11. if (!datasets) {
  12. throw new Error(`Chart datasets are ${datasets}`);
  13. }
  14. scale._parseValue = scaleParseValue;
  15. const obj = datasets[datasetIndex].data[index];
  16. const value = scale._parseValue(obj);
  17. const start =
  18. value.start === undefined
  19. ? 0
  20. : value.max >= 0 && value.min >= 0
  21. ? value.min
  22. : value.max;
  23. const length =
  24. value.start === undefined
  25. ? value.end
  26. : value.max >= 0 && value.min >= 0
  27. ? value.max - value.min
  28. : value.min - value.max;
  29. const base = scale.getPixelForValue(start);
  30. const head = scale.getPixelForValue(start + length);
  31. const size = head - base;
  32. return {
  33. size: size,
  34. base: base,
  35. head: head,
  36. center: head + size / 2,
  37. };
  38. };
  39. Chart.controllers.horizontalBar.prototype.calculateBarIndexPixels = function (
  40. datasetIndex,
  41. index,
  42. ruler,
  43. options
  44. ) {
  45. const rowHeight = options.barThickness;
  46. const size = rowHeight * options.categoryPercentage;
  47. const center = ruler.start + (datasetIndex * rowHeight + rowHeight / 2);
  48. return {
  49. base: center - size / 2,
  50. head: center + size / 2,
  51. center,
  52. size,
  53. };
  54. };
  55. Chart.controllers.horizontalBar.prototype.draw = function () {
  56. const rects = this.getMeta().data;
  57. const len = rects.length;
  58. const dataset = this.getDataset();
  59. if (len !== dataset.data.length) {
  60. // View does not match dataset (wait for redraw)
  61. return;
  62. }
  63. const chart = this.chart;
  64. const scale = this._getValueScale();
  65. scale._parseValue = scaleParseValue;
  66. const ctx: CanvasRenderingContext2D = chart.ctx;
  67. const chartArea: { left; top; right; bottom } = chart.chartArea;
  68. Chart.helpers.canvas.clipArea(ctx, chartArea);
  69. if (!this.lineHeight) {
  70. this.lineHeight =
  71. Math.ceil(ctx.measureText('0').actualBoundingBoxAscent) + 2;
  72. }
  73. const lineHeight = this.lineHeight;
  74. let range = 0;
  75. for (let i = 0; i < len; ++i) {
  76. const rect = rects[i];
  77. const view = rect._view;
  78. if (!intersects(view.base, view.x, chartArea.left, chartArea.right)) {
  79. // Do not draw elements outside of the chart's viewport
  80. continue;
  81. }
  82. const obj = dataset.data[i];
  83. const val = scale._parseValue(obj);
  84. if (!isNaN(val.min) && !isNaN(val.max)) {
  85. const { dataType } = obj;
  86. let { stats } = obj;
  87. const isPart = dataType === 'part';
  88. const isFragmentHint = dataType === 'fragmentHint';
  89. const isFragment = dataType === 'fragment' || isPart || isFragmentHint;
  90. const isCue = dataType === 'cue';
  91. if (isCue) {
  92. view.y += view.height * 0.5 * (i % 2) - view.height * 0.25;
  93. } else if (isPart) {
  94. view.height -= 22;
  95. }
  96. const bounds = boundingRects(view);
  97. const drawText = bounds.w > lineHeight * 1.5 && !isFragmentHint;
  98. if (isFragment || isCue) {
  99. if (drawText) {
  100. view.borderWidth = 1;
  101. if (i === 0) {
  102. view.borderSkipped = false;
  103. }
  104. } else {
  105. range =
  106. range ||
  107. scale.getValueForPixel(chartArea.right) -
  108. scale.getValueForPixel(chartArea.left);
  109. if (range > 300 || isCue) {
  110. view.borderWidth = 0;
  111. }
  112. }
  113. if (isFragmentHint) {
  114. view.borderWidth = 0;
  115. view.backgroundColor = 'rgba(0, 0, 0, 0.1)';
  116. } else {
  117. view.backgroundColor = `rgba(0, 0, 0, ${0.05 + (i % 2) / 12})`;
  118. }
  119. }
  120. rect.draw();
  121. if (isFragment) {
  122. if (!stats) {
  123. stats = {};
  124. }
  125. if (isPart) {
  126. ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
  127. ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
  128. }
  129. if (stats.aborted) {
  130. ctx.fillStyle = 'rgba(100, 0, 0, 0.3)';
  131. ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
  132. }
  133. if (stats.loaded && stats.total) {
  134. ctx.fillStyle = 'rgba(50, 20, 100, 0.3)';
  135. ctx.fillRect(
  136. bounds.x,
  137. bounds.y,
  138. (bounds.w * stats.loaded) / stats.total,
  139. bounds.h
  140. );
  141. }
  142. } else if (isCue) {
  143. if (obj.active) {
  144. ctx.fillStyle = 'rgba(100, 100, 10, 0.4)';
  145. ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
  146. }
  147. }
  148. if (drawText) {
  149. const start = val.start; // obj.start;
  150. ctx.fillStyle = 'rgb(0, 0, 0)';
  151. if (stats) {
  152. const snBounds = Object.assign({}, bounds);
  153. if (obj.cc) {
  154. const ccLabel = `cc:${obj.cc}`;
  155. const ccWidth = Math.min(
  156. ctx.measureText(ccLabel).width + 2,
  157. snBounds.w / 2 - 2
  158. );
  159. if (ccWidth) {
  160. ctx.fillText(
  161. ccLabel,
  162. snBounds.x + 2,
  163. snBounds.y + lineHeight,
  164. snBounds.w / 2 - 4
  165. );
  166. snBounds.x += ccWidth;
  167. snBounds.w -= ccWidth;
  168. }
  169. }
  170. const snLabel = isPart ? `part: ${obj.index}` : `sn: ${obj.sn}`;
  171. const textWidth = Math.min(
  172. ctx.measureText(snLabel).width + 2,
  173. snBounds.w - 2
  174. );
  175. ctx.fillText(
  176. snLabel,
  177. snBounds.x + snBounds.w - textWidth,
  178. snBounds.y + lineHeight,
  179. snBounds.w - 4
  180. );
  181. }
  182. if (isCue) {
  183. const strLength = Math.min(
  184. 30,
  185. Math.ceil(bounds.w / (lineHeight / 3))
  186. );
  187. ctx.fillText(
  188. ('' + obj.content).slice(0, strLength),
  189. bounds.x + 2,
  190. bounds.y + bounds.h - 3,
  191. bounds.w - 5
  192. );
  193. } else if (!isPart) {
  194. const float = start !== (start | 0);
  195. const fixedDigits = float
  196. ? Math.min(5, Math.max(1, Math.floor(bounds.w / 10 - 1)))
  197. : 0;
  198. const startString = hhmmss(start, fixedDigits);
  199. ctx.fillText(
  200. startString,
  201. bounds.x + 2,
  202. bounds.y + bounds.h - 3,
  203. bounds.w - 5
  204. );
  205. }
  206. }
  207. }
  208. }
  209. Chart.helpers.canvas.unclipArea(chart.ctx);
  210. };
  211. export function applyChartInstanceOverrides(chart) {
  212. Object.keys(chart.scales).forEach((axis) => {
  213. const scale = chart.scales![axis];
  214. scale._parseValue = scaleParseValue;
  215. });
  216. }
  217. function scaleParseValue(value: number[] | any) {
  218. if (value === undefined) {
  219. console.warn('Chart values undefined (update chart)');
  220. return {};
  221. }
  222. let start;
  223. let end;
  224. let min;
  225. let max;
  226. if (Array.isArray(value)) {
  227. start = +this.getRightValue(value[0]);
  228. end = +this.getRightValue(value[1]);
  229. min = Math.min(start, end);
  230. max = Math.max(start, end);
  231. } else {
  232. start = +this.getRightValue(value.start);
  233. if ('end' in value) {
  234. end = +this.getRightValue(value.end);
  235. } else {
  236. end = +this.getRightValue(value.start + value.duration);
  237. }
  238. min = Math.min(start, end);
  239. max = Math.max(start, end);
  240. }
  241. return {
  242. min,
  243. max,
  244. start,
  245. end,
  246. };
  247. }
  248. function intersects(x1, x2, x3, x4) {
  249. return x2 > x3 && x1 < x4;
  250. }
  251. function boundingRects(vm) {
  252. const half = vm.height / 2;
  253. const left = Math.min(vm.x, vm.base);
  254. const right = Math.max(vm.x, vm.base);
  255. const top = vm.y - half;
  256. const bottom = vm.y + half;
  257. return {
  258. x: left,
  259. y: top,
  260. w: right - left,
  261. h: bottom - top,
  262. };
  263. }
  264. export function hhmmss(value, fixedDigits) {
  265. const h = (value / 3600) | 0;
  266. const m = ((value / 60) | 0) % 60;
  267. const s = value % 60;
  268. return `${h}:${pad(m, 2)}:${pad(
  269. s.toFixed(fixedDigits),
  270. fixedDigits ? fixedDigits + 3 : 2
  271. )}`.replace(/^(?:0+:?)*(\d.*?)(?:\.0*)?$/, '$1');
  272. }
  273. function pad(str, length) {
  274. str = '' + str;
  275. while (str.length < length) {
  276. str = '0' + str;
  277. }
  278. return str;
  279. }