diff --git a/.gitignore b/.gitignore index e32f3a1..8aa082a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/*.png \ No newline at end of file +/*.png +test diff --git a/config/CanvasInstance.ts b/config/CanvasInstance.ts index add703f..8cd8511 100644 --- a/config/CanvasInstance.ts +++ b/config/CanvasInstance.ts @@ -1,18 +1,14 @@ -import Canvas, { - EmulatedCanvas2D, - CanvasRenderingContext2D, -} from '../deps.ts'; - +import Canvas, { type EmulatedCanvas2D, type CanvasRenderingContext2D } from "../deps.ts"; export class CanvasInstance { // CANVAS API INSTANCES - private static _canvas: EmulatedCanvas2D | null = null; + private static _canvas: EmulatedCanvas2D | null = null; private static _ctx: CanvasRenderingContext2D | null = null; // CANVAS CONFIGURATION - private static _WIDTH: number = 200; // Default 200 - private static _HEIGHT: number = 200; // Default 200 - + private static _WIDTH: number = 200; // Default 200 + private static _HEIGHT: number = 200; // Default 200 + private constructor() {} /** @@ -26,18 +22,18 @@ export class CanvasInstance { * Instantiates a Canvas Instance * @param width Canvas Width * @param height Canvas Height - * @returns + * @returns */ - public static init(width: number, height: number) { + public static init(width: number, height: number): CanvasRenderingContext2D | null { if (this._canvas === null) { - this._canvas = Canvas.MakeCanvas(width, height); - this._ctx = this._canvas.getContext('2d'); - this._WIDTH = width; - this._HEIGHT = height; + this._canvas = Canvas.MakeCanvas(width, height); + this._ctx = this._canvas.getContext("2d"); + this._WIDTH = width; + this._HEIGHT = height; } return this._ctx; } - + public static get WIDTH(): number { return this._WIDTH; } @@ -54,4 +50,4 @@ export class CanvasInstance { if (this._ctx === null) this.init(200, 200); return this._ctx as CanvasRenderingContext2D; } -}; \ No newline at end of file +} diff --git a/config/index.ts b/config/index.ts index ee99a3d..b6c1efd 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1 +1 @@ -export * from './CanvasInstance.ts'; \ No newline at end of file +export * from "./CanvasInstance.ts"; diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..8a87cba --- /dev/null +++ b/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@josefabio/denochart", + "version": "1.2.3", + "exports": "./mod.ts", + "imports": { + "deno-canvas": "jsr:@gfx/canvas-wasm@^0.4.2" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..b810a4a --- /dev/null +++ b/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "4", + "specifiers": { + "jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2", + "jsr:@std/encoding@1.0.5": "1.0.5" + }, + "jsr": { + "@gfx/canvas-wasm@0.4.2": { + "integrity": "d653be3bd12cb2fa9bbe5d1b1f041a81b91d80b68502761204aaf60e4592532a", + "dependencies": [ + "jsr:@std/encoding" + ] + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + } + }, + "workspace": { + "dependencies": [ + "jsr:@gfx/canvas-wasm@~0.4.2" + ] + } +} diff --git a/deps.ts b/deps.ts index f8e3dcf..bff08d8 100644 --- a/deps.ts +++ b/deps.ts @@ -1,4 +1,4 @@ -import Canvas from 'https://deno.land/x/canvas@v1.2.2/mod.ts'; +import Canvas from "deno-canvas"; export default Canvas; -export * from 'https://deno.land/x/canvas@v1.2.2/mod.ts'; \ No newline at end of file +export * from "deno-canvas"; diff --git a/graph/GraphInstance.ts b/graph/GraphInstance.ts index 054fb16..2e4929e 100644 --- a/graph/GraphInstance.ts +++ b/graph/GraphInstance.ts @@ -1,60 +1,63 @@ -import { CanvasInstance } from '../config/index.ts'; -import { normalize, max, background, drawTextWithFont } from '../utils/index.ts'; - +import { CanvasInstance } from "../config/index.ts"; +import { normalize, max, background, drawTextWithFont } from "../utils/index.ts"; interface RGBA { - r: number, - g: number, - b: number, - a: number, + r: number; + g: number; + b: number; + a: number; } export interface GraphOptions { // Canvas Width & Height - width: number, - height: number, - backgroundColor: RGBA, - + width: number; + height: number; + backgroundColor: RGBA; + // Graph Text - titleText: string, - xAxisText: string, - yAxisText: string, + titleText: string; + xAxisText: string; + yAxisText: string; - // Y-Max Normalized Value - yMax: number, + /** Y-Max Normalized Value */ + yMax: number; // Graph Outer-Padding - yPadding: number, - xPadding: number, - + yPadding: number; + xPadding: number; + // Bar Config - bar_width: number, - bar_spacing: number, + bar_width: number; + bar_spacing: number; // Graph Segements - graphSegments_Y: number, - graphSegments_X: number, + graphSegments_Y: number; + graphSegments_X: number; // Text Color - titleColor: string, - xTextColor: string, - yTextColor: string, - + titleColor: string; + xTextColor: string; + yTextColor: string; + /** Defaults to 10pt */ + yTextSize: number; + // Segmentation Color - xSegmentColor: string, - ySegmentColor: string, + xSegmentColor: string; + ySegmentColor: string; // Graph Values - graphValuePrecision: number, // Rounds floating-point values to given nth number + graphValuePrecision: number; // Rounds floating-point values to given nth number // DEBUG: Options - verbose: boolean, // Enable/Disable Logging + verbose: boolean; // Enable/Disable Logging } export interface BarEntry { - val: number, - label?: string, - color: string, + val: number; + label?: string; + color: string; + /** If null, it won't show it. If undefined, it will show the value. */ + valueLabel?: string | null; } export class Graph { @@ -69,7 +72,6 @@ export class Graph { // Graph Configuration private _options: GraphOptions; - /** * Constructs Graph Configuration * @param config (Optional) Graph Configuration @@ -80,41 +82,42 @@ export class Graph { // Configure Graph this._options = { - height: config && config.height || 480, - width: config && config.width || 720, - backgroundColor: config && config.backgroundColor || { - r: 50, - g: 50, - b: 50, + height: (config && config.height) ?? 480, + width: (config && config.width) ?? 720, + backgroundColor: (config && config.backgroundColor) ?? { + r: 50, + g: 50, + b: 50, a: 0.5, }, - - titleText: config && config.titleText || 'title', - xAxisText: config && config.xAxisText || 'X-Axis', - yAxisText: config && config.yAxisText || 'Y-Axis', - yMax: config && config.yMax || -1, + titleText: (config && config.titleText) ?? "title", + xAxisText: (config && config.xAxisText) ?? "X-Axis", + yAxisText: (config && config.yAxisText) ?? "Y-Axis", - yPadding: config && config.yPadding || 0, - xPadding: config && config.xPadding || 0, - - bar_width: config && config.bar_width || 10, - bar_spacing: config && config.bar_spacing || 5, + yMax: (config && config.yMax) ?? -1, - graphSegments_X: config && config.graphSegments_X || 10, - graphSegments_Y: config && config.graphSegments_Y || graphSegments_Y, + yPadding: (config && config.yPadding) ?? 0, + xPadding: (config && config.xPadding) ?? 0, - titleColor: config && config.titleColor || 'rgb(255,255,255)', - xTextColor: config && config.xTextColor || 'rgb(255,255,255)', - yTextColor: config && config.yTextColor || 'rgb(255,255,255)', - - xSegmentColor: config && config.xSegmentColor || 'rgb(255,255,255)', - ySegmentColor: config && config.ySegmentColor || 'rgb(255,255,255)', + bar_width: (config && config.bar_width) ?? 10, + bar_spacing: (config && config.bar_spacing) ?? 5, - graphValuePrecision: config && config.graphValuePrecision || 2, + graphSegments_X: (config && config.graphSegments_X) ?? 10, + graphSegments_Y: (config && config.graphSegments_Y) ?? graphSegments_Y, - verbose: config && config.verbose || false, - } + titleColor: (config && config.titleColor) ?? "rgb(255,255,255)", + xTextColor: (config && config.xTextColor) ?? "rgb(255,255,255)", + yTextColor: (config && config.yTextColor) ?? "rgb(255,255,255)", + yTextSize: (config && config.yTextSize) ?? 10, + + xSegmentColor: (config && config.xSegmentColor) ?? "rgb(255,255,255)", + ySegmentColor: (config && config.ySegmentColor) ?? "rgb(255,255,255)", + + graphValuePrecision: (config && config.graphValuePrecision) ?? 2, + + verbose: (config && config.verbose) ?? false, + }; // Apply Graph Padding this._x_padding += this._options.xPadding; @@ -130,8 +133,7 @@ export class Graph { console.log(`Initialized Canvas Instance W[${this._options.width}] H[${this._options.height}]`); } - if (this._options.verbose) - console.log('Create Graph with Options:', this._options); + if (this._options.verbose) console.log("Create Graph with Options:", this._options); } /** @@ -141,16 +143,16 @@ export class Graph { public add(entry: BarEntry): void { this._entries.push(entry); } - + /** * Internal: Draws Graph outline */ private _draw_graph_outline() { const { ctx, HEIGHT, WIDTH } = CanvasInstance; - + // CTX Config ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; + ctx.imageSmoothingQuality = "high"; // Drawing Style Config ctx.save(); @@ -159,12 +161,14 @@ export class Graph { ctx.lineWidth = 1.5; // Graph Title - drawTextWithFont(this._options.titleText, (WIDTH / 2) - 30, this._y_offset, '12pt Cochin'); + if (this._options.titleText) + drawTextWithFont(this._options.titleText, WIDTH / 2 - 30, this._y_offset, "12pt Cochin"); // X-Axis ctx.strokeStyle = this._options.xTextColor; ctx.fillStyle = this._options.xTextColor; - ctx.fillText(this._options.xAxisText, (WIDTH / 2) - 10, (HEIGHT - this._y_offset / 2) + 10); + if (this._options.xAxisText) + ctx.fillText(this._options.xAxisText, WIDTH / 2 - 10, HEIGHT - this._y_offset / 2 + 10); ctx.beginPath(); ctx.lineTo(this._x_padding, HEIGHT - this._y_padding); @@ -175,7 +179,7 @@ export class Graph { // Y-Axis ctx.strokeStyle = this._options.yTextColor; ctx.fillStyle = this._options.yTextColor; - ctx.fillText(this._options.yAxisText, (this._x_offset / 2) - 8, (HEIGHT / 2)); + if (this._options.yAxisText) ctx.fillText(this._options.yAxisText, this._x_offset / 2 - 8, HEIGHT / 2); ctx.beginPath(); ctx.lineTo(this._x_padding, HEIGHT - this._y_padding); @@ -193,29 +197,22 @@ export class Graph { const { graphSegments_X, graphSegments_Y, graphValuePrecision } = this._options; // Evaluate yMax if selected - this._options.yMax = this._options.yMax === -1 - ? max(this._entries.map(elt => elt.val)) - : this._options.yMax; - - if (this._options.verbose) - console.log('yMax Evaluated to: ', this._options.yMax); + if (this._options.yMax === -1) { + this._options.yMax = max(this._entries.map((elt) => elt.val)); + if (this._options.verbose) console.log("yMax Evaluated to: ", this._options.yMax); + } // Y-Axis Segmentations - const Y_SEGMENTS = HEIGHT / graphSegments_Y; - const maxY_segment = Y_SEGMENTS * (graphSegments_Y - 2); - + const y_height_px = HEIGHT / graphSegments_Y; + const maxY_segment = y_height_px * (graphSegments_Y - 2); + for (let i = 0; i < graphSegments_Y - 1; i++) { - const Y = (HEIGHT - (Y_SEGMENTS * i)) - this._y_padding; - - ctx.fillStyle = '#ECF0F1'; - ctx.strokeStyle = '#ECF0F1'; + const Y = HEIGHT - y_height_px * i - this._y_padding; + + ctx.fillStyle = "#ECF0F1"; + ctx.strokeStyle = "#ECF0F1"; ctx.beginPath(); - ctx.arc( - this._x_padding, - Y, - 2, - 0, Math.PI * 2, - ); + ctx.arc(this._x_padding, Y, 2, 0, Math.PI * 2); ctx.closePath(); ctx.fill(); @@ -223,13 +220,11 @@ export class Graph { ctx.fillStyle = this._options.ySegmentColor; ctx.strokeStyle = this._options.ySegmentColor; - const normalized = normalize((HEIGHT - this._y_padding - Y), 0, maxY_segment); - const yVal = normalized * this._options.yMax; - ctx.fillText( - (!(yVal % 1) ? yVal : yVal.toFixed(graphValuePrecision)).toString(), - this._x_padding - ((yVal % 1 === 0) ? 25 :35), - Y, - ); + const yVal: number = normalize((HEIGHT - this._y_padding - Y) * this._options.yMax, 0, maxY_segment); + const yValStr: string = yVal % 1 === 0 ? yVal.toString() : yVal.toFixed(graphValuePrecision); + const offset: number = 10 + yValStr.length * 5.5; + + ctx.fillText(yValStr, this._x_padding - offset, Y + 3); } // X-Axis Segmentations @@ -237,37 +232,36 @@ export class Graph { for (let i = 0; i < graphSegments_X - 1; i++) { const X = this._x_padding + X_SEGMENTS * i; - ctx.fillStyle = '#ECF0F1'; - ctx.strokeStyle = '#ECF0F1'; + ctx.fillStyle = "#ECF0F1"; + ctx.strokeStyle = "#ECF0F1"; ctx.beginPath(); - ctx.arc( - X, - HEIGHT - this._y_padding, - 2, - 0, Math.PI * 2, - ); + ctx.arc(X, HEIGHT - this._y_padding, 2, 0, Math.PI * 2); ctx.closePath(); ctx.fill(); // X Value Text (Index) const entry = this._entries[i]; - const entryFloatVal = entry && entry.label && Number.parseFloat(entry.label) || NaN; ctx.fillStyle = this._options.xSegmentColor; ctx.strokeStyle = this._options.xSegmentColor; - ctx.fillText( - (entry && entry.label - && ( // Set fixed floating point decimal IF parsable float - isNaN(entryFloatVal) - ? entry.label - : !(entryFloatVal % 1) // Only set fixed precision for Floating-point values + ctx.font = this._options.yTextSize + "px Cochin"; + + if (entry?.label !== "") { + const entryFloatVal = (entry && entry.label !== undefined && Number.parseFloat(entry.label)) || NaN; + ctx.fillText( + ( + (entry && + entry.label !== undefined && // Set fixed floating point decimal IF parsable float + (isNaN(entryFloatVal) + ? entry.label + : !(entryFloatVal % 1) // Only set fixed precision for Floating-point values ? entryFloatVal - : entryFloatVal.toFixed(graphValuePrecision) - ) - || i - ).toString(), - X, - HEIGHT - this._y_offset + 12 - ); + : entryFloatVal.toFixed(graphValuePrecision))) || + i + ).toString(), + X, + HEIGHT - this._y_offset + 12 + ); + } } } @@ -276,57 +270,48 @@ export class Graph { */ private _draw_bars() { const { ctx, HEIGHT, WIDTH } = CanvasInstance; - const { bar_width, graphSegments_X, graphSegments_Y, graphValuePrecision } = this._options; - + const { bar_width, graphSegments_X, graphSegments_Y, graphValuePrecision, yMax } = this._options; + ctx.save(); // Find max bar value to map based on yMax const Y_SEGMENTS = HEIGHT / graphSegments_Y; const maxY_segment = Y_SEGMENTS * (graphSegments_Y - 2); - const maxBarValue = max(this._entries.map(elt => elt.val)); - + // Space out each Entry to given Segments const X_SEGMENTS = (WIDTH - this._x_padding) / graphSegments_X; for (let i = 0; i < graphSegments_X; i++) { // Constrain to # of entries - if (i >= this._entries.length) - break; - + if (i >= this._entries.length) break; + const entry = this._entries[i]; - const { val: y, color } = entry; - + const { val: y, color, valueLabel } = entry; + ctx.fillStyle = color; ctx.beginPath(); - + // Max X & Y Points const X = this._x_padding + X_SEGMENTS * i; - const Y = normalize(y, 0, maxBarValue) * maxY_segment; - - ctx.fillRect( - X, - HEIGHT - this._y_offset - Y, - bar_width, - Y, - ); + const Y = normalize(y, 0, yMax) * maxY_segment; + + ctx.fillRect(X, HEIGHT - this._y_offset - Y, bar_width, Y); ctx.closePath(); // Y Value text (Value) - const val = y % 1 !== 0 - ? y.toFixed(graphValuePrecision) - : y; - ctx.fillText(val.toString(), X + 5, HEIGHT - this._y_offset - Y - 10); + const val = y % 1 !== 0 ? y.toFixed(graphValuePrecision) : y; + if (valueLabel !== null) ctx.fillText(valueLabel ?? val.toString(), X + 5, HEIGHT - this._y_offset - Y - 10); } - + ctx.restore(); } - + /** * Draws graph with entries to Canvas Context */ public draw() { const { r, g, b, a } = this._options.backgroundColor; background(r, g, b, a); - + this._draw_bars(); this._draw_graph_outline(); this._draw_graph_segments(); @@ -341,16 +326,14 @@ export class Graph { const imageBuffer = canvas.toBuffer(); Deno.writeFileSync(imagePath, imageBuffer); - if (this._options.verbose) - console.log(`Graph save to '${imagePath}'`); + if (this._options.verbose) console.log(`Graph save to '${imagePath}'`); } /** * @returns Image buffer */ - public toBuffer() { + public toBuffer(): Uint8Array { const { canvas } = CanvasInstance; return canvas.toBuffer(); } - -}; \ No newline at end of file +} diff --git a/graph/index.ts b/graph/index.ts index 327db0f..de09b2e 100644 --- a/graph/index.ts +++ b/graph/index.ts @@ -1 +1 @@ -export * from './GraphInstance.ts'; \ No newline at end of file +export * from "./GraphInstance.ts"; diff --git a/mod.ts b/mod.ts index c17a3a8..a0cd0da 100644 --- a/mod.ts +++ b/mod.ts @@ -1,12 +1,12 @@ // DEFAULT EXPORT -import { Graph } from './graph/index.ts'; +import { Graph } from "./graph/index.ts"; export default Graph; // GRAPH INTERFACE EXPORT -export * from './graph/index.ts'; +export * from "./graph/index.ts"; // CONFIGURATION EXPORT (Canvas API & Context...) -export * from './config/index.ts'; +export * from "./config/index.ts"; // UTILITIES EXPORT (max, normalize, Vector2D...) -export * from './utils/index.ts'; \ No newline at end of file +export * from "./utils/index.ts"; diff --git a/utils/Helpers.ts b/utils/Helpers.ts index a3205aa..223fd70 100644 --- a/utils/Helpers.ts +++ b/utils/Helpers.ts @@ -1,5 +1,4 @@ -import { CanvasInstance } from '../config/index.ts'; - +import { CanvasInstance } from "../config/index.ts"; /** * Sets the background color @@ -14,7 +13,6 @@ export function background(r: number, g: number, b: number, a = 1.0) { ctx.fillRect(0, 0, WIDTH, HEIGHT); } - /** * Helper function that draws given text with a font, restoring the font * back to the original @@ -47,5 +45,5 @@ export function normalize(val: number, min: number, max: number): number { * @param array Array of number to find max number of */ export function max(array: number[]): number { - return array.reduce((acc, val) => val > acc ? val : acc, 0); -} \ No newline at end of file + return array.reduce((acc, val) => (val > acc ? val : acc), 0); +} diff --git a/utils/index.ts b/utils/index.ts index ac6211d..32ecfc3 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1 +1 @@ -export * from './Helpers.ts'; \ No newline at end of file +export * from "./Helpers.ts";