feat: pane support maximize and minimize

This commit is contained in:
liihuu 2024-09-26 03:33:35 +08:00
parent 8c658bf881
commit a14489f604
9 changed files with 222 additions and 106 deletions

View File

@ -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)

View File

@ -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<C extends Axis = Axis> extends Pane {
private _axis: C
private readonly _options: PickPartial<DeepRequired<Omit<PaneOptions, 'id' | 'height'>>, 'position'> = { minHeight: PANE_MIN_HEIGHT, dragEnabled: true, axis: { name: 'normal', scrollZoomEnabled: true } }
private readonly _options: PickPartial<DeepRequired<Omit<PaneOptions, 'id' | 'height'>>, 'position'> = {
minHeight: PANE_MIN_HEIGHT,
dragEnabled: true,
state: PaneState.Normal,
axis: { name: 'normal', scrollZoomEnabled: true }
}
constructor (rootContainer: HTMLElement, afterElement: Nullable<HTMLElement>, chart: Chart, id: string, options: Omit<PaneOptions, 'id' | 'height'>) {
super(rootContainer, afterElement, chart, id)

View File

@ -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<HTMLElement>, 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<Bounding>): 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`

View File

@ -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<AxisCreate>
}

View File

@ -23,7 +23,7 @@ import type { AxisTick, Axis } from '../component/Axis'
import View from './View'
export default abstract class AxisView<C extends Axis = Axis> extends View<C> {
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<C extends Axis = Axis> extends View<C> {
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)
}
}
}
}

View File

@ -58,10 +58,10 @@ export default abstract class View<C extends Axis = Axis> 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
}

View File

@ -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<DrawPane<YAxis>> {
}
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<DrawPane<YAxis>> {
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)
}
}

View File

@ -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<SeparatorPane> {
private _dragFlag = false
@ -34,6 +35,9 @@ export default class SeparatorWidget extends Widget<SeparatorPane> {
private _topPaneHeight = 0
private _bottomPaneHeight = 0
private _topPane: Nullable<DrawPane> = null
private _bottomPane: Nullable<DrawPane> = 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<SeparatorPane> {
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<SeparatorPane> {
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<AxisPane> = null
let increasedPane: Nullable<AxisPane> = 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<DrawPane> = null
let increasedPane: Nullable<DrawPane> = 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<SeparatorPane> {
private _mouseLeaveEvent (): boolean {
if (!this._dragFlag) {
this.getContainer().style.background = ''
this.getContainer().style.background = 'transparent'
return true
}
return false

View File

@ -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<DrawPane<YAxis>> {
}
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)
}
}
}