Загрузка данных
import { getThemeStore } from '@src/theme';
import { SettingField, SettingsTab, SettingsValues } from '@src/types';
export interface DiapsonStyle {
borderColor: string;
fillColor: string;
}
export interface DiapsonTextStyle {
labelTextColor: string;
labelBackgroundColor: string;
fontSize: number;
isBold: boolean;
isItalic: boolean;
}
export type DiapsonSettings = SettingsValues & DiapsonStyle & DiapsonTextStyle;
export function createDefaultSettings(): DiapsonSettings {
const { colors } = getThemeStore();
return {
borderColor: colors.diapsonStrokeFill,
fillColor: colors.diapsonAreaFill,
labelTextColor: colors.axisRangeTooltipText,
labelBackgroundColor: colors.axisRangeTooltipFill,
fontSize: 10,
isBold: false,
isItalic: false,
};
}
export function getDiapsonSettingsTabs(settings: DiapsonSettings): SettingsTab[] {
const styleFields: SettingField[] = [
{
key: 'borderColor',
label: 'Цвет границы',
type: 'color',
defaultValue: settings.borderColor,
},
{
key: 'fillColor',
label: 'Цвет фона диапазона',
type: 'color',
defaultValue: settings.fillColor,
},
];
const textFields: SettingField[] = [
{
key: 'labelTextColor',
label: 'Цвет текста',
type: 'color',
defaultValue: settings.labelTextColor,
},
{
key: 'labelBackgroundColor',
label: 'Цвет фона',
type: 'color',
defaultValue: settings.labelBackgroundColor,
},
{
key: 'fontSize',
label: 'Размер текста',
type: 'number',
defaultValue: settings.fontSize,
min: 8,
max: 24,
},
{
key: 'isBold',
label: 'Жирный текст',
type: 'boolean',
defaultValue: settings.isBold,
},
{
key: 'isItalic',
label: 'Курсив',
type: 'boolean',
defaultValue: settings.isItalic,
},
];
return [
{
key: 'style',
label: 'Стиль',
fields: styleFields,
},
{
key: 'text',
label: 'Текст',
fields: textFields,
},
];
}
import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';
import { getThemeStore } from '@src/theme';
import { Diapson, DiapsonRenderData } from './diapson';
import type { DiapsonTextStyle } from './settings';
const UI = {
lineWidth: 1,
handleRadius: 6,
handleBorderWidth: 2,
labelPadding: 4,
labelRadius: 2,
labelBottomOffset: 8,
arrowSize: 7,
lineHeightMultiplier: 1.2,
};
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 pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
const lineWidth = UI.lineWidth * pixelRatio;
const arrowSize = UI.arrowSize * pixelRatio;
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);
}
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 pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
const radius = UI.handleRadius * pixelRatio;
const lineWidth = UI.handleBorderWidth * pixelRatio;
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,
textStyle: DiapsonTextStyle,
): void {
const pixelRatio = Math.max(horizontalPixelRatio, verticalPixelRatio);
const fontSize = textStyle.fontSize * pixelRatio;
const padding = UI.labelPadding * pixelRatio;
const lineHeight = Math.round(textStyle.fontSize * UI.lineHeightMultiplier) * verticalPixelRatio;
const radius = UI.labelRadius * pixelRatio;
const bottomOffset = UI.labelBottomOffset * verticalPixelRatio;
const paneLeft = data.left * horizontalPixelRatio;
const paneRight = data.right * horizontalPixelRatio;
const paneBottom = data.bottom * verticalPixelRatio;
context.save();
context.font = getLabelFont(textStyle, fontSize);
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 = textStyle.labelBackgroundColor;
fillRoundedRect(context, boxLeft, boxTop, labelWidth, labelHeight, radius);
context.fillStyle = textStyle.labelTextColor;
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 getLabelFont(style: DiapsonTextStyle, fontSize: number): string {
const italic = style.isItalic ? 'italic ' : '';
const bold = style.isBold ? '700 ' : '';
return `${italic}${bold}${fontSize}px Inter, sans-serif`;
}
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));
}