Загрузка данных
import { IChartApi, IPrimitivePaneView, PrimitiveHoveredItem, Time, UTCTimestamp } from 'lightweight-charts';
import { Observable } from 'rxjs';
import {
CustomPriceAxisPaneView,
CustomPriceAxisView,
CustomTimeAxisPaneView,
CustomTimeAxisView,
} from '@core/Drawings/axis';
import { SeriesDrawingBase } from '@core/Drawings/common';
import {
getTimeFromXCoordinate,
getXCoordinateFromTime,
getYCoordinateFromPrice,
isNearPoint,
shiftTimeByPixels,
} from '@core/Drawings/helpers';
import { updateViews } from '@core/Drawings/utils';
import { getThemeStore } from '@src/theme';
import { Defaults } from '@src/types/defaults';
import { formatPrice } from '@src/utils';
import { formatDate } from '@src/utils/formatter';
import { RegressionTrendPaneView } from './paneView';
import {
createDefaultSettings,
getRegressionTrendSettingsTabs,
RegressionTrendSettings,
RegressionTrendStyle,
RegressionTrendTextStyle,
} from './settings';
import type { DrawingInteraction, ISeriesDrawing } from '@core/Drawings/common';
import type { AxisLabel, AxisSegment, Point, SeriesApi } from '@core/Drawings/types';
import type { ChartOptionsModel, SettingsTab } from '@src/types';
type RegressionTrendMode =
| 'idle'
| 'drawing'
| 'ready'
| 'dragging-start'
| 'dragging-end'
| 'dragging-body';
type TimeLabelKind = 'start' | 'end';
type PriceLabelKind = 'start' | 'end';
interface RegressionTrendParams {
container: HTMLElement;
interaction: DrawingInteraction;
formatObservable?: Observable<ChartOptionsModel>;
removeSelf?: () => void;
openSettings?: () => void;
}
interface RegressionTrendState {
hidden: boolean;
mode: RegressionTrendMode;
startTime: Time | null;
endTime: Time | null;
settings: RegressionTrendSettings;
}
interface RegressionTrendGeometry {
startPoint: Point;
endPoint: Point;
startPrice: number;
endPrice: number;
left: number;
right: number;
top: number;
bottom: number;
}
interface RegressionResult {
startPrice: number;
endPrice: number;
}
interface SeriesDataItem {
time: Time;
[key: string]: unknown;
}
export interface RegressionTrendRenderData
extends RegressionTrendGeometry,
RegressionTrendStyle,
RegressionTrendTextStyle {
showHandles: boolean;
}
const LINE_HIT_TOLERANCE = 6;
const HANDLE_HIT_TOLERANCE = 8;
const MIN_LINE_SIZE = 4;
export class RegressionTrend extends SeriesDrawingBase<RegressionTrendSettings> implements ISeriesDrawing {
private removeSelf?: () => void;
private openSettings?: () => void;
protected settings: RegressionTrendSettings = createDefaultSettings();
protected mode: RegressionTrendMode = 'idle';
private startTime: Time | null = null;
private endTime: Time | null = null;
private dragPointerId: number | null = null;
private dragStartPoint: Point | null = null;
private dragStateSnapshot: RegressionTrendState | null = null;
private displayFormat: ChartOptionsModel = {
dateFormat: Defaults.dateFormat,
timeFormat: Defaults.timeFormat,
showTime: Defaults.showTime,
};
private readonly paneView: RegressionTrendPaneView;
private readonly timeAxisPaneView: CustomTimeAxisPaneView;
private readonly priceAxisPaneView: CustomPriceAxisPaneView;
private readonly startTimeAxisView: CustomTimeAxisView;
private readonly endTimeAxisView: CustomTimeAxisView;
private readonly startPriceAxisView: CustomPriceAxisView;
private readonly endPriceAxisView: CustomPriceAxisView;
constructor(
chart: IChartApi,
series: SeriesApi,
{
container,
interaction,
formatObservable,
removeSelf,
openSettings,
}: RegressionTrendParams,
) {
super({ chart, series, container, interaction });
this.removeSelf = removeSelf;
this.openSettings = openSettings;
this.paneView = new RegressionTrendPaneView(this);
this.timeAxisPaneView = new CustomTimeAxisPaneView({
getAxisSegments: () => this.getTimeAxisSegments(),
});
this.priceAxisPaneView = new CustomPriceAxisPaneView({
getAxisSegments: () => this.getPriceAxisSegments(),
});
this.startTimeAxisView = new CustomTimeAxisView({
getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
labelKind: 'start',
});
this.endTimeAxisView = new CustomTimeAxisView({
getAxisLabel: (kind) => this.getTimeAxisLabel(kind),
labelKind: 'end',
});
this.startPriceAxisView = new CustomPriceAxisView({
getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
labelKind: 'start',
});
this.endPriceAxisView = new CustomPriceAxisView({
getAxisLabel: (kind) => this.getPriceAxisLabel(kind),
labelKind: 'end',
});
if (formatObservable) {
this.subscriptions.add(
formatObservable.subscribe((format) => {
this.displayFormat = format;
this.render();
}),
);
}
this.series.attachPrimitive(this);
}
public isCreationPending(): boolean {
return this.mode === 'idle' || this.mode === 'drawing';
}
public getState(): RegressionTrendState {
return {
hidden: this.hidden,
mode: this.mode,
startTime: this.startTime,
endTime: this.endTime,
settings: { ...this.settings },
};
}
public setState(state: unknown): void {
if (!state || typeof state !== 'object') {
return;
}
const nextState = state as Partial<RegressionTrendState>;
if ('hidden' in nextState && typeof nextState.hidden === 'boolean') {
this.hidden = nextState.hidden;
}
if ('mode' in nextState && nextState.mode) {
this.mode = isDraggingMode(nextState.mode)
? 'ready'
: nextState.mode;
}
if ('startTime' in nextState) {
this.startTime = nextState.startTime ?? null;
}
if ('endTime' in nextState) {
this.endTime = nextState.endTime ?? null;
}
if ('settings' in nextState && nextState.settings) {
this.settings = {
...createDefaultSettings(),
...nextState.settings,
};
}
this.render();
}
public getSettingsTabs(): SettingsTab[] {
return getRegressionTrendSettingsTabs(this.settings);
}
public updateAllViews(): void {
updateViews([
this.paneView,
this.timeAxisPaneView,
this.priceAxisPaneView,
this.startTimeAxisView,
this.endTimeAxisView,
this.startPriceAxisView,
this.endPriceAxisView,
]);
}
public paneViews(): readonly IPrimitivePaneView[] {
return [this.paneView];
}
public timeAxisPaneViews(): readonly IPrimitivePaneView[] {
return [this.timeAxisPaneView];
}
public priceAxisPaneViews(): readonly IPrimitivePaneView[] {
return [this.priceAxisPaneView];
}
public timeAxisViews() {
return [this.startTimeAxisView, this.endTimeAxisView];
}
public priceAxisViews() {
return [this.startPriceAxisView, this.endPriceAxisView];
}
public getRenderData(): RegressionTrendRenderData | null {
if (this.hidden) {
return null;
}
const geometry = this.getGeometry();
if (!geometry) {
return null;
}
return {
...geometry,
showHandles: this.shouldShowHandles(),
...this.settings,
};
}
protected getHoveredItem(x: number, y: number): PrimitiveHoveredItem | null {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return null;
}
const point = { x, y };
if (this.getPointTarget(point)) {
return {
cursorStyle: 'move',
externalId: 'regression-trend',
zOrder: 'top',
};
}
if (!this.isPointNearLine(point)) {
return null;
}
return {
cursorStyle: 'grab',
externalId: 'regression-trend',
zOrder: 'top',
};
}
protected getTimeAxisSegments(): AxisSegment[] {
if (!this.isSelected() && !this.isCreationPending()) {
return [];
}
const geometry = this.getGeometry();
if (!geometry) {
return [];
}
const { colors } = getThemeStore();
return [
{
from: geometry.left,
to: geometry.right,
color: colors.axisMarkerAreaFill,
},
];
}
protected getPriceAxisSegments(): AxisSegment[] {
if (!this.isSelected() && !this.isCreationPending()) {
return [];
}
const geometry = this.getGeometry();
if (!geometry) {
return [];
}
const { colors } = getThemeStore();
return [
{
from: geometry.top,
to: geometry.bottom,
color: colors.axisMarkerAreaFill,
},
];
}
protected getTimeAxisLabel(kind: string): AxisLabel | null {
if (
(!this.isSelected() && !this.isCreationPending()) ||
(kind !== 'start' && kind !== 'end')
) {
return null;
}
const coordinate = this.getTimeCoordinate(kind);
const text = this.getTimeText(kind);
if (coordinate === null || !text) {
return null;
}
const { colors } = getThemeStore();
return {
coordinate,
text,
textColor: colors.chartPriceLineText,
backgroundColor: colors.axisMarkerLabelFill,
};
}
protected getPriceAxisLabel(kind: string): AxisLabel | null {
if (
(!this.isSelected() && !this.isCreationPending()) ||
(kind !== 'start' && kind !== 'end')
) {
return null;
}
const geometry = this.getGeometry();
if (!geometry) {
return null;
}
const price = kind === 'start'
? geometry.startPrice
: geometry.endPrice;
const coordinate = getYCoordinateFromPrice(this.series, price);
if (coordinate === null) {
return null;
}
const { colors } = getThemeStore();
return {
coordinate,
text: formatPrice(price) ?? '',
textColor: colors.chartPriceLineText,
backgroundColor: colors.axisMarkerLabelFill,
};
}
protected handleDoubleClick = (event: MouseEvent): void => {
if (this.hidden || this.mode === 'idle' || this.mode === 'drawing') {
return;
}
const point = this.getEventPoint(event as PointerEvent);
if (!this.isPointNearLine(point)) {
return;
}
event.preventDefault();
event.stopPropagation();
this.openSettings?.();
};
protected handlePointerDown = (event: PointerEvent): void => {
if (this.hidden || event.button !== 0) {
return;
}
const point = this.getEventPoint(event);
if (this.mode === 'idle') {
event.preventDefault();
event.stopPropagation();
this.startDrawing(point);
return;
}
if (this.mode === 'drawing') {
event.preventDefault();
event.stopPropagation();
this.updateDrawing(point);
this.finishDrawing();
return;
}
if (this.mode !== 'ready') {
return;
}
const pointTarget = this.getPointTarget(point);
const isNearLine = this.isPointNearLine(point);
const isDrawingHit = pointTarget !== null || isNearLine;
if (!this.isSelected()) {
if (!isDrawingHit) {
return;
}
event.preventDefault();
event.stopPropagation();
this.select();
return;
}
if (pointTarget === 'start') {
event.preventDefault();
event.stopPropagation();
this.startDragging('dragging-start', point, event.pointerId);
return;
}
if (pointTarget === 'end') {
event.preventDefault();
event.stopPropagation();
this.startDragging('dragging-end', point, event.pointerId);
return;
}
if (isNearLine) {
event.preventDefault();
event.stopPropagation();
this.startDragging('dragging-body', point, event.pointerId);
return;
}
this.deselect();
};
protected handlePointerMove = (event: PointerEvent): void => {
const point = this.getEventPoint(event);
if (this.mode === 'drawing') {
this.updateDrawing(point);
return;
}
if (this.dragPointerId !== event.pointerId) {
return;
}
if (
this.mode === 'dragging-start' ||
this.mode === 'dragging-end'
) {
event.preventDefault();
event.stopPropagation();
this.movePoint(point);
this.render();
return;
}
if (this.mode === 'dragging-body') {
event.preventDefault();
event.stopPropagation();
this.moveBody(point);
this.render();
}
};
protected handlePointerUp = (event: PointerEvent): void => {
if (this.dragPointerId !== event.pointerId) {
return;
}
if (isDraggingMode(this.mode)) {
this.finishDragging();
}
};
private startDrawing(point: Point): void {
const time = this.createTime(point);
if (time === null) {
return;
}
this.startTime = time;
this.endTime = time;
this.mode = 'drawing';
this.render();
}
private updateDrawing(point: Point): void {
const time = this.createTime(point);
if (time === null) {
return;
}
this.endTime = time;
this.render();
}
private finishDrawing(): void {
const geometry = this.getGeometry();
if (!geometry) {
this.removeSelf?.();
return;
}
const lineSize = Math.hypot(
geometry.endPoint.x - geometry.startPoint.x,
geometry.endPoint.y - geometry.startPoint.y,
);
if (lineSize < MIN_LINE_SIZE) {
this.removeSelf?.();
return;
}
this.mode = 'ready';
this.resolveReady?.();
this.render();
}
private startDragging(
mode: RegressionTrendMode,
point: Point,
pointerId: number,
): void {
this.dragStateSnapshot = this.getState();
this.mode = mode;
this.dragPointerId = pointerId;
this.dragStartPoint = point;
this.hideCrosshair();
this.render();
}
private finishDragging(): void {
if (!this.getGeometry() && this.dragStateSnapshot) {
this.startTime = this.dragStateSnapshot.startTime;
this.endTime = this.dragStateSnapshot.endTime;
}
this.mode = 'ready';
this.dragPointerId = null;
this.dragStartPoint = null;
this.dragStateSnapshot = null;
this.showCrosshair();
this.render();
}
private movePoint(point: Point): void {
const time = this.createTime(point);
if (time === null) {
return;
}
if (this.mode === 'dragging-start') {
this.startTime = time;
}
if (this.mode === 'dragging-end') {
this.endTime = time;
}
}
private moveBody(point: Point): void {
const snapshot = this.dragStateSnapshot;
if (
snapshot?.startTime === null ||
snapshot?.endTime === null ||
!snapshot ||
!this.dragStartPoint
) {
return;
}
const offsetX = point.x - this.dragStartPoint.x;
const nextStartTime = shiftTimeByPixels(
this.chart,
snapshot.startTime,
offsetX,
this.series,
);
const nextEndTime = shiftTimeByPixels(
this.chart,
snapshot.endTime,
offsetX,
this.series,
);
if (nextStartTime === null || nextEndTime === null) {
return;
}
this.startTime = nextStartTime;
this.endTime = nextEndTime;
}
private createTime(point: Point): Time | null {
return getTimeFromXCoordinate(this.chart, point.x);
}
private getGeometry(): RegressionTrendGeometry | null {
if (this.startTime === null || this.endTime === null) {
return null;
}
const regression = this.calculateRegression();
if (!regression) {
return null;
}
const startX = getXCoordinateFromTime(
this.chart,
this.startTime,
this.series,
);
const endX = getXCoordinateFromTime(
this.chart,
this.endTime,
this.series,
);
const startY = getYCoordinateFromPrice(
this.series,
regression.startPrice,
);
const endY = getYCoordinateFromPrice(
this.series,
regression.endPrice,
);
if (
startX === null ||
endX === null ||
startY === null ||
endY === null
) {
return null;
}
const startPoint = {
x: Math.round(Number(startX)),
y: Math.round(Number(startY)),
};
const endPoint = {
x: Math.round(Number(endX)),
y: Math.round(Number(endY)),
};
return {
startPoint,
endPoint,
startPrice: regression.startPrice,
endPrice: regression.endPrice,
left: Math.min(startPoint.x, endPoint.x),
right: Math.max(startPoint.x, endPoint.x),
top: Math.min(startPoint.y, endPoint.y),
bottom: Math.max(startPoint.y, endPoint.y),
};
}
private calculateRegression(): RegressionResult | null {
const startTime = getNumericTime(this.startTime);
const endTime = getNumericTime(this.endTime);
if (startTime === null || endTime === null) {
return null;
}
const rangeStart = Math.min(startTime, endTime);
const rangeEnd = Math.max(startTime, endTime);
const prices: number[] = [];
const data = this.series.data() as readonly SeriesDataItem[];
for (const item of data) {
const time = getNumericTime(item.time);
if (time === null || time < rangeStart || time > rangeEnd) {
continue;
}
const price = getDataPrice(item);
if (price !== null) {
prices.push(price);
}
}
if (prices.length < 2) {
return null;
}
const { slope, intercept } = calculateLinearRegression(prices);
const firstPrice = intercept;
const lastPrice = intercept + slope * (prices.length - 1);
if (startTime <= endTime) {
return {
startPrice: firstPrice,
endPrice: lastPrice,
};
}
return {
startPrice: lastPrice,
endPrice: firstPrice,
};
}
private getPointTarget(point: Point): 'start' | 'end' | null {
const geometry = this.getGeometry();
if (!geometry) {
return null;
}
if (
isNearPoint(
point,
geometry.startPoint.x,
geometry.startPoint.y,
HANDLE_HIT_TOLERANCE,
)
) {
return 'start';
}
if (
isNearPoint(
point,
geometry.endPoint.x,
geometry.endPoint.y,
HANDLE_HIT_TOLERANCE,
)
) {
return 'end';
}
return null;
}
private isPointNearLine(point: Point): boolean {
const geometry = this.getGeometry();
if (!geometry) {
return false;
}
return (
getDistanceToSegment(
point,
geometry.startPoint,
geometry.endPoint,
) <= LINE_HIT_TOLERANCE
);
}
private getTimeCoordinate(kind: TimeLabelKind): number | null {
const time = kind === 'start'
? this.startTime
: this.endTime;
if (time === null) {
return null;
}
const coordinate = getXCoordinateFromTime(
this.chart,
time,
this.series,
);
return coordinate === null
? null
: Number(coordinate);
}
private getTimeText(kind: TimeLabelKind): string {
const time = kind === 'start'
? this.startTime
: this.endTime;
if (typeof time !== 'number') {
return '';
}
return formatDate(
time as UTCTimestamp,
this.displayFormat.dateFormat,
this.displayFormat.timeFormat,
this.displayFormat.showTime,
);
}
}
function calculateLinearRegression(prices: readonly number[]): {
slope: number;
intercept: number;
} {
const count = prices.length;
const meanX = (count - 1) / 2;
const meanY =
prices.reduce((sum, price) => sum + price, 0) / count;
let numerator = 0;
let denominator = 0;
for (let index = 0; index < count; index += 1) {
const xOffset = index - meanX;
numerator += xOffset * (prices[index] - meanY);
denominator += xOffset * xOffset;
}
const slope = denominator === 0
? 0
: numerator / denominator;
return {
slope,
intercept: meanY - slope * meanX,
};
}
function getDataPrice(data: unknown): number | null {
if (!data || typeof data !== 'object') {
return null;
}
if (
'close' in data &&
typeof data.close === 'number' &&
Number.isFinite(data.close)
) {
return data.close;
}
if (
'value' in data &&
typeof data.value === 'number' &&
Number.isFinite(data.value)
) {
return data.value;
}
return null;
}
function getNumericTime(time: Time | null): number | null {
if (typeof time !== 'number' || !Number.isFinite(time)) {
return null;
}
return time;
}
function getDistanceToSegment(
point: Point,
startPoint: Point,
endPoint: Point,
): number {
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
if (dx === 0 && dy === 0) {
return Math.hypot(
point.x - startPoint.x,
point.y - startPoint.y,
);
}
const projection = Math.max(
0,
Math.min(
1,
((point.x - startPoint.x) * dx +
(point.y - startPoint.y) * dy) /
(dx * dx + dy * dy),
),
);
const projectionX = startPoint.x + projection * dx;
const projectionY = startPoint.y + projection * dy;
return Math.hypot(
point.x - projectionX,
point.y - projectionY,
);
}
function isDraggingMode(
mode: RegressionTrendMode,
): boolean {
return (
mode === 'dragging-start' ||
mode === 'dragging-end' ||
mode === 'dragging-body'
);
}
import {
IPrimitivePaneRenderer,
IPrimitivePaneView,
PrimitivePaneViewZOrder,
} from 'lightweight-charts';
import { RegressionTrendPaneRenderer } from './paneRenderer';
import { RegressionTrend } from './regressionTrend';
export class RegressionTrendPaneView implements IPrimitivePaneView {
private readonly regressionTrend: RegressionTrend;
constructor(regressionTrend: RegressionTrend) {
this.regressionTrend = regressionTrend;
}
public update(): void {}
public renderer(): IPrimitivePaneRenderer {
return new RegressionTrendPaneRenderer(this.regressionTrend);
}
public zOrder(): PrimitivePaneViewZOrder {
return 'top';
}
}
import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';
import { getThemeStore } from '@src/theme';
import { RegressionTrend } from './regressionTrend';
const UI = {
lineWidth: 2,
handleRadius: 5,
handleBorderWidth: 2,
textLineHeightMultiplier: 1.2,
textOffset: 4,
};
export class RegressionTrendPaneRenderer implements IPrimitivePaneRenderer {
private readonly regressionTrend: RegressionTrend;
constructor(regressionTrend: RegressionTrend) {
this.regressionTrend = regressionTrend;
}
public draw(target: CanvasRenderingTarget2D): void {
const data = this.regressionTrend.getRenderData();
if (!data) {
return;
}
const { colors } = getThemeStore();
target.useBitmapCoordinateSpace(
({
context,
horizontalPixelRatio,
verticalPixelRatio,
}) => {
const pixelRatio = Math.max(
horizontalPixelRatio,
verticalPixelRatio,
);
const startX =
data.startPoint.x * horizontalPixelRatio;
const startY =
data.startPoint.y * verticalPixelRatio;
const endX =
data.endPoint.x * horizontalPixelRatio;
const endY =
data.endPoint.y * verticalPixelRatio;
context.save();
context.lineWidth = UI.lineWidth * pixelRatio;
context.strokeStyle = data.lineColor;
context.beginPath();
context.moveTo(startX, startY);
context.lineTo(endX, endY);
context.stroke();
if (data.text.trim()) {
drawTextAlongLine(context, {
startX,
startY,
endX,
endY,
text: data.text,
fontSize: data.fontSize,
isBold: data.isBold,
isItalic: data.isItalic,
textColor: data.textColor,
verticalPixelRatio,
});
}
if (data.showHandles) {
context.fillStyle = colors.chartBackground;
context.strokeStyle = colors.chartLineColor;
context.lineWidth =
UI.handleBorderWidth * pixelRatio;
drawHandle(
context,
startX,
startY,
UI.handleRadius * pixelRatio,
);
drawHandle(
context,
endX,
endY,
UI.handleRadius * pixelRatio,
);
}
context.restore();
},
);
}
}
function drawHandle(
context: CanvasRenderingContext2D,
x: number,
y: number,
radius: number,
): void {
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
context.stroke();
}
function drawTextAlongLine(
context: CanvasRenderingContext2D,
{
startX,
startY,
endX,
endY,
text,
fontSize,
isBold,
isItalic,
textColor,
verticalPixelRatio,
}: {
startX: number;
startY: number;
endX: number;
endY: number;
text: string;
fontSize: number;
isBold: boolean;
isItalic: boolean;
textColor: string;
verticalPixelRatio: number;
},
): void {
const lines = text.split('\n');
const safeFontSize = Math.max(1, fontSize);
const fontSizePx =
safeFontSize * verticalPixelRatio;
const lineHeight =
safeFontSize *
UI.textLineHeightMultiplier *
verticalPixelRatio;
const dx = endX - startX;
const dy = endY - startY;
let angle = Math.atan2(dy, dx);
if (angle > Math.PI / 2 || angle < -Math.PI / 2) {
angle += Math.PI;
}
const centerX = (startX + endX) / 2;
const centerY = (startY + endY) / 2;
const fontWeight = isBold ? '700 ' : '';
const fontStyle = isItalic ? 'italic ' : '';
context.save();
context.translate(centerX, centerY);
context.rotate(angle);
context.font =
`${fontStyle}${fontWeight}${fontSizePx}px Inter, sans-serif`;
context.fillStyle = textColor;
context.textAlign = 'center';
context.textBaseline = 'middle';
const blockHeight = lines.length * lineHeight;
const textOffset = UI.textOffset * verticalPixelRatio;
const textCenterY = -(blockHeight / 2 + textOffset);
const startLineY =
textCenterY - blockHeight / 2 + lineHeight / 2;
lines.forEach((line, index) => {
context.fillText(
line,
0,
startLineY + index * lineHeight,
);
});
context.restore();
}
import { getThemeStore } from '@src/theme';
import { t } from '@src/translations';
import {
SettingField,
SettingsTab,
SettingsValues,
} from '@src/types';
export interface RegressionTrendStyle {
lineColor: string;
}
export interface RegressionTrendTextStyle {
fontSize: number;
text: string;
isBold: boolean;
isItalic: boolean;
textColor: string;
}
export type RegressionTrendSettings =
RegressionTrendStyle &
RegressionTrendTextStyle &
SettingsValues;
export function createDefaultSettings(): RegressionTrendSettings {
const { colors } = getThemeStore();
return {
lineColor: colors.chartLineColor,
fontSize: 14,
text: '',
isBold: false,
isItalic: false,
textColor: colors.chartLineColor,
};
}
export function getRegressionTrendSettingsTabs(
settings: RegressionTrendSettings,
): SettingsTab[] {
const styleFields: SettingField[] = [
{
key: 'lineColor',
label: t('Line'),
type: 'color',
defaultValue: settings.lineColor,
toolbar: {
control: 'color',
role: 'line',
},
},
];
const textFields: SettingField[] = [
{
key: 'fontSize',
label: t('Font size'),
type: 'number',
defaultValue: settings.fontSize,
min: 8,
max: 24,
},
{
key: 'text',
label: t('Text'),
type: 'textarea',
defaultValue: settings.text,
placeholder: t('Enter text'),
},
{
key: 'isBold',
label: t('Bold'),
type: 'boolean',
defaultValue: settings.isBold,
},
{
key: 'isItalic',
label: t('Italic'),
type: 'boolean',
defaultValue: settings.isItalic,
},
{
key: 'textColor',
label: t('Text'),
type: 'color',
defaultValue: settings.textColor,
toolbar: {
control: 'color',
role: 'text',
},
},
];
return [
{
key: 'style',
label: t('Style'),
fields: styleFields,
},
{
key: 'text',
label: t('Text'),
fields: textFields,
},
];
}
import { RegressionTrendPaneRenderer } from './paneRenderer';
import { RegressionTrendPaneView } from './paneView';
import {
RegressionTrend,
RegressionTrendRenderData,
} from './regressionTrend';
export {
RegressionTrend,
RegressionTrendPaneRenderer,
RegressionTrendPaneView,
};
export type { RegressionTrendRenderData };