pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
289 lines (243 loc) • 10.5 kB
text/typescript
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);
}
}