/// 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(); } } }