import Chart from 'chart.js'; // Modify horizontalBar so that each dataset (fragments, timeRanges) draws on the same row (level, track or buffer) Chart.controllers.horizontalBar.prototype.calculateBarValuePixels = function ( datasetIndex, index, options ) { const chart = this.chart; const scale = this._getValueScale(); const datasets = chart.data.datasets; if (!datasets) { throw new Error(`Chart datasets are ${datasets}`); } scale._parseValue = scaleParseValue; const obj = datasets[datasetIndex].data[index]; const value = scale._parseValue(obj); const start = value.start === undefined ? 0 : value.max >= 0 && value.min >= 0 ? value.min : value.max; const length = value.start === undefined ? value.end : value.max >= 0 && value.min >= 0 ? value.max - value.min : value.min - value.max; const base = scale.getPixelForValue(start); const head = scale.getPixelForValue(start + length); const size = head - base; return { size: size, base: base, head: head, center: head + size / 2, }; }; Chart.controllers.horizontalBar.prototype.calculateBarIndexPixels = function ( datasetIndex, index, ruler, options ) { const rowHeight = options.barThickness; const size = rowHeight * options.categoryPercentage; const center = ruler.start + (datasetIndex * rowHeight + rowHeight / 2); return { base: center - size / 2, head: center + size / 2, center, size, }; }; Chart.controllers.horizontalBar.prototype.draw = function () { const rects = this.getMeta().data; const len = rects.length; const dataset = this.getDataset(); if (len !== dataset.data.length) { // View does not match dataset (wait for redraw) return; } const chart = this.chart; const scale = this._getValueScale(); scale._parseValue = scaleParseValue; const ctx: CanvasRenderingContext2D = chart.ctx; const chartArea: { left; top; right; bottom } = chart.chartArea; Chart.helpers.canvas.clipArea(ctx, chartArea); if (!this.lineHeight) { this.lineHeight = Math.ceil(ctx.measureText('0').actualBoundingBoxAscent) + 2; } const lineHeight = this.lineHeight; let range = 0; for (let i = 0; i < len; ++i) { const rect = rects[i]; const view = rect._view; if (!intersects(view.base, view.x, chartArea.left, chartArea.right)) { // Do not draw elements outside of the chart's viewport continue; } const obj = dataset.data[i]; const val = scale._parseValue(obj); if (!isNaN(val.min) && !isNaN(val.max)) { const { dataType } = obj; let { stats } = obj; const isPart = dataType === 'part'; const isFragmentHint = dataType === 'fragmentHint'; const isFragment = dataType === 'fragment' || isPart || isFragmentHint; const isCue = dataType === 'cue'; if (isCue) { view.y += view.height * 0.5 * (i % 2) - view.height * 0.25; } else if (isPart) { view.height -= 22; } const bounds = boundingRects(view); const drawText = bounds.w > lineHeight * 1.5 && !isFragmentHint; if (isFragment || isCue) { if (drawText) { view.borderWidth = 1; if (i === 0) { view.borderSkipped = false; } } else { range = range || scale.getValueForPixel(chartArea.right) - scale.getValueForPixel(chartArea.left); if (range > 300 || isCue) { view.borderWidth = 0; } } if (isFragmentHint) { view.borderWidth = 0; view.backgroundColor = 'rgba(0, 0, 0, 0.1)'; } else { view.backgroundColor = `rgba(0, 0, 0, ${0.05 + (i % 2) / 12})`; } } rect.draw(); if (isFragment) { if (!stats) { stats = {}; } if (isPart) { ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h); } if (stats.aborted) { ctx.fillStyle = 'rgba(100, 0, 0, 0.3)'; ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h); } if (stats.loaded && stats.total) { ctx.fillStyle = 'rgba(50, 20, 100, 0.3)'; ctx.fillRect( bounds.x, bounds.y, (bounds.w * stats.loaded) / stats.total, bounds.h ); } } else if (isCue) { if (obj.active) { ctx.fillStyle = 'rgba(100, 100, 10, 0.4)'; ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h); } } if (drawText) { const start = val.start; // obj.start; ctx.fillStyle = 'rgb(0, 0, 0)'; if (stats) { const snBounds = Object.assign({}, bounds); if (obj.cc) { const ccLabel = `cc:${obj.cc}`; const ccWidth = Math.min( ctx.measureText(ccLabel).width + 2, snBounds.w / 2 - 2 ); if (ccWidth) { ctx.fillText( ccLabel, snBounds.x + 2, snBounds.y + lineHeight, snBounds.w / 2 - 4 ); snBounds.x += ccWidth; snBounds.w -= ccWidth; } } const snLabel = isPart ? `part: ${obj.index}` : `sn: ${obj.sn}`; const textWidth = Math.min( ctx.measureText(snLabel).width + 2, snBounds.w - 2 ); ctx.fillText( snLabel, snBounds.x + snBounds.w - textWidth, snBounds.y + lineHeight, snBounds.w - 4 ); } if (isCue) { const strLength = Math.min( 30, Math.ceil(bounds.w / (lineHeight / 3)) ); ctx.fillText( ('' + obj.content).slice(0, strLength), bounds.x + 2, bounds.y + bounds.h - 3, bounds.w - 5 ); } else if (!isPart) { const float = start !== (start | 0); const fixedDigits = float ? Math.min(5, Math.max(1, Math.floor(bounds.w / 10 - 1))) : 0; const startString = hhmmss(start, fixedDigits); ctx.fillText( startString, bounds.x + 2, bounds.y + bounds.h - 3, bounds.w - 5 ); } } } } Chart.helpers.canvas.unclipArea(chart.ctx); }; export function applyChartInstanceOverrides(chart) { Object.keys(chart.scales).forEach((axis) => { const scale = chart.scales![axis]; scale._parseValue = scaleParseValue; }); } function scaleParseValue(value: number[] | any) { if (value === undefined) { console.warn('Chart values undefined (update chart)'); return {}; } let start; let end; let min; let max; if (Array.isArray(value)) { start = +this.getRightValue(value[0]); end = +this.getRightValue(value[1]); min = Math.min(start, end); max = Math.max(start, end); } else { start = +this.getRightValue(value.start); if ('end' in value) { end = +this.getRightValue(value.end); } else { end = +this.getRightValue(value.start + value.duration); } min = Math.min(start, end); max = Math.max(start, end); } return { min, max, start, end, }; } function intersects(x1, x2, x3, x4) { return x2 > x3 && x1 < x4; } function boundingRects(vm) { const half = vm.height / 2; const left = Math.min(vm.x, vm.base); const right = Math.max(vm.x, vm.base); const top = vm.y - half; const bottom = vm.y + half; return { x: left, y: top, w: right - left, h: bottom - top, }; } export function hhmmss(value, fixedDigits) { const h = (value / 3600) | 0; const m = ((value / 60) | 0) % 60; const s = value % 60; return `${h}:${pad(m, 2)}:${pad( s.toFixed(fixedDigits), fixedDigits ? fixedDigits + 3 : 2 )}`.replace(/^(?:0+:?)*(\d.*?)(?:\.0*)?$/, '$1'); } function pad(str, length) { str = '' + str; while (str.length < length) { str = '0' + str; } return str; }