UNPKG

rms-sparklines

Version:
329 lines (295 loc) 15.8 kB
// Copyright2018 Rodrigo Silveira // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import { SparklineLineInterface } from './sparkline-line-interface'; import * as mathjs from 'mathjs'; export class SparklineLine { attributes: SparklineLineInterface; // Local attributes measurementsArray: number[]; coordinatesWorld: any []; coordinatesViewport: any = []; coordinatesCanvas: any = []; coordinatesTips: any = []; canvasScreenOffsetX: number; canvasScreenOffsetY: number; decorationPointsArray: any = []; canvasTip: any; // static offset(el: HTMLElement) { // const rect: any = el.getBoundingClientRect(), // scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, // scrollTop = window.pageYOffset || document.documentElement.scrollTop; // return { top: rect.top + scrollTop, left: rect.left + scrollLeft }; // } constructor() {} static handleMouseOut() { const tooltip = document.getElementById('rms-sparkline-inline-tooltip'); if (tooltip) { // console.log(`SparklineLine::handleMouseMove deleting tooltip`); tooltip.parentElement.removeChild(tooltip); } } draw(attributes: SparklineLineInterface) { // Save attributes // console.log(`SparklineLine::draw ctx: ` + attributes.ctx); // console.log(`SparklineLine::draw className: ` + attributes.className); // console.log(`SparklineLine::draw decorationPoints: ` + JSON.stringify(attributes.decorationPoints)); // console.log(`SparklineLine::draw dotRadius: ` + attributes.dotRadius); // console.log(`SparklineLine::draw height: ` + attributes.height); // console.log(`SparklineLine::draw lineColor: ` + attributes.lineColor); // console.log(`SparklineLine::draw linePoints: ` + JSON.stringify(attributes.linePoints)); // console.log(`SparklineLine::draw lineWidth: ` + attributes.lineWidth); // console.log(`SparklineLine::draw shadeColor: ` + attributes.shadeColor); // console.log(`SparklineLine::draw toolTip: ` + attributes.toolTip); // console.log(`SparklineLine::draw width: ` + attributes.width); this.attributes = attributes; const ctx: CanvasRenderingContext2D = this.attributes.ctx; /** * Build coordinatesWorld * * Note thatwe add the 0 point the start, and the zero point at the end. * This is required for the proper drawing of the shade if necessary. These point * are removed before the actual line is drawn */ this.measurementsArray = this.attributes.linePoints.slice(0); this.coordinatesWorld = []; for (let i = 0; i < this.measurementsArray.length; i++) { this.coordinatesWorld.push([i, this.measurementsArray[i], 1]); // console.log(`coordinatesWorld(` + i +`): ` + JSON.stringify(this.coordinatesWorld[i])); } // console.log(`coordinatesWorld: ` + JSON.stringify(this.coordinatesWorld)); /** * Build coordinatesViewPort * The sparkline is drawn on the canvas viewport, a subset of the canvas * drawing area, with padding between the two to make room for drawing * decoration shapes, when necessary. */ this.coordinatesViewport = []; const sX = (this.attributes.width - this.attributes.dotRadius * 2) / this.measurementsArray.length; const sY = (this.attributes.height - this.attributes.dotRadius * 2 ) / Math.max(...this.measurementsArray); const worldToViewport = [ [sX, 0, 0], [0, sY, 0], [0, 0, 1]] ; for (let i = 0; i < this.measurementsArray.length; i++) { // From world to viewport const temp1 = mathjs.multiply(this.coordinatesWorld[i], worldToViewport); // console.log(`coordinatesViewport temp1(` + i +`): ` + JSON.stringify(temp1)); this.coordinatesViewport.push(temp1); } // console.log(`coordinatesViewport: ` + JSON.stringify(this.coordinatesViewport)); /** * Build coordinatesCanvas * The world coordinates origin is at the bottom left, whereas the canvas and * canvasViewport coordinates origin is at the top left, requiring the * translation from bottom left to top left origin. * * Since canvas drawing always uses the canvas coordinates and the * canvasViewport amounts to a padding around the canvas, a translation is * required to move the canvasViewport coordinates: */ this.coordinatesCanvas = []; const dX = this.attributes.dotRadius; const myHeight = this.attributes.height - this.attributes.dotRadius; const bottomLeftToTopLeft = [ [1, 0, 0], [0, 1, 0], [dX, 0, 1] ]; for (let i = 0; i < this.measurementsArray.length; i++) { // Tricky calculation ... we want the delta to move the current coordinate // const dY = myHeight - 2 * this.coordinatesViewport[i][1]; // console.log(`coordinatesCanvas dY(` + i +`): ` + myHeight - 2 * this.coordinatesViewport[i][1]); bottomLeftToTopLeft[2][1] = myHeight - 2 * this.coordinatesViewport[i][1]; // console.log(`coordinatesCanvas bottomLeftToTopLeft(` + i +`): ` + JSON.stringify(bottomLeftToTopLeft)); this.coordinatesCanvas.push(mathjs.multiply(this.coordinatesViewport[i], bottomLeftToTopLeft)); } // console.log(`coordinatesViewport: ` + JSON.stringify(this.coordinatesViewport)); /** * Now we draw the sparkline, in the following order * - Shaded ared * - line * - decorations */ ctx.clearRect(0, 0, this.attributes.width, this.attributes.height); // console.log(`shadeColor: ` + this.attributes.shadeColor); if (this.attributes.shadeColor) { // Fill up the area ctx.beginPath(); ctx.fillStyle = this.attributes.shadeColor; ctx.moveTo(this.coordinatesCanvas[0][0], this.attributes.height); for (let i = 0; i < this.measurementsArray.length; i++) { ctx.lineTo(this.coordinatesCanvas[i][0], this.coordinatesCanvas[i][1]); // console.log(`drawing(` + i +`): ` + JSON.stringify(this.coordinatesCanvas[i])); } ctx.lineTo(this.coordinatesCanvas[this.measurementsArray.length - 1][0], this.attributes.height); ctx.fill(); } // Draw the path, on top of the shade area // https://www.w3schools.com/graphics/canvas_coordinates.asp ctx.beginPath(); ctx.lineWidth = this.attributes.lineWidth; ctx.strokeStyle = this.attributes.lineColor; ctx.moveTo(this.coordinatesCanvas[0][0], this.coordinatesCanvas[0][1]); for (let i = 1; i < this.measurementsArray.length; i++) { ctx.lineTo(this.coordinatesCanvas[i][0], this.coordinatesCanvas[i][1]); // console.log(`drawing(` + i +`): ` + JSON.stringify(this.coordinatesCanvas[i])); } ctx.stroke(); /** * Draw decorations */ this.decorationPointsArray = this.attributes.decorationPoints.slice(0); if (this.attributes.dotRadius > 0 && this.decorationPointsArray.length > 0) { // console.log('decorationPoints = ' + JSON.stringify(this.attributes.decorationPoints)); for (let i = 0; i < this.decorationPointsArray.length; i++) { // todo: a hack to solve a problem when running inside vaadin-grid if (this.decorationPointsArray[i]['index'] > this.measurementsArray.length - 1) { // do nothing } else { ctx.beginPath(); const index = this.decorationPointsArray[i]['index']; ctx.arc(this.coordinatesCanvas[index][0], this.coordinatesCanvas[index][1], this.attributes.dotRadius, 0, Math.PI * 2); ctx.fillStyle = this.decorationPointsArray[i]['color']; ctx.fill(); } } } /** * Add tooltip support. * Build coordinateScreen */ // const rect: any = this.attributes.canvasEl.getBoundingClientRect(); // console.log(`SparklineLine::canvasEl rect.left: ` + rect.left + `, rect.top: ` + rect.top); // // this.canvasScreenOffsetX = rect.left + window.pageXOffset || document.documentElement.scrollLeft; // this.canvasScreenOffsetY = rect.top + window.pageYOffset || document.documentElement.scrollTop; // console.log(`canvasScreenOffsetX: ` + this.canvasScreenOffsetX); // console.log(`canvasScreenOffsetY: ` + this.canvasScreenOffsetY); this.coordinatesTips = []; for (let i = 0; i < this.measurementsArray.length; i++) { this.coordinatesTips.push({ x: this.coordinatesCanvas[i][0], y: this.coordinatesCanvas[i][1], r: 5, rXr: 25, color: 'red', tip: this.measurementsArray[i] }); // console.log(`coordinatesTips(` + i +`): ` + JSON.stringify(this.coordinatesTips[i])); } } /** * Sparkline Inline Mouse Move Handler * @param $event * @param canvasEl */ handleMouseMove($event: MouseEvent, canvasEl: HTMLElement) { let body: any; let mySpan: any; const fontDefinition = '12px FUTURA'; let width: number; let height: number; let scrollLeft; let scrollTop; let rect; let mouseX: number; let mouseY: number; let tooltip; // console.log(`\n`); // Get the position of the canvas element relative to the document // https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/ rect = canvasEl.getBoundingClientRect(); scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; scrollTop = window.pageYOffset || document.documentElement.scrollTop; rect = { left: rect.left + scrollLeft, top: rect.top + scrollTop }; // Get the mouse coordinates relative to the canvas mouseX = $event.clientX - rect.left; mouseY = $event.clientY - rect.top; // Debug code // let msg: string; // msg = ''; // msg += `rect.left: ` + rect.left.toFixed(2); // msg += `, rect.top: ` + rect.top.toFixed(2); // msg += `, rect.width: ` + rect.width.toFixed(2); // msg += `, rect.height: ` + rect.height.toFixed(2); // console.log(msg); // msg = ''; // msg += `$event.clientX: ` + $event.clientX.toFixed(2); // msg += `, $event.clientY: ` + $event.clientY.toFixed(2); // console.log(msg); // msg = ''; // msg += `dot.x: ` + this.coordinatesTips[0].x.toFixed(2); // msg += `, dot.y: ` + this.coordinatesTips[0].y.toFixed(2); // console.log(msg); // msg = ''; // msg += `mouseX: ` + mouseX.toFixed(2); // msg += `, mouseY: ` + mouseY.toFixed(2); // console.log(msg); for (let i = 0; i < this.measurementsArray.length; i++) { const dot = this.coordinatesTips[i]; const dx = mouseX - dot.x; const dy = mouseY - dot.y; // console.log(`SparklineLine::handleMouseMove dx: ` + Math.floor(dx) + `, dy: ` + Math.floor(dy) + `, dot.rXr: ` + dot.rXr); if (dx * dx + dy * dy < dot.rXr) { // console.log(`SparklineLine::handleMouseMove found a match`); // A trick to find the width of the canvas to host the tooltip mySpan = document.createElement('span'); mySpan.id = 'mySpanId'; mySpan.style.font = fontDefinition; mySpan.style.textAlign = 'center'; mySpan.innerHTML = '' + this.measurementsArray[i]; body = document.getElementsByTagName('body')[0]; body.appendChild(mySpan); mySpan = document.getElementById('mySpanId'); width = mySpan.getBoundingClientRect().width + 2; height = mySpan.getBoundingClientRect().height + 2; mySpan.parentElement.removeChild(mySpan); // console.log(`mySpan: ` + width); // Remove tooltip if already there tooltip = document.getElementById('rms-sparkline-inline-tooltip'); if (tooltip) { // console.log(`SparklineLine::handleMouseMove deleting tooltip`); tooltip.parentElement.removeChild(tooltip); } // Add a new tooltip this.canvasTip = document.createElement('CANVAS'); this.canvasTip.id = 'rms-sparkline-inline-tooltip'; this.canvasTip.width = width; this.canvasTip.height = height; this.canvasTip.style.position = 'absolute'; this.canvasTip.style.left = ($event.clientX + 5) + 'px'; this.canvasTip.style.top = ($event.clientY + 7) + 'px'; this.canvasTip.style.border = '1px solid'; this.canvasTip.style.border = '1px solid'; this.canvasTip.style.zIndex = '20'; this.canvasTip.style.textAlign = 'center'; const ctx = (this.canvasTip as HTMLCanvasElement).getContext('2d'); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); ctx.fill(); ctx.fillStyle = 'red'; ctx.font = fontDefinition; ctx.fillText('' + this.measurementsArray[i], 1, height - 2); body = document.getElementsByTagName('body')[0]; body.appendChild(this.canvasTip); break; // the first match gets the tooltip } else { // Remove tooltip if still there tooltip = document.getElementById('rms-sparkline-inline-tooltip'); if (tooltip) { // console.log(`SparklineLine::handleMouseMove deleting tooltip`); tooltip.parentElement.removeChild(tooltip); } } } } }