From a14489f6044b8d21ebbd008f9ced4e23ac76c1ef Mon Sep 17 00:00:00 2001 From: liihuu Date: Thu, 26 Sep 2024 03:33:35 +0800 Subject: [PATCH] feat: pane support `maximize` and `minimize` --- src/Chart.ts | 104 +++++++++++++++++++++----------- src/pane/DrawPane.ts | 9 ++- src/pane/Pane.ts | 24 +++++++- src/pane/types.ts | 7 +++ src/view/AxisView.ts | 37 ++++++------ src/view/View.ts | 6 +- src/widget/IndicatorWidget.ts | 15 +++-- src/widget/SeparatorWidget.ts | 108 ++++++++++++++++++++++------------ src/widget/YAxisWidget.ts | 18 ++++-- 9 files changed, 222 insertions(+), 106 deletions(-) diff --git a/src/Chart.ts b/src/Chart.ts index fd2d0aaf3..ac0d81bcf 100644 --- a/src/Chart.ts +++ b/src/Chart.ts @@ -46,7 +46,7 @@ import XAxisPane from './pane/XAxisPane' import type DrawPane from './pane/DrawPane' import SeparatorPane from './pane/SeparatorPane' -import { type PaneOptions, PanePosition, PANE_DEFAULT_HEIGHT, PaneIdConstants } from './pane/types' +import { type PaneOptions, PanePosition, PANE_DEFAULT_HEIGHT, PaneIdConstants, PaneState, PANE_MIN_HEIGHT } from './pane/types' import type AxisImp from './component/Axis' import { AxisPosition, type Axis } from './component/Axis' @@ -152,12 +152,14 @@ export default class ChartImp implements Chart { this._chartContainer = createDom('div', { position: 'relative', width: '100%', + height: '100%', outline: 'none', borderStyle: 'none', cursor: 'crosshair', boxSizing: 'border-box', userSelect: 'none', webkitUserSelect: 'none', + overflow: 'hidden', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error msUserSelect: 'none', @@ -170,8 +172,8 @@ export default class ChartImp implements Chart { } _cacheChartBounding (): void { - this._chartBounding.width = Math.floor(this._container.clientWidth) - this._chartBounding.height = Math.floor(this._container.clientHeight) + this._chartBounding.width = Math.floor(this._chartContainer.clientWidth) + this._chartBounding.height = Math.floor(this._chartContainer.clientHeight) } private _initPanes (options?: Options): void { @@ -303,42 +305,70 @@ export default class ChartImp implements Chart { const totalHeight = this._chartBounding.height const separatorSize = this._chartStore.getStyles().separator.size const xAxisHeight = this._xAxisPane.getAxisComponent().getAutoSize() - let paneExcludeXAxisHeight = totalHeight - xAxisHeight - this._separatorPanes.size * separatorSize - if (paneExcludeXAxisHeight < 0) { - paneExcludeXAxisHeight = 0 + let remainingHeight = totalHeight - xAxisHeight + if (remainingHeight < 0) { + remainingHeight = 0 } - let indicatorPaneTotalHeight = 0 - - this._drawPanes.forEach(pane => { - if (pane.getId() !== PaneIdConstants.CANDLE && pane.getId() !== PaneIdConstants.X_AXIS) { - let paneHeight = pane.getBounding().height - const paneMinHeight = pane.getOptions().minHeight - if (paneHeight < paneMinHeight) { - paneHeight = paneMinHeight + const maximizePane = this._drawPanes.find(pane => pane.getId() !== PaneIdConstants.X_AXIS && pane.getOptions().state === PaneState.Maximize) + if (isValid(maximizePane)) { + maximizePane.setBounding({ height: remainingHeight }) + this._separatorPanes.get(maximizePane)?.setVisible(false) + this._drawPanes.forEach(pane => { + const paneId = pane.getId() + if (paneId !== maximizePane.getId() && paneId !== PaneIdConstants.X_AXIS) { + pane.setVisible(false) + this._separatorPanes.get(pane)?.setVisible(false) } - if (indicatorPaneTotalHeight + paneHeight > paneExcludeXAxisHeight) { - indicatorPaneTotalHeight = paneExcludeXAxisHeight - paneHeight = Math.max(paneExcludeXAxisHeight - indicatorPaneTotalHeight, 0) - } else { - indicatorPaneTotalHeight += paneHeight + }) + } else { + this._drawPanes.forEach(pane => { + pane.setVisible(true) + this._separatorPanes.get(pane)?.setVisible(true) + const paneId = pane.getId() + if (paneId !== PaneIdConstants.CANDLE && paneId !== PaneIdConstants.X_AXIS) { + if (isValid(this._separatorPanes.get(pane))) { + remainingHeight -= separatorSize + } + let paneHeight = PANE_MIN_HEIGHT + if (pane.getOptions().state !== PaneState.Minimize) { + paneHeight = pane.getOriginalBounding().height + const paneMinHeight = pane.getOptions().minHeight + if (paneHeight < paneMinHeight) { + paneHeight = paneMinHeight + } + } + if (paneHeight > remainingHeight) { + paneHeight = Math.max(remainingHeight, 0) + remainingHeight = 0 + } else { + remainingHeight -= paneHeight + } + pane.setBounding({ height: paneHeight }) } - pane.setBounding({ height: paneHeight }) + }) + if (isValid(this._candlePane) && isValid(this._separatorPanes.get(this._candlePane))) { + remainingHeight -= separatorSize } - }) - const candlePaneHeight = paneExcludeXAxisHeight - indicatorPaneTotalHeight - this._candlePane?.setBounding({ height: candlePaneHeight }) - this._xAxisPane.setBounding({ height: xAxisHeight }) - let top = 0 - this._drawPanes.forEach(pane => { - const separatorPane = this._separatorPanes.get(pane) - if (isValid(separatorPane)) { - separatorPane.setBounding({ height: separatorSize, top }) - top += separatorSize + let candlePaneHeight = PANE_MIN_HEIGHT + if (this._candlePane?.getOptions().state !== PaneState.Minimize) { + candlePaneHeight = Math.max(remainingHeight, 0) + this._candlePane?.setOriginalBounding({ height: candlePaneHeight }) } - pane.setBounding({ top }) - top += pane.getBounding().height - }) + this._candlePane?.setBounding({ height: candlePaneHeight }) + this._xAxisPane.setBounding({ height: xAxisHeight }) + + let top = 0 + this._drawPanes.forEach(pane => { + const separatorPane = this._separatorPanes.get(pane) + if (isValid(separatorPane)) { + separatorPane.setBounding({ height: separatorSize, top }) + top += separatorSize + } + pane.setBounding({ top }) + top += pane.getBounding().height + }) + } } private _measurePaneWidth (): void { @@ -412,10 +442,14 @@ export default class ChartImp implements Chart { if (options.id !== PaneIdConstants.CANDLE && isNumber(options.height) && options.height > 0) { const minHeight = Math.max(options.minHeight ?? pane.getOptions().minHeight, 0) const height = Math.max(minHeight, options.height) - pane.setBounding({ height }) + pane.setOriginalBounding({ height }) shouldAdjust = true shouldMeasureHeight = true } + if (isValid(options.state)) { + shouldMeasureHeight = true + shouldAdjust = true + } if (isValid(options.axis)) { shouldAdjust = true } @@ -704,7 +738,7 @@ export default class ChartImp implements Chart { paneId ??= createId(PaneIdConstants.INDICATOR) const pane = this._createPane(IndicatorPane, paneId, paneOptions ?? {}) const height = paneOptions?.height ?? PANE_DEFAULT_HEIGHT - pane.setBounding({ height }) + pane.setOriginalBounding({ height }) const result = this._chartStore.getIndicatorStore().addInstance(indicator, paneId, isStack ?? false) if (result) { this.adjustPaneViewport(true, true, true, true, true) diff --git a/src/pane/DrawPane.ts b/src/pane/DrawPane.ts index 16bb208e3..a38094e0d 100644 --- a/src/pane/DrawPane.ts +++ b/src/pane/DrawPane.ts @@ -25,7 +25,7 @@ import type DrawWidget from '../widget/DrawWidget' import type YAxisWidget from '../widget/YAxisWidget' import Pane from './Pane' -import { type PaneOptions, PANE_MIN_HEIGHT, PaneIdConstants } from './types' +import { type PaneOptions, PANE_MIN_HEIGHT, PaneIdConstants, PaneState } from './types' import type Chart from '../Chart' @@ -40,7 +40,12 @@ export default abstract class DrawPane extends Pane { private _axis: C - private readonly _options: PickPartial>, 'position'> = { minHeight: PANE_MIN_HEIGHT, dragEnabled: true, axis: { name: 'normal', scrollZoomEnabled: true } } + private readonly _options: PickPartial>, 'position'> = { + minHeight: PANE_MIN_HEIGHT, + dragEnabled: true, + state: PaneState.Normal, + axis: { name: 'normal', scrollZoomEnabled: true } + } constructor (rootContainer: HTMLElement, afterElement: Nullable, chart: Chart, id: string, options: Omit) { super(rootContainer, afterElement, chart, id) diff --git a/src/pane/Pane.ts b/src/pane/Pane.ts index 6b4481770..9cdb2fc1e 100644 --- a/src/pane/Pane.ts +++ b/src/pane/Pane.ts @@ -16,17 +16,22 @@ import type Updater from '../common/Updater' import { UpdateLevel } from '../common/Updater' import type Bounding from '../common/Bounding' import { createDefaultBounding } from '../common/Bounding' +import { createDom } from '../common/utils/dom' +import { merge } from '../common/utils/typeChecks' import type Chart from '../Chart' -import { createDom } from '../common/utils/dom' export default abstract class Pane implements Updater { private _rootContainer: HTMLElement private _container: HTMLElement private readonly _id: string private readonly _chart: Chart - private readonly _bounding: Bounding = createDefaultBounding() + private readonly _bounding = createDefaultBounding() + + private readonly _originalBounding = createDefaultBounding() + + private _visible = true constructor (rootContainer: HTMLElement, afterElement: Nullable, chart: Chart, id: string) { this._chart = chart @@ -55,6 +60,13 @@ export default abstract class Pane implements Updater { return this._container } + setVisible (visible: boolean): void { + if (this._visible !== visible) { + this._container.style.display = visible ? 'block' : 'none' + this._visible = visible + } + } + getId (): string { return this._id } @@ -67,6 +79,14 @@ export default abstract class Pane implements Updater { return this._bounding } + setOriginalBounding (bounding: Partial): void { + merge(this._originalBounding, bounding) + } + + getOriginalBounding (): Bounding { + return this._originalBounding + } + update (level?: UpdateLevel): void { if (this._bounding.height !== this._container.clientHeight) { this._container.style.height = `${this._bounding.height}px` diff --git a/src/pane/types.ts b/src/pane/types.ts index e61263731..eb214c037 100644 --- a/src/pane/types.ts +++ b/src/pane/types.ts @@ -19,12 +19,19 @@ export const enum PanePosition { Bottom = 'bottom' } +export const enum PaneState { + Normal = 'normal', + Maximize = 'maximize', + Minimize = 'minimize' +} + export interface PaneOptions { id?: string height?: number minHeight?: number dragEnabled?: boolean position?: PanePosition + state?: PaneState, axis?: Partial } diff --git a/src/view/AxisView.ts b/src/view/AxisView.ts index 93a9e196b..2ee91f758 100644 --- a/src/view/AxisView.ts +++ b/src/view/AxisView.ts @@ -23,7 +23,7 @@ import type { AxisTick, Axis } from '../component/Axis' import View from './View' export default abstract class AxisView extends View { - override drawImp (ctx: CanvasRenderingContext2D): void { + override drawImp (ctx: CanvasRenderingContext2D, extend: unknown[]): void { const widget = this.getWidget() const pane = widget.getPane() const bounding = widget.getBounding() @@ -37,25 +37,28 @@ export default abstract class AxisView extends View { styles: styles.axisLine })?.draw(ctx) } - const ticks = axis.getTicks() - if (styles.tickLine.show) { - const lines = this.createTickLines(ticks, bounding, styles) - lines.forEach(line => { + if (!(extend[0] as boolean)) { + const ticks = axis.getTicks() + if (styles.tickLine.show) { + const lines = this.createTickLines(ticks, bounding, styles) + lines.forEach(line => { + this.createFigure({ + name: 'line', + attrs: line, + styles: styles.tickLine + })?.draw(ctx) + }) + } + if (styles.tickText.show) { + const texts = this.createTickTexts(ticks, bounding, styles) this.createFigure({ - name: 'line', - attrs: line, - styles: styles.tickLine + name: 'text', + attrs: texts, + styles: styles.tickText })?.draw(ctx) - }) - } - if (styles.tickText.show) { - const texts = this.createTickTexts(ticks, bounding, styles) - this.createFigure({ - name: 'text', - attrs: texts, - styles: styles.tickText - })?.draw(ctx) + } } + } } diff --git a/src/view/View.ts b/src/view/View.ts index 189251045..317c94fed 100644 --- a/src/view/View.ts +++ b/src/view/View.ts @@ -58,10 +58,10 @@ export default abstract class View extends Eventful { return null } - draw (ctx: CanvasRenderingContext2D): void { + draw (ctx: CanvasRenderingContext2D, ...extend: unknown[]): void { this.clear() - this.drawImp(ctx) + this.drawImp(ctx, extend) } - protected abstract drawImp (ctx: CanvasRenderingContext2D): void + protected abstract drawImp (ctx: CanvasRenderingContext2D, ...extend: unknown[]): void } diff --git a/src/widget/IndicatorWidget.ts b/src/widget/IndicatorWidget.ts index aab5d3813..f28e6ad53 100644 --- a/src/widget/IndicatorWidget.ts +++ b/src/widget/IndicatorWidget.ts @@ -16,6 +16,7 @@ import type DrawPane from '../pane/DrawPane' import { WidgetNameConstants } from './types' import DrawWidget from './DrawWidget' +import { PaneState } from '../pane/types' import type { YAxis } from '../component/YAxis' @@ -48,9 +49,11 @@ export default class IndicatorWidget extends DrawWidget> { } protected updateMain (ctx: CanvasRenderingContext2D): void { - this.updateMainContent(ctx) - this._indicatorView.draw(ctx) - this._gridView.draw(ctx) + if (this.getPane().getOptions().state !== PaneState.Minimize) { + this.updateMainContent(ctx) + this._indicatorView.draw(ctx) + this._gridView.draw(ctx) + } } protected createTooltipView (): IndicatorTooltipView { @@ -61,8 +64,10 @@ export default class IndicatorWidget extends DrawWidget> { protected updateMainContent (_ctx: CanvasRenderingContext2D): void {} override updateOverlay (ctx: CanvasRenderingContext2D): void { - this._overlayView.draw(ctx) - this._crosshairLineView.draw(ctx) + if (this.getPane().getOptions().state !== PaneState.Minimize) { + this._overlayView.draw(ctx) + this._crosshairLineView.draw(ctx) + } this._tooltipView.draw(ctx) } } diff --git a/src/widget/SeparatorWidget.ts b/src/widget/SeparatorWidget.ts index 7998d2938..438238e06 100644 --- a/src/widget/SeparatorWidget.ts +++ b/src/widget/SeparatorWidget.ts @@ -24,8 +24,9 @@ import Widget from './Widget' import { WidgetNameConstants, REAL_SEPARATOR_HEIGHT } from './types' import type SeparatorPane from '../pane/SeparatorPane' - -import type AxisPane from '../pane/DrawPane' +import type DrawPane from '../pane/DrawPane' +import { PaneState } from '../pane/types' +import { isValid } from '../common/utils/typeChecks' export default class SeparatorWidget extends Widget { private _dragFlag = false @@ -34,6 +35,9 @@ export default class SeparatorWidget extends Widget { private _topPaneHeight = 0 private _bottomPaneHeight = 0 + private _topPane: Nullable = null + private _bottomPane: Nullable = null + constructor (rootContainer: HTMLElement, pane: SeparatorPane) { super(rootContainer, pane) // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -66,13 +70,41 @@ export default class SeparatorWidget extends Widget { this._dragFlag = true this._dragStartY = event.pageY const pane = this.getPane() - this._topPaneHeight = pane.getTopPane().getBounding().height - this._bottomPaneHeight = pane.getBottomPane().getBounding().height + const chart = pane.getChart() + this._topPane = pane.getTopPane() + this._bottomPane = pane.getBottomPane() + const drawPanes = chart.getDrawPanes() + if (this._topPane.getOptions().state === PaneState.Minimize) { + const index = drawPanes.findIndex(pane => pane.getId() === this._topPane?.getId()) + for (let i = index - 1; i > -1; i--) { + const pane = drawPanes[i] + if (pane.getOptions().state !== PaneState.Minimize) { + this._topPane = pane + break + } + } + } + if (this._bottomPane.getOptions().state === PaneState.Minimize) { + const index = drawPanes.findIndex(pane => pane.getId() === this._bottomPane?.getId()) + for (let i = index + 1; i < drawPanes.length; i++) { + const pane = drawPanes[i] + if (pane.getOptions().state !== PaneState.Minimize) { + this._bottomPane = pane + break + } + } + } + this._topPaneHeight = this._topPane.getBounding().height + this._bottomPaneHeight = this._bottomPane.getBounding().height return true } private _mouseUpEvent (): boolean { this._dragFlag = false + this._topPane = null + this._bottomPane = null + this._topPaneHeight = 0 + this._bottomPaneHeight = 0 return this._mouseLeaveEvent() } @@ -81,40 +113,44 @@ export default class SeparatorWidget extends Widget { private _pressedTouchMouseMoveEvent (event: MouseTouchEvent): boolean { const dragDistance = event.pageY - this._dragStartY - const currentPane = this.getPane() - const topPane = currentPane.getTopPane() - const bottomPane = currentPane.getBottomPane() + const isUpDrag = dragDistance < 0 - if ( - topPane !== null && - bottomPane?.getOptions().dragEnabled - ) { - let reducedPane: Nullable = null - let increasedPane: Nullable = null - let startDragReducedPaneHeight = 0 - let startDragIncreasedPaneHeight = 0 - if (isUpDrag) { - reducedPane = topPane - increasedPane = bottomPane - startDragReducedPaneHeight = this._topPaneHeight - startDragIncreasedPaneHeight = this._bottomPaneHeight - } else { - reducedPane = bottomPane - increasedPane = topPane - startDragReducedPaneHeight = this._bottomPaneHeight - startDragIncreasedPaneHeight = this._topPaneHeight - } - const reducedPaneMinHeight = reducedPane.getOptions().minHeight - if (startDragReducedPaneHeight > reducedPaneMinHeight) { - const reducedPaneHeight = Math.max(startDragReducedPaneHeight - Math.abs(dragDistance), reducedPaneMinHeight) - const diffHeight = startDragReducedPaneHeight - reducedPaneHeight - reducedPane.setBounding({ height: reducedPaneHeight }) - increasedPane.setBounding({ height: startDragIncreasedPaneHeight + diffHeight }) - const chart = currentPane.getChart() - chart.getChartStore().getActionStore().execute(ActionType.OnPaneDrag, { paneId: currentPane.getId() }) - chart.adjustPaneViewport(true, true, true, true, true) + if (isValid(this._topPane) && isValid(this._bottomPane)) { + const bottomPaneOptions = this._bottomPane.getOptions() + if ( + this._topPane.getOptions().state !== PaneState.Minimize && + bottomPaneOptions.state !== PaneState.Minimize && + bottomPaneOptions.dragEnabled + ) { + let reducedPane: Nullable = null + let increasedPane: Nullable = null + let startDragReducedPaneHeight = 0 + let startDragIncreasedPaneHeight = 0 + if (isUpDrag) { + reducedPane = this._topPane + increasedPane = this._bottomPane + startDragReducedPaneHeight = this._topPaneHeight + startDragIncreasedPaneHeight = this._bottomPaneHeight + } else { + reducedPane = this._bottomPane + increasedPane = this._topPane + startDragReducedPaneHeight = this._bottomPaneHeight + startDragIncreasedPaneHeight = this._topPaneHeight + } + const reducedPaneMinHeight = reducedPane.getOptions().minHeight + if (startDragReducedPaneHeight > reducedPaneMinHeight) { + const reducedPaneHeight = Math.max(startDragReducedPaneHeight - Math.abs(dragDistance), reducedPaneMinHeight) + const diffHeight = startDragReducedPaneHeight - reducedPaneHeight + reducedPane.setOriginalBounding({ height: reducedPaneHeight }) + increasedPane.setOriginalBounding({ height: startDragIncreasedPaneHeight + diffHeight }) + const currentPane = this.getPane() + const chart = currentPane.getChart() + chart.getChartStore().getActionStore().execute(ActionType.OnPaneDrag, { paneId: currentPane.getId() }) + chart.adjustPaneViewport(true, true, true, true, true) + } } } + return true } @@ -132,7 +168,7 @@ export default class SeparatorWidget extends Widget { private _mouseLeaveEvent (): boolean { if (!this._dragFlag) { - this.getContainer().style.background = '' + this.getContainer().style.background = 'transparent' return true } return false diff --git a/src/widget/YAxisWidget.ts b/src/widget/YAxisWidget.ts index 17070842c..7bd2ce9ab 100644 --- a/src/widget/YAxisWidget.ts +++ b/src/widget/YAxisWidget.ts @@ -16,6 +16,7 @@ import type DrawPane from '../pane/DrawPane' import { WidgetNameConstants } from './types' import DrawWidget from './DrawWidget' +import { PaneState } from '../pane/types' import type { YAxis } from '../component/YAxis' @@ -43,15 +44,20 @@ export default class YAxisWidget extends DrawWidget> { } override updateMain (ctx: CanvasRenderingContext2D): void { - this._yAxisView.draw(ctx) - if (this.getPane().getAxisComponent().isInCandle()) { - this._candleLastPriceLabelView.draw(ctx) + const minimize = this.getPane().getOptions().state === PaneState.Minimize + this._yAxisView.draw(ctx, minimize) + if (!minimize) { + if (this.getPane().getAxisComponent().isInCandle()) { + this._candleLastPriceLabelView.draw(ctx) + } + this._indicatorLastValueView.draw(ctx) } - this._indicatorLastValueView.draw(ctx) } override updateOverlay (ctx: CanvasRenderingContext2D): void { - this._overlayYAxisView.draw(ctx) - this._crosshairHorizontalLabelView.draw(ctx) + if (this.getPane().getOptions().state !== PaneState.Minimize) { + this._overlayYAxisView.draw(ctx) + this._crosshairHorizontalLabelView.draw(ctx) + } } }