341 строка
13 KiB
TypeScript
341 строка
13 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
module TDev.RT.Charts {
|
|
export class Point {
|
|
constructor (public x: number, public y: number) { }
|
|
}
|
|
|
|
export class CanvasChart {
|
|
public lineColor = "#f00";
|
|
public gridColor = "#ccc";
|
|
public backgroundColor :string = undefined;
|
|
public axesColor = "#fff";
|
|
public gridLineWidth = 1;
|
|
public graphLineWidth = 2;
|
|
public axesFontSize = 6;
|
|
public gridRows = 11;
|
|
public gridCols = 11;
|
|
public area = true;
|
|
|
|
// Variables used for data configuration.
|
|
private points: Point[];
|
|
|
|
// Variables used for canvas / drawing.
|
|
private canvas : HTMLCanvasElement;
|
|
private context: CanvasRenderingContext2D;
|
|
|
|
// Variables used for grid creation.
|
|
private gridWidth: number;
|
|
private gridHeight: number;
|
|
|
|
// Variables that control the rendered area.
|
|
private chartWidth: number;
|
|
private chartHeight: number;
|
|
private scaleXMin: number;
|
|
private scaleXMax: number;
|
|
private scaleYMin: number;
|
|
private scaleYMax: number;
|
|
|
|
// Variables that control styling.
|
|
private axesPaddingX: number;
|
|
private axesPaddingY: number;
|
|
|
|
constructor() {
|
|
this.axesPaddingX = 20;
|
|
this.axesPaddingY = 30;
|
|
}
|
|
|
|
public drawChart(canvas: HTMLCanvasElement, points: TDev.RT.Charts.Point[]) {
|
|
this.initialize(canvas, points);
|
|
if (this.points.length < 2) return;
|
|
// Sort the points so our line doesn't cross.
|
|
this.points.sort(function (left, right) {
|
|
if (left.x > right.x) {
|
|
return 1;
|
|
}
|
|
if (left.x < right.x) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// Determine the scale for drawing axes / points.
|
|
this.calculateScale();
|
|
this.drawAxes();
|
|
this.drawChartGrid();
|
|
this.drawGraphPoints();
|
|
}
|
|
|
|
private initialize(canvas: HTMLCanvasElement, points: Point[]) {
|
|
this.canvas = canvas;
|
|
this.context = this.canvas.getContext("2d");
|
|
this.points = points;
|
|
|
|
// Calculate the area that the graph/chart will be drawn in.
|
|
this.chartWidth = canvas.width - this.axesPaddingY;
|
|
this.chartHeight = canvas.height - this.axesPaddingX;
|
|
this.context.save();
|
|
this.context.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
private drawChartGrid() {
|
|
if (this.backgroundColor) {
|
|
this.context.save();
|
|
this.context.fillStyle = this.backgroundColor;
|
|
this.context.fillRect(0, 0, this.chartWidth, this.chartHeight);
|
|
this.context.restore();
|
|
}
|
|
|
|
this.context.save();
|
|
this.context.strokeStyle = this.gridColor;
|
|
this.context.lineWidth = this.gridLineWidth;
|
|
this.context.strokeRect(0, 0, this.chartWidth, this.chartHeight);
|
|
|
|
var tipLength = 5;
|
|
|
|
for (var i = 0; i < this.gridCols; i++) {
|
|
this.context.beginPath();
|
|
this.context.moveTo(i * this.gridWidth, this.chartHeight);
|
|
this.context.lineTo(i * this.gridWidth, this.chartHeight - tipLength);
|
|
this.context.stroke();
|
|
|
|
this.context.beginPath();
|
|
this.context.moveTo(i * this.gridWidth, 0);
|
|
this.context.lineTo(i * this.gridWidth, tipLength);
|
|
this.context.stroke();
|
|
}
|
|
for (var i = 0; i < this.gridRows; i++) {
|
|
this.context.beginPath();
|
|
this.context.moveTo(0, i * this.gridHeight);
|
|
this.context.lineTo(tipLength, i * this.gridHeight);
|
|
this.context.stroke();
|
|
|
|
this.context.beginPath();
|
|
this.context.moveTo(this.chartWidth, i * this.gridHeight);
|
|
this.context.lineTo(this.chartWidth - tipLength, i * this.gridHeight);
|
|
this.context.stroke();
|
|
}
|
|
this.context.restore();
|
|
}
|
|
|
|
////drawAxes
|
|
// Draws the axes based on how the chart is configured
|
|
// Parameters : none
|
|
// Returns : none
|
|
//
|
|
// Notes: This could have a better handling for determining how to
|
|
// label the axes, for example, it could determine the scales and
|
|
// so forth for the font size and positioning.
|
|
private drawAxes() {
|
|
this.context.save();
|
|
var xRange = this.scaleXMax - this.scaleXMin;
|
|
var yRange = this.scaleYMax - this.scaleYMin;
|
|
|
|
var xUnit = xRange / this.gridCols;
|
|
var yUnit = yRange / this.gridRows;
|
|
|
|
this.context.fillStyle = this.axesColor;
|
|
this.context.font = this.axesFontSize + "pt Arial";
|
|
|
|
// Draw the y-axes labels.
|
|
var text = '';
|
|
for (var i = 0; i <= this.gridRows; i++) {
|
|
text = Math_.round_with_precision(this.scaleYMax - (i * yUnit), 2).toString();
|
|
var y = i * this.gridHeight + this.axesFontSize / 2;
|
|
if (i === this.gridRows)
|
|
y -= this.axesFontSize / 2;
|
|
else if (i === 0)
|
|
y += this.axesFontSize / 2;
|
|
this.context.fillText(text, this.chartWidth + 5, y);
|
|
}
|
|
|
|
// Draw the x-axis labels
|
|
for (i = 0; i <= this.gridCols; i++) {
|
|
text = Math_.round_with_precision(this.scaleXMin + (i * xUnit), 2).toString();
|
|
this.context.fillText(text, i * this.gridWidth, this.chartHeight + (this.axesPaddingX - this.axesFontSize));
|
|
}
|
|
this.context.restore();
|
|
}
|
|
|
|
////calculateScale
|
|
// Determines what the axes should be for graphing
|
|
//
|
|
// Parameters:
|
|
// points - Array of points with x and y values
|
|
//
|
|
// Returns: none
|
|
private calculateScale() {
|
|
|
|
this.scaleXMin = this.points[0].x;
|
|
this.scaleXMax = this.points[0].x;
|
|
this.scaleYMax = this.points[0].y;
|
|
this.scaleYMin = this.points[0].y;
|
|
for (var j = 0, len2 = this.points.length; j < len2; j++) {
|
|
if (this.scaleXMax < this.points[j].x) {
|
|
this.scaleXMax = this.points[j].x;
|
|
}
|
|
if (this.scaleYMax < this.points[j].y) {
|
|
this.scaleYMax = this.points[j].y;
|
|
}
|
|
if (this.scaleXMin > this.points[j].x) {
|
|
this.scaleXMin = this.points[j].x;
|
|
}
|
|
if (this.scaleYMin > this.points[j].y) {
|
|
this.scaleYMin = this.points[j].y;
|
|
}
|
|
}
|
|
|
|
// update axis to look better
|
|
var rx = CanvasChart.generateSteps(this.scaleXMin, this.scaleXMax, this.gridCols);
|
|
this.scaleXMin = rx[0];
|
|
this.scaleXMax = rx[1];
|
|
this.gridCols = rx[2];
|
|
var ry = CanvasChart.generateSteps(this.scaleYMin, this.scaleYMax, this.gridRows);
|
|
this.scaleYMin = ry[0];
|
|
this.scaleYMax = ry[1];
|
|
this.gridRows = ry[2];
|
|
|
|
// avoid empty interval
|
|
if (this.scaleXMin === this.scaleXMax) {
|
|
this.scaleXMin = 0.5;
|
|
this.scaleXMax = 0.5;
|
|
}
|
|
if (this.scaleYMin === this.scaleYMax) {
|
|
this.scaleYMin = 0.5;
|
|
this.scaleYMax = 0.5;
|
|
}
|
|
|
|
// 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).
|
|
}
|
|
|
|
static generateSteps(start: number, end: number, numberOfTicks: number) : number[] {
|
|
var bases = [1, 5, 2, 3]; // Tick bases selection
|
|
var currentBase: number;
|
|
var n: number;
|
|
var intervalSize: number, upperBound: number, lowerBound: number;
|
|
var nIntervals: number, nMaxIntervals: number;
|
|
var the_intervalsize = 0.1;
|
|
|
|
var exponentYmax =
|
|
Math.floor(Math.max(Math_.log10(Math.abs(start)), Math_.log10(Math.abs(end))));
|
|
var mantissaYmax = end / Math.pow(10.0, exponentYmax);
|
|
|
|
// now check if numbers can be cleaned...
|
|
// make it pretty
|
|
var significative_numbers = Math.min(3, Math.abs(exponentYmax) + 1);
|
|
|
|
var expo = Math.pow(10.0, significative_numbers);
|
|
var start_norm = Math.abs(start) * expo;
|
|
var end_norm = Math.abs(end) * expo;
|
|
var mant_norm = Math.abs(mantissaYmax) * expo;
|
|
|
|
// trunc ends
|
|
var ip_start, ip_end;
|
|
var start = ip_start = Math.floor(start_norm * Math_.sign(start));
|
|
var end = ip_end = Math.ceil(end_norm * Math_.sign(end));
|
|
|
|
mantissaYmax = Math.ceil(mant_norm);
|
|
|
|
nMaxIntervals = 0;
|
|
for (var 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];
|
|
}
|
|
|
|
////graphPoints
|
|
// Draws the points on a chart.
|
|
//
|
|
// Parameters:
|
|
// points - An array of points to draw.
|
|
//
|
|
// Returns: none
|
|
private drawGraphPoints() {
|
|
this.context.save();
|
|
|
|
// Determine the scaling factor based on the min / max ranges.
|
|
var xRange = this.scaleXMax - this.scaleXMin;
|
|
var yRange = this.scaleYMax - this.scaleYMin;
|
|
|
|
var xFactor = this.chartWidth / xRange;
|
|
var yFactor = this.chartHeight / yRange;
|
|
|
|
var draw = (close: boolean) => {
|
|
var nextX = (this.points[0].x - this.scaleXMin) * xFactor;
|
|
var nextY = (this.points[0].y - this.scaleYMin) * yFactor;
|
|
var startX = nextX;
|
|
var startY = nextY;
|
|
this.context.moveTo(nextX, this.chartHeight - nextY);
|
|
for (var i = 1, len = this.points.length; i < len; i++) {
|
|
nextX = (this.points[i].x - this.scaleXMin) * xFactor,
|
|
nextY = (this.points[i].y - this.scaleYMin) * yFactor;
|
|
this.context.lineTo(nextX, (this.chartHeight - nextY));
|
|
}
|
|
if (close) {
|
|
this.context.lineTo(nextX, this.chartHeight);
|
|
this.context.lineTo(startX, this.chartHeight);
|
|
this.context.closePath();
|
|
}
|
|
}
|
|
|
|
// If we use a 'miterlimit' of .5 the elbow width, the elbow covers the line.
|
|
this.context.miterLimit = this.graphLineWidth / 4;
|
|
this.context.strokeStyle = this.lineColor;
|
|
this.context.lineWidth = this.graphLineWidth;
|
|
|
|
if (this.area) {
|
|
this.context.fillStyle = this.lineColor;
|
|
this.context.globalAlpha = 0.3;
|
|
this.context.beginPath();
|
|
draw(true);
|
|
this.context.fill();
|
|
this.context.globalAlpha = 1;
|
|
}
|
|
|
|
this.context.beginPath();
|
|
draw(false);
|
|
this.context.stroke();
|
|
|
|
this.context.restore();
|
|
}
|
|
}
|
|
} |