diff --git a/LineUtils.d.ts b/LineUtils.d.ts index 065bf2a..d69d404 100644 --- a/LineUtils.d.ts +++ b/LineUtils.d.ts @@ -19,7 +19,11 @@ export interface LabelConfig { size?: number; position?: LabelPosition; lineOffsetFactor?: number; - rotated?: boolean; + /** + * Default: false. If set to true, the label will be rotated with the same angle as the axis against a horizontal line + * If a number is provided, the label will be rotated by that angle, in radians + */ + rotated?: boolean|number; font?: FontConfig; /** * See strokeStyle: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle @@ -46,6 +50,8 @@ export interface TicksConfig { style?: string; font?: FontConfig; grid?: boolean; + /** rotate labels by some angle, in radians */ + labelRotation?: number; } export declare type TicksValuesConfig = TicksConfig & ({ valueRange: [number, number]; @@ -77,8 +83,8 @@ export interface SingleAxisConfig { keepOffsetContent?: boolean; } export interface AxesConfig { - x: boolean | Partial; - y: boolean | Partial; + x: boolean|(Partial&{position?: "bottom"|"top"}); + y: boolean|(Partial&{position?: "left"|"right"}); font?: FontConfig; /** * See strokeStyle: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle diff --git a/index.d.ts b/index.d.ts index b088735..fde8d2b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -111,8 +111,9 @@ export declare class Canvas2dZoom extends HTMLElement { resetZoomPan(): void; /** * Delete all content written previously + * @param options->keepCustomDrawn: if set to true, then everything added via drawCustom(), incl. axes drawn via LineUtils, will be retained */ - clear(): void; + clear(options?: {keepCustomDrawn?: boolean}): void; /** * Zoom the canvas * @param scale a number > 0; to zoom in, provide a value > 1 (2 is a good example), to zoom out provide a value < 1 (e.g. 0.5) diff --git a/package.json b/package.json index 272f5c2..9fbe152 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "canvas2d-zoom", - "version": "0.1.4", + "version": "0.1.6", "description": "A webcomponent that adds zoom and pan behaviour to a canvas element", "type": "module", "exports": { diff --git a/src/ContextProxy.ts b/src/ContextProxy.ts index 00fd7b3..39c3d04 100644 --- a/src/ContextProxy.ts +++ b/src/ContextProxy.ts @@ -66,7 +66,7 @@ export class ContextProxy implements ProxyHandler { } resetZoom() { - if (this.#pipe.length === 0) + if (this.#pipe.length === 0) // FIXME problematic, since this may be relevant to custom drawn elements return; const ctx: CanvasRenderingContext2D = this.#pipe[0].target; ctx.restore(); @@ -88,7 +88,7 @@ export class ContextProxy implements ProxyHandler { ctx.restore(); } - clear() { + clear(options?: {dispatch?: boolean}) { if (this.#pipe.length === 0) return; const ctx: CanvasRenderingContext2D = this.#pipe[0].target; @@ -96,6 +96,8 @@ export class ContextProxy implements ProxyHandler { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(-this.#clearXBorder, -this.#clearYBorder, ctx.canvas.width + this.#clearXBorder, ctx.canvas.height + this.#clearYBorder); this._eventDispatcher.dispatchEvent(new Event("clear")); + if (options?.dispatch) + this._dispatch(ctx, ctx.getTransform(), ctx.getTransform(), true, true); // ? } private _dispatch(ctx: CanvasRenderingContext2D, oldTransform: DOMMatrix, newTransform: DOMMatrix, zoom: boolean, pan: boolean) { diff --git a/src/LineUtils.ts b/src/LineUtils.ts index 26dd329..2f2cf1f 100644 --- a/src/LineUtils.ts +++ b/src/LineUtils.ts @@ -51,8 +51,8 @@ class AxesMgmt { readonly #x: boolean; readonly #y: boolean; - readonly #xConfig: SingleAxisConfig; - readonly #yConfig: SingleAxisConfig; + readonly #xConfig: SingleAxisConfig&{position?: "bottom"|"top"}; + readonly #yConfig: SingleAxisConfig&{position?: "left"|"right"}; constructor(_config: Partial) { this.#x = _config?.x !== false; @@ -116,26 +116,35 @@ class AxesMgmt { let yOffset: number = this.#xConfig.offsetBoundary; if (yOffset < 0) yOffset = Math.min(Math.round(height/10), 50); + const isXTop: boolean = this.#x && this.#xConfig.position === "top"; + const isYRight: boolean = this.#y && this.#yConfig.position === "right"; if (this.#x) { const c: SingleAxisConfig = this.#xConfig; if (!c.keepOffsetContent) { // draw a white rectangle ctx.fillStyle = "white"; - ctx.fillRect(0, height-yOffset, width, height); + if (isXTop) + ctx.fillRect(0, 0, width, yOffset); + else + ctx.fillRect(0, height-yOffset, width, height); } + const yPosition: number = isXTop ? yOffset : height - yOffset; // @ts-ignore - LineUtils._drawLine(ctx, c.offsetDrawn ? 0 : xOffset, height - yOffset, width, height - yOffset, c.lineConfig); + LineUtils._drawLine(ctx, isYRight || c.offsetDrawn ? 0 : xOffset, yPosition, isYRight && !c.offsetDrawn ? width - xOffset : width, yPosition, c.lineConfig); // @ts-ignore if (c.ticks && (c.ticks.values || c.ticks.valueRange)) { const config: TicksConfig = c.ticks as TicksConfig; - const tickEndX: number = c.lineConfig?.arrows?.end ? width - xOffset : width; - const ticks: Array = AxesMgmt._getTickPositions(xOffset, height - yOffset, tickEndX, height - yOffset, config, state.newTransformation); + const tickEndX: number = isYRight || c.lineConfig?.arrows?.end ? width - xOffset : width; + const tickPosition: LabelPosition = isXTop ? LabelPosition.LEFT : LabelPosition.RIGHT; + const tickOffset: number = isXTop ? -config.length : config.length; + const ticks: Array = AxesMgmt._getTickPositions(isYRight ? 0 : xOffset, yPosition, tickEndX, yPosition, config, state.newTransformation); + const gridExtensionY: number = isXTop ? height : 0; for (const tick of ticks) { const lineConfig: Partial = {}; if (tick.label) { lineConfig.label = { // TODO set stroke color, width etc text: tick.label, - position: LabelPosition.LEFT + position: tickPosition }; if (config.font) lineConfig.label.font = config.font; @@ -143,12 +152,14 @@ class AxesMgmt { lineConfig.label.style = config.style; lineConfig.style = config.style; } + if (config.labelRotation) + lineConfig.label.rotated = config.labelRotation; } // @ts-ignore - LineUtils._drawLine(ctx, tick.x, tick.y + config.length, tick.x, tick.y, lineConfig); + LineUtils._drawLine(ctx, tick.x, tick.y + tickOffset, tick.x, tick.y, lineConfig); if (config.grid) { // @ts-ignore - LineUtils._drawLine(ctx, tick.x, tick.y, tick.x, 0, AxesMgmt._GRID_CONFIG); + LineUtils._drawLine(ctx, tick.x, tick.y, tick.x, gridExtensionY, AxesMgmt._GRID_CONFIG); } } } @@ -158,21 +169,28 @@ class AxesMgmt { if (!c.keepOffsetContent) { // draw a white rectangle ctx.fillStyle = "white"; - ctx.fillRect(0, 0, xOffset, height); + if (isYRight) + ctx.fillRect(width-xOffset, 0, width, height); + else + ctx.fillRect(0, 0, xOffset, height); } + const xPosition: number = isYRight ? width - xOffset : xOffset; // @ts-ignore - LineUtils._drawLine(ctx, xOffset, c.offsetDrawn ? height : height - yOffset, xOffset, 0, c.lineConfig); + LineUtils._drawLine(ctx, xPosition, isXTop || c.offsetDrawn ? height : height - yOffset, xPosition, isXTop && !c.offsetDrawn ? yOffset : 0, c.lineConfig); // @ts-ignore if (c.ticks && (c.ticks.values || c.ticks.valueRange)) { const config: TicksConfig = c.ticks as TicksConfig; - const tickEndY: number = c.lineConfig?.arrows?.end ? yOffset : 0; - const ticks: Array = AxesMgmt._getTickPositions(xOffset, height - yOffset, xOffset, tickEndY, config, state.newTransformation); + const tickEndY: number = isXTop || c.lineConfig?.arrows?.end ? yOffset : 0; + const ticks: Array = AxesMgmt._getTickPositions(xPosition, isXTop ? height : height - yOffset, xPosition, tickEndY, config, state.newTransformation); + const tickPosition: LabelPosition = isYRight ? LabelPosition.RIGHT : LabelPosition.LEFT; + const tickOffset: number = isYRight ? config.length : -config.length; + const gridExtensionX: number = isYRight ? 0 : width; for (const tick of ticks) { const lineConfig: Partial = {}; if (tick.label) { lineConfig.label = { // TODO set stroke color, width etc text: tick.label, - position: LabelPosition.LEFT + position: tickPosition }; if (config.font) lineConfig.label.font = config.font; @@ -180,12 +198,14 @@ class AxesMgmt { lineConfig.label.style = config.style; lineConfig.style = config.style; } + if (config.labelRotation) + lineConfig.label.rotated = config.labelRotation; } // @ts-ignore - LineUtils._drawLine(ctx, tick.x-config.length, tick.y, tick.x, tick.y, lineConfig); + LineUtils._drawLine(ctx, tick.x + tickOffset, tick.y, tick.x, tick.y, lineConfig); if (config.grid) { // @ts-ignore - LineUtils._drawLine(ctx, tick.x, tick.y, width, tick.y, AxesMgmt._GRID_CONFIG); + LineUtils._drawLine(ctx, tick.x, tick.y, gridExtensionX, tick.y, AxesMgmt._GRID_CONFIG); } } } @@ -300,7 +320,11 @@ export interface LabelConfig { size?: number; // TODO specify position?: LabelPosition; lineOffsetFactor?: number; - rotated?: boolean; // default: false + /** + * Default: false. If set to true, the label will be rotated with the same angle as the axis against a horizontal line + * If a number is provided, the label will be rotated by that angle, in radians + */ + rotated?: boolean|number; font?: FontConfig; /** * See strokeStyle: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle @@ -332,6 +356,8 @@ export interface TicksConfig { font?: FontConfig; // TODO option to define grid style grid?: boolean; + /** rotate labels by some angle, in radians */ + labelRotation?: number; } // FIXME even for string values a zooming effect may be desirable! @@ -373,8 +399,8 @@ export interface SingleAxisConfig { } export interface AxesConfig { - x: boolean|Partial; - y: boolean|Partial; + x: boolean|(Partial&{position?: "bottom"|"top"}); + y: boolean|(Partial&{position?: "left"|"right"}); font?: FontConfig; /** * See strokeStyle: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle @@ -489,11 +515,14 @@ export class LineUtils { ctx.fillStyle = config.label.style || "black"; if (config.label.font) ctx.font = LineUtils._toFontString(config.label.font); - const position: [number, number] = LineUtils._getLabelPosition(length, angle, config.label, ctx); - const rotated: boolean = config.label.rotated; + const rotated: boolean|number = config.label.rotated; + const angle2 = isFinite(rotated as number) ? angle - (rotated as number) : angle; + const position: [number, number] = LineUtils._getLabelPosition(length, angle2, config.label, ctx); ctx.translate(position[0], position[1]); - if (!rotated && angle !== 0) + if ((rotated === false || rotated === undefined) && angle !== 0) ctx.rotate(-angle); + else if (isFinite(rotated as number)) + ctx.rotate(-angle2); ctx.fillText(config.label.text, 0, 0); } ctx.restore(); diff --git a/src/canvas2d-zoom.ts b/src/canvas2d-zoom.ts index bfba443..a04c55c 100644 --- a/src/canvas2d-zoom.ts +++ b/src/canvas2d-zoom.ts @@ -438,11 +438,14 @@ export class Canvas2dZoom extends HTMLElement { /** * Delete all content written previously + * @param options->keepCustomDrawn: if set to true, then everything added via drawCustom(), incl. axes drawn via LineUtils, will be retained */ - clear() { - Array.from(this.#zoomListeners.values()).forEach(listener => this.removeEventListener("zoom", listener)); - this.#zoomListeners.clear(); - this.#proxy.clear(); + clear(options?: {keepCustomDrawn?: boolean}) { + if (!options?.keepCustomDrawn) { + Array.from(this.#zoomListeners.values()).forEach(listener => this.removeEventListener("zoom", listener)); + this.#zoomListeners.clear(); + } + this.#proxy.clear({dispatch: options.keepCustomDrawn}); // ensure redrawing of custom elements } /**