UNPKG

pxt-common-packages

Version:
289 lines (243 loc) 10.5 kB
namespace display { class Chart { // Variables used for data configuration. private font: image.Font; private times: number[]; private values: number[]; // grid private gridRows: number; private gridCols: number; private gridWidth: number; private gridHeight: number; // chart rendering private chartWidth: number; private chartHeight: number; private scaleXMin: number; private scaleXMax: number; private scaleYMin: number; private scaleYMax: number; private axisPaddingX: number; private axisPaddingY: number; // estimated best number of entries private maxEntries: number; public backgroundColor: number; public axisColor: number; public lineColor: number; constructor() { this.font = image.font5; this.backgroundColor = 0; this.axisColor = 1; this.lineColor = 1; this.axisPaddingX = 22; this.axisPaddingY = this.font.charHeight + 4; this.gridRows = 2; this.gridCols = 2; // computed on the fly this.times = []; this.values = []; this.chartWidth = screen.width - this.axisPaddingX; this.chartHeight = screen.height - this.axisPaddingY; this.maxEntries = (this.chartWidth - 2) / 2; } public addPoint(value: number) { this.times.push(control.millis() / 1000); this.values.push(value); if (this.times.length > this.maxEntries * 2) { this.times = this.times.slice(this.times.length - this.maxEntries - 1, this.times.length - 1); this.values = this.values.slice(this.values.length - this.maxEntries - 1, this.values.length - 1); } } public render() { if (this.times.length < 2) return; this.calculateScale(); screen.fill(this.backgroundColor); this.drawAxes(); this.drawChartGrid(); this.drawGraphPoints(); } private calculateScale() { this.scaleYMax = this.values[0]; this.scaleYMin = this.values[0]; for (let j = 0, len2 = this.values.length; j < len2; j++) { if (this.scaleYMax < this.values[j]) { this.scaleYMax = this.values[j]; } if (this.scaleYMin > this.values[j]) { this.scaleYMin = this.values[j]; } } // avoid empty interval if (this.scaleXMin === this.scaleXMax) this.scaleXMax = this.scaleXMin + 1; // TODO if (this.scaleYMin === this.scaleYMax) this.scaleYMax = this.scaleYMin + 1; // TODO // update axis to look better let rx = generateSteps(0, this.times[this.times.length - 1] - this.times[0], 4); this.scaleXMin = rx[0]; this.scaleXMax = rx[1]; this.gridCols = rx[2]; let ry = generateSteps(this.scaleYMin, this.scaleYMax, 6); this.scaleYMin = ry[0]; this.scaleYMax = ry[1]; this.gridRows = ry[2]; // update y-axis width let xl = 0; const yRange = this.scaleYMax - this.scaleYMin; const yUnit = yRange / this.gridRows; for (let i = 0; i <= this.gridRows; ++i) xl = Math.max(roundWithPrecision(this.scaleYMax - (i * yUnit), 2).toString().length, xl); this.axisPaddingX = xl * this.font.charWidth + 4; this.chartWidth = screen.width - this.axisPaddingX; this.maxEntries = (this.chartWidth - 2) / 2; // Calculate the grid for background / scale. this.gridWidth = this.chartWidth / this.gridCols; // This is the width of the grid cells (background and axes). this.gridHeight = this.chartHeight / this.gridRows; // This is the height of the grid cells (background axes). } private drawChartGrid() { const c = this.axisColor; const tipLength = 3; screen.drawRect(0, 0, this.chartWidth, this.chartHeight, c); for (let i = 0; i < this.gridCols; i++) { screen.drawLine(i * this.gridWidth, this.chartHeight, i * this.gridWidth, this.chartHeight - tipLength, c); screen.drawLine(i * this.gridWidth, 0, i * this.gridWidth, tipLength, c); } for (let i = 0; i < this.gridRows; i++) { screen.drawLine(0, i * this.gridHeight, tipLength, i * this.gridHeight, c); screen.drawLine(this.chartWidth, i * this.gridHeight, this.chartWidth - tipLength, i * this.gridHeight, c); } } private drawAxes() { const c = this.axisColor; const xRange = this.scaleXMax - this.scaleXMin; const yRange = this.scaleYMax - this.scaleYMin; const xUnit = xRange / this.gridCols; const yUnit = yRange / this.gridRows; // Draw the y-axes labels. let text = ''; for (let i = 0; i <= this.gridRows; i++) { text = roundWithPrecision(this.scaleYMax - (i * yUnit), 2).toString(); let y = i * this.gridHeight - this.font.charHeight / 2; if (i == this.gridRows) y -= this.font.charHeight / 2; else if (i == 0) y += this.font.charHeight / 2; screen.print(text, this.chartWidth + 5, y, c, this.font); } // Draw the x-axis labels for (let i = 0; i <= this.gridCols; i++) { text = roundWithPrecision((i * xUnit), 2).toString(); let x = i * this.gridWidth; if (i > 0) x -= this.font.charWidth / 2; // move one char to the left screen.print(text, x, this.chartHeight + (this.axisPaddingY - 2 - this.font.charHeight), c, this.font); } } private drawGraphPoints() { const c = this.lineColor; // Determine the scaling factor based on the min / max ranges. const xRange = this.scaleXMax - this.scaleXMin; const yRange = this.scaleYMax - this.scaleYMin; const xFactor = this.chartWidth / xRange; let yFactor = this.chartHeight / yRange; let nextX = 0; let nextY = (this.values[0] - this.scaleYMin) * yFactor; const startX = nextX; const startY = nextY; for (let i = 1; i < this.values.length; i++) { let prevX = nextX; let prevY = nextY; nextX = (this.times[i] - this.times[0]) * xFactor; nextY = (this.values[i] - this.scaleYMin) * yFactor; screen.drawLine(prevX, prevY, nextX, nextY, c); } } } // helpers function log10(x: number): number { return Math.log(x) / Math.log(10); } function roundWithPrecision(x: number, digits: number): number { if (digits <= 0) return Math.round(x); let d = Math.pow(10, digits); return Math.round(x * d) / d; } function generateSteps(start: number, end: number, numberOfTicks: number): number[] { let bases = [1, 5, 2, 3]; // Tick bases selection let currentBase: number; let n: number; let intervalSize: number, upperBound: number, lowerBound: number; let nIntervals: number, nMaxIntervals: number; let the_intervalsize = 0.1; let exponentYmax = Math.floor(Math.max(log10(Math.abs(start)), log10(Math.abs(end)))); let mantissaYmax = end / Math.pow(10.0, exponentYmax); // now check if numbers can be cleaned... // make it pretty let significative_numbers = Math.min(3, Math.abs(exponentYmax) + 1); let expo = Math.pow(10.0, significative_numbers); let start_norm = Math.abs(start) * expo; let end_norm = Math.abs(end) * expo; let mant_norm = Math.abs(mantissaYmax) * expo; // trunc ends let ip_start = Math.floor(start_norm * Math.sign(start)); let ip_end = Math.ceil(end_norm * Math.sign(end)); start = ip_start; end = ip_end; mantissaYmax = Math.ceil(mant_norm); nMaxIntervals = 0; for (let k = 0; k < bases.length; ++k) { // Loop initialisation currentBase = bases[k]; n = 4; // This value only allows results smaller than about 1000 = 10^n do // Tick vector length reduction { --n; intervalSize = currentBase * Math.pow(10.0, exponentYmax - n); upperBound = Math.ceil(mantissaYmax * Math.pow(10.0, n) / currentBase) * intervalSize; nIntervals = Math.ceil((upperBound - start) / intervalSize); lowerBound = upperBound - nIntervals * intervalSize; } while (nIntervals > numberOfTicks); if (nIntervals > nMaxIntervals) { nMaxIntervals = nIntervals; ip_start = ip_start = lowerBound; ip_end = upperBound; the_intervalsize = intervalSize; } } // trunc ends if (start < 0) start = Math.floor(ip_start) / expo; else start = Math.ceil(ip_start) / expo; if (end < 0) end = Math.floor(ip_end) / expo; else end = Math.ceil(ip_end) / expo; return [start, end, nMaxIntervals]; } let chart: Chart; /** * Adds a new point to the trend chart and renders it to the screen. */ //% group="Charts" //% blockId=graphadd block="graph %value" //% blockGap=8 export function graph(value: number) { if (!chart) chart = new Chart(); chart.addPoint(value); chart.render(); } /** * Clears the trend chart and the screen */ //% group="Charts" //% blockid=graphclear block="graph clear" export function graphClear() { chart = undefined; screen.fill(0); } }