| /** |
| * @module modules/plot-simple-sk |
| * @description <h2><code>plot-simple-sk</code></h2> |
| * |
| * A custom element for plotting x,y graphs. |
| * |
| * @evt trace_selected - Event produced when the user clicks on a line. |
| * The e.detail contains the id of the line and the index of the |
| * point in the line closest to the mouse, and the [x, y] value |
| * of the point in 'pt'. |
| * |
| * <pre> |
| * e.detail = { |
| * id: 'id of trace', |
| * index: 3, |
| * pt: [2, 34.5], |
| * } |
| * </pre> |
| * |
| * @evt trace_focused - Event produced when the user moves the mouse close |
| * to a line. The e.detail contains the id of the line and the index of the |
| * point in the line closest to the mouse. |
| * <pre> |
| * e.detail = { |
| * id: 'id of trace', |
| * index: 3, |
| * pt: [2, 34.5], |
| * } |
| * </pre> |
| * |
| * @evt zoom - Event produced when the user has zoomed into a region |
| * by dragging. |
| * |
| * @attr width - The width of the element in px. |
| * @attr height - The height of the element in px. |
| * |
| */ |
| import { html, render } from 'lit-html' |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk' |
| import { Chart } from 'chart.js' |
| import 'chartjs-plugin-annotation' |
| import 'chartjs-plugin-zoom' |
| |
| /** |
| * @constant {string} - Prefix for trace ids that are not real traces. |
| */ |
| const SPECIAL = 'special'; |
| |
| /** |
| * @constant {Array} - Colors used for traces. |
| */ |
| const colors = [ |
| '#000000', |
| '#1B9E77', |
| '#D95F02', |
| '#7570B3', |
| '#E7298A', |
| '#66A61E', |
| '#E6AB02', |
| '#A6761D', |
| '#666666', |
| ]; |
| |
| const template = (ele) => html` |
| <canvas width=${ele.width} height=${ele.height}></canvas> |
| `; |
| |
| window.customElements.define('plot-simple-sk', class extends ElementSk { |
| constructor() { |
| super(template); |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| |
| // Only create the _chart once. |
| if (!this._chart) { |
| this._render(); |
| |
| // The location of the XBar. See setXBar(). |
| this._xbarx = 0; |
| |
| // The locations of the background bands. See setBanding(). |
| this._bands = []; |
| |
| this._chart = new Chart(this.querySelector('canvas'), { |
| type: 'line', |
| data: { |
| datasets: [], |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| spanGaps: true, |
| animation: { |
| duration: 0, // general animation time |
| }, |
| hover: { |
| animationDuration: 0, // duration of animations when hovering an item |
| }, |
| annotation: { |
| annotations: [], |
| }, |
| responsiveAnimationDuration: 0, // animation duration after a resize |
| elements: { |
| line: { |
| tension: 0 // disables bezier curves |
| } |
| }, |
| tooltips: { |
| intersect: false, |
| mode: 'nearest', |
| animationDuration: 0, |
| caretPadding: 10, |
| callbacks: { |
| label: (tooltipItem, data) => { |
| const label = data.datasets[tooltipItem.datasetIndex].label || ''; |
| const tooltipValue = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; |
| let detail = { |
| id: label, |
| value: tooltipValue, |
| index: tooltipItem.index, |
| pt: [tooltipItem.index, tooltipItem.value], |
| }; |
| this.dispatchEvent(new CustomEvent('trace_focused', {detail: detail, bubbles: true})); |
| |
| return parseFloat(tooltipValue).toLocaleString(); |
| } |
| }, |
| }, |
| scales: { |
| xAxes: [{ |
| type: 'time', |
| position: 'bottom', |
| time: { |
| source: 'labels', |
| displayFormats: { |
| 'millisecond': 'h:mm:ss.SSS A', |
| 'second': 'h:mm:ss A', |
| 'minute': 'h:mm A', |
| 'hour': 'ddd h A', |
| 'day': 'ddd h A', |
| 'week': 'D MMM', |
| 'month': 'D MMM', |
| }, |
| }, |
| distribution: 'series', |
| ticks: { |
| autoSkip: true, |
| autoSkipPadding: 10, |
| source: 'data', |
| minRotation: 60, |
| autoSkip: true, |
| maxTicksLimit: 10, |
| }, |
| }], |
| yAxes: [{ |
| ticks: { |
| callback: function(value, index, values) { |
| return parseFloat(value).toLocaleString(); |
| }, |
| }, |
| }] |
| }, |
| legend: { |
| display: false, |
| }, |
| onClick: (e) => { |
| let eles = this._chart.getElementAtEvent(e); |
| if (!eles.length) { |
| return |
| } |
| let ele = eles[0]; |
| let id = this._chart.data.datasets[ele._datasetIndex].label; |
| if (id.startsWith(SPECIAL)) { |
| return |
| } |
| let index = ele._index; |
| let value = this._chart.data.datasets[ele._datasetIndex].data[ele._index]; |
| let detail = { |
| id: id, |
| index: index, |
| value: value, |
| pt: [index, value], |
| }; |
| this.dispatchEvent(new CustomEvent('trace_selected', {detail: detail, bubbles: true})); |
| this.setHighlight([id]); |
| }, |
| plugins: { |
| zoom: { |
| pan: { |
| enabled: false, |
| }, |
| zoom: { |
| enabled: true, |
| drag: true, |
| |
| drag: { |
| borderColor: 'lightgray', |
| borderWidth: 3, |
| }, |
| |
| mode: 'xy', |
| rangeMin: { |
| x: null, |
| y: null |
| }, |
| rangeMax: { |
| x: null, |
| y: null |
| }, |
| |
| // Speed of zoom via mouse wheel |
| // (percentage of zoom on a wheel event) |
| speed: 0.1, |
| |
| onZoom: (c) => { |
| console.log(c.chart.scales); |
| let detail = { |
| xMin: c.chart.scales['x-axis-0'].min, |
| xMax: c.chart.scales['x-axis-0'].max, |
| yMin: c.chart.scales['y-axis-0'].min, |
| yMax: c.chart.scales['y-axis-0'].max, |
| }; |
| this.dispatchEvent(new CustomEvent('zoom', {detail: detail, bubbles: true})); |
| }, |
| } |
| } |
| } |
| }, |
| }); |
| } |
| } |
| |
| // Convert the different in time between d1 and d2 into the units to |
| // when displaying ticks. See https://www.chartjs.org/docs/latest/axes/cartesian/time.html#display-formats |
| // and https://momentjs.com/docs/#/displaying/format/ |
| // |
| // This works in coordination with the values set in time.displayFormats. |
| _diffDateToUnits(d1, d2) { |
| let diff = d2 - d1; |
| diff = diff / 1000; |
| if (diff < 1) { |
| return 'millisecond'; |
| } |
| diff = diff / 60; |
| if (diff < 1) { |
| return 'second'; |
| } |
| diff = diff / 60; |
| if (diff < 1) { |
| return 'minute'; |
| } |
| diff = diff / 24; |
| if (diff < 1) { |
| return 'hour'; |
| } |
| diff = diff / 7; |
| if (diff < 1) { |
| return 'day'; |
| } |
| diff = diff / 365; |
| if (diff < 1) { |
| return 'week'; |
| } |
| return 'month'; |
| } |
| |
| /** |
| * This is a super simple hash (h = h * 31 + x_i) currently used |
| * for things like assigning colors to graphs based on trace ids. It |
| * shouldn't be used for anything more serious than that. |
| * |
| * @return {Number} A 32 bit hash for the given string. |
| */ |
| _hashString(s) { |
| let hash = 0; |
| for (let i = s.length - 1; i >= 0; i--) { |
| hash = ((hash << 5) - hash) + s.charCodeAt(i); |
| hash |= 0; |
| } |
| return Math.abs(hash); |
| } |
| |
| /** |
| * Adds lines to be displayed. |
| * |
| * Any line id that begins with 'special' will be treated specially, |
| * i.e. it will be presented as a dashed black line that doesn't |
| * generate events. This may be useful for adding a line at y=0, |
| * or a reference trace. |
| * |
| * @param {Object} lines - A map from trace id to arrays of [x, y] pairs. |
| * @param {Array=} labels - An array of Date's that represent the x values of |
| * data to plot. |
| * |
| * TODO(jcgregorio) Switch lines to be a map to an Array of y values |
| * since that's what chart.js expects. |
| * |
| * @example |
| * |
| * let lines = { |
| * foo: [ |
| * [0.1, 3.7], |
| * [0.2, 3.8], |
| * [0.4, 3.0], |
| * ], |
| * bar: [ |
| * [0.0, 2.5], |
| * [0.2, 4.2], |
| * [0.5, 3.9], |
| * ], |
| * }; |
| * let labels = [new Date(), new Date()]; |
| * plot.addLines(lines, labels); |
| */ |
| addLines(lines, labels) { |
| if (labels) { |
| this._chart.data.labels = labels; |
| let unit = this._diffDateToUnits(labels[0], labels[labels.length-1]); |
| this._chart.options.scales.xAxes[0].time.unit = unit; |
| } |
| |
| let exists = {}; |
| this._chart.data.datasets.forEach((d) => { |
| exists[d.label] = true; |
| }); |
| Object.keys(lines).forEach((id) => { |
| if (exists[id]) { |
| return; |
| } |
| let data = lines[id].map(arr => arr[1]); |
| this._chart.data.datasets.push({ |
| label: id, |
| data: data, |
| fill: false, |
| borderColor: colors[(this._hashString(id) % 8) + 1], |
| borderWidth: 1, |
| }); |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Delete a line from being plotted. |
| * |
| * @param {string} id - The trace id. |
| */ |
| deleteLine(id) { |
| let ds = this._chart.data.datasets; |
| for (let i = 0; i < ds.length; i++) { |
| if (ds[i].label === id) { |
| this._chart.data.datasets.splice(i, 1); |
| } |
| } |
| this._chart.update(); |
| } |
| |
| /** |
| * Remove all lines from plot. |
| */ |
| removeAll() { |
| this._chart.data.datasets = []; |
| this._chart.update(); |
| } |
| |
| /** |
| * Highlight one or more traces. |
| * |
| * @param {Array} ids - An array of trace ids. |
| */ |
| setHighlight(ids) { |
| this._chart.data.datasets.forEach((dataset) => { |
| if (ids.indexOf(dataset.label) != -1) { |
| dataset.borderWidth = 3; |
| } else { |
| dataset.borderWidth = 1; |
| } |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Returns the trace ids of all highlighted traces. |
| * |
| * @return {Array} Trace ids. |
| */ |
| highlighted() { |
| let h = []; |
| this._chart.data.datasets.forEach((dataset) => { |
| if (dataset.borderWidth === 3) { |
| h.push(dataset.label); |
| } |
| }); |
| return h; |
| } |
| |
| /** |
| * Clears all highlighting from all traces. |
| */ |
| clearHighlight() { |
| this._chart.data.datasets.forEach((dataset) => { |
| dataset.borderWidth = 1; |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Turns on a vertical bar at the given position. |
| * |
| * @param {Number} x - The offset into the labels where the bar |
| * should be positioned. |
| */ |
| setXBar(x) { |
| this.clearXBar(); |
| this._chart.options.annotation.annotations.push({ |
| id: 'xbar', |
| type: 'line', |
| mode: 'vertical', |
| scaleID: 'x-axis-0', |
| value: this._chart.data.labels[x], |
| borderColor: 'red', |
| borderWidth: 3, |
| drawTime: 'beforeDatasetsDraw', |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Removes the x-bar from being displayed. |
| * |
| * @param {Number} x - The offset into the labels where the bar |
| * should be removed from. |
| */ |
| clearXBar(x) { |
| this._chart.options.annotation.annotations = |
| this._chart.options.annotation.annotations.filter(ann => { |
| return ann.id != 'xbar'; |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Sets the banding over ranges of labels. |
| * |
| * @param {Array} bands - A list of [x1, x2] offsets |
| * into labels. |
| * |
| * @example |
| * |
| * let bands = [ |
| * [0.0, 0.1], |
| * [0.5, 1.2], |
| * ]; |
| * plot.setBanding(bands); |
| */ |
| setBanding(bands) { |
| this._chart.options.annotation.annotations = []; |
| bands.forEach((band, i) => { |
| this._chart.options.annotation.annotations.push({ |
| id: `band-${i}`, |
| type: 'box', |
| mode: 'vertical', |
| xScaleID: 'x-axis-0', |
| xMin: this._chart.data.labels[band[0]], |
| xMax: this._chart.data.labels[band[1]], |
| backgroundColor: 'rgba(0, 0, 0, 0.1)', |
| drawTime: 'beforeDatasetsDraw', |
| }); |
| }); |
| this._chart.update(); |
| } |
| |
| /** |
| * Resets the zoom to default. |
| */ |
| resetAxes() { |
| if (this._chart) { |
| this._chart.resetZoom(); |
| } |
| } |
| |
| static get observedAttributes() { |
| return ['width', 'height']; |
| } |
| |
| /** @prop width {string} Mirrors the width attribute. */ |
| get width() { return this.getAttribute('width'); } |
| set width(val) { this.setAttribute('width', val); } |
| |
| /** @prop height {string} Mirrors the height attribute. */ |
| get height() { return this.getAttribute('height'); } |
| set height(val) { this.setAttribute('height', val); } |
| |
| attributeChangedCallback(name, oldValue, newValue) { |
| if (oldValue !== newValue) { |
| this._render(); |
| } |
| } |
| |
| }); |
| |