Загрузка данных
import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';
import { getThemeStore } from '@src/theme';
import { Diapson, DiapsonRenderData } from './diapson';
const UI = {
lineWidth: 1,
handleRadius: 6,
handleBorderWidth: 2,
labelPadding: 4,
labelLineHeight: 12,
labelFontSize: 10,
labelRadius: 2,
labelBottomOffset: 8,
arrowSize: 7,
};
export class DiapsonPaneRenderer implements IPrimitivePaneRenderer {
private readonly diapson: Diapson;
constructor(diapson: Diapson) {
this.diapson = diapson;
}
public draw(target: CanvasRenderingTarget2D): void {
const data = this.diapson.getRenderData();
if (!data) {
return;
}
const { colors } = getThemeStore();
target.useBitmapCoordinateSpace(({ context, horizontalPixelRatio, verticalPixelRatio }) => {
const left = data.left * horizontalPixelRatio;
const right = data.right * horizontalPixelRatio;
const top = data.top * verticalPixelRatio;
const bottom = data.bottom * verticalPixelRatio;
const width = Math.max(right - left, 0);
const height = Math.max(bottom - top, 0);
const lineWidth = UI.lineWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);
const arrowSize = UI.arrowSize * Math.max(horizontalPixelRatio, verticalPixelRatio);
context.save();
if (data.showFill) {
context.fillStyle = data.fillColor;
context.fillRect(left, top, width, height);
}
context.strokeStyle = data.borderColor;
context.lineWidth = lineWidth;
if (data.rangeMode === 'date') {
drawVerticalBoundary(context, left, top, bottom);
drawVerticalBoundary(context, right, top, bottom);
drawHorizontalArrow(
context,
left,
right,
(top + bottom) / 2,
arrowSize,
data.startPoint.x <= data.endPoint.x ? 'right' : 'left',
);
} else {
drawHorizontalBoundary(context, top, left, right);
drawHorizontalBoundary(context, bottom, left, right);
drawVerticalArrow(
context,
(left + right) / 2,
top,
bottom,
arrowSize,
data.startPoint.y <= data.endPoint.y ? 'down' : 'up',
);
}
if (data.showHandles) {
drawHandle(
context,
data.startPoint.x * horizontalPixelRatio,
data.startPoint.y * verticalPixelRatio,
horizontalPixelRatio,
verticalPixelRatio,
colors.chartBackground,
colors.chartLineColor,
);
drawHandle(
context,
data.endPoint.x * horizontalPixelRatio,
data.endPoint.y * verticalPixelRatio,
horizontalPixelRatio,
verticalPixelRatio,
colors.chartBackground,
colors.chartLineColor,
);
}
if (data.labelLines.length > 0) {
drawLabel(
context,
data,
horizontalPixelRatio,
verticalPixelRatio,
data.infoBackgroundColor,
data.infoTextColor,
);
}
context.restore();
});
}
}
function drawVerticalBoundary(context: CanvasRenderingContext2D, x: number, top: number, bottom: number): void {
context.beginPath();
context.moveTo(x, top);
context.lineTo(x, bottom);
context.stroke();
}
function drawHorizontalBoundary(context: CanvasRenderingContext2D, y: number, left: number, right: number): void {
context.beginPath();
context.moveTo(left, y);
context.lineTo(right, y);
context.stroke();
}
function drawHorizontalArrow(
context: CanvasRenderingContext2D,
left: number,
right: number,
y: number,
arrowSize: number,
direction: 'left' | 'right',
): void {
context.beginPath();
context.moveTo(left, y);
context.lineTo(right, y);
context.stroke();
if (direction === 'right') {
context.beginPath();
context.moveTo(right, y);
context.lineTo(right - arrowSize, y - arrowSize);
context.moveTo(right, y);
context.lineTo(right - arrowSize, y + arrowSize);
context.stroke();
return;
}
context.beginPath();
context.moveTo(left, y);
context.lineTo(left + arrowSize, y - arrowSize);
context.moveTo(left, y);
context.lineTo(left + arrowSize, y + arrowSize);
context.stroke();
}
function drawVerticalArrow(
context: CanvasRenderingContext2D,
x: number,
top: number,
bottom: number,
arrowSize: number,
direction: 'up' | 'down',
): void {
context.beginPath();
context.moveTo(x, top);
context.lineTo(x, bottom);
context.stroke();
if (direction === 'down') {
context.beginPath();
context.moveTo(x, bottom);
context.lineTo(x - arrowSize, bottom - arrowSize);
context.moveTo(x, bottom);
context.lineTo(x + arrowSize, bottom - arrowSize);
context.stroke();
return;
}
context.beginPath();
context.moveTo(x, top);
context.lineTo(x - arrowSize, top + arrowSize);
context.moveTo(x, top);
context.lineTo(x + arrowSize, top + arrowSize);
context.stroke();
}
function drawHandle(
context: CanvasRenderingContext2D,
x: number,
y: number,
horizontalPixelRatio: number,
verticalPixelRatio: number,
fillStyle: string,
strokeStyle: string,
): void {
const radius = UI.handleRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);
const lineWidth = UI.handleBorderWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);
context.save();
context.fillStyle = fillStyle;
context.strokeStyle = strokeStyle;
context.lineWidth = lineWidth;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
context.stroke();
context.restore();
}
function drawLabel(
context: CanvasRenderingContext2D,
data: DiapsonRenderData,
horizontalPixelRatio: number,
verticalPixelRatio: number,
backgroundColor: string,
textColor: string,
): void {
const fontSize = UI.labelFontSize * Math.max(horizontalPixelRatio, verticalPixelRatio);
const padding = UI.labelPadding * Math.max(horizontalPixelRatio, verticalPixelRatio);
const lineHeight = UI.labelLineHeight * verticalPixelRatio;
const radius = UI.labelRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);
const bottomOffset = UI.labelBottomOffset * verticalPixelRatio;
const paneLeft = data.left * horizontalPixelRatio;
const paneRight = data.right * horizontalPixelRatio;
const paneBottom = data.bottom * verticalPixelRatio;
context.save();
context.font = `${fontSize}px sans-serif`;
const maxTextWidth = data.labelLines.reduce((maxWidth, line) => {
return Math.max(maxWidth, context.measureText(line).width);
}, 0);
const labelWidth = maxTextWidth + padding * 2;
const labelHeight = data.labelLines.length * lineHeight + padding * 2;
const rangeCenterX = (paneLeft + paneRight) / 2;
const maxLeft = Math.max(4, context.canvas.width - labelWidth - 4);
const maxTop = Math.max(4, context.canvas.height - labelHeight - 4);
const boxLeft = clampNumber(rangeCenterX - labelWidth / 2, 4, maxLeft);
const boxTop = clampNumber(paneBottom + bottomOffset, 4, maxTop);
context.fillStyle = backgroundColor;
fillRoundedRect(context, boxLeft, boxTop, labelWidth, labelHeight, radius);
context.fillStyle = textColor;
context.textAlign = 'center';
context.textBaseline = 'middle';
data.labelLines.forEach((line, index) => {
const lineY = boxTop + padding + lineHeight * index + lineHeight / 2;
context.fillText(line, boxLeft + labelWidth / 2, lineY);
});
context.restore();
}
function fillRoundedRect(
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
): void {
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width - radius, y);
context.quadraticCurveTo(x + width, y, x + width, y + radius);
context.lineTo(x + width, y + height - radius);
context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - radius);
context.lineTo(x, y + radius);
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
context.fill();
}
function clampNumber(value: number, min: number, max: number): number {
if (max < min) {
return min;
}
return Math.max(min, Math.min(value, max));
}