Skip to content

Hit test on price scale #1941

@vishnuc

Description

@vishnuc

Hi , i want to show a small button on bottom of price scale.. and when clicked i wanna call chart.fitcontent method, what is the procedure ?

Image

also does hittest method works for price pane view ? i want to detect hittest and display cursor pointer to the button

Image , i created button but not able to click it

import type {
	ISeriesPrimitive,
	IPrimitivePaneView,
	IPrimitivePaneRenderer,
	ISeriesPrimitiveAxisView,
	PrimitivePaneViewZOrder,
	Coordinate,
	MouseEventParams
} from 'lightweight-charts';
import { PluginBase } from '../../tvplugin/plugin-base';
import { getUid } from '@/lib/helper/uid';

export class AutoButtonPlugin extends PluginBase implements ISeriesPrimitive {
	myChart: any;
	mySeries: any;
	uid: string;
	_priceAxisViews: AutoScaleAxisView[];
	_priceAxisPaneViews: AutoScaleButtonPaneView[];
	_clickHandler: (param: MouseEventParams) => void;

	constructor(chart: any, series: any, paneIndex?: number, grid?: string) {
		super();
		this.uid = getUid('autobutton');
		this.myChart = chart;
		this.mySeries = series;
		this._priceAxisViews = [new AutoScaleAxisView(this)];
		this._priceAxisPaneViews = [new AutoScaleButtonPaneView(this)];
		
		this._clickHandler = (param: MouseEventParams) => this._handleClick(param);
		this.myChart.subscribeClick(this._clickHandler);
	}

	_handleClick(param: MouseEventParams) {
		if (!param.point) return;
		
		console.log('Click detected at:', param.point.x, param.point.y);
		
		const paneView = this._priceAxisPaneViews[0];
		if (paneView) {
			console.log('Button rect:', paneView._buttonRect);
			
			// For price axis pane views, coordinates need conversion
			const chartSize = this.myChart?.paneSize();
			const priceScaleWidth = this.myChart?.priceScale()?.width() || 60;
			
			if (chartSize) {
				// Convert chart coordinates to price axis coordinates
				const priceAxisX = param.point.x - (chartSize.width - priceScaleWidth);
				console.log('Price axis X:', priceAxisX, 'Chart width:', chartSize.width, 'Price scale width:', priceScaleWidth);
				
				const hitResult = paneView.hitTest(priceAxisX, param.point.y);
				console.log('Hit result:', hitResult);
				
				if (hitResult) {
					console.log('Button hit detected!');
					this.handleAutoScale();
				}
			}
		}
	}

	remove() {
		if (this.myChart && this._clickHandler) {
			this.myChart.unsubscribeClick(this._clickHandler);
		}
		super.remove();
	}

	handleAutoScale() {
		alert('Auto scale button clicked!');
		if (this.mySeries && this.myChart) {
			this.mySeries.priceScale().applyOptions({
				autoScale: true
			});
			this.myChart.timeScale().fitContent();
		}
	}

	getCurrentTheme(): 'light' | 'dark' {
		const chartOptions = this.myChart?.options();
		const bgColor = chartOptions?.layout?.background?.color;
		
		if (bgColor === '#FFFFFF' || bgColor === 'white') {
			return 'light';
		}
		return 'dark';
	}

	paneViews() {
		return [];
	}

	priceAxisViews() {
		return [];
	}

	priceAxisPaneViews() {
		return this._priceAxisPaneViews;
	}

	updateAllViews() {
		this._priceAxisViews.forEach((view) => view.update());
		this._priceAxisPaneViews.forEach((view) => view.update());
	}
}

class AutoScaleAxisView implements ISeriesPrimitiveAxisView {
	_source: AutoButtonPlugin;
	_pos: Coordinate | null = null;
	_isClicked: boolean = false;

	constructor(source: AutoButtonPlugin) {
		this._source = source;
	}

	update(): void {
		const chartSize = this._source.myChart?.paneSize();
		if (chartSize) {
			this._pos = (chartSize.height - 8) as Coordinate;
		}
	}

	coordinate(): Coordinate {
		return this._pos ?? -1 as Coordinate;
	}

	text(): string {
		return 'A';
	}

	textColor(): string {
		const theme = this._source.getCurrentTheme();
		if (this._isClicked) {
			return '#FFFFFF';
		}
		return theme === 'light' ? '#191919' : '#E2E8F0';
	}

	backColor(): string {
		const theme = this._source.getCurrentTheme();
		
		if (this._isClicked) {
			return theme === 'light' ? '#0EA5E9' : '#38BDF8';
		}
		
		return theme === 'light' ? '#F8FAFC' : '#334155';
	}

	visible(): boolean {
		return true;
	}

	tickVisible(): boolean {
		return false;
	}

	handleClick(): void {
		this._isClicked = true;
		this._source.handleAutoScale();
		
		setTimeout(() => {
			this._isClicked = false;
			this._source.requestUpdate();
		}, 150);
	}
}

class AutoScaleButtonPaneView implements IPrimitivePaneView {
	_source: AutoButtonPlugin;
	_buttonRect: { x: number; y: number; width: number; height: number } | null = null;

	constructor(source: AutoButtonPlugin) {
		this._source = source;
	}

	zOrder(): PrimitivePaneViewZOrder {
		return 'top';
	}

	update(): void {
		const chartSize = this._source.myChart?.paneSize();
		const priceScaleWidth = this._source.myChart?.priceScale()?.width() || 60;
		
		if (chartSize) {
			this._buttonRect = {
				x: priceScaleWidth - 25,
				y: chartSize.height - 20,
				width: 20,
				height: 15
			};
		}
	}

	hitTest(x: number, y: number) {
		if (!this._buttonRect) return null;
		
		if (
			x >= this._buttonRect.x &&
			x <= this._buttonRect.x + this._buttonRect.width &&
			y >= this._buttonRect.y &&
			y <= this._buttonRect.y + this._buttonRect.height
		) {
			return {
				cursorStyle: 'pointer',
				externalId: this._source.uid,
			};
		}
		
		return null;
	}

	renderer() {
		return new AutoScaleButtonRenderer(this._buttonRect, this._source);
	}
}

class AutoScaleButtonRenderer implements IPrimitivePaneRenderer {
	_buttonRect: { x: number; y: number; width: number; height: number } | null;
	_source: AutoButtonPlugin;

	constructor(buttonRect: { x: number; y: number; width: number; height: number } | null, source: AutoButtonPlugin) {
		this._buttonRect = buttonRect;
		this._source = source;
	}

	draw(target: any) {
		if (!this._buttonRect) return;

		target.useBitmapCoordinateSpace((scope: any) => {
			const ctx = scope.context;
			const { horizontalPixelRatio, verticalPixelRatio } = scope;
			
			const x = this._buttonRect!.x * horizontalPixelRatio;
			const y = this._buttonRect!.y * verticalPixelRatio;
			const width = this._buttonRect!.width * horizontalPixelRatio;
			const height = this._buttonRect!.height * verticalPixelRatio;
			const radius = 3 * horizontalPixelRatio;

			const theme = this._source.getCurrentTheme();
			const axisView = this._source._priceAxisViews[0];
			
			const bgColor = axisView?.backColor() || (theme === 'light' ? '#F8FAFC' : '#334155');
			const textColor = axisView?.textColor() || (theme === 'light' ? '#191919' : '#E2E8F0');
			const borderColor = theme === 'light' ? '#E2E8F0' : '#475569';

			ctx.fillStyle = bgColor;
			ctx.beginPath();
			ctx.roundRect(x, y, width, height, radius);
			ctx.fill();

			ctx.strokeStyle = borderColor;
			ctx.lineWidth = 1 * horizontalPixelRatio;
			ctx.beginPath();
			ctx.roundRect(x, y, width, height, radius);
			ctx.stroke();

			ctx.fillStyle = textColor;
			ctx.font = `bold ${10 * verticalPixelRatio}px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif`;
			ctx.textAlign = 'center';
			ctx.textBaseline = 'middle';
			ctx.fillText('A', x + width / 2, y + height / 2);
		});
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions