diff --git a/package-lock.json b/package-lock.json index d178a974..1df9a482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.1", "license": "Apache-2.0", "dependencies": { + "@types/chroma-js": "^3.1.2", + "chroma-js": "^3.1.2", "lodash": "^4.17.21", "solid-js": "^1.6.11" }, @@ -35,7 +37,7 @@ "vitest": "^0.28.4" }, "peerDependencies": { - "klinecharts": ">=9.0.0" + "klinecharts": "^10.0.0-alpha9" } }, "node_modules/@adobe/css-tools": { @@ -1523,6 +1525,11 @@ "@types/chai": "*" } }, + "node_modules/@types/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==" + }, "node_modules/@types/eslint": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz", @@ -2876,6 +2883,11 @@ "node": "*" } }, + "node_modules/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -5224,9 +5236,10 @@ } }, "node_modules/klinecharts": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/klinecharts/-/klinecharts-9.1.1.tgz", - "integrity": "sha512-OnHeStm2zFf6iTPMPB720+gpD8psXmajZfd7eqZN+Z5FxnYF7sSsPE02J84JTJ0AVrXYOdLJShIdYkr7aNN5LA==", + "version": "10.0.0-alpha9", + "resolved": "file:klinecharts-10.0.0-alpha9.tgz", + "integrity": "sha512-GzGZwSBOsTjeLlHMid9eaSQeAHc+abSw0PNxP3oCBLy1qNQUBHHGv7fXc4ccBUj9ex1mCy+y89uYOCcnJT8XjA==", + "license": "Apache-2.0", "peer": true }, "node_modules/klona": { @@ -8612,6 +8625,11 @@ "@types/chai": "*" } }, + "@types/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==" + }, "@types/eslint": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz", @@ -9674,6 +9692,11 @@ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, + "chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==" + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -11421,9 +11444,8 @@ } }, "klinecharts": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/klinecharts/-/klinecharts-9.1.1.tgz", - "integrity": "sha512-OnHeStm2zFf6iTPMPB720+gpD8psXmajZfd7eqZN+Z5FxnYF7sSsPE02J84JTJ0AVrXYOdLJShIdYkr7aNN5LA==", + "version": "10.0.0-alpha9", + "integrity": "sha512-GzGZwSBOsTjeLlHMid9eaSQeAHc+abSw0PNxP3oCBLy1qNQUBHHGv7fXc4ccBUj9ex1mCy+y89uYOCcnJT8XjA==", "peer": true }, "klona": { diff --git a/package.json b/package.json index 71098f39..13b85c20 100644 --- a/package.json +++ b/package.json @@ -59,10 +59,12 @@ "vitest": "^0.28.4" }, "dependencies": { + "@types/chroma-js": "^3.1.2", + "chroma-js": "^3.1.2", "lodash": "^4.17.21", "solid-js": "^1.6.11" }, "peerDependencies": { - "klinecharts": ">=9.0.0" + "klinecharts": "^10.0.0-alpha9" } } diff --git a/src/Chart.ts b/src/Chart.ts new file mode 100644 index 00000000..1ff3f19d --- /dev/null +++ b/src/Chart.ts @@ -0,0 +1,337 @@ +import { + ActionCallback, ActionType, BarSpace, Bounding, ConvertFilter, Coordinate, Crosshair, DataLoader, DecimalFold, + DeepPartial, DomPosition, Formatter, Indicator, IndicatorCreate, IndicatorFilter, init, KLineData, Nullable, + Options, Overlay, OverlayCreate, OverlayFilter, PaneOptions, Period, PickPartial, PickRequired, Point, Styles, + SymbolInfo, ThousandsSeparator, VisibleRange, Chart as KLineChart, + ZoomBehavior +} from "klinecharts"; +import { ProChart, UndoOptions } from "./types/types"; +import { OrderOverlay, OrderOverlayCreate } from "./types/overlayTypes"; +import { isArray } from "lodash"; + +export default class Chart implements ProChart +{ + private _chart: KLineChart; + private _charts: Map = new Map(); + + public id: string; + + get chart (): KLineChart { + return this._chart; + } + + get charts (): KLineChart[] { + return Array.from(this._charts.values()); + } + + constructor (ds: string | HTMLElement, options?: Options | undefined) { + const chart = init(ds, options); + + if (!chart) { + throw new Error('Failed to initialize chart'); + } + this._chart = chart; + this.id = chart.id; + } + + static init (ds: string | HTMLElement, options?: Options | undefined): Chart { + return new Chart(ds, options); + } + + /** + * Base Proxy Methods + */ + + getDom (paneId?: string, position?: DomPosition): Nullable { + return this._chart.getDom(paneId, position); + } + getSize (paneId?: string, position?: DomPosition): Nullable { + return this._chart.getSize(paneId, position); + } + createIndicator (value: string | IndicatorCreate, isStack?: boolean, paneOptions?: PaneOptions): Nullable { + return this._chart.createIndicator(value, isStack, paneOptions); + } + getIndicators (filter?: IndicatorFilter): Indicator[] { + return this._chart.getIndicators(filter); + } + createOverlay (value: string | OverlayCreate | Array): Nullable | Array> { + return this._chart.createOverlay(value); + } + getOverlays (filter?: OverlayFilter): Overlay[] { + return this._chart.getOverlays(filter); + } + setPaneOptions (options: PaneOptions): void { + return this._chart.setPaneOptions(options); + } + getPaneOptions (id?: string): Nullable | PaneOptions[] { + return this._chart.getPaneOptions(id); + } + scrollByDistance (distance: number, animationDuration?: number): void { + return this._chart.scrollByDistance(distance, animationDuration); + } + scrollToRealTime (animationDuration?: number): void { + return this._chart.scrollToRealTime(animationDuration); + } + scrollToDataIndex (dataIndex: number, animationDuration?: number): void { + return this._chart.scrollToDataIndex(dataIndex, animationDuration); + } + scrollToTimestamp (timestamp: number, animationDuration?: number): void { + return this._chart.scrollToTimestamp(timestamp, animationDuration); + } + zoomAtCoordinate (scale: number, coordinate?: Coordinate, animationDuration?: number): void { + return this._chart.zoomAtCoordinate(scale, coordinate, animationDuration); + } + zoomAtDataIndex (scale: number, dataIndex: number, animationDuration?: number): void { + return this._chart.zoomAtDataIndex(scale, dataIndex, animationDuration); + } + zoomAtTimestamp (scale: number, timestamp: number, animationDuration?: number): void { + return this._chart.zoomAtTimestamp(scale, timestamp, animationDuration); + } + convertToPixel (points: Partial | Array>, filter?: ConvertFilter): Partial | Array> { + return this._chart.convertToPixel(points, filter); + } + convertFromPixel (coordinates: Array>, filter?: ConvertFilter): Partial | Array> { + return this._chart.convertFromPixel(coordinates, filter); + } + executeAction (type: ActionType, data: Crosshair): void { + return this._chart.executeAction(type, data); + } + subscribeAction (type: ActionType, callback: ActionCallback): void { + return this._chart.subscribeAction(type, callback); + } + unsubscribeAction (type: ActionType, callback?: ActionCallback): void { + return this._chart.unsubscribeAction(type, callback); + } + getConvertPictureUrl (includeOverlay?: boolean, type?: "png" | "jpeg" | "bmp", backgroundColor?: string): string { + return this._chart.getConvertPictureUrl(includeOverlay, type, backgroundColor); + } + resize (): void { + return this._chart.resize(); + } + + + /** + * Store Proxy Methods + */ + setStyles (value: string | DeepPartial): void + { + return this._chart.setStyles(value); + } + getStyles (): Styles + { + return this._chart.getStyles(); + } + setFormatter (formatter: Partial): void + { + return this._chart.setFormatter(formatter); + } + getFormatter (): Formatter + { + return this._chart.getFormatter(); + } + setLocale (locale: string): void + { + return this._chart.setLocale(locale); + } + getLocale (): string + { + return this._chart.getLocale(); + } + setTimezone (timezone: string): void + { + return this._chart.setTimezone(timezone); + } + getTimezone (): string + { + return this._chart.getTimezone(); + } + setThousandsSeparator (thousandsSeparator: Partial): void + { + return this._chart.setThousandsSeparator(thousandsSeparator); + } + getThousandsSeparator (): ThousandsSeparator + { + return this._chart.getThousandsSeparator(); + } + setDecimalFold (decimalFold: Partial): void + { + return this._chart.setDecimalFold(decimalFold); + } + getDecimalFold (): DecimalFold + { + return this._chart.getDecimalFold(); + } + //Still deciding about using type from klinecharts or our own type + setSymbol (symbol: PickPartial): void + { + return this._chart.setSymbol(symbol); + } + //Still deciding about using type from klinecharts or our own type + getSymbol (): Nullable + { + return this._chart.getSymbol(); + } + //Still deciding about using type from klinecharts or our own type + setPeriod (period: Period): void + { + return this._chart.setPeriod(period); + } + //Still deciding about using type from klinecharts or our own type + getPeriod (): Nullable + { + return this._chart.getPeriod(); + } + getDataList (): KLineData[] + { + return this._chart.getDataList(); + } + setOffsetRightDistance (distance: number): void + { + return this._chart.setOffsetRightDistance(distance); + } + getOffsetRightDistance (): number + { + return this._chart.getOffsetRightDistance(); + } + setMaxOffsetLeftDistance (distance: number): void + { + return this._chart.setMaxOffsetLeftDistance(distance); + } + setMaxOffsetRightDistance (distance: number): void + { + return this._chart.setMaxOffsetRightDistance(distance); + } + setLeftMinVisibleBarCount (barCount: number): void + { + return this._chart.setLeftMinVisibleBarCount(barCount); + } + setRightMinVisibleBarCount (barCount: number): void + { + return this._chart.setRightMinVisibleBarCount(barCount); + } + setBarSpace (space: number): void + { + return this._chart.setBarSpace(space); + } + getBarSpace (): BarSpace + { + return this._chart.getBarSpace(); + } + getVisibleRange (): VisibleRange + { + return this._chart.getVisibleRange(); + } + setDataLoader (dataLoader: DataLoader): void + { + return this._chart.setDataLoader(dataLoader); + } + overrideIndicator (override: IndicatorCreate): boolean + { + return this._chart.overrideIndicator(override); + } + removeIndicator (filter?: IndicatorFilter): boolean + { + return this._chart.removeIndicator(filter); + } + overrideOverlay (override: Partial): boolean + { + return this._chart.overrideOverlay(override); + } + removeOverlay (filter?: OverlayFilter): boolean + { + return this._chart.removeOverlay(filter); + } + setZoomEnabled (enabled: boolean): void + { + return this._chart.setZoomEnabled(enabled); + } + isZoomEnabled (): boolean + { + return this._chart.isZoomEnabled(); + } + setZoomBehavior (behavior: ZoomBehavior): void + { + this._chart.setZoomBehavior(behavior); + } + zoomBehavior (): ZoomBehavior + { + return this._chart.zoomBehavior(); + } + setScrollEnabled (enabled: boolean): void + { + return this._chart.setScrollEnabled(enabled); + } + isScrollEnabled (): boolean + { + return this._chart.isScrollEnabled(); + } + resetData (): void + { + return this._chart.resetData(); + } + + /** + * Custom methods + */ + + setActiveChart (id: string) { + const chart = this._charts.get(id); + if (chart) { + this._chart = chart; + } + + return this + } + + chartById (id: string): KLineChart | undefined { + return this._charts.get(id); + } + + getOverlay (filter?: PickRequired): Overlay[] { + return this._chart.getOverlays(filter); + } + + createOrderLine (options?: UndoOptions): Nullable { + const dataList = this._chart.getDataList() + const overlays = this._chart.createOverlay({ + name: 'orderLine', + paneId: 'candle_pane', + points: [{ + timestamp: dataList[dataList.length - 40].timestamp, + value: dataList[dataList.length - 40].close + }] + }); + if (!overlays) { + return null + } + + ///@ts-expect-error + return this._chart.getOverlays({ id: overlays as string, paneId: 'candle_pane' })[0] as Nullable; + } + + createOrderLines (nums: number, options?: UndoOptions): Array> { + const points: Array> = [] + const dataList = this._chart.getDataList() + const step = Math.floor(dataList.length / (nums + 1)) + for (let i = 1; i <= nums; i++) { + points.push({ + timestamp: dataList[step * i].timestamp, + value: dataList[step * i].close + }) + } + + const values: OverlayCreate = { + name: 'orderLine', + paneId: 'candle_pane', + points: points + } + const overlays = this._chart.createOverlay(values); + + if (!overlays || (isArray(overlays) && overlays.length === 0)) { + return []; + } + + ///@ts-expect-error + return (overlays as Array).map(o => this._chart.getOverlays({ id: o!, paneId: 'canlde_pane' })[0]) as Array>; + } +} \ No newline at end of file diff --git a/src/ChartProComponent.tsx b/src/ChartProComponent.tsx index 47696350..6a1eeb59 100644 --- a/src/ChartProComponent.tsx +++ b/src/ChartProComponent.tsx @@ -15,8 +15,9 @@ import { createSignal, createEffect, onMount, Show, onCleanup, startTransition, Component } from 'solid-js' import { - init, dispose, utils, Nullable, Chart, OverlayMode, Styles, - TooltipIconPosition, ActionType, PaneOptions, Indicator, DomPosition, FormatDateType + init, dispose, utils, Nullable, OverlayMode, Styles, + PaneOptions, Indicator, FormatDateParams, TooltipFeatureStyle, + IndicatorTooltipData } from 'klinecharts' import lodashSet from 'lodash/set' @@ -31,54 +32,27 @@ import { import { translateTimezone } from './widget/timezone-modal/data' -import { SymbolInfo, Period, ChartProOptions, ChartPro } from './types' - -export interface ChartProComponentProps extends Required> { - ref: (chart: ChartPro) => void -} +import { SymbolInfo, Period, ChartProOptions, ChartPro, ProChart } from './types/types' +import ChartDataLoader from './DataLoader' +import Chart from './Chart' +import { ChartProComponentProps, instanceapi, loadingVisible, period, setInstanceapi, setPeriod, setSymbol, symbol } from './store/chartStore' +import { useChartState } from './store/chartStateStore' +const { createIndicator, modifyIndicator, popIndicator, pushOverlay, pushMainIndicator, pushSubIndicator, redraOverlaysIndiAndFigs } = useChartState() interface PrevSymbolPeriod { symbol: SymbolInfo period: Period } -function createIndicator (widget: Nullable, indicatorName: string, isStack?: boolean, paneOptions?: PaneOptions): Nullable { - if (indicatorName === 'VOL') { - paneOptions = { gap: { bottom: 2 }, ...paneOptions } - } - return widget?.createIndicator({ - name: indicatorName, - // @ts-expect-error - createTooltipDataSource: ({ indicator, defaultStyles }) => { - const icons = [] - if (indicator.visible) { - icons.push(defaultStyles.tooltip.icons[1]) - icons.push(defaultStyles.tooltip.icons[2]) - icons.push(defaultStyles.tooltip.icons[3]) - } else { - icons.push(defaultStyles.tooltip.icons[0]) - icons.push(defaultStyles.tooltip.icons[2]) - icons.push(defaultStyles.tooltip.icons[3]) - } - return { icons } - } - }, isStack, paneOptions) ?? null -} - const ChartProComponent: Component = props => { let widgetRef: HTMLDivElement | undefined = undefined - let widget: Nullable = null let priceUnitDom: HTMLElement - let loading = false - const [theme, setTheme] = createSignal(props.theme) const [styles, setStyles] = createSignal(props.styles) const [locale, setLocale] = createSignal(props.locale) - const [symbol, setSymbol] = createSignal(props.symbol) - const [period, setPeriod] = createSignal(props.period) const [indicatorModalVisible, setIndicatorModalVisible] = createSignal(false) const [mainIndicators, setMainIndicators] = createSignal([...(props.mainIndicators!)]) const [subIndicators, setSubIndicators] = createSignal({}) @@ -95,124 +69,76 @@ const ChartProComponent: Component = props => { const [symbolSearchModalVisible, setSymbolSearchModalVisible] = createSignal(false) - const [loadingVisible, setLoadingVisible] = createSignal(false) - const [indicatorSettingModalParams, setIndicatorSettingModalParams] = createSignal({ visible: false, indicatorName: '', paneId: '', calcParams: [] as Array }) + setPeriod(props.period) + setSymbol(props.symbol) props.ref({ setTheme, getTheme: () => theme(), setStyles, - getStyles: () => widget!.getStyles(), + getStyles: () => instanceapi()!.getStyles(), setLocale, getLocale: () => locale(), setTimezone: (timezone: string) => { setTimezone({ key: timezone, text: translateTimezone(props.timezone, locale()) }) }, getTimezone: () => timezone().key, setSymbol, - getSymbol: () => symbol(), + getSymbol: () => symbol()!, setPeriod, - getPeriod: () => period() + getPeriod: () => period()!, + getInstanceApi: () => instanceapi(), + resize: () => instanceapi()?.resize(), + dispose: () => {} }) const documentResize = () => { - widget?.resize() - } - - const adjustFromTo = (period: Period, toTimestamp: number, count: number) => { - let to = toTimestamp - let from = to - switch (period.timespan) { - case 'minute': { - to = to - (to % (60 * 1000)) - from = to - count * period.multiplier * 60 * 1000 - break - } - case 'hour': { - to = to - (to % (60 * 60 * 1000)) - from = to - count * period.multiplier * 60 * 60 * 1000 - break - } - case 'day': { - to = to - (to % (60 * 60 * 1000)) - from = to - count * period.multiplier * 24 * 60 * 60 * 1000 - break - } - case 'week': { - const date = new Date(to) - const week = date.getDay() - const dif = week === 0 ? 6 : week - 1 - to = to - dif * 60 * 60 * 24 - const newDate = new Date(to) - to = new Date(`${newDate.getFullYear()}-${newDate.getMonth() + 1}-${newDate.getDate()}`).getTime() - from = count * period.multiplier * 7 * 24 * 60 * 60 * 1000 - break - } - case 'month': { - const date = new Date(to) - const year = date.getFullYear() - const month = date.getMonth() + 1 - to = new Date(`${year}-${month}-01`).getTime() - from = count * period.multiplier * 30 * 24 * 60 * 60 * 1000 - const fromDate = new Date(from) - from = new Date(`${fromDate.getFullYear()}-${fromDate.getMonth() + 1}-01`).getTime() - break - } - case 'year': { - const date = new Date(to) - const year = date.getFullYear() - to = new Date(`${year}-01-01`).getTime() - from = count * period.multiplier * 365 * 24 * 60 * 60 * 1000 - const fromDate = new Date(from) - from = new Date(`${fromDate.getFullYear()}-01-01`).getTime() - break - } - } - return [from, to] + instanceapi()?.resize() } onMount(() => { window.addEventListener('resize', documentResize) - widget = init(widgetRef!, { - customApi: { - formatDate: (dateTimeFormat: Intl.DateTimeFormat, timestamp, format: string, type: FormatDateType) => { - const p = period() - switch (p.timespan) { + setInstanceapi(Chart.init(widgetRef!, { + formatter: { + formatDate: (params: FormatDateParams) => { + const p = period()! + switch (p.type) { case 'minute': { - if (type === FormatDateType.XAxis) { - return utils.formatDate(dateTimeFormat, timestamp, 'HH:mm') + if (params.type === 'xAxis') { + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'HH:mm') } - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD HH:mm') + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD HH:mm') } case 'hour': { - if (type === FormatDateType.XAxis) { - return utils.formatDate(dateTimeFormat, timestamp, 'MM-DD HH:mm') + if (params.type === 'xAxis') { + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'MM-DD HH:mm') } - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD HH:mm') + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD HH:mm') } case 'day': - case 'week': return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD') + case 'week': return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD') case 'month': { - if (type === FormatDateType.XAxis) { - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM') + if (params.type === 'xAxis') { + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM') } - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD') + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD') } case 'year': { - if (type === FormatDateType.XAxis) { - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY') + if (params.type === 'xAxis') { + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY') } - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD') + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD') } } - return utils.formatDate(dateTimeFormat, timestamp, 'YYYY-MM-DD HH:mm') + return utils.formatDate(params.dateTimeFormat, params.timestamp, 'YYYY-MM-DD HH:mm') } } - }) + })) - if (widget) { - const watermarkContainer = widget.getDom('candle_pane', DomPosition.Main) + if (instanceapi()) { + console.info('ChartPro widget initialized') + const watermarkContainer = instanceapi()!.getDom('candle_pane', 'main') if (watermarkContainer) { let watermark = document.createElement('div') watermark.className = 'klinecharts-pro-watermark' @@ -225,71 +151,96 @@ const ChartProComponent: Component = props => { watermarkContainer.appendChild(watermark) } - const priceUnitContainer = widget.getDom('candle_pane', DomPosition.YAxis) + const priceUnitContainer = instanceapi()!.getDom('candle_pane', 'yAxis') priceUnitDom = document.createElement('span') priceUnitDom.className = 'klinecharts-pro-price-unit' priceUnitContainer?.appendChild(priceUnitDom) - } - mainIndicators().forEach(indicator => { - createIndicator(widget, indicator, true, { id: 'candle_pane' }) - }) - const subIndicatorMap = {} - props.subIndicators!.forEach(indicator => { - const paneId = createIndicator(widget, indicator, true) - if (paneId) { - // @ts-expect-error - subIndicatorMap[indicator] = paneId - } - }) - setSubIndicators(subIndicatorMap) - widget?.loadMore(timestamp => { - loading = true - const get = async () => { - const p = period() - const [to] = adjustFromTo(p, timestamp!, 1) - const [from] = adjustFromTo(p, to, 500) - const kLineDataList = await props.datafeed.getHistoryKLineData(symbol(), p, from, to) - widget?.applyMoreData(kLineDataList, kLineDataList.length > 0) - loading = false - } - get() - }) - widget?.subscribeAction(ActionType.OnTooltipIconClick, (data) => { - if (data.indicatorName) { - switch (data.iconId) { - case 'visible': { - widget?.overrideIndicator({ name: data.indicatorName, visible: true }, data.paneId) - break - } - case 'invisible': { - widget?.overrideIndicator({ name: data.indicatorName, visible: false }, data.paneId) - break - } - case 'setting': { - const indicator = widget?.getIndicatorByPaneId(data.paneId, data.indicatorName) as Indicator - setIndicatorSettingModalParams({ - visible: true, indicatorName: data.indicatorName, paneId: data.paneId, calcParams: indicator.calcParams - }) - break - } - case 'close': { - if (data.paneId === 'candle_pane') { - const newMainIndicators = [...mainIndicators()] - widget?.removeIndicator('candle_pane', data.indicatorName) - newMainIndicators.splice(newMainIndicators.indexOf(data.indicatorName), 1) - setMainIndicators(newMainIndicators) - } else { - const newIndicators = { ...subIndicators() } - widget?.removeIndicator(data.paneId, data.indicatorName) - // @ts-expect-error - delete newIndicators[data.indicatorName] - setSubIndicators(newIndicators) + instanceapi()?.setZoomBehavior({ main: 'last_bar', xAxis: 'last_bar'}) + + instanceapi()?.subscribeAction('onCrosshairFeatureClick', (data) => { + console.info('onCrosshairFeatureClick', data) + }) + + instanceapi()?.subscribeAction('onIndicatorTooltipFeatureClick', (data) => { + console.info('onIndicatorTooltipFeatureClick', data) + const _data = data as { paneId: string, feature: TooltipFeatureStyle, indicator: Indicator } + // if (_data.indicatorName) { + switch (_data.feature.id) { + case 'visible': { + instanceapi()?.overrideIndicator({ name: _data.indicator.name, visible: true, paneId: _data.paneId }) + break + } + case 'invisible': { + instanceapi()?.overrideIndicator({ name: _data.indicator.name, visible: false, paneId: _data.paneId }) + break + } + case 'setting': { + const indicator = instanceapi()?.getIndicators({ paneId: _data.paneId, name: _data.indicator.name, id: _data.indicator.id }).at(0) + if (!indicator) return + setIndicatorSettingModalParams({ + visible: true, indicatorName: _data.indicator.name, paneId: _data.paneId, calcParams: indicator.calcParams + }) + break + } + case 'close': { + if (_data.paneId === 'candle_pane') { + const newMainIndicators = [...mainIndicators()] + instanceapi()?.removeIndicator({ paneId: _data.paneId, name: _data.indicator.name, id: _data.indicator.id }) + newMainIndicators.splice(newMainIndicators.indexOf(_data.indicator.name), 1) + setMainIndicators(newMainIndicators) + } else { + const newIndicators = { ...subIndicators() } + instanceapi()?.removeIndicator({ paneId: _data.paneId, name: _data.indicator.name, id: _data.indicator.id }) + // @ts-expect-error + delete newIndicators[_data.indicator.name] + setSubIndicators(newIndicators) + } } } - } + // } + }) + + instanceapi()?.subscribeAction('onCandleTooltipFeatureClick', (data) => { + console.info('onCandleTooltipFeatureClick', data) + }) + + instanceapi()?.subscribeAction('onZoom', (data) => { + console.info('chart zoomed: ', data) + }) + + instanceapi()?.subscribeAction('onCrosshairChange', (data) => { + console.info('crosshair change: ', data) + }) + + const s = symbol() + if (s?.priceCurrency) { + priceUnitDom.innerHTML = s?.priceCurrency.toLocaleUpperCase() + priceUnitDom.style.display = 'flex' + } else { + priceUnitDom.style.display = 'none' } - }) + instanceapi()?.setSymbol({ ticker: s!.ticker, pricePrecision: s?.pricePrecision ?? 2, volumePrecision: s?.volumePrecision ?? 0 }) + instanceapi()?.setPeriod(period()!) + instanceapi()?.setDataLoader(props.dataloader) + } + + const w = instanceapi() + + if (w) { + mainIndicators().forEach(indicator => { + createIndicator(w, indicator, true, { id: 'candle_pane' }) + }) + const subIndicatorMap = {} + props.subIndicators!.forEach(indicator => { + const paneId = createIndicator(w, indicator, true) + if (paneId) { + // @ts-expect-error + subIndicatorMap[indicator] = paneId + } + }) + setSubIndicators(subIndicatorMap) + } }) onCleanup(() => { @@ -297,63 +248,61 @@ const ChartProComponent: Component = props => { dispose(widgetRef!) }) - createEffect(() => { - const s = symbol() - if (s?.priceCurrency) { - priceUnitDom.innerHTML = s?.priceCurrency.toLocaleUpperCase() - priceUnitDom.style.display = 'flex' - } else { - priceUnitDom.style.display = 'none' - } - widget?.setPriceVolumePrecision(s?.pricePrecision ?? 2, s?.volumePrecision ?? 0) - }) - createEffect((prev?: PrevSymbolPeriod) => { - if (!loading) { - if (prev) { - props.datafeed.unsubscribe(prev.symbol, prev.period) - } + console.info('symbol or period changed effect', symbol(), period(), prev) + + if (!props.dataloader.loading) { + console.info('setLoadingVisible false by effect') const s = symbol() const p = period() - loading = true - setLoadingVisible(true) - const get = async () => { - const [from, to] = adjustFromTo(p, new Date().getTime(), 500) - const kLineDataList = await props.datafeed.getHistoryKLineData(s, p, from, to) - widget?.applyNewData(kLineDataList, kLineDataList.length > 0) - props.datafeed.subscribe(s, p, data => { - widget?.updateData(data) - }) - loading = false - setLoadingVisible(false) + + if (prev?.period.span !== p!.span && prev?.period.type !== p!.type) { + console.info('period changed: set period', p) + instanceapi()?.setPeriod(p!) } - get() - return { symbol: s, period: p } + if (prev?.symbol?.ticker !== s!.ticker) + console.info('ticker changed: set symbol', s) + instanceapi()?.setSymbol({ + ticker: s!.ticker, + pricePrecision: s!.pricePrecision, + volumePrecision: s!.volumePrecision, + }) + + onCleanup(() => { + // Optional cleanup logic before re-run + }) + + return { symbol: s!, period: p! } } + console.info('props.dataloader.loading is true, skip setLoadingVisible false') + return prev }) createEffect(() => { const t = theme() - widget?.setStyles(t) + instanceapi()?.setStyles(t) const color = t === 'dark' ? '#929AA5' : '#76808F' - widget?.setStyles({ + instanceapi()?.setStyles({ indicator: { tooltip: { - icons: [ + features: [ { id: 'visible', - position: TooltipIconPosition.Middle, + position: 'middle', marginLeft: 8, - marginTop: 7, + marginTop: 1, marginRight: 0, marginBottom: 0, paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, - icon: '\ue903', - fontFamily: 'icomoon', + type: 'icon_font', + content: { + code: '\ue903', + family: 'icomoon', + }, size: 14, color: color, activeColor: color, @@ -362,17 +311,20 @@ const ChartProComponent: Component = props => { }, { id: 'invisible', - position: TooltipIconPosition.Middle, + position: 'middle', marginLeft: 8, - marginTop: 7, + marginTop: 1, marginRight: 0, marginBottom: 0, paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, - icon: '\ue901', - fontFamily: 'icomoon', + type: 'icon_font', + content: { + code: '\ue901', + family: 'icomoon', + }, size: 14, color: color, activeColor: color, @@ -381,17 +333,20 @@ const ChartProComponent: Component = props => { }, { id: 'setting', - position: TooltipIconPosition.Middle, + position: 'middle', marginLeft: 6, - marginTop: 7, + marginTop: 1, marginBottom: 0, marginRight: 0, paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, - icon: '\ue902', - fontFamily: 'icomoon', + type: 'icon_font', + content: { + code: '\ue902', + family: 'icomoon', + }, size: 14, color: color, activeColor: color, @@ -400,17 +355,20 @@ const ChartProComponent: Component = props => { }, { id: 'close', - position: TooltipIconPosition.Middle, + position: 'middle', marginLeft: 6, - marginTop: 7, + marginTop: 1, marginRight: 0, marginBottom: 0, paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, - icon: '\ue900', - fontFamily: 'icomoon', + type: 'icon_font', + content: { + code: '\ue900', + family: 'icomoon', + }, size: 14, color: color, activeColor: color, @@ -424,17 +382,17 @@ const ChartProComponent: Component = props => { }) createEffect(() => { - widget?.setLocale(locale()) + instanceapi()?.setLocale(locale()) }) createEffect(() => { - widget?.setTimezone(timezone().key) + instanceapi()?.setTimezone(timezone().key) }) createEffect(() => { if (styles()) { - widget?.setStyles(styles()) - setWidgetDefaultStyles(lodashClone(widget!.getStyles())) + instanceapi()?.setStyles(styles()) + setWidgetDefaultStyles(lodashClone(instanceapi()!.getStyles())) } }) @@ -444,7 +402,7 @@ const ChartProComponent: Component = props => { { setSymbol(symbol) }} onClose={() => { setSymbolSearchModalVisible(false) }}/> @@ -457,25 +415,26 @@ const ChartProComponent: Component = props => { onMainIndicatorChange={data => { const newMainIndicators = [...mainIndicators()] if (data.added) { - createIndicator(widget, data.name, true, { id: 'candle_pane' }) + createIndicator(instanceapi()!, data.name, true, { id: 'candle_pane' }) newMainIndicators.push(data.name) } else { - widget?.removeIndicator('candle_pane', data.name) + instanceapi()?.removeIndicator({name: data.name, paneId: 'candle_pane', id: data.id ?? undefined}) newMainIndicators.splice(newMainIndicators.indexOf(data.name), 1) } setMainIndicators(newMainIndicators) }} onSubIndicatorChange={data => { + console.info('onSubIndicatorChange', data) const newSubIndicators = { ...subIndicators() } if (data.added) { - const paneId = createIndicator(widget, data.name) - if (paneId) { + const id = createIndicator(instanceapi()!, data.name) + if (id) { // @ts-expect-error - newSubIndicators[data.name] = paneId + newSubIndicators[data.name] = id } } else { - if (data.paneId) { - widget?.removeIndicator(data.paneId, data.name) + if (data.id) { + instanceapi()?.removeIndicator({name: data.name, id: data.id}) // @ts-expect-error delete newSubIndicators[data.name] } @@ -494,10 +453,10 @@ const ChartProComponent: Component = props => { { setSettingModalVisible(false) }} onChange={style => { - widget?.setStyles(style) + instanceapi()?.setStyles(style) }} onRestoreDefault={(options: SelectDataSourceItem[]) => { const style = {} @@ -505,7 +464,7 @@ const ChartProComponent: Component = props => { const key = option.key lodashSet(style, key, utils.formatValue(widgetDefaultStyles(), key)) }) - widget?.setStyles(style) + instanceapi()?.setStyles(style) }} /> @@ -523,20 +482,20 @@ const ChartProComponent: Component = props => { onClose={() => { setIndicatorSettingModalParams({ visible: false, indicatorName: '', paneId: '', calcParams: [] }) }} onConfirm={(params)=> { const modalParams = indicatorSettingModalParams() - widget?.overrideIndicator({ name: modalParams.indicatorName, calcParams: params }, modalParams.paneId) + instanceapi()?.overrideIndicator({ name: modalParams.indicatorName, calcParams: params, paneId: modalParams.paneId }) }} /> { try { await startTransition(() => setDrawingBarVisible(!drawingBarVisible())) - widget?.resize() + instanceapi()?.resize() } catch (e) {} }} onSymbolClick={() => { setSymbolSearchModalVisible(!symbolSearchModalVisible()) }} @@ -545,8 +504,8 @@ const ChartProComponent: Component = props => { onTimezoneClick={() => { setTimezoneModalVisible((visible => !visible)) }} onSettingClick={() => { setSettingModalVisible((visible => !visible)) }} onScreenshotClick={() => { - if (widget) { - const url = widget.getConvertPictureUrl(true, 'jpeg', props.theme === 'dark' ? '#151517' : '#ffffff') + if (instanceapi()) { + const url = instanceapi()!.getConvertPictureUrl(true, 'jpeg', props.theme === 'dark' ? '#151517' : '#ffffff') setScreenshotUrl(url) } }} @@ -559,11 +518,11 @@ const ChartProComponent: Component = props => { { widget?.createOverlay(overlay) }} - onModeChange={mode => { widget?.overrideOverlay({ mode: mode as OverlayMode }) }} - onLockChange={lock => { widget?.overrideOverlay({ lock }) }} - onVisibleChange={visible => { widget?.overrideOverlay({ visible }) }} - onRemoveClick={(groupId) => { widget?.removeOverlay({ groupId }) }}/> + onDrawingItemClick={overlay => { instanceapi()?.createOverlay(overlay) }} + onModeChange={mode => { instanceapi()?.overrideOverlay({ mode: mode as OverlayMode }) }} + onLockChange={lock => { instanceapi()?.overrideOverlay({ lock }) }} + onVisibleChange={visible => { instanceapi()?.overrideOverlay({ visible }) }} + onRemoveClick={(groupId) => { instanceapi()?.removeOverlay({ groupId }) }}/>
{ + console.info('ChartDataLoader getBars', params); + const { type, timestamp: _t, symbol: _s, period: _p, callback } = params; + if (type === 'backward' || type === 'update') { + console.info('getBars: type is backward or update (no forward support yet)'); + callback([], false); + return; + } + this._loading = true + setLoadingVisible(true) + const timestamp = _t ?? new Date().getTime() + const get = async () => { + const p = period()! + const s =symbol()! + const [to] = this.adjustFromTo(p, timestamp!, 1) + const [from] = this.adjustFromTo(p, to, 500) + const kLineDataList = await this._datafeed.getHistoryKLineData(s, p, from, to) + callback(kLineDataList, kLineDataList.length > 0) + this._loading = false + setLoadingVisible(false) + } + await get(); + } + + subscribeBar (params: DataLoaderSubscribeBarParams): void { + console.info('ChartDataLoader subscribeBar', params); + const { symbol: _s, period: _p, callback } = params; + this._datafeed.subscribe(symbol()!, period()!, callback) + } + + unsubscribeBar (params: DataLoaderUnsubscribeBarParams): void { + console.info('ChartDataLoader unsubscribeBar', params); + const { symbol: _s, period: _p } = params; + this._datafeed.unsubscribe(symbol()!, period()!) + } + + searchSymbols(search?: string): Promise { + return this._datafeed.searchSymbols(search) + } + + get loading(): boolean { + return this._loading; + } + + set loading(value: boolean) { + this._loading = value; + } + + adjustFromTo(period: Period, toTimestamp: number, count: number) { + let to = toTimestamp + let from = to + + switch (period.type) { + case 'minute': + to -= to % (60 * 1000) + from = to - count * period.span * 60 * 1000 + break + + case 'hour': + to -= to % (60 * 60 * 1000) + from = to - count * period.span * 60 * 60 * 1000 + break + + case 'day': + to -= to % (24 * 60 * 60 * 1000) + from = to - count * period.span * 24 * 60 * 60 * 1000 + break + + case 'week': { + const date = new Date(to) + const day = date.getDay() || 7 // Sunday -> 7 + date.setHours(0, 0, 0, 0) + to = date.getTime() - (day - 1) * 24 * 60 * 60 * 1000 + from = to - count * period.span * 7 * 24 * 60 * 60 * 1000 + break + } + + case 'month': { + const date = new Date(to) + to = new Date(date.getFullYear(), date.getMonth(), 1).getTime() + const _from = new Date(to - count * period.span * 30 * 24 * 60 * 60 * 1000) + from = new Date(_from.getFullYear(), _from.getMonth(), 1).getTime() + break + } + + case 'year': { + const date = new Date(to) + to = new Date(date.getFullYear(), 0, 1).getTime() + const _from = new Date(to - count * period.span * 365 * 24 * 60 * 60 * 1000) + from = new Date(_from.getFullYear(), 0, 1).getTime() + break + } + } + + return [from, to] + } +} diff --git a/src/DefaultDatafeed.ts b/src/DefaultDatafeed.ts index 1267a7ee..27862ce5 100644 --- a/src/DefaultDatafeed.ts +++ b/src/DefaultDatafeed.ts @@ -14,7 +14,7 @@ import { KLineData } from 'klinecharts' -import { Datafeed, SymbolInfo, Period, DatafeedSubscribeCallback } from './types' +import { Datafeed, SymbolInfo, Period, DatafeedSubscribeCallback } from './types/types' export default class DefaultDatafeed implements Datafeed { @@ -44,7 +44,7 @@ export default class DefaultDatafeed implements Datafeed { } async getHistoryKLineData (symbol: SymbolInfo, period: Period, from: number, to: number): Promise { - const response = await fetch(`https://api.polygon.io/v2/aggs/ticker/${symbol.ticker}/range/${period.multiplier}/${period.timespan}/${from}/${to}?apiKey=${this._apiKey}`) + const response = await fetch(`https://api.polygon.io/v2/aggs/ticker/${symbol.ticker}/range/${period.span}/${period.type}/${from}/${to}?apiKey=${this._apiKey}`) const result = await response.json() return await (result.results || []).map((data: any) => ({ timestamp: data.t, diff --git a/src/DefaultOrderController.ts b/src/DefaultOrderController.ts new file mode 100644 index 00000000..23a08b3f --- /dev/null +++ b/src/DefaultOrderController.ts @@ -0,0 +1,159 @@ +import { OrderInfo, OrderModalType, OrderModifyInfo, OrderPlacedCallback, OrderResource, OrderType } from "./types/types"; + +type MethodType = 'POST'|'GET'|'DELETE'|'PUT' + +export default class DefaultOrderController implements OrderResource { + constructor (_apiurl: string, _apikey: string, _accound_id: number|string) { + this.apiurl = _apiurl + this.apikey = _apikey + this.testsesson_id = _accound_id + } + + private apikey: string + private apiurl: string + private testsesson_id: number|string + + async retrieveOrder(order_id: number): Promise { + const response = await this.makeFetchWithAuthAndBody('GET', `${this.apiurl}/positions/${order_id}`) + const resp = await response?.json() + return { + entryPoint: resp.data.entrypoint, + stopLoss: resp.data.stoploss, + takeProfit: resp.data.takeprofit, + lotSize: resp.data.lotsize, + pl: resp.data.pl, + accountId: resp.data.account_id, + orderId: resp.data.id, + entryTime: resp.data.entrytime, + exitTime: resp.data.exittime, + exitPoint: resp.data.exitpoint, + action: resp.data.action + } + } + + async retrieveOrders(action?: OrderType, session_id?: number): Promise { + try { + const response = await this.makeFetchWithAuthAndBody('GET', `${this.apiurl}/positions`) + const result = await response!.json() + return (result.data || []).map((data: any) => ({ + entryPoint: data.entrypoint, + stopLoss: data.stoploss, + takeProfit: data.takeprofit, + lotSize: data.lotsize, + pl: data.pl, + accountId: data.account_id, + orderId: data.id, + entryTime: data.entrytime, + exitTime: data.exittime, + exitPoint: data.exitpoint, + action: data.action + })) + } catch (err) { + return [] + } + } + + async openOrder(action: OrderType, lot_size: number, entry_price: number, stop_loss?: number, take_profit?: number): Promise { + const response = await this.makeFetchWithAuthAndBody('POST', `${this.apiurl}/positions`, { + account_id: this.testsesson_id, + action: action, + entrypoint: entry_price, + stoploss: stop_loss, + takeprofit: take_profit, + }) + + const data = await response?.json() + return { + orderId: data.id, + accountId: data.account_id, + action: data.action, + entryPoint: data.entrypoint, + exitPoint: data.exitpoint, + stopLoss: data.stoploss, + takeProfit: data.takeprofit, + lotSize: data.lotsize, + pips: data.pips, + pl: data.pl, + entryTime: data.entrytime, + exitTime: data.exittime, + exitType: data.exittype, + partials: data.partials + } + } + + async closeOrder(order_id: number, lotsize?: number): Promise { + try { + const response = await this.makeFetchWithAuthAndBody('PUT', `${this.apiurl}/positions/${order_id}`) + const data = await response?.json() + return data + } catch (err) { + return null + } + } + + async modifyOrder(order: OrderModifyInfo): Promise { + const response = await this.makeFetchWithAuthAndBody('PUT', `${this.apiurl}/positions/${order.id}`, order) + const data = await response?.json() + return { + orderId: data.id, + accountId: data.account_id, + action: data.action, + entryPoint: data.entrypoint, + exitPoint: data.exitpoint, + stopLoss: data.stoploss, + takeProfit: data.takeprofit, + lotSize: data.lotsize, + pips: data.pips, + pl: data.pl, + entryTime: data.entrytime, + exitTime: data.exittime, + exitType: data.exittype, + partials: data.partials + } + } + + async unsetSlOrTP(order_id: string|number, slortp: 'sl'|'tp'): Promise { + const response = await this.makeFetchWithAuthAndBody('PUT', `${this.apiurl}/positions/${order_id}/unset/${slortp}`) + const data = await response?.json() + return { + orderId: data.id, + accountId: data.account_id, + action: data.action, + entryPoint: data.entrypoint, + exitPoint: data.exitpoint, + stopLoss: data.stoploss, + takeProfit: data.takeprofit, + lotSize: data.lotsize, + pips: data.pips, + pl: data.pl, + entryTime: data.entrytime, + exitTime: data.exittime, + exitType: data.exittype, + partials: data.partials + } + } + + launchOrderModal(type: OrderModalType, callback: OrderPlacedCallback, order?: OrderModifyInfo): void { + return ; + } + + private async makeFetchWithAuthAndBody (method: MethodType, endpoint: string, params?: object): Promise { + const options: RequestInit = { + method: method, + credentials: "include", + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${this.apikey}` + }, + body: params ? JSON.stringify(params) : null + }; + try { + const res = await fetch(`${endpoint}`, options) + return res + } catch (err:any) { + alert(err.message ?? 'An error occured') + return null + } + } +} \ No newline at end of file diff --git a/src/DummyOrderController.ts b/src/DummyOrderController.ts new file mode 100644 index 00000000..5c9c0094 --- /dev/null +++ b/src/DummyOrderController.ts @@ -0,0 +1,137 @@ +import { Nullable } from "klinecharts"; +import { OrderInfo, OrderModalType, OrderModifyInfo, OrderPlacedCallback, OrderResource, OrderType } from "./types/types"; + +export default class DummyOrderController implements OrderResource { + constructor (storage_name = 'dummy_order_controller') { + this.storage_name = storage_name + } + + private storage_name: string + + async retrieveOrder(order_id: number): Promise> { + return this.retrieveOrderFromLs(order_id) + } + + async retrieveOrders(action?: OrderType, account_id?: number): Promise { + let orders = this.retrieveOrdersFromLs() + + if (action) + orders = orders.filter((o, i) => o.action == action) + + if (account_id) + orders = orders.filter(o => o.accountId == account_id) + + return orders + } + + async openOrder(action: OrderType, size: number, entry: number, stop_loss?: number, take_profit?: number): Promise { + const orders = this.retrieveOrdersFromLs() + + const order = { + orderId: orders.length ? orders.at(orders.length - 1)!.orderId + 1 : 0, + action: action, + entryPoint: entry, + exitPoint: undefined, + stopLoss: stop_loss, + takeProfit: take_profit, + lotSize: size, + pips: 0, + pl: 0, + entryTime: new Date().getTime().toString(), + exitTime: undefined, + exitType: undefined, + partials: undefined + } + this.storeOrder(order, orders) + return order + } + + async closeOrder(order_id: number, lotsize?: number): Promise { + try { + const orders = this.retrieveOrdersFromLs() + const index = orders.findIndex(o => o.orderId == order_id) + if (index < 0) + return null + + const order = orders[index] + + if (!lotsize || lotsize > order.lotSize) + lotsize = order.lotSize + + order.lotSize -= lotsize + this.storeOrder(order, orders, index) + return order + } catch (err) { + return null + } + } + + async modifyOrder(_order: OrderModifyInfo): Promise { + const orders = this.retrieveOrdersFromLs() + const index = orders.findIndex(o => o.orderId == _order.id) + if (index < 0) + return null + + const order = orders[index] + //TODO: add order modify validations to prevent performing some actions on already activated order + + order.action = _order.action ?? order.action + order.entryPoint = _order.entrypoint ?? order.entryPoint + order.exitPoint = _order.exitpoint ?? order.exitPoint + order.stopLoss = _order.stoploss ?? order.stopLoss + order.takeProfit = _order.takeprofit ?? order.takeProfit + order.lotSize = _order.lotsize ?? order.lotSize + order.pips = _order.pips ?? order.pips + order.pl = _order.pl ?? order.pl + order.entryTime = ['buy', 'sell'].includes(order.action) ? new Date().getTime().toString() : order.entryTime + order.exitTime = order.exitPoint && order.exitTime ? new Date().getTime().toString() : order.exitTime + + this.storeOrder(order, orders, index) + return order + } + + async unsetSlOrTP(order_id: string|number, slortp: 'sl'|'tp'): Promise { + const orders = this.retrieveOrdersFromLs() + const index = orders.findIndex(o => o.orderId == order_id) + if (index < 0) + return null + + const order = orders[index] + if (slortp == 'sl') + order.stopLoss = undefined + else + order.takeProfit = undefined + + this.storeOrder(order, orders, index) + return order + } + + launchOrderModal(type: OrderModalType, callback: OrderPlacedCallback, order?: OrderModifyInfo): void { + return ; + } + + private retrieveOrdersFromLs (): OrderInfo[] { + return JSON.parse(localStorage.getItem(this.storage_name) ?? '[]') as OrderInfo[] + } + + private retrieveOrderFromLs(id: number): OrderInfo|null + { + return this.retrieveOrdersFromLs().find(o => o.orderId == id) ?? null + } + + private storeOrder(order: OrderInfo, orders?: OrderInfo[], index?: number) + { + if (!orders) + orders = this.retrieveOrdersFromLs() + + if (index && index > orders.length) + throw new Error('storeOrder: index cannot be greater than total order length') + + if (index) + orders[index] = order + else + orders.push(order) + + localStorage.setItem(this.storage_name, JSON.stringify(orders)) + } +} \ No newline at end of file diff --git a/src/KLineChartPro.tsx b/src/KLineChartPro.tsx index bac9e893..f7b88876 100644 --- a/src/KLineChartPro.tsx +++ b/src/KLineChartPro.tsx @@ -14,11 +14,13 @@ import { render } from 'solid-js/web' -import { utils, Nullable, DeepPartial, Styles } from 'klinecharts' +import { utils, Nullable, DeepPartial, Styles, dispose } from 'klinecharts' import ChartProComponent from './ChartProComponent' -import { SymbolInfo, Period, ChartPro, ChartProOptions } from './types' +import { SymbolInfo, Period, ChartPro, ChartProOptions, ProChart } from './types/types' +import ChartDataLoader from './DataLoader' +import { instanceapi } from './store/chartStore' const Logo = (
{ setOpen(o => !o) }} + // onBlur={_ => { + // if (!rangeFocused()) { + // closeColorPallete(); + // } + // }} + > +
{ + setOpen(true) + }} + > + {/* {props.value} */} + +
+ + +
+ ) +} +// style={`background-color: ${finalColor()}; border: 1px solid ${selectedColor()}`} + +export default Color diff --git a/src/component/index.less b/src/component/index.less index 5bf0a0cf..83104d6c 100644 --- a/src/component/index.less +++ b/src/component/index.less @@ -1,5 +1,6 @@ @import './button/index.less'; @import './checkbox/index.less'; +@import './color/index.less'; @import './list/index.less'; @import './modal/index.less'; @import './select/index.less'; diff --git a/src/component/index.tsx b/src/component/index.tsx index 981b2c2a..59d853a2 100644 --- a/src/component/index.tsx +++ b/src/component/index.tsx @@ -20,9 +20,10 @@ import Select, { SelectDataSourceItem } from './select' import Input from './input' import Loading from './loading' import Switch from './switch' +import Color from './color' export { - Button, Checkbox, List, Modal, Select, Input, Loading, Switch + Button, Checkbox, List, Modal, Select, Input, Loading, Switch, Color } export type { SelectDataSourceItem } \ No newline at end of file diff --git a/src/component/input/index.tsx b/src/component/input/index.tsx index 0e295d14..fc8db81c 100644 --- a/src/component/input/index.tsx +++ b/src/component/input/index.tsx @@ -12,7 +12,7 @@ * limitations under the License. */ -import { JSX, Component, mergeProps, Show, createSignal } from 'solid-js' +import { JSX, Component, mergeProps, Show, createSignal, createEffect } from 'solid-js' export interface InputProps { class?: string @@ -25,34 +25,47 @@ export interface InputProps { placeholder?: string value: string | number disabled?: boolean - onChange?: (v: string | number) => void + focus?: boolean + onChange?: (v: string | number) => void + onKeyDown?: (event: KeyboardEvent) => void } +export const [inputClass, setInputClass] = createSignal('klinecharts-pro-input klinecharts-pro-timeframe-modal-input') const Input: Component = p => { const props = mergeProps({ min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }, p) - let input: HTMLInputElement + let input: HTMLInputElement|undefined const [status, setStatus] = createSignal('normal') + const focusInput = (element: HTMLInputElement | null) => { + if (element) { + element.focus(); + } + }; + + createEffect(() => { + focusInput(document.querySelector("#myInput")); + }); return (
{ input?.focus() }}> {props.prefix} { input = el }} + id="myInput" + ref={input} class="value" placeholder={props.placeholder ?? ''} value={props.value} + autofocus={props.focus === true ? props.focus : false} onFocus={() => { setStatus('focus') }} onBlur={() => { setStatus('normal') }} onChange={(e) => { - // @ts-expect-error - const v = e.target.value + const v = (e.target as HTMLInputElement).value if ('precision' in props) { let reg const decimalDigit = Math.max(0, Math.floor(props.precision!)) @@ -67,7 +80,8 @@ const Input: Component = p => { } else { props.onChange?.(v) } - }}/> + }} + onKeyDown={props.onKeyDown}/> {props.suffix} diff --git a/src/extension/fibonacciExtension.ts b/src/extension/fibonacciExtension.ts index a7035aec..cb6adc38 100644 --- a/src/extension/fibonacciExtension.ts +++ b/src/extension/fibonacciExtension.ts @@ -20,10 +20,20 @@ const fibonacciExtension: OverlayTemplate = { needDefaultPointFigure: true, needDefaultXAxisFigure: true, needDefaultYAxisFigure: true, - createPointFigures: ({ coordinates, overlay, precision }) => { + createPointFigures: ({ chart, yAxis, coordinates, overlay }) => { const fbLines: LineAttrs[] = [] const texts: TextAttrs[] = [] if (coordinates.length > 2) { + let precision = 0 + const symbol = chart.getSymbol() + if ((yAxis?.isInCandle() ?? true) && symbol) { + precision = symbol.pricePrecision + } else { + const indicators = chart.getIndicators({ paneId: overlay.paneId }) + indicators.forEach(indicator => { + precision = Math.max(precision, indicator.precision) + }) + } const points = overlay.points // @ts-expect-error const valueDif = points[1].value - points[0].value diff --git a/src/extension/fibonacciSegment.ts b/src/extension/fibonacciSegment.ts index 3d8439bd..fabe84db 100644 --- a/src/extension/fibonacciSegment.ts +++ b/src/extension/fibonacciSegment.ts @@ -20,10 +20,20 @@ const fibonacciSegment: OverlayTemplate = { needDefaultPointFigure: true, needDefaultXAxisFigure: true, needDefaultYAxisFigure: true, - createPointFigures: ({ coordinates, overlay, precision }) => { + createPointFigures: ({ coordinates, overlay, chart, yAxis }) => { const lines: LineAttrs[] = [] const texts: TextAttrs[] = [] if (coordinates.length > 1) { + let precision = 0 + const symbol = chart.getSymbol() + if ((yAxis?.isInCandle() ?? true) && symbol) { + precision = symbol.pricePrecision + } else { + const indicators = chart.getIndicators({ paneId: overlay.paneId }) + indicators.forEach(indicator => { + precision = Math.max(precision, indicator.precision) + }) + } const textX = coordinates[1].x > coordinates[0].x ? coordinates[0].x : coordinates[1].x const percents = [1, 0.786, 0.618, 0.5, 0.382, 0.236, 0] const yDif = coordinates[0].y - coordinates[1].y @@ -56,4 +66,4 @@ const fibonacciSegment: OverlayTemplate = { } } -export default fibonacciSegment +export default fibonacciSegment \ No newline at end of file diff --git a/src/extension/index.ts b/src/extension/index.ts index 89a44b15..dfd17bee 100644 --- a/src/extension/index.ts +++ b/src/extension/index.ts @@ -17,12 +17,15 @@ import anyWaves from './anyWaves' import abcd from './abcd' import xabcd from './xabcd' +import positionLine from './position/orderLine' + const overlays = [ arrow, circle, rect, triangle, parallelogram, fibonacciCircle, fibonacciSegment, fibonacciSpiral, fibonacciSpeedResistanceFan, fibonacciExtension, gannBox, - threeWaves, fiveWaves, eightWaves, anyWaves, abcd, xabcd + threeWaves, fiveWaves, eightWaves, anyWaves, abcd, xabcd, + positionLine(), ] export default overlays diff --git a/src/extension/position/orderLine.ts b/src/extension/position/orderLine.ts new file mode 100644 index 00000000..77ef9637 --- /dev/null +++ b/src/extension/position/orderLine.ts @@ -0,0 +1,404 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Coordinate, LineStyle, LineType, OverlayEvent, Point, TextStyle, utils } from 'klinecharts' +import { OrderLineProperties, OrderOverlay, OrderOverlayCreate, OverlayEventListenerParams } from '../../types/overlayTypes' +import { buyStyle } from '../../store/overlayStyle/positionStyleStore' +import { convertFontweightNameToNumber, getPrecision } from '../../helpers' +import { FontWeights } from "../../types/types" +// import { useOverlaySettings } from '../../../store/overlaySettingStore' + +const executeCallback = (listener?: OverlayEventListenerParams, event?: OverlayEvent) => { + if (listener) { + const { params, callback } = listener + callback(params, event) + } +} + +const OrderLine = (): OrderOverlayCreate => { + let properties: OrderLineProperties = { + isBodyVisible: true, + isCancelButtonVisible: true, + isQuantityVisible: true, + marginRight: 10 + } + + const lineStyle = (): LineStyle => { + return { + style: properties.lineStyle ?? buyStyle().lineStyle.style, + size: properties.lineWidth ?? buyStyle().lineStyle.size, + color: properties.lineColor ?? buyStyle().lineStyle.color, + dashedValue: properties.lineDashedValue ?? buyStyle().lineStyle.dashedValue + } + } + + const labelStyle = (type: 'body'|'quantity'|'cancel-button'): TextStyle => { + return { + style: buyStyle().labelStyle.style, + size: (type == 'body' ? properties.bodySize : type == 'quantity' ? properties.quantitySize : properties.cancelButtonSize) ?? properties.bodySize ?? buyStyle().labelStyle.size, + weight: (type == 'body' ? properties.bodyWeight : type == 'quantity' ? properties.quantityWeight : properties.cancelButtonWeight) ?? properties.bodyWeight ?? buyStyle().labelStyle.weight, + family: (type == 'body' ? properties.bodyFont : type == 'quantity' ? properties.quantityFont : 'icomoon') ?? properties.bodyFont ?? buyStyle().labelStyle.family, + color: (type == 'body' ? properties.bodyTextColor : type == 'quantity' ? properties.quantityColor : properties.cancelButtonIconColor) ?? properties.bodyTextColor ?? buyStyle().labelStyle.color, + backgroundColor: (type == 'body' ? properties.bodyBackgroundColor : type == 'quantity' ? properties.quantityBackgroundColor : properties.cancelButtonBackgroundColor) ?? properties.bodyBackgroundColor ?? buyStyle().labelStyle.backgroundColor, + borderColor: (type == 'body' ? properties.bodyBorderColor : type == 'quantity' ? properties.quantityBorderColor : properties.cancelButtonBorderColor) ?? properties.bodyBorderColor ?? buyStyle().labelStyle.borderColor, + borderStyle: properties.borderStyle ?? buyStyle().labelStyle.borderStyle, + borderSize: properties.borderSize ?? buyStyle().labelStyle.borderSize, + borderDashedValue: properties.borderDashedValue ?? buyStyle().labelStyle.borderDashedValue, + borderRadius: properties.borderRadius ?? buyStyle().labelStyle.borderRadius, + paddingLeft: (type == 'body' ? properties.bodyPaddingLeft : type == 'quantity' ? properties.quantityPaddingLeft : properties.cancelButtonPaddingLeft) ?? properties.bodyPaddingLeft ?? buyStyle().labelStyle.paddingLeft, + paddingRight: (type == 'body' ? properties.bodyPaddingRight : type == 'quantity' ? properties.quantityPaddingRight : properties.cancelButtonPaddingRight) ?? properties.bodyPaddingRight ?? buyStyle().labelStyle.paddingRight, + paddingTop: (type == 'body' ? properties.bodyPaddingTop : type == 'quantity' ? properties.quantityPaddingTop : properties.cancelButtonPaddingTop) ?? properties.bodyPaddingTop ?? buyStyle().labelStyle.paddingTop, + paddingBottom: (type == 'body' ? properties.bodyPaddingBottom : type == 'quantity' ? properties.quantityPaddingBottom : properties.cancelButtonPaddingBottom) ?? properties.bodyPaddingBottom ?? buyStyle().labelStyle.paddingBottom + }} + + return { + name: 'orderLine', + totalStep: 2, + needDefaultPointFigure: false, + needDefaultXAxisFigure: false, + needDefaultYAxisFigure: true, + createPointFigures: ({chart, coordinates, bounding }) => { + const bodyStyle = labelStyle('body') + const quantityStyle = labelStyle('quantity') + const cancelStyle = labelStyle('cancel-button') + const cancelText = '\ue900' + const quantityText = (properties.quantity ?? 'Size').toString() + const bodyText = properties.text ?? 'Position Line' + const cancelMarginRight = properties.marginRight + const quantityMarginRight = utils.calcTextWidth(cancelText) + cancelStyle.paddingLeft + cancelStyle.paddingRight + cancelMarginRight - (properties.borderSize ?? buyStyle().labelStyle.borderSize) + const bodyMarginRight = utils.calcTextWidth((quantityText).toString()) + quantityStyle.paddingLeft + quantityStyle.paddingRight + quantityMarginRight + const lineMarginRight = utils.calcTextWidth((bodyText).toString()) + bodyStyle.paddingLeft + bodyStyle.paddingRight + bodyMarginRight + + return [ + { + key: 'price-line', + type: 'line', + attrs: { + coordinates: [ + { + x: 0, + y: properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y + }, + { + x: bounding.width - lineMarginRight, + y: properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y + } + ] + }, + styles: lineStyle(), + ignoreEvent: true + }, + { + key: 'price-line-two', + type: 'line', + attrs: { + coordinates: [ + { + x: bounding.width - cancelMarginRight, + y: properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y + }, + { + x: bounding.width, + y: properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y + } + ] + }, + styles: lineStyle(), + ignoreEvent: true + }, + { + key: 'body', + type: 'text', + attrs: { + x: bounding.width - bodyMarginRight, + y:properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y, + text:bodyText, + align: 'right', + baseline: 'middle' + }, + styles: bodyStyle + }, + { + key: 'quantity', + type: 'text', + attrs: { + x: bounding.width - quantityMarginRight, + y:properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y, + text: quantityText, + align: 'right', + baseline: 'middle' + }, + styles: quantityStyle + }, + { + key: 'cancel-button', + type: 'text', + attrs: { + x: bounding.width - cancelMarginRight, + y:properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y, + text: cancelText, + align: 'right', + baseline: 'middle' + }, + styles: cancelStyle + } + ] + }, + createYAxisFigures: ({ chart, overlay, coordinates, bounding, yAxis }) => { + const precision = getPrecision(chart, overlay, yAxis) + const isFromZero = yAxis?.isFromZero() ?? false + let textAlign: CanvasTextAlign + let x: number + if (isFromZero) { + textAlign = 'left' + x = 0 + } else { + textAlign = 'right' + x = bounding.width + } + let text: string|null|undefined + if (properties.price !== undefined) { + overlay.points[0].value = properties.price + } + + if (utils.isValid(overlay.extendData)) { + if (!utils.isFunction(overlay.extendData)) { + text = (overlay.extendData ?? '') as string + } else { + text = overlay.extendData(overlay) as string + } + } + if (!utils.isValid(text) && overlay.points[0].value !== undefined) { + text = utils.formatPrecision(overlay.points[0].value, precision.price) + } + + return { + type: 'text', + attrs: { + x, + y: properties.price ? (chart.convertToPixel({ timestamp: chart.getDataList().at(chart.getDataList().length -1)?.timestamp, value: properties.price }) as Partial ).y : coordinates[0].y, + text: text ?? '', + align: textAlign, baseline: + 'middle' + }, + styles: labelStyle('body') + } + }, + onSelected: (event) => { + if (event.preventDefault) + event.preventDefault() + event.overlay.mode = 'normal' + return false + }, + onRightClick: (event): boolean => { + if (event.preventDefault) + event.preventDefault() + return false + }, + onPressedMoveStart: (event): boolean => { + executeCallback(properties.onMoveStart, event) + + return false + }, + onPressedMoving: (event): boolean => { + properties.price = (event.chart.convertFromPixel([{ y: event.y, x: event.x }], { paneId: 'candle_pane' }) as Partial).value + executeCallback(properties.onMove, event) + + return false + }, + onPressedMoveEnd: (event): boolean => { + executeCallback(properties.onMoveEnd, event) + + return false + }, + onClick: (event): boolean => { + switch(event.figure?.key) { + case 'cancel-button': + executeCallback(properties.onCancel, event) + break; + case 'quantity': + executeCallback(properties.onModify, event) + break; + default: + } + return false + }, + + setPrice(price: number) { + properties.price = price + return this as OrderOverlay + }, + setText(text: string) { + properties.text = text + return this as OrderOverlay + }, + setQuantity(quantity: number|string) { + properties.quantity = quantity + return this as OrderOverlay + }, + setModifyTooltip(tooltip: string) { + properties.modifyTooltip = tooltip + return this as OrderOverlay + }, + setTooltip(tooltip: string) { + properties.tooltip = tooltip + return this as OrderOverlay + }, + + setLineColor(color: string) { + properties.lineColor = color + return this as OrderOverlay + }, + setLineWidth(width: number) { + properties.lineWidth = width + return this as OrderOverlay + }, + setLineStyle(style: LineType) { + properties.lineStyle = style + return this as OrderOverlay + }, + setLineLength(length: number) { + properties.lineDashedValue = [length, length] + return this as OrderOverlay + }, + setLineDashedValue(dashedValue: number[]) { + properties.lineDashedValue = dashedValue + return this as OrderOverlay + }, + + setBodyFont(font: string) { + properties.bodyFont = font + return this as OrderOverlay + }, + setBodyFontWeight(weight: FontWeights | number) { + if (utils.isString(weight)) { + weight = convertFontweightNameToNumber(weight as FontWeights) + } + properties.bodyWeight = weight + return this as OrderOverlay + }, + setBodyTextColor(color: string) { + properties.bodyTextColor = color + return this as OrderOverlay + }, + setBodyBackgroundColor(color: string) { + properties.bodyBackgroundColor = color + return this as OrderOverlay + }, + setBodyBorderColor(color: string) { + properties.bodyBorderColor = color + return this as OrderOverlay + }, + + setQuantityFont(font: string) { + properties.quantityFont = font + return this as OrderOverlay + }, + setQuantityFontWeight(weight: FontWeights | number) { + if (utils.isString(weight)) { + weight = convertFontweightNameToNumber(weight as FontWeights) + } + properties.quantityWeight = weight + return this as OrderOverlay + }, + setQuantityColor(color: string) { + properties.quantityColor = color + return this as OrderOverlay + }, + setQuantityBackgroundColor(color: string) { + properties.quantityBackgroundColor = color + return this as OrderOverlay + }, + setQuantityBorderColor(color: string) { + properties.quantityBorderColor = color + return this as OrderOverlay + }, + + setCancelButtonFontWeight(weight: FontWeights | number) { + if (utils.isString(weight)) { + weight = convertFontweightNameToNumber(weight as FontWeights) + } + properties.cancelButtonWeight = weight + return this as OrderOverlay + }, + setCancelButtonIconColor(color: string) { + properties.cancelButtonIconColor = color + return this as OrderOverlay + }, + setCancelButtonBackgroundColor(color: string) { + properties.cancelButtonBackgroundColor = color + return this as OrderOverlay + }, + setCancelButtonBorderColor(color: string) { + properties.cancelButtonBorderColor = color + return this as OrderOverlay + }, + + setBorderStyle(style: LineType) { + properties.borderStyle = style + return this as OrderOverlay + }, + setBorderSize(size: number) { + properties.borderSize = size + return this as OrderOverlay + }, + setBorderDashedValue(dashedValue: number[]) { + properties.borderDashedValue = dashedValue + return this as OrderOverlay + }, + setBorderRadius(radius: number) { + properties.borderRadius = radius + return this as OrderOverlay + }, + + onMoveStart(params: T, callback: (params: T) => void) { + properties.onMoveStart = { + params: params, + callback: callback as (params: unknown) => void, + } + return this as OrderOverlay + }, + onMove(params: T, callback: (params: T) => void) { + properties.onMove = { + params: params, + callback: callback as (params: unknown) => void, + } + return this as OrderOverlay + }, + onMoveEnd(params: T, callback: (params: T) => void) { + properties.onMoveEnd = { + params: params, + callback: callback as (params: unknown) => void, + } + return this as OrderOverlay + }, + onCancel(params: T, callback: (params: T) => void) { + properties.onCancel = { + params: params, + callback: callback as (params: unknown) => void, + } + return this as OrderOverlay + }, + onModify(params: T, callback: (params: T) => void) { + properties.onModify = { + params: params, + callback: callback as (params: unknown) => void, + } + return this as OrderOverlay + } + + } +} + +export default OrderLine \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..5bb1ee58 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,34 @@ +import { Chart, Nullable, Overlay, YAxis } from "klinecharts" +import { FontWeights } from "./types/types" + +export const getScreenSize = () => { + return {x: window.innerWidth, y: window.innerHeight} +} + +export const getPrecision = (chart: Chart, overlay: Overlay, yAxis: Nullable):{ price: number, volume: number } => { + const precision = { + price: 0, + volume: 0 + } + + const symbol = chart.getSymbol() + if ((yAxis?.isInCandle() ?? true) && symbol) { + precision.price = symbol.pricePrecision + precision.volume = symbol.volumePrecision + } else { + const indicators = chart.getIndicators({ paneId: overlay.paneId }) + indicators.forEach(indicator => { + precision.price = Math.max(precision.price, indicator.precision) + }) + } + + return precision +} + +export const convertFontweightNameToNumber = (weight: FontWeights): number => { + const weights: { [key: string]: number } = { + 'thin': 100, 'extra-light': 200, 'light': 300, 'normal': 400, 'medium': 500, 'semi-bold': 600, 'bold': 700, 'extra-bold': 800, 'black': 900 + } + + return weights[weight] +} \ No newline at end of file diff --git a/src/iconfonts-old/fonts/icomoon.eot b/src/iconfonts-old/fonts/icomoon.eot new file mode 100644 index 00000000..2a328108 Binary files /dev/null and b/src/iconfonts-old/fonts/icomoon.eot differ diff --git a/src/iconfonts-old/fonts/icomoon.svg b/src/iconfonts-old/fonts/icomoon.svg new file mode 100644 index 00000000..f7844ec9 --- /dev/null +++ b/src/iconfonts-old/fonts/icomoon.svg @@ -0,0 +1,14 @@ + + + +Generated by IcoMoon + + + + + + + + + + \ No newline at end of file diff --git a/src/iconfonts-old/fonts/icomoon.ttf b/src/iconfonts-old/fonts/icomoon.ttf new file mode 100644 index 00000000..6e7c6be5 Binary files /dev/null and b/src/iconfonts-old/fonts/icomoon.ttf differ diff --git a/src/iconfonts-old/fonts/icomoon.woff b/src/iconfonts-old/fonts/icomoon.woff new file mode 100644 index 00000000..53378b79 Binary files /dev/null and b/src/iconfonts-old/fonts/icomoon.woff differ diff --git a/src/iconfonts-old/style.css b/src/iconfonts-old/style.css new file mode 100644 index 00000000..d72d33a5 --- /dev/null +++ b/src/iconfonts-old/style.css @@ -0,0 +1,47 @@ +@font-face { + font-family: 'icomoon'; + src: url('fonts/icomoon.eot?f4efml'); + src: url('fonts/icomoon.eot?f4efml#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?f4efml') format('truetype'), + url('fonts/icomoon.woff?f4efml') format('woff'), + url('fonts/icomoon.svg?f4efml#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-icon-close:before { + content: "\e900"; + color: #fff; +} +.icon-icon-invisible:before { + content: "\e901"; + color: #fff; +} +.icon-icon-setting:before { + content: "\e902"; + color: #fff; +} +.icon-icon-visible:before { + content: "\e903"; + color: #fff; +} +.icon-icon-cancel:before { + content: "\ea0f"; + color: #fff; +} diff --git a/src/iconfonts/fonts/icomoon.eot b/src/iconfonts/fonts/icomoon.eot index 2a328108..9bb535ce 100644 Binary files a/src/iconfonts/fonts/icomoon.eot and b/src/iconfonts/fonts/icomoon.eot differ diff --git a/src/iconfonts/fonts/icomoon.svg b/src/iconfonts/fonts/icomoon.svg index f7844ec9..03bf270f 100644 --- a/src/iconfonts/fonts/icomoon.svg +++ b/src/iconfonts/fonts/icomoon.svg @@ -1,14 +1,44 @@ -Generated by IcoMoon + + + +{ + "fontFamily": "icomoon", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "icomoon", + "psName": "icomoon", + "subFamily": "Regular", + "fullName": "icomoon", + "description": "Font generated by IcoMoon." +} + + + - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/iconfonts/fonts/icomoon.ttf b/src/iconfonts/fonts/icomoon.ttf index 6e7c6be5..3f471aa8 100644 Binary files a/src/iconfonts/fonts/icomoon.ttf and b/src/iconfonts/fonts/icomoon.ttf differ diff --git a/src/iconfonts/fonts/icomoon.woff b/src/iconfonts/fonts/icomoon.woff index 53378b79..28520aa2 100644 Binary files a/src/iconfonts/fonts/icomoon.woff and b/src/iconfonts/fonts/icomoon.woff differ diff --git a/src/iconfonts/ie7/ie7.css b/src/iconfonts/ie7/ie7.css new file mode 100644 index 00000000..3ebeed6a --- /dev/null +++ b/src/iconfonts/ie7/ie7.css @@ -0,0 +1,55 @@ +.icon-icon-close { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-icon-invisible { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-icon-setting { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-icon-visible { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-lock { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-unlocked { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-bin { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-plus { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-minus { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-cancel-circle { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-blocked { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-cross { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-checkmark { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-checkmark2 { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-enter { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-exit { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-circle-down { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} +.icon-circle-left { + *zoom: expression(this.runtimeStyle['zoom'] = '1', this.innerHTML = ''); +} + diff --git a/src/iconfonts/ie7/ie7.js b/src/iconfonts/ie7/ie7.js new file mode 100644 index 00000000..fdaf1d9f --- /dev/null +++ b/src/iconfonts/ie7/ie7.js @@ -0,0 +1,49 @@ +/* To avoid CSS expressions while still supporting IE 7 and IE 6, use this script */ +/* The script tag referencing this file must be placed before the ending body tag. */ + +/* Use conditional comments in order to target IE 7 and older: + + + +*/ + +(function() { + function addIcon(el, entity) { + var html = el.innerHTML; + el.innerHTML = '' + entity + '' + html; + } + var icons = { + 'icon-icon-close': '', + 'icon-icon-invisible': '', + 'icon-icon-setting': '', + 'icon-icon-visible': '', + 'icon-lock': '', + 'icon-unlocked': '', + 'icon-bin': '', + 'icon-plus': '', + 'icon-minus': '', + 'icon-cancel-circle': '', + 'icon-blocked': '', + 'icon-cross': '', + 'icon-checkmark': '', + 'icon-checkmark2': '', + 'icon-enter': '', + 'icon-exit': '', + 'icon-circle-down': '', + 'icon-circle-left': '', + '0': 0 + }, + els = document.getElementsByTagName('*'), + i, c, el; + for (i = 0; ; i += 1) { + el = els[i]; + if(!el) { + break; + } + c = el.className; + c = c.match(/icon-[^\s'"]+/); + if (c && icons[c[0]]) { + addIcon(el, icons[c[0]]); + } + } +}()); diff --git a/src/iconfonts/style.css b/src/iconfonts/style.css index cd63227f..81e61cd9 100644 --- a/src/iconfonts/style.css +++ b/src/iconfonts/style.css @@ -1,14 +1,15 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?f4efml'); - src: url('fonts/icomoon.eot?f4efml#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?f4efml') format('truetype'), - url('fonts/icomoon.woff?f4efml') format('woff'), - url('fonts/icomoon.svg?f4efml#icomoon') format('svg'); + src: url('fonts/icomoon.eot?mr6pu2'); + src: url('fonts/icomoon.eot?mr6pu2#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?mr6pu2') format('truetype'), + url('fonts/icomoon.woff?mr6pu2') format('woff'), + url('fonts/icomoon.svg?mr6pu2#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; } +@import url("./ie7/ie7.css"); [class^="icon-"], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ @@ -41,3 +42,59 @@ content: "\e903"; color: #fff; } +.icon-lock:before { + content: "\e98f"; + color: #fff; +} +.icon-unlocked:before { + content: "\e990"; + color: #fff; +} +.icon-bin:before { + content: "\e9ac"; + color: #fff; +} +.icon-plus:before { + content: "\ea0a"; + color: #fff; +} +.icon-minus:before { + content: "\ea0b"; + color: #fff; +} +.icon-cancel-circle:before { + content: "\ea0d"; + color: #fff; +} +.icon-blocked:before { + content: "\ea0e"; + color: #fff; +} +.icon-cross:before { + content: "\ea0f"; + color: #fff; +} +.icon-checkmark:before { + content: "\ea10"; + color: #fff; +} +.icon-checkmark2:before { + content: "\ea11"; + color: #fff; +} +.icon-enter:before { + content: "\ea13"; + color: #fff; +} +.icon-exit:before { + content: "\ea14"; + color: #fff; +} +.icon-circle-down:before { + content: "\ea43"; + color: #fff; +} +.icon-circle-left:before { + content: "\ea44"; + color: #fff; +} diff --git a/src/index.ts b/src/index.ts index bcb178a9..9ff0512d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,11 +17,16 @@ import { registerOverlay } from 'klinecharts' import overlays from './extension' import DefaultDatafeed from './DefaultDatafeed' +import DefaultOrderController from './DefaultOrderController' +import DummyOrderController from './DummyOrderController' import KLineChartPro from './KLineChartPro' import { load } from './i18n' -import { Datafeed, SymbolInfo, Period, DatafeedSubscribeCallback, ChartProOptions, ChartPro } from './types' +import { OrderType, ExitType, OrderModalType, OrderInfo, OrderModifyInfo, OrderResource, Datafeed, + SymbolInfo, Period, DatafeedSubscribeCallback, OrderPlacedCallback, ChartProOptions, ChartPro, ChartObjType, + ProChart +} from './types/types' import './index.less' @@ -29,10 +34,13 @@ overlays.forEach(o => { registerOverlay(o) }) export { DefaultDatafeed, + DefaultOrderController, + DummyOrderController, KLineChartPro, load as loadLocales } export type { - Datafeed, SymbolInfo, Period, DatafeedSubscribeCallback, ChartProOptions, ChartPro -} + OrderInfo, OrderModifyInfo, OrderType, ExitType, OrderModalType, OrderResource, Datafeed, SymbolInfo, + Period, DatafeedSubscribeCallback, OrderPlacedCallback, ChartProOptions, ChartPro, ChartObjType, ProChart +} \ No newline at end of file diff --git a/src/store/chartStateStore.ts b/src/store/chartStateStore.ts new file mode 100644 index 00000000..887ad0c9 --- /dev/null +++ b/src/store/chartStateStore.ts @@ -0,0 +1,570 @@ +import { Chart, DeepPartial, Indicator, IndicatorCreate, IndicatorTooltipData, Nullable, Overlay, OverlayCreate, OverlayEvent, OverlayStyle, PaneOptions, SmoothLineStyle, TooltipFeatureStyle, dispose } from 'klinecharts' +import { ChartObjType, OrderInfo, OrderResource, OrderStylesType, ProChart, SymbolInfo } from "../types" +import { createSignal } from "solid-js" +import _, { cloneDeep, keys, set } from "lodash" +import { Datafeed } from "../types" +import { tickTimestamp } from "./tickStore" +import { ctrlKeyedDown, timerid, widgetref } from "./keyEventStore" +import { OtherTypes, overlayType, useOverlaySettings } from "./overlaySettingStore" +import { buyLimitStyle, buyStopStyle, buyStyle, sellLimitStyle, sellStopStyle, sellStyle, setBuyLimitStyle, setBuyStopStyle, setBuyStyle, setSellLimitStyle, setSellStopStyle, setSellStyle, setStopLossStyle, setTakeProfitStyle, stopLossStyle, takeProfitStyle } from "./overlayStyle/positionStyleStore" +import { straightLineStyle } from "./overlayStyle/inbuiltOverlayStyleStore" +import { useGetOverlayStyle } from "./overlayStyle/useOverlayStyles" +import { instanceapi } from './chartStore' + +export const [mainIndicators, setMainIndicators] = createSignal(['']) +export const [subIndicators, setSubIndicators] = createSignal({}) +export const [chartModified, setChartModified] = createSignal(false) +export const [theme, setTheme] = createSignal('') +export const [fullScreen, setFullScreen] = createSignal(false) +export const [orderModalVisible, setOrderModalVisible] = createSignal(false) +export const [range, setRange] = createSignal(1) +export const [datafeed, setDatafeed] = createSignal() + +export const documentResize = () => { + instanceapi()?.resize() +} + +export const cleanup = async () => { //Cleanup objects when leaving chart page + const doJob = async () => { + // clearInterval(timerid()) + // datafeed()!.unsubscribe() + // dispose(widgetref()!) + } + doJob() +} + +type IndicatorChageType = { + name: string + paneId: string + added: boolean +} + +type IndicatorSettingsType = { + visible: boolean; + indicatorName: string; + paneId: string; + calcParams: any[]; +} + +const refineIndiObj = (indicator: Indicator): IndicatorCreate => { + const keys = [ + 'calc', 'figures', 'regenerateFigures', 'draw', 'createTooltipDataSource' + ] + + let cleanIndicator: IndicatorCreate = indicator + + keys.forEach (key => { + // @ts-expect-error + delete cleanIndicator[key] + }) + + return cleanIndicator +} + +/** + * Removes all event listeners from the overlay object + * + * @param overlay + * @returns + */ +const refineOverlayObj = (overlay: Overlay): OverlayCreate => { + const keys = [ + 'onDrawStart', 'onDrawing', 'onDrawEnd', 'onClick', 'onDoubleClick', 'onRightClick', + 'onMouseEnter', 'onMouseLeave', 'onPressedMoveStart', 'onPressedMoving', 'onPressedMoveEnd', + 'onRemoved', 'onSelected', 'onDeselected', 'performEventMoveForDrawing', 'performEventPressedMove', + '_prevPressedPoint', '_prevPressedPoints', 'createPointFigures', 'createXAxisFigures', 'createYAxisFigures' + ] + let cleanOverlay: OverlayCreate = overlay + + keys.forEach(key => { + // @ts-expect-error + delete cleanOverlay[key] + }) + return cleanOverlay +} + +export const useChartState = () => { + const syncIndiObject = (indicator: Indicator, isStack?: boolean, paneOptions?: PaneOptions): boolean => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + let chartObj: ChartObjType + + const indi = refineIndiObj(_.cloneDeep(indicator)) + if (chartStateObj) { + chartObj = JSON.parse(chartStateObj!) + if (!chartObj.indicators) { + chartObj.indicators = [{ + value: indi, + isStack: isStack, + paneOptions + }] + // chartObj = { + // styleObj: chartObj.styleObj, + // overlays: chartObj.overlays, + // figures: chartObj.figures, + // indicators: [{ + // value: indi, + // isStack: isStack, + // paneOptions + // }] + // } + } else { + if (chartObj.indicators.find(_indi => _indi.value?.name === indi.name && _indi.paneOptions?.id === paneOptions?.id)) { + chartObj.indicators = chartObj.indicators.map(_indi => (_indi.value?.id !== indi.id ? _indi : { + value: indi, + isStack, + paneOptions + })) + } else { + chartObj.indicators!.push({ + value: indi, + isStack, + paneOptions + }) + } + } + } + else { + chartObj = { + indicators: [{ + value: indi, + isStack, + paneOptions + }] + } + } + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) + return false + } + + const syncObject = (overlay: Overlay): boolean => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + let chartObj: ChartObjType + + const overly = refineOverlayObj(_.cloneDeep(overlay)) + if (chartStateObj) { + chartObj = JSON.parse(chartStateObj!) + if (!chartObj.overlays) { + chartObj.overlays = [{ + value: overlay, + paneId: overlay.paneId + }] + // chartObj = { + // styleObj: chartObj.styleObj, + // overlays: [{ + // value: overly, + // paneId: overlay.paneId + // }], + // figures: chartObj.figures, + // indicators: chartObj.indicators + // } + } else { + if (chartObj.overlays.find(ovaly => ovaly.value?.id === overly.id)) { + chartObj.overlays = chartObj.overlays.map(ovaly => (ovaly.value?.id !== overly.id ? ovaly : { + value: overly, + paneId: overlay.paneId + })) + } else { + chartObj.overlays!.push({ + value: overly, + paneId: overlay.paneId + }) + } + } + } + else { + chartObj = { + overlays: [{ + value: overly, + paneId: overlay.paneId + }] + } + } + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) + return false + } + + function createIndicator (widget: ProChart, indicatorName: string, isStack?: boolean, paneOptions?: PaneOptions, docallback = false): Nullable { + if (indicatorName === 'VOL') { + paneOptions = { axis: { gap: { bottom: 2 } }, ...paneOptions } + } + const id = widget.createIndicator({ + name: indicatorName, + createTooltipDataSource: (param): IndicatorTooltipData => { + const indiStyles = param.chart.getStyles().indicator + const features = indiStyles.tooltip.features + const icons: TooltipFeatureStyle[] = [] + + icons.push(param.indicator.visible ? features[1] : features[0]) + icons.push(features[2]) + icons.push(features[3]) + + return { + name: `${indicatorName}_${id}`, + calcParamsText: indicatorName, + features: icons, + legends: [] + } + } + }, isStack, paneOptions) ?? null + + if (id && docallback) { + const indi = widget?.getIndicators({id, name: indicatorName})[0] + if (indi) + syncIndiObject(indi as Indicator, isStack, { id: id }) + } + + return id + } + + const pushOverlay = (overlay: OverlayCreate, paneId?: string, redrawing = false) => { + const id = (instanceapi()?.createOverlay({ ...overlay, paneId }) as Nullable) + + if (!id) + return false + + const ovrly = instanceapi()?.getOverlays({id: id})[0] + if (ovrly) { + const style = !redrawing && useGetOverlayStyle[`${ovrly.name}Style`] ? useGetOverlayStyle[`${ovrly.name}Style`]() : undefined + instanceapi()?.overrideOverlay({ + id: ovrly.id, + styles: overlay.styles ?? style, + onDrawEnd: (event) => { + if (!['measure'].includes(ovrly.name)) + return syncObject(event.overlay) + return false + }, + onPressedMoveEnd: (event) => { + if (!['measure'].includes(ovrly.name)) + return syncObject(event.overlay) + return false + }, + onRightClick: ovrly.onRightClick ? ovrly.onRightClick : (event) => { + if(ctrlKeyedDown()) { + popOverlay(event.overlay.id) + return true + } + useOverlaySettings().openPopup(event, {overlayType: (event.overlay.name as overlayType)}) + // popOverlay(event.overlay.id) + return true + } + }) + if (!redrawing) + syncObject(instanceapi()!.getOverlays({id})[0]) + } + } + + const popOverlay = (id: string) => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + if (chartStateObj) { + let chartObj: ChartObjType = JSON.parse(chartStateObj) + + chartObj.overlays = chartObj.overlays?.filter(overlay => overlay.value?.id !== id) + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) + } + instanceapi()?.removeOverlay({id}) + } + + const pushMainIndicator = (data: IndicatorChageType) => { + const newMainIndicators = [...mainIndicators()] + if (data.added) { + createIndicator(instanceapi()!, data.name, true, { id: 'candle_pane' }, true) + newMainIndicators.push(data.name) + } else { + popIndicator(data.name, 'candle_pane') + newMainIndicators.splice(newMainIndicators.indexOf(data.name), 1) + } + setMainIndicators(newMainIndicators) + } + + const pushSubIndicator = (data: IndicatorChageType) => { + const newSubIndicators = { ...subIndicators() } + if (data.added) { + const paneId = createIndicator(instanceapi()!, data.name, false, undefined, true) + if (paneId) { + // @ts-expect-error + newSubIndicators[data.name] = paneId + } + } else { + if (data.paneId) { + popIndicator(data.name, data.paneId) + // @ts-expect-error + delete newSubIndicators[data.name] + } + } + setSubIndicators(newSubIndicators) + } + + const modifyIndicator = (modalParams: IndicatorSettingsType, params: any) => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + if (chartStateObj) { + let chartObj: ChartObjType = JSON.parse(chartStateObj) + + chartObj.indicators = chartObj.indicators?.map(indi => { + if (indi.value?.name === modalParams.indicatorName) { + indi.value.name = modalParams.indicatorName + indi.value.calcParams = params + indi.paneOptions!.id = modalParams.paneId + } + return indi + }) + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) + instanceapi()?.overrideIndicator({ name: modalParams.indicatorName, calcParams: params, paneId: modalParams.paneId }) + } + } + const popIndicator = (id: string, name?: string, paneId?: string) => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + instanceapi()?.removeIndicator({ id, paneId, name }) + + if (chartStateObj) { + let chartObj: ChartObjType = JSON.parse(chartStateObj) + + chartObj.indicators = chartObj.indicators?.filter(indi => indi.paneOptions?.id !== paneId && indi.value?.name !== name) + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) + } + return + } + + const redraOverlaysIndiAndFigs = async () => { + const redraw = (chartStateObj: string) => { + const chartObj = (JSON.parse(chartStateObj) as ChartObjType) + + if (chartObj.figures) { + } + if (chartObj.overlays) { + chartObj.overlays.forEach(overlay => { + pushOverlay(overlay.value!, overlay.paneId, true) + }) + } + if (chartObj.indicators) { + setTimeout(() => { + const newMainIndicators = [...mainIndicators()] + const newSubIndicators = {...subIndicators} + + chartObj.indicators!.forEach(indicator => { + if (indicator.value) { + instanceapi()?.createIndicator(indicator.value, indicator.isStack, indicator.paneOptions) + if (indicator.paneOptions?.id === 'candle_pane') { + newMainIndicators.push(indicator.value.name) + } else { + //@ts-expect-error + newSubIndicators[indicator.value.name] = indicator.paneOptions?.id + } + } + }) + setMainIndicators(newMainIndicators) + setSubIndicators(newSubIndicators) + }, 500) + } + if (chartObj.styleObj) { + instanceapi()?.setStyles(chartObj.styleObj) + } + if (chartObj.orderStyles) { + const styles = chartObj.orderStyles + syncOrderStyles(styles) + } + } + + // if (chartsession()?.chart) { + // let chartStateObj = atob(chartsession()?.chart!) + + // if (chartStateObj) { + // if (chartStateObj !== localStorage.getItem(`chartstatedata`)) + // localStorage.setItem(`chartstatedata`, chartStateObj) + + // return redraw(chartStateObj) + // } + // } + + const chartStateObj = localStorage.getItem(`chartstatedata`)! + if (chartStateObj) + redraw(chartStateObj) + } + + return { createIndicator, modifyIndicator, popIndicator, syncIndiObject, syncObject, pushOverlay, popOverlay, pushMainIndicator, pushSubIndicator, redraOverlaysIndiAndFigs } +} + +const syncOrderStyles = (styles: OrderStylesType) => { + if (styles.buyStyle) { + setBuyStyle((prevbuystyle) => { + const buystyle = cloneDeep(prevbuystyle) + if (styles.buyStyle?.lineStyle) { + for (const key in styles.buyStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buystyle, `lineStyle.${key}`, styles.buyStyle.lineStyle[key]) + // buystyle.lineStyle[key] = styles.buyStyle.lineStyle[key] + } + } + } + if (styles.buyStyle?.labelStyle) { + for (const key in styles.buyStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buystyle, `labelStyle.${key}`, styles.buyStyle.labelStyle[key]) + // buystyle.labelStyle[key] = styles.buyStyle.labelStyle[key] + } + } + } + return buystyle + }) + } + if (styles.buyLimitStyle) { + setBuyLimitStyle((prevBuyLimitStyle) => { + const buylimitstyle = cloneDeep(prevBuyLimitStyle) + if (styles.buyLimitStyle?.lineStyle) { + for (const key in styles.buyLimitStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buylimitstyle, `lineStyle.${key}`, styles.buyLimitStyle.lineStyle[key]) + } + } + } + if (styles.buyLimitStyle?.labelStyle) { + for (const key in styles.buyLimitStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buylimitstyle, `labelStyle.${key}`, styles.buyLimitStyle.labelStyle[key]) + } + } + } + return buylimitstyle + }) + } + if (styles.buyStopStyle) { + setBuyStopStyle((prevbuystopstyle) => { + const buystopstyle = cloneDeep(prevbuystopstyle) + if (styles.buyStopStyle?.lineStyle) { + for (const key in styles.buyStopStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buystopstyle, `lineStyle.${key}`, styles.buyStopStyle.lineStyle[key]) + } + } + } + if (styles.buyStopStyle?.labelStyle) { + for (const key in styles.buyStopStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(buystopstyle, `labelStyle.${key}`, styles.buyStopStyle.labelStyle[key]) + } + } + } + return buystopstyle + }) + } + if (styles.sellStyle) { + setSellStyle((prevsellstyle) => { + const sellstyle = cloneDeep(prevsellstyle) + if (styles.sellStyle?.lineStyle) { + for (const key in styles.sellStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(sellstyle, `lineStyle.${key}`, styles.sellStyle.lineStyle[key]) + } + } + } + if (styles.sellStyle?.labelStyle) { + for (const key in styles.sellStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(sellstyle, `labelStyle.${key}`, styles.sellStyle.labelStyle[key]) + } + } + } + return sellstyle + }) + } + if (styles.sellLimitStyle) { + setSellLimitStyle((prevselllimitstyle) => { + const selllimitstyle = cloneDeep(prevselllimitstyle) + if (styles.sellLimitStyle?.lineStyle) { + for (const key in styles.sellLimitStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(selllimitstyle, `lineStyle.${key}`, styles.sellLimitStyle.lineStyle[key]) + } + } + } + if (styles.sellLimitStyle?.labelStyle) { + for (const key in styles.sellLimitStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(selllimitstyle, `labelStyle.${key}`, styles.sellLimitStyle.labelStyle[key]) + } + } + } + return selllimitstyle + }) + } + if (styles.sellStopStyle) { + setSellStopStyle((prevsellstopstyle) => { + const sellstopstyle = cloneDeep(prevsellstopstyle) + if (styles.sellStopStyle?.lineStyle) { + for (const key in styles.sellStopStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(sellstopstyle, `lineStyle.${key}`, styles.sellStopStyle.lineStyle[key]) + } + } + } + if (styles.sellStopStyle?.labelStyle) { + for (const key in styles.sellStopStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(sellstopstyle, `labelStyle.${key}`, styles.sellStopStyle.labelStyle[key]) + } + } + } + return sellstopstyle + }) + } + if (styles.stopLossStyle) { + setStopLossStyle((prevstoplossstyle) => { + const stoplossstyle = cloneDeep(prevstoplossstyle) + if (styles.stopLossStyle?.lineStyle) { + for (const key in styles.stopLossStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(stoplossstyle, `lineStyle.${key}`, styles.stopLossStyle.lineStyle[key]) + } + } + } + if (styles.stopLossStyle?.labelStyle) { + for (const key in styles.stopLossStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(stoplossstyle, `labelStyle.${key}`, styles.stopLossStyle.labelStyle[key]) + } + } + } + return stoplossstyle + }) + } + if (styles.takeProfitStyle) { + setTakeProfitStyle((prevtakeprofitstyle) => { + const takeprofitstyle = cloneDeep(takeProfitStyle()) + if (styles.takeProfitStyle?.lineStyle) { + for (const key in styles.takeProfitStyle.lineStyle) { + if (key !== undefined) { + //@ts-expect-error + set(takeprofitstyle, `lineStyle.${key}`, styles.takeProfitStyle.lineStyle[key]) + } + } + } + if (styles.takeProfitStyle?.labelStyle) { + for (const key in styles.takeProfitStyle.labelStyle) { + if (key !== undefined) { + //@ts-expect-error + set(takeprofitstyle, `labelStyle.${key}`, styles.takeProfitStyle.labelStyle[key]) + } + } + } + return takeprofitstyle + }) + } +} \ No newline at end of file diff --git a/src/store/chartStore.ts b/src/store/chartStore.ts new file mode 100644 index 00000000..f1e8fa18 --- /dev/null +++ b/src/store/chartStore.ts @@ -0,0 +1,23 @@ +import { createSignal } from "solid-js" +import { ChartDataLoaderType, ChartPro, ChartProOptions, Period, ProChart, SymbolInfo } from "../types" +import { Nullable } from "klinecharts" + +export interface ChartProComponentProps extends Required> { + ref: (chart: ChartPro) => void + dataloader: ChartDataLoaderType +} + +export const [drawingBarVisible, setDrawingBarVisible] = createSignal(false) +export const [orderPanelVisible, setOrderPanelVisible] = createSignal(false) +export const [settingModalVisible, setSettingModalVisible] = createSignal(false) +export const [indicatorModalVisible, setIndicatorModalVisible] = createSignal(false) +export const [periodModalVisible, setPeriodModalVisible] = createSignal(false) +export const [showSpeed, setShowSpeed] = createSignal(false) + +export const [screenshotUrl, setScreenshotUrl] = createSignal('') +export const [rootlelID, setRooltelId] = createSignal('') + +export const [loadingVisible, setLoadingVisible] = createSignal(false) +export const [symbol, setSymbol] = createSignal>(null) +export const [period, setPeriod] = createSignal>(null) +export const [instanceapi, setInstanceapi] = createSignal>(null) \ No newline at end of file diff --git a/src/store/keyEventStore.ts b/src/store/keyEventStore.ts new file mode 100644 index 00000000..085b95a4 --- /dev/null +++ b/src/store/keyEventStore.ts @@ -0,0 +1,145 @@ +import { createSignal, startTransition } from "solid-js"; +import { indicatorModalVisible, instanceapi, orderPanelVisible, periodModalVisible, rootlelID, screenshotUrl, setIndicatorModalVisible, setOrderPanelVisible, setPeriodModalVisible, setScreenshotUrl, setSettingModalVisible, settingModalVisible } from "./chartStore"; +import { documentResize, fullScreen, orderModalVisible, theme } from "./chartStateStore"; +import { Chart } from 'klinecharts'; +import { periodInputValue, setPeriodInputValue } from "../widget/timeframe-modal"; +import { setInputClass } from "../component/input"; +import { showOverlaySetting } from "./overlaySettingStore"; + +export const [ctrlKeyedDown, setCtrlKeyedDown] = createSignal(false) +export const [widgetref, setWidgetref] = createSignal('') +export const [timerid, setTimerid] = createSignal() + +export const useKeyEvents = () => { + const handleKeyDown = (event:KeyboardEvent) => { + if (event.ctrlKey || event.metaKey) { + event.preventDefault() + setCtrlKeyedDown(true) + } + if (ctrlKeyedDown()) { + switch (event.key) { + case 'o': + break; + case 'l': + showOrderlist() + break; + case 'i': + if (allModalHidden('indi')) { + setIndicatorModalVisible(visible => !visible) + } + break; + case 's': + if (allModalHidden('settings')) { + setSettingModalVisible(visible => !visible) + } + break; + case 'z': + //TODO: we should undo one step + break; + case 'y': + //TODO: we should redo one step + break; + case 'c': + //TODO: we should copy any selected overlay to clipboard + break; + case 'v': + //TODO: we should paste the copied overlay from clipboard + break; + case 'p': + if (allModalHidden('screenshot')) + takeScreenshot() + break; + case 'f': + toggleFullscreen() + break; + case 'Backspace': + break; + } + + return + } + if (['1','2','3','4','5','6','7','8','9'].includes(event.key) && allModalHidden('period')) { + // if (periodInputValue().length < 1) + // setPeriodInputValue(event.key) + if (!periodModalVisible()) { + setPeriodModalVisible(true) + setInputClass('klinecharts-pro-input klinecharts-pro-timeframe-modal-input input-error') + } + } else if (event.key === ' ') { + } else if (event.key === 'ArrowDown') { + } else if (event.key === 'ArrowUp') { + } else if (event.key === 'Delete') { + instanceapi()?.removeOverlay() + } else if (event.key === 'Escape') { + //TODO: this should hide all modals + setPeriodModalVisible(false) + setPeriodInputValue('') + + setSettingModalVisible(false) + setOrderPanelVisible(false) + setIndicatorModalVisible(false) + setScreenshotUrl('') + } + } + + const handleKeyUp = (event:KeyboardEvent) => { + if (!event.ctrlKey || !event.metaKey) { + setCtrlKeyedDown(false) + event.preventDefault() + } + } + + return { handleKeyDown, handleKeyUp } +} + +const allModalHidden = (except: 'settings'|'indi'|'screenshot'|'order'|'period') => { + let value = false + switch (except) { + case 'settings': + value = !indicatorModalVisible() && screenshotUrl() === '' && !orderModalVisible() && !periodModalVisible() && !showOverlaySetting() + case 'indi': + value = !settingModalVisible() && screenshotUrl() === '' && !orderModalVisible() && !periodModalVisible() && !showOverlaySetting() + break + case 'screenshot': + value = !settingModalVisible() && !indicatorModalVisible() && !orderModalVisible() && !periodModalVisible() && !showOverlaySetting() + break + case 'order': + value = !settingModalVisible() && !indicatorModalVisible() && screenshotUrl() === '' && !periodModalVisible() && !showOverlaySetting() + break + case 'period': + value = !settingModalVisible() && !indicatorModalVisible() && screenshotUrl() === '' && !orderModalVisible() && !showOverlaySetting() + break + } + return value +} + +const showOrderlist = async () => { + try { + await startTransition(() => setOrderPanelVisible(!orderPanelVisible())) + documentResize() + } catch (e) {} +} + +const takeScreenshot = () => { + const url = instanceapi()!.getConvertPictureUrl(true, 'jpeg', theme() === 'dark' ? '#151517' : '#ffffff') + setScreenshotUrl(url) +} + +const toggleFullscreen = () => { + if (!fullScreen()) { + // const el = ref?.parentElement + const el = document.querySelector(`#${rootlelID()}`) + if (el) { + // @ts-expect-error + const enterFullScreen = el.requestFullscreen ?? el.webkitRequestFullscreen ?? el.mozRequestFullScreen ?? el.msRequestFullscreen + enterFullScreen.call(el) + // setFullScreen(true) + } else { + } + } else { + // @ts-expect-error + const exitFullscreen = document.exitFullscreen ?? document.msExitFullscreen ?? document.mozCancelFullScreen ?? document.webkitExitFullscreen + exitFullscreen.call(document) + // setFullScreen(false) + } +} \ No newline at end of file diff --git a/src/store/overlaySettingStore.ts b/src/store/overlaySettingStore.ts new file mode 100644 index 00000000..df25633c --- /dev/null +++ b/src/store/overlaySettingStore.ts @@ -0,0 +1,46 @@ +import { Overlay, OverlayEvent } from 'klinecharts'; +import { createSignal } from 'solid-js'; +import { ExitType } from '../types'; +import { ctrlKeyedDown, setCtrlKeyedDown } from './keyEventStore'; +import { getScreenSize } from '../helpers'; + +export type overlayType = +'point'|'line'|'rect'|'polygon'|'circle'|'arc'|'text'|'horizontalStraightLine'|'horizontalRayLine'|'horizontalSegment'| +'verticalStraightLine'|'verticalRayLine'|'verticalSegment'|'straightLine'|'rayLine'|'segment'|'arrow'|'priceLine' +export interface OtherTypes { + exitType?: ExitType + overlayType?: overlayType +} + +export const [showOverlayPopup, setShowOverlayPopup] = createSignal(false) +export const [popupTop, setPopupTop] = createSignal(0) +export const [popupLeft, setPopupLeft] = createSignal(0) +export const [popupOverlay, setPopupOverlay] = createSignal() +export const [popupOtherInfo, setPopupOtherInfo] = createSignal() + +export const [showPositionSetting, setShowPositionSetting] = createSignal(false) +export const [showOverlaySetting, setShowOverlaySetting] = createSignal(false) +export const [showSellSetting, setShowSellSetting] = createSignal(false) +export const [showTpSetting, setShowTpSetting] = createSignal(false) +export const [showSlSetting, setShowSlSetting] = createSignal(false) + +export const getOverlayType = () => { + return popupOverlay()?.name ?? 'Object' +} + +export const useOverlaySettings = () => { + const openPopup = (event: OverlayEvent, others?: OtherTypes) => { + setPopupTop(getScreenSize().y - event.pageY! > 200 ? event.pageY! : getScreenSize().y-200) + setPopupLeft(getScreenSize().x - event.pageX! > 200 ? event.pageX! : getScreenSize().x-200) + setPopupOverlay(event.overlay) + setPopupOtherInfo(others) + setShowOverlayPopup(true) + } + + const closePopup = () => { + setShowOverlayPopup(false) + // setPopupOtherInfo({}) + } + + return { openPopup, closePopup } +} diff --git a/src/store/overlayStyle/inbuiltOverlayStyleStore.ts b/src/store/overlayStyle/inbuiltOverlayStyleStore.ts new file mode 100644 index 00000000..ffdbe733 --- /dev/null +++ b/src/store/overlayStyle/inbuiltOverlayStyleStore.ts @@ -0,0 +1,130 @@ +import { createSignal } from 'solid-js'; + +const point = { + color: '#1677FF', + borderColor: 'rgba(22, 119, 255, 0.35)', + borderSize: 1, + radius: 5, + activeColor: '#1677FF', + activeBorderColor: 'rgba(22, 119, 255, 0.35)', + activeBorderSize: 3, + activeRadius: 5 +} + +const text = { + // 'fill' | 'stroke' | 'stroke_fill' + style: 'fill', + color: '#FFFFFF', + size: 12, + family: 'Helvetica Neue', + weight: 'normal', + // 'solid' | 'dashed' + borderStyle: 'solid', + borderDashedValue: [2, 2], + borderSize: 0, + borderRadius: 2, + borderColor: '#1677FF', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + backgroundColor: 'transparent' +} + +const circle = { + // 'fill' | 'stroke' | 'stroke_fill' + style: 'fill', + color: 'rgba(22, 119, 255, 0.25)', + borderColor: '#1677FF', + borderSize: 1, + // 'solid' | 'dashed' + borderStyle: 'solid', + borderDashedValue: [2, 2] +} + +const polygon = circle +const rect = { + ...circle, + borderRadius: 0 +} + +const arc = { + // 'solid' | 'dashed' + style: 'solid', + color: '#1677FF', + size: 1, + dashedValue: [2, 2] +} + +const line = { + ...arc, + smooth: false, +} + +export const [pointStyle, setPointStyle] = createSignal({ + point +}) + +export const [horizontalStraightLineStyle, setHorizontalStraightLineStyle] = createSignal({ + line +}) + +export const [horizontalSegmentStyle, setHorizontalSegmentStyle] = createSignal({ + line +}) + +export const [horizontalRayLineStyle, setHorizontalRayLineStyle] = createSignal({ + line +}) + +export const [verticalRayLineStyle, setVerticalRayLineStyle] = createSignal({ + line +}) + +export const [verticalSegmentStyle, setVerticalSegmentStyle] = createSignal({ + line +}) + +export const [verticalStraightLineStyle, setVerticalStraightLineStyle] = createSignal({ + line +}) + +export const [rayLineStyle, setRayLineStyle] = createSignal({ + line +}) + +export const [segmentStyle, setSegmentStyle] = createSignal({ + line +}) + +export const [arrowStyle, setArrowStyle] = createSignal({ + line +}) + +export const [priceLineStyle, setPriceLineStyle] = createSignal({ + line +}) + +export const [straightLineStyle, setStraightLineStyle] = createSignal({ + line +}) + +export const [rectStyle, setRectStyle] = createSignal({ + polygon +}) + +export const [polygonStyle, setPolygonStyle] = createSignal({ + polygon +}) + +export const [circleStyle, setCircleStyle] = createSignal({ + circle +}) + +export const [arcStyle, setArcStyle] = createSignal({ + arc +}) + +export const [textStyle, setTextStyle] = createSignal({ + text +}) \ No newline at end of file diff --git a/src/store/overlayStyle/positionStyleStore.ts b/src/store/overlayStyle/positionStyleStore.ts new file mode 100644 index 00000000..f000027f --- /dev/null +++ b/src/store/overlayStyle/positionStyleStore.ts @@ -0,0 +1,197 @@ +import { LineStyle, StateTextStyle } from 'klinecharts'; +import { createSignal } from 'solid-js'; + +export const [buyStyle, setBuyStyle] = createSignal<{ lineStyle: LineStyle, labelStyle: StateTextStyle}>({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#00698b', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'stroke_fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + borderDashedValue: [0,0], + borderRadius: 3, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#00698b', + show: true, + } +}) + +export const [buyLimitStyle, setBuyLimitStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#00698b', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#00698b' + } +}) + +export const [buyStopStyle, setBuyStopStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#00698b', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#00698b' + } +}) + +export const [sellStyle, setSellStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#fb7b50', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#fb7b50' + } +}) + +export const [sellLimitStyle, setSellLimitStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#fb7b50', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#fb7b50' + } +}) + +export const [sellStopStyle, setSellStopStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#fb7b50', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#fb7b50' + } +}) + +export const [takeProfitStyle, setTakeProfitStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#00698b', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#00698b' + } +}) + +export const [stopLossStyle, setStopLossStyle] = createSignal({ + lineStyle: { + style: 'dashed', + size: 1, + color: '#fb7b50', + dashedValue: [4, 4] + }, + labelStyle: { + style: 'fill', + size: 12, + family:'Helvetica Neue', + weight: 'normal', + paddingLeft: 5, + paddingRight: 5, + paddingBottom: 5, + paddingTop: 5, + borderStyle: 'solid', + borderSize: 1, + color: '#FFFFFF', + borderColor: '#00698b', + backgroundColor: '#fb7b50' + } +}) \ No newline at end of file diff --git a/src/store/overlayStyle/useOverlayStyles.ts b/src/store/overlayStyle/useOverlayStyles.ts new file mode 100644 index 00000000..84b517ea --- /dev/null +++ b/src/store/overlayStyle/useOverlayStyles.ts @@ -0,0 +1,49 @@ +import { Accessor, Setter } from 'solid-js' +import { + setPointStyle, setRectStyle, setPolygonStyle, setCircleStyle, setArcStyle, setTextStyle, pointStyle, rectStyle, polygonStyle, + circleStyle, arcStyle, textStyle, horizontalStraightLineStyle, horizontalRayLineStyle, horizontalSegmentStyle, verticalStraightLineStyle, + verticalRayLineStyle, setRayLineStyle, rayLineStyle, segmentStyle, verticalSegmentStyle, arrowStyle, straightLineStyle, priceLineStyle, setHorizontalStraightLineStyle, + setHorizontalRayLineStyle, setHorizontalSegmentStyle, setVerticalStraightLineStyle, setVerticalRayLineStyle, setVerticalSegmentStyle, + setStraightLineStyle, setSegmentStyle, setPriceLineStyle, setArrowStyle } from './inbuiltOverlayStyleStore' + +// Define an object that maps function names to functions +export const useGetOverlayStyle: { [key: string]: Accessor } = { + pointStyle, + horizontalStraightLineStyle, + horizontalRayLineStyle, + horizontalSegmentStyle, + verticalStraightLineStyle, + verticalRayLineStyle, + verticalSegmentStyle, + straightLineStyle, + rayLineStyle, + segmentStyle, + arrowStyle, + priceLineStyle, + rectStyle, + polygonStyle, + circleStyle, + arcStyle, + textStyle +} + +// Define an object that maps function names to functions +export const useSetOverlayStyle: { [key: string]: Setter} = { + setpointStyle: (value: any) => setPointStyle(value), + sethorizontalStraightLineStyle: (value: any) => setHorizontalStraightLineStyle(value), + sethorizontalRayLineStyle: (value: any) => setHorizontalRayLineStyle(value), + sethorizontalSegmentStyle: (value: any) => setHorizontalSegmentStyle(value), + setverticalStraightLineStyle: (value: any) => setVerticalStraightLineStyle(value), + setverticalRayLineStyle: (value: any) => setVerticalRayLineStyle(value), + setverticalSegmentStyle: (value: any) => setVerticalSegmentStyle(value), + setstraightLineStyle: (value: any) => setStraightLineStyle(value), + setrayLineStyle: (value: any) => setRayLineStyle(value), + setsegmentStyle: (value: any) => setSegmentStyle(value), + setarrowStyle: (value: any) => setArrowStyle(value), + setpriceLineStyle: (value: any) => setPriceLineStyle(value), + setrectStyle: (value: any) => setRectStyle(value), + setpolygonStyle: (value: any) => setPolygonStyle(value), + setcircleStyle: (value: any) => setCircleStyle(value), + setarcStyle: (value: any) => setArcStyle(value), + settextStyle: (value: any) => setTextStyle(value) +} \ No newline at end of file diff --git a/src/store/tickStore.ts b/src/store/tickStore.ts new file mode 100644 index 00000000..ad5e0026 --- /dev/null +++ b/src/store/tickStore.ts @@ -0,0 +1,5 @@ +import { KLineData, Nullable } from 'klinecharts'; +import { createSignal } from 'solid-js'; + +export const [currenttick, setCurrentTick] = createSignal>(null); +export const [tickTimestamp, setTickTimestamp] = createSignal() diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 8f1b97c9..00000000 --- a/src/types.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - - * http://www.apache.org/licenses/LICENSE-2.0 - - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { KLineData, Styles, DeepPartial } from 'klinecharts' - -export interface SymbolInfo { - ticker: string - name?: string - shortName?: string - exchange?: string - market?: string - pricePrecision?: number - volumePrecision?: number - priceCurrency?: string - type?: string - logo?: string -} - -export interface Period { - multiplier: number - timespan: string - text: string -} - -export type DatafeedSubscribeCallback = (data: KLineData) => void - -export interface Datafeed { - searchSymbols (search?: string): Promise - getHistoryKLineData (symbol: SymbolInfo, period: Period, from: number, to: number): Promise - subscribe (symbol: SymbolInfo, period: Period, callback: DatafeedSubscribeCallback): void - unsubscribe (symbol: SymbolInfo, period: Period): void -} - -export interface ChartProOptions { - container: string | HTMLElement - styles?: DeepPartial - watermark?: string | Node - theme?: string - locale?: string - drawingBarVisible?: boolean - symbol: SymbolInfo - period: Period - periods?: Period[] - timezone?: string - mainIndicators?: string[] - subIndicators?: string[] - datafeed: Datafeed -} - -export interface ChartPro { - setTheme(theme: string): void - getTheme(): string - setStyles(styles: DeepPartial): void - getStyles(): Styles - setLocale(locale: string): void - getLocale(): string - setTimezone(timezone: string): void - getTimezone(): string - setSymbol(symbol: SymbolInfo): void - getSymbol(): SymbolInfo - setPeriod(period: Period): void - getPeriod(): Period -} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..55cde177 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './overlayTypes' +export * from './types' \ No newline at end of file diff --git a/src/types/overlayTypes.ts b/src/types/overlayTypes.ts new file mode 100644 index 00000000..18e437cf --- /dev/null +++ b/src/types/overlayTypes.ts @@ -0,0 +1,126 @@ +import { LineType, Overlay, OverlayEvent, OverlayTemplate, PickPartial, StateLineStyle } from "klinecharts" +import { FontWeights } from "./types" + +export interface OverlayEventListenerParams { + params: unknown, + callback: (params: unknown, event?: OverlayEvent) => void +} + +export interface OverlayProperties { + text?: string + color?: string + lineWidth?: number + lineStyle?: LineType + lineLength?: number + lineDashedValue?: number[] + tooltip?: string + backgroundColor?: string + borderColor?: string + borderWidth?: number +} + +export interface OrderLineProperties { + price?: number + text?: string + quantity?: number|string + modifyTooltip?: string + tooltip?: string + marginRight: number + + lineColor?: string + lineWidth?: number + lineStyle?: LineType + lineDashedValue?: number[] + + bodySize?: number + bodyWeight?: number | string + bodyFont?: string + bodyBackgroundColor?: string + bodyBorderColor?: string + bodyTextColor?: string + bodyPaddingLeft?: number + bodyPaddingRight?: number + bodyPaddingTop?: number + bodyPaddingBottom?: number + isBodyVisible: boolean + + quantitySize?: number + quantityWeight?: number | string + quantityFont?: string + quantityColor?: string + quantityBackgroundColor?: string + quantityBorderColor?: string + quantityPaddingLeft?: number + quantityPaddingRight?: number + quantityPaddingTop?: number + quantityPaddingBottom?: number + isQuantityVisible: boolean + + cancelButtonSize?: number + cancelButtonWeight?: number | string + cancelButtonIconColor?: string + cancelButtonBackgroundColor?: string + cancelButtonBorderColor?: string + cancelButtonPaddingLeft?: number + cancelButtonPaddingRight?: number + cancelButtonPaddingTop?: number + cancelButtonPaddingBottom?: number + isCancelButtonVisible: boolean + + borderStyle?: LineType, + borderSize?: number, + borderDashedValue?: number[], + borderRadius?: number + + onMoveStart?: OverlayEventListenerParams + onMove?: OverlayEventListenerParams + onMoveEnd?: OverlayEventListenerParams + onCancel?: OverlayEventListenerParams + onModify?: OverlayEventListenerParams +} + +type OrderOverlayAttributes = { + setPrice: (price: number) => OrderOverlay + setText: (text: string) => OrderOverlay + setQuantity: (quantity: string) => OrderOverlay + setModifyTooltip: (tooltip: string) => OrderOverlay + setTooltip: (tooltip: string) => OrderOverlay + + setLineColor: (color: string) => OrderOverlay + setLineWidth: (width: number) => OrderOverlay + setLineStyle: (style: LineType) => OrderOverlay + setLineLength: (length: number) => OrderOverlay + setLineDashedValue: (dashedValue: number[]) => OrderOverlay + + setBodyFont: (font: string) => OrderOverlay + setBodyFontWeight: (weight: FontWeights | number) => OrderOverlay + setBodyTextColor: (color: string) => OrderOverlay + setBodyBackgroundColor: (color: string) => OrderOverlay + setBodyBorderColor: (color: string) => OrderOverlay + + setQuantityFont: (font: string) => OrderOverlay + setQuantityFontWeight: (weight: FontWeights | number) => OrderOverlay + setQuantityColor: (color: string) => OrderOverlay + setQuantityBackgroundColor: (color: string) => OrderOverlay + setQuantityBorderColor: (color: string) => OrderOverlay + + setCancelButtonIconColor: (color: string) => OrderOverlay + setCancelButtonFontWeight: (weight: FontWeights | number) => OrderOverlay + setCancelButtonBackgroundColor: (color: string) => OrderOverlay + setCancelButtonBorderColor: (color: string) => OrderOverlay + + setBorderStyle: (style: LineType) => OrderOverlay + setBorderSize: (size: number) => OrderOverlay + setBorderDashedValue: (dashedValue: number[]) => OrderOverlay + setBorderRadius: (radius: number) => OrderOverlay + + onMoveStart: (params: T, callback: (params: T, event?: OverlayEvent) => void) => OrderOverlay + onMove: (params: T, callback: (params: T, event?: OverlayEvent) => void) => OrderOverlay + onMoveEnd: (params: T, callback: (params: T, event?: OverlayEvent) => void) => OrderOverlay + onCancel: (params: T, callback: (params: T, event?: OverlayEvent) => void) => OrderOverlay + onModify: (params: T, callback: (params: T, event?: OverlayEvent) => void) => OrderOverlay +} + +export type OrderOverlay = Pick & OrderOverlayAttributes + +export interface OrderOverlayCreate extends OverlayTemplate, OrderOverlayAttributes {} \ No newline at end of file diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..1f092cc1 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,212 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KLineData, Styles, DeepPartial, Nullable, Chart, DataLoader, Period as DefaultPeriod, IndicatorCreate, PaneOptions, OverlayCreate, FigureCreate, PickRequired, OverlayFilter, Overlay } from 'klinecharts' +import { OrderOverlay } from './overlayTypes' + + +export type FontWeights = 'thin' | 'extra-light' | 'light' | 'normal' | 'medium' | 'semi-bold' | 'bold' | 'extra-bold' | 'black' +export type OrderType = 'buy'|'sell'|'buystop'|'buylimit'|'sellstop'|'selllimit' +export type OrderModalType = 'placeorder'|'modifyorder'|'closepartial' +export type ExitType = 'stoploss'|'takeprofit'|'breakeven'|'manualclose'|'cancel' + +export type DatafeedSubscribeCallback = (data: KLineData, timestamp?: number) => void +export type OrderPlacedCallback = (data: OrderInfo|null) => void //this should be called when a user has successfully placed an order from consumer project side + +export interface UndoOptions { + /** + * A boolean flag. Controls if undo should be disabled. + */ + disableUndo?: boolean +} + +export interface SymbolInfo { + ticker: string + name?: string + shortName?: string + exchange?: string + market?: string + pricePrecision?: number + volumePrecision?: number + priceCurrency?: string + dollarPerPip?: number + type?: string + logo?: string +} + +export interface OrderInfo { + orderId: number + action: OrderType + entryPoint: number + exitPoint?: number + stopLoss?: number + takeProfit?: number + lotSize: number + pips?: number + pl?: number + entryTime?: string + exitTime?: string + exitType?: ExitType + partials?: string + accountId?: number +} + +export interface OrderModifyInfo { + id: number + action?: OrderType + entrypoint?: number + exitpoint?: number + stoploss?: number + takeprofit?: number + lotsize?: number + pips?: number + pl?: number + exittime?: string + exittype?: ExitType + partials?: string +} + +export interface Period extends DefaultPeriod { + text: string +} + +export interface ProChart extends Chart { + chart: Chart + charts: Array + + setActiveChart (id: string): void + chartById (id: string): Chart | undefined + getOverlay (filter?: PickRequired): Overlay[] + createOrderLine (options?: UndoOptions): Nullable + createOrderLines (nums: number, options?: UndoOptions): Array> +} + +type IndicatorsType = { + value?: IndicatorCreate, + isStack?: boolean, + paneOptions?: PaneOptions +} + +type OverlaysType = { + value?: OverlayCreate, + paneId: string +} + +type FiguresType = { + value?: string|FigureCreate, + ctx: CanvasRenderingContext2D +} + +type OrderStyleType = { + lineStyle?: { + style?: string, + size?: number, + color?: string, + dashedValue?: number[] + }, + labelStyle?: { + style?: string, + size?: number, + family?: string, + weight?: string, + paddingLeft?: number, + paddingRight?: number, + paddingBottom?: number, + paddingTop?: number, + borderStyle?: string, + borderSize?: number, + color?: string, + borderColor?: string, + backgroundColor?: string + } +} + +export type OrderStylesType = { + buyStyle?: OrderStyleType, + buyLimitStyle?: OrderStyleType, + buyStopStyle?: OrderStyleType, + sellStyle?: OrderStyleType, + sellLimitStyle?: OrderStyleType, + sellStopStyle?: OrderStyleType, + stopLossStyle?: OrderStyleType, + takeProfitStyle?: OrderStyleType +} + +export interface ChartObjType { + styleObj?: DeepPartial + overlays?: OverlaysType[] + figures?: FiguresType[] + indicators?: IndicatorsType[] + orderStyles?: OrderStylesType +} + +export interface Datafeed { + searchSymbols (search?: string): Promise + getHistoryKLineData (symbol: SymbolInfo, period: Period, from: number, to: number): Promise + subscribe (symbol: SymbolInfo, period: Period, callback: DatafeedSubscribeCallback): void + unsubscribe (symbol: SymbolInfo, period: Period): void +} + +export interface ChartDataLoaderType extends DataLoader { + searchSymbols (search?: string): Promise + loading: boolean +} + +export interface OrderResource { + retrieveOrder (order_id: number): Promise + retrieveOrders (action?: OrderType, account_id?: number|string): Promise + openOrder (action: OrderType, lot_size: number, entry_price: number, stop_loss?: number, take_profit?: number): Promise + closeOrder (order_id: number, lotsize?: number): Promise + modifyOrder (order: OrderModifyInfo): Promise + unsetSlOrTP (order_id: string|number, slortp: 'sl'|'tp'): Promise + launchOrderModal (type: OrderModalType, callback: OrderPlacedCallback, order?: OrderModifyInfo): void +} + +export interface ChartProOptions { + container: string | HTMLElement + rootElementId?: string + styles?: DeepPartial + watermark?: string | Node + theme?: string + locale?: string + drawingBarVisible?: boolean + orderPanelVisible?: boolean + symbol: SymbolInfo + period: Period + periods?: Period[] + timezone?: string + mainIndicators?: string[] + subIndicators?: string[] + datafeed: Datafeed + dataTimestamp?: number + orderController: OrderResource +} + +export interface ChartPro { + setTheme(theme: string): void + getTheme(): string + setStyles(styles: DeepPartial): void + getStyles(): Styles + setLocale(locale: string): void + getLocale(): string + setTimezone(timezone: string): void + getTimezone(): string + setSymbol(symbol: SymbolInfo): void + getSymbol(): SymbolInfo + setPeriod(period: Period): void + getPeriod(): Period + getInstanceApi(): Nullable + resize(): void + dispose(): void +} diff --git a/src/widget/drawing-bar/index.tsx b/src/widget/drawing-bar/index.tsx index 936c0a93..40a2581c 100644 --- a/src/widget/drawing-bar/index.tsx +++ b/src/widget/drawing-bar/index.tsx @@ -26,7 +26,7 @@ import { export interface DrawingBarProps { locale: string - onDrawingItemClick: (overlay: OverlayCreate) => void + onDrawingItemClick: (value: string | OverlayCreate) => void onModeChange: (mode: string) => void, onLockChange: (lock: boolean) => void onVisibleChange: (visible: boolean) => void diff --git a/src/widget/indicator-modal/index.tsx b/src/widget/indicator-modal/index.tsx index 0a36729d..1c80f3e5 100644 --- a/src/widget/indicator-modal/index.tsx +++ b/src/widget/indicator-modal/index.tsx @@ -21,7 +21,8 @@ import i18n from '../../i18n' type OnIndicatorChange = ( params: { name: string - paneId: string + paneId?: string + id: string added: boolean } ) => void @@ -54,7 +55,7 @@ const IndicatorModal: Component = props => {
  • { - props.onMainIndicatorChange({ name, paneId: 'candle_pane', added: !checked }) + props.onMainIndicatorChange({ name, id: 'candle_pane', added: !checked }) }}>
  • @@ -76,7 +77,7 @@ const IndicatorModal: Component = props => { class="row" onClick={_ => { // @ts-expect-error - props.onSubIndicatorChange({ name, paneId: props.subIndicators[name] ?? '', added: !checked }); + props.onSubIndicatorChange({ name, id: props.subIndicators[name] ?? '', added: !checked }); }}> diff --git a/src/widget/period-bar/index.tsx b/src/widget/period-bar/index.tsx index 974b7089..9d154ac1 100644 --- a/src/widget/period-bar/index.tsx +++ b/src/widget/period-bar/index.tsx @@ -14,7 +14,7 @@ import { Component, Show, createSignal, onMount, onCleanup } from 'solid-js' -import { SymbolInfo, Period } from '../../types' +import { SymbolInfo, Period } from '../../types/types' import i18n from '../../i18n' diff --git a/src/widget/setting-modal/data.ts b/src/widget/setting-modal/data.ts index 1f95c8b5..ddcae7b5 100644 --- a/src/widget/setting-modal/data.ts +++ b/src/widget/setting-modal/data.ts @@ -13,61 +13,42 @@ */ import i18n from '../../i18n' +import useDataSource from './settings/dataSource' +import getCandleSettings from './settings/candle' +import getIndicatorSettings from './settings/indicator' +import getXAxisSettings from './settings/xAxis' +import getYAxisSettings from './settings/yAxis' +import getGridSettings from './settings/grid' +import getCrosshairSettings from './settings/crosshair' +import getOverlaySettings from './settings/overlay' export function getOptions (locale: string) { + const { size } = useDataSource(locale) return [ + ...getCandleSettings(locale), + ...getIndicatorSettings(locale), + ...getXAxisSettings(locale), + ...getYAxisSettings(locale), + ...getGridSettings(locale), + ...getCrosshairSettings(locale), + ...getOverlaySettings(locale), + + // seperator { - key: 'candle.type', - text: i18n('candle_type', locale), - component: 'select', - dataSource: [ - { key: 'candle_solid', text: i18n('candle_solid', locale) }, - { key: 'candle_stroke', text: i18n('candle_stroke', locale) }, - { key: 'candle_up_stroke', text: i18n('candle_up_stroke', locale) }, - { key: 'candle_down_stroke', text: i18n('candle_down_stroke', locale) }, - { key: 'ohlc', text: i18n('ohlc', locale) }, - { key: 'area', text: i18n('area', locale) } - ] - }, - { - key: 'candle.priceMark.last.show', - text: i18n('last_price_show', locale), - component: 'switch' - }, - { - key: 'candle.priceMark.high.show', - text: i18n('high_price_show', locale), - component: 'switch' - }, - { - key: 'candle.priceMark.low.show', - text: i18n('low_price_show', locale), - component: 'switch' - }, - { - key: 'indicator.lastValueMark.show', - text: i18n('indicator_last_value_show', locale), - component: 'switch' - }, - { - key: 'yAxis.type', - text: i18n('price_axis_type', locale), + key: 'separator.size', + text: i18n('Seperator size', locale), component: 'select', - dataSource: [ - { key: 'normal', text: i18n('normal', locale) }, - { key: 'percentage', text: i18n('percentage', locale) }, - { key: 'log', text: i18n('log', locale) } - ], + dataSource: size }, { - key: 'yAxis.reverse', - text: i18n('reverse_coordinate', locale), + key: 'separator.fill', + text: i18n('Fill', locale), component: 'switch', }, { - key: 'grid.show', - text: i18n('grid_show', locale), - component: 'switch', + key: 'separator.activeBackgroundColor', + text: i18n('Background color', locale), + component: 'color', } ] -} +} \ No newline at end of file diff --git a/src/widget/setting-modal/index.less b/src/widget/setting-modal/index.less index 107b65e4..2225421e 100644 --- a/src/widget/setting-modal/index.less +++ b/src/widget/setting-modal/index.less @@ -1,10 +1,47 @@ @import (reference) '../../base.less'; .@{prefix-cls}-setting-modal-content { - display: grid; - grid-template-columns: auto auto auto auto; - grid-row-gap: 20px; - margin-top: 20px; - margin-bottom: 30px; - align-items: center; + display: flex; + align-items: start; + gap: 16px; + height: 400px; + + .sidebar{ + padding: 16px 8px; + display: flex; + flex-direction: column; + width: fit-content; + border-right: 1px solid var(--klinecharts-pro-border-color); + overflow: auto; + height: 100%; + + button{ + padding: 8px; + text-align: left; + color: var(--klinecharts-pro-primary-color); + border-radius: 2px; + + &.selected{ + background-color: var(--klinecharts-pro-primary-color); + color: white + } + } + } + .content{ + height: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; + margin-bottom: 10px; + overflow: auto; + + .component{ + display: grid; + align-items: center; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + } + } } diff --git a/src/widget/setting-modal/index.tsx b/src/widget/setting-modal/index.tsx index 33e0e34b..3ce9560f 100644 --- a/src/widget/setting-modal/index.tsx +++ b/src/widget/setting-modal/index.tsx @@ -17,11 +17,14 @@ import { Styles, utils, DeepPartial } from 'klinecharts' import lodashSet from 'lodash/set' -import { Modal, Select, Switch } from '../../component' +import { Modal, Select, Switch, Color } from '../../component' import type { SelectDataSourceItem } from '../../component' import i18n from '../../i18n' import { getOptions } from './data' +import { ChartObjType } from '../../types' +// import { chartsession, chartsessionCtr } from '../../ChartProComponent' +import { setChartModified } from '../../store/chartStateStore' export interface SettingModalProps { locale: string @@ -34,14 +37,32 @@ export interface SettingModalProps { const SettingModal: Component = props => { const [styles, setStyles] = createSignal(props.currentStyles) const [options, setOptions] = createSignal(getOptions(props.locale)) + const [currentSetting, setCurrentSetting] = createSignal('candle') + + let stylee: DeepPartial = {} createEffect(() => { setOptions(getOptions(props.locale)) }) const update = (option: SelectDataSourceItem, newValue: any) => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + let chartObj: ChartObjType + if (chartStateObj) { + chartObj = (JSON.parse(chartStateObj) as ChartObjType) + chartObj.styleObj = chartObj.styleObj ?? {} + } else { + chartObj = { + styleObj: {} + } + } + + lodashSet(chartObj.styleObj!, option.key, newValue) + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + setChartModified(true) const style = {} lodashSet(style, option.key, newValue) + lodashSet(style, option.key, newValue) const ss = utils.clone(styles()) lodashSet(ss, option.key, newValue) setStyles(ss) @@ -49,63 +70,115 @@ const SettingModal: Component = props => { props.onChange(style) } + const restoreDefault = () => { + const chartStateObj = localStorage.getItem(`chartstatedata`) + let chartObj: ChartObjType + if (chartStateObj) { + chartObj = (JSON.parse(chartStateObj) as ChartObjType) + if (chartObj.styleObj) + chartObj.styleObj = {} + + localStorage.setItem(`chartstatedata`, JSON.stringify(chartObj)) + } + props.onRestoreDefault(options()) + props.onClose() + } + + const settingsButton = [ + {text: 'Candle', key: 'candle'}, + {text: 'Indicator', key: 'indicator'}, + {text: 'Grid', key: 'grid'}, + {text: 'X-axis', key: 'xAxis'}, + {text: 'Y-axis', key: 'yAxis'}, + {text: 'Separator', key: 'separator'}, + {text: 'Crosshair', key: 'crosshair'}, + {text: 'Overlay', key: 'overlay'}, + ] + return ( { - props.onRestoreDefault(options()) - props.onClose() + restoreDefault() } } ]} - onClose={props.onClose}> -
    - - { - option => { - let component - const value = utils.formatValue(styles(), option.key) - switch (option.component) { - case 'select': { - component = ( - { + const newValue = (data as SelectDataSourceItem).key + update(option, newValue) + }}/> + ) + break + } + case 'switch': { + const open = !!value + component = ( + { + const newValue = !open + update(option, newValue) + }}/> + ) + break + } + case 'color': { + component = ( + { + const newValue = el + update(option, newValue) + }} + /> + ) + break + } + } + return ( + <> +
    + {option.text} + {component} +
    + + ) - break } } - return ( - <> - {option.text} - {component} - - ) - } - } -
    + +
    ) diff --git a/src/widget/setting-modal/settings/candle.ts b/src/widget/setting-modal/settings/candle.ts new file mode 100644 index 00000000..2ccef159 --- /dev/null +++ b/src/widget/setting-modal/settings/candle.ts @@ -0,0 +1,247 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getCandleSettings = (locale:string) => { + const { font_family, font_size, font_weight, size, solid_dashed, fill_stroke, none_always_followCross } = useDataSource(locale) + return [ + { + key: 'candle.type', + text: i18n('candle_type', locale), + component: 'select', + dataSource: [ + { key: 'candle_solid', text: i18n('candle_solid', locale) }, + { key: 'candle_stroke', text: i18n('candle_stroke', locale) }, + { key: 'candle_up_stroke', text: i18n('candle_up_stroke', locale) }, + { key: 'candle_down_stroke', text: i18n('candle_down_stroke', locale) }, + { key: 'ohlc', text: i18n('ohlc', locale) }, + { key: 'area', text: i18n('area', locale) } + ] + }, + { + key: 'candle.bar.upColor', + text: i18n('Bar up color', locale), + component: 'color' + }, + { + key: 'candle.bar.downColor', + text: i18n('Bar down color', locale), + component: 'color' + }, + { + key: 'candle.bar.noChangeColor', + text: i18n('Bar no-change color', locale), + component: 'color' + }, + { + key: 'candle.bar.upBorderColor', + text: i18n('Bar up-border color', locale), + component: 'color' + }, + { + key: 'candle.bar.downBorderColor', + text: i18n('Bar down-border color', locale), + component: 'color' + }, + { + key: 'candle.bar.noChangeBorderColor', + text: i18n('Bar no-change-border color', locale), + component: 'color' + }, + { + key: 'candle.bar.upWickColor', + text: i18n('Bar up-wick color', locale), + component: 'color' + }, + { + key: 'candle.bar.downWickColor', + text: i18n('Bar down-wick color', locale), + component: 'color' + }, + { + key: 'candle.bar.noChangeWickColor', + text: i18n('Bar no-change-wick color', locale), + component: 'color' + }, + { + key: 'candle.area.lineSize', + text: i18n('Area linesize', locale), + component: 'select', + dataSource: size + }, + { + key: 'candle.area.lineColor', + text: i18n('Area line color', locale), + component: 'color' + }, + + { + key: 'candle.priceMark.high.show', + text: i18n('high_price_show', locale), + component: 'switch' + }, + { + key: 'candle.priceMark.high.color', + text: i18n('Pricemark high color', locale), + component: 'color' + }, + { + key: 'candle.priceMark.high.textSize', + text: i18n('Pricemark High Textsize', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'candle.priceMark.high.textFamily', + text: i18n('Pricemark High Font family', locale), + component: 'select', + dataSource: font_family + }, + { + key: 'candle.priceMark.high.textWeight', + text: i18n('Pricemark High Font Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'candle.priceMark.low.show', + text: i18n('low_price_show', locale), + component: 'switch' + }, + { + key: 'candle.priceMark.low.color', + text: i18n('Pricemark low color', locale), + component: 'color' + }, + { + key: 'candle.priceMark.low.textSize', + text: i18n('Pricemark Low Textsize', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'candle.priceMark.low.textFamily', + text: i18n('Pricemark Low Font family', locale), + component: 'select', + dataSource: font_family + }, + { + key: 'candle.priceMark.low.textWeight', + text: i18n('Pricemark Low Font Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'candle.priceMark.last.show', + text: i18n('Show Pricemark last', locale), + component: 'switch' + }, + { + key: 'candle.priceMark.last.upColor', + text: i18n('Pricemark last up-color', locale), + component: 'color' + }, + { + key: 'candle.priceMark.last.downColor', + text: i18n('Pricemark last down-color', locale), + component: 'color' + }, + { + key: 'candle.priceMark.last.noChangeColor', + text: i18n('Pricemark last no-change-color', locale), + component: 'color' + }, + { + key: 'candle.priceMark.last.line.show', + text: i18n('Show Pricemark last line', locale), + component: 'switch' + }, + { + key: 'candle.priceMark.last.line.style', + text: i18n('Pricemark last line style', locale), + component: 'select', + dataSource: solid_dashed + }, + { + key: 'candle.priceMark.last.line.size', + text: i18n('Pricemark last line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'candle.priceMark.last.text.show', + text: i18n('Show Pricemark last text', locale), + component: 'switch' + }, + { + key: 'candle.priceMark.last.text.style', + text: i18n('Pricemark last text style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'candle.priceMark.last.text.size', + text: i18n('Pricemark last text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'candle.priceMark.last.text.color', + text: i18n('Pricemark last text color', locale), + component: 'color' + }, + // candle tooltip + { + key: 'candle.tooltip.showRule', + text: i18n('candle_tooltip', locale), + component: 'select', + dataSource: none_always_followCross + }, + { + key: 'candle.tooltip.showType', + text: i18n('Tooltip showtype', locale), + component: 'select', + dataSource: [ + { key: 'rect', text: i18n('Rect', locale) }, + { key: 'standard', text: i18n('Standard', locale) } + ] + }, + { + key: 'candle.tooltip.rect.position', + text: i18n('Tooltip rect position', locale), + component: 'select', + dataSource: [ + { key: 'fixed', text: i18n('Fixed', locale) }, + { key: 'pointer', text: i18n('Pointer', locale) } + ] + }, + { + key: 'candle.tooltip.rect.color', + text: i18n('Tooltip rect color', locale), + component: 'color', + }, + { + key: 'candle.tooltip.rect.borderColor', + text: i18n('Tooltip rect border-color', locale), + component: 'color', + }, + { + key: 'candle.tooltip.text.size', + text: i18n('Tooltip text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'candle.tooltip.text.weight', + text: i18n('Tooltip text weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'candle.tooltip.text.color', + text: i18n('Tooltip text color', locale), + component: 'color', + } + ] +} + +export default getCandleSettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/crosshair.ts b/src/widget/setting-modal/settings/crosshair.ts new file mode 100644 index 00000000..d736a87e --- /dev/null +++ b/src/widget/setting-modal/settings/crosshair.ts @@ -0,0 +1,147 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getCrosshairSettings = (locale:string) => { + const { solid_dashed, dashed_value, fill_stroke, size, font_size, font_weight } = useDataSource(locale) + return [ + { + key: 'crosshair.show', + text: i18n('Show Crosshair', locale), + component: 'switch', + }, + { + key: 'crosshair.horizontal.show', + text: i18n('Show horizontal', locale), + component: 'switch', + }, + { + key: 'crosshair.horizontal.line.show', + text: i18n('Show horizontal line', locale), + component: 'switch', + }, + { + key: 'crosshair.horizontal.line.style', + text: i18n('Horizontal Line Style', locale), + component: 'select', + dataSource: solid_dashed + }, + { + key: 'crosshair.horizontal.line.dashed_value', + text: i18n('Horizontal Line Style', locale), + component: 'select', + dataSource: dashed_value + }, + { + key: 'crosshair.horizontal.line.size', + text: i18n('Horizontal Line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'crosshair.horizontal.line.color', + text: i18n('Horizontal Line color', locale), + component: 'color' + }, + { + key: 'crosshair.horizontal.text.show', + text: i18n('Show horizontal text', locale), + component: 'switch', + }, + { + key: 'crosshair.horizontal.text.style', + text: i18n('Horizontal text Style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'crosshair.horizontal.text.size', + text: i18n('Horizontal text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'crosshair.horizontal.text.weight', + text: i18n('Horizontal text weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'crosshair.horizontal.text.color', + text: i18n('Horizontal text color', locale), + component: 'color' + }, + { + key: 'crosshair.horizontal.text.backgroundColor', + text: i18n('Horizontal text background color', locale), + component: 'color' + }, + { + key: 'crosshair.vertical.show', + text: i18n('Show vertical', locale), + component: 'switch', + }, + { + key: 'crosshair.vertical.line.show', + text: i18n('Show vertical line', locale), + component: 'switch', + }, + { + key: 'crosshair.vertical.line.style', + text: i18n('Vertical Line Style', locale), + component: 'select', + dataSource: solid_dashed + }, + { + key: 'crosshair.vertical.line.size', + text: i18n('Vertical Line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'crosshair.vertical.line.size', + text: i18n('Vertical Line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'crosshair.vertical.line.color', + text: i18n('Vertical line color', locale), + component: 'color' + }, + { + key: 'crosshair.vertical.text.show', + text: i18n('Show vertical text', locale), + component: 'switch', + }, + { + key: 'crosshair.vertical.text.style', + text: i18n('Vertical text Style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'crosshair.vertical.text.size', + text: i18n('Vertical text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'crosshair.vertical.text.weight', + text: i18n('Vertical text weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'crosshair.vertical.text.color', + text: i18n('Vertical text color', locale), + component: 'color' + }, + { + key: 'crosshair.vertical.text.backgroundColor', + text: i18n('Vertical text background color', locale), + component: 'color' + }, + ] +} + +export default getCrosshairSettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/dataSource.ts b/src/widget/setting-modal/settings/dataSource.ts new file mode 100644 index 00000000..8dff6da3 --- /dev/null +++ b/src/widget/setting-modal/settings/dataSource.ts @@ -0,0 +1,63 @@ +import i18n from '../../../i18n' + +const useDataSource = (locale:string) => { + const solid_dashed = [ + { key: 'solid', text: i18n('solid', locale) }, + { key: 'dashed', text: i18n('dashed', locale) } + ] + const dashed_value = [ + { key: [4, 4]} + ] + const true_false = [ + { key: 'true', text: true }, + { key: 'false', text: false } + ] + const size = [ + { key: 1, text: 1 }, + { key: 2, text: 2 }, + { key: 3, text: 3 }, + { key: 4, text: 4 }, + { key: 5, text: 5 }, + { key: 6, text: 6 }, + { key: 7, text: 7 }, + { key: 8, text: 8 }, + { key: 9, text: 9 }, + { key: 10, text: 10 }, + { key: 11, text: 11 }, + { key: 12, text: 12 }, + { key: 13, text: 13 }, + { key: 14, text: 14 }, + { key: 15, text: 15 } + ] + const fill_stroke = [ + { key: 'fill', text: i18n('fill', locale) }, + { key: 'stroke', text: i18n('stroke', locale) }, + { key: 'stroke_fill', text: i18n('stroke_fill', locale) } + ] + const none_always_followCross = [ + { key: 'none', text: i18n('none', locale) }, + { key: 'always', text: i18n('always', locale) }, + { key: 'follow_cross', text: i18n('follow_cross', locale) } + ] + const font_size = [ + { key: 10, text: 10 }, + { key: 11, text: 11 }, + { key: 12, text: 12 }, + { key: 14, text: 14 }, + { key: 16, text: 16 }, + { key: 18, text: 18 }, + { key: 20, text: 20 }, + { key: 22, text: 22 }, + { key: 24, text: 24 }, + ] + const font_weight = [ + { key: 'normal', text: i18n('Normal', locale) } + ] + const font_family = [ + { key: 'Helvetica Neue', text: i18n('Helvetica Neue', locale) } + ] + + return {solid_dashed, dashed_value, size, fill_stroke, font_family, font_size, font_weight, none_always_followCross, true_false} +} + +export default useDataSource \ No newline at end of file diff --git a/src/widget/setting-modal/settings/grid.ts b/src/widget/setting-modal/settings/grid.ts new file mode 100644 index 00000000..69757e62 --- /dev/null +++ b/src/widget/setting-modal/settings/grid.ts @@ -0,0 +1,47 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getGridSettings = (locale:string) => { + const { size } = useDataSource(locale) + return [ + { + key: 'grid.show', + text: i18n('grid_show', locale), + component: 'switch', + }, + { + key: 'grid.horizontal.show', + text: i18n('Show horizontal', locale), + component: 'switch', + }, + { + key: 'grid.horizontal.show', + text: i18n('Horizontal size', locale), + component: 'select', + dataSource: size + }, + { + key: 'grid.horizontal.color', + text: i18n('Horizontal color', locale), + component: 'color' + }, + { + key: 'grid.vertical.show', + text: i18n('Show vertical', locale), + component: 'switch', + }, + { + key: 'grid.vertical.show', + text: i18n('Vertical size', locale), + component: 'select', + dataSource: size + }, + { + key: 'grid.vertical.color', + text: i18n('Vertical color', locale), + component: 'color' + } + ] +} + +export default getGridSettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/indicator.ts b/src/widget/setting-modal/settings/indicator.ts new file mode 100644 index 00000000..f6704ae4 --- /dev/null +++ b/src/widget/setting-modal/settings/indicator.ts @@ -0,0 +1,131 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getIndicatorSettings = (locale:string) => { + const { fill_stroke, font_size, font_weight, none_always_followCross } = useDataSource(locale) + + return [ + // { + // key: 'indicator.bars.style', + // text: i18n('Bar style', locale), + // component: 'select', + // dataSource: [ + // { key: 'fill', text: i18n('Fill', locale) }, + // { key: 'stroke', text: i18n('Stroke', locale) }, + // { key: 'stroke_fill', text: i18n('Stroke fill', locale) } + // ] + // }, + // { + // key: 'indicator.bars.borderStyle', + // text: i18n('Bar border style', locale), + // component: 'select', + // dataSource: [ + // { key: 'solid', text: i18n('Solid', locale) }, + // { key: 'dashed', text: i18n('Dashed', locale) } + // ] + // }, + // { + // key: 'indicator.bars.borderSize', + // text: i18n('Bar border thickness', locale), + // component: 'select', + // dataSource: [ + // { key: 1, text: 1}, + // { key: 2, text: 2}, + // { key: 3, text: 3}, + // { key: 4, text: 4} + // ] + // }, + { + key: 'indicator.ohlc.upColor', + text: i18n('Ohlc up-color', locale), + component: 'color' + }, + { + key: 'indicator.ohlc.downColor', + text: i18n('Ohlc down-color', locale), + component: 'color' + }, + { + key: 'indicator.ohlc.noChangeColor', + text: i18n('Ohlc no-change-color', locale), + component: 'color' + }, + { + key: 'indicator.lastValueMark.show', + text: i18n('indicator_last_value_show', locale), + component: 'switch' + }, + { + key: 'indicator.lastValueMark.text.show', + text: i18n('Show last value mark text', locale), + component: 'switch' + }, + { + key: 'indicator.lastValueMark.text.style', + text: i18n('Last value mark text style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'indicator.lastValueMark.text.size', + text: i18n('Last value mark text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'indicator.lastValueMark.text.weight', + text: i18n('Last value mark text Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'indicator.lastValueMark.text.color', + text: i18n('Last value mark text color', locale), + component: 'color' + }, + { + key: 'indicator.tooltip.showRule', + text: i18n('Tooltip showrule', locale), + component: 'select', + dataSource: none_always_followCross + }, + { + key: 'indicator.tooltip.showType', + text: i18n('Tooltip showtype', locale), + component: 'select', + dataSource: [ + { key: 'standard', text: i18n('Standard', locale) }, + { key: 'rect', text: i18n('Rect', locale) } + ] + }, + { + key: 'indicator.tooltip.showName', + text: i18n('Show tooltip name', locale), + component: 'switch' + }, + { + key: 'indicator.tooltip.showParams', + text: i18n('Show tooltip params', locale), + component: 'switch' + }, + { + key: 'indicator.tooltip.text.size', + text: i18n('Tooltip text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'indicator.tooltip.text.weight', + text: i18n('Tooltip text Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'indicator.tooltip.text.color', + text: i18n('Tooltip text color', locale), + component: 'color', + } + ] +} + +export default getIndicatorSettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/overlay.ts b/src/widget/setting-modal/settings/overlay.ts new file mode 100644 index 00000000..302edabb --- /dev/null +++ b/src/widget/setting-modal/settings/overlay.ts @@ -0,0 +1,179 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getOverlaySettings = (locale:string) => { + const { solid_dashed, fill_stroke, size, font_size, font_weight, font_family } = useDataSource(locale) + return [ + { + key: 'overlay.point.color', + text: i18n('Point color', locale), + component: 'color' + }, + { + key: 'overlay.point.borderColor', + text: i18n('Point border color', locale), + component: 'color' + }, + { + key: 'overlay.point.activeColor', + text: i18n('Point active color', locale), + component: 'color' + }, + { + key: 'overlay.point.activeBorderColor', + text: i18n('Point active border color', locale), + component: 'color' + }, + { + key: 'overlay.line.style', + text: i18n('Line Style', locale), + component: 'select', + dataSource: solid_dashed + }, + { + key: 'overlay.line.smooth', + text: i18n('Smooth line', locale), + component: 'switch', + }, + { + key: 'overlay.line.size', + text: i18n('Line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'overlay.line.color', + text: i18n('Line color', locale), + component: 'color', + }, + + { + key: 'overlay.rect.style', + text: i18n('Rect Style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'overlay.rect.color', + text: i18n('Rect color', locale), + component: 'color' + }, + { + key: 'overlay.rect.borderColor', + text: i18n('Rect border color', locale), + component: 'color' + }, + + { + key: 'overlay.polygon.style', + text: i18n('Polygon Style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'overlay.polygon.color', + text: i18n('Polygon color', locale), + component: 'color' + }, + { + key: 'overlay.polygon.borderColor', + text: i18n('Polygon border color', locale), + component: 'color' + }, + + { + key: 'overlay.circle.style', + text: i18n('Circle Style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'overlay.circle.color', + text: i18n('Circle color', locale), + component: 'color' + }, + { + key: 'overlay.circle.borderColor', + text: i18n('Circle border color', locale), + component: 'color' + }, + + { + key: 'overlay.arc.style', + text: i18n('Arc Style', locale), + component: 'select', + dataSource: solid_dashed + }, + { + key: 'overlay.arc.size', + text: i18n('Arc size', locale), + component: 'select', + dataSource: size + }, + { + key: 'overlay.arc.color', + text: i18n('Arc color', locale), + component: 'color' + }, + + { + key: 'overlay.text.size', + text: i18n('Text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'overlay.text.weight', + text: i18n('Text weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'overlay.text.color', + text: i18n('Text color', locale), + component: 'color' + }, + + { + key: 'overlay.rectText.style', + text: i18n('Rect_text style', locale), + component: 'select', + dataSource: fill_stroke + }, + { + key: 'overlay.rectText.size', + text: i18n('Rect_text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'overlay.rectText.family', + text: i18n('Rect_text font family', locale), + component: 'select', + dataSource: font_family + }, + { + key: 'overlay.rectText.weight', + text: i18n('Rect_text weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'overlay.rectText.color', + text: i18n('Rect_text color', locale), + component: 'color', + }, + { + key: 'overlay.rectText.backgroundColor', + text: i18n('Rect_text background color', locale), + component: 'color', + }, + { + key: 'overlay.rectText.borderColor', + text: i18n('Rect_text border color', locale), + component: 'color', + } + ] +} + +export default getOverlaySettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/xAxis.ts b/src/widget/setting-modal/settings/xAxis.ts new file mode 100644 index 00000000..4c2fc362 --- /dev/null +++ b/src/widget/setting-modal/settings/xAxis.ts @@ -0,0 +1,75 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getXAxisSettings = (locale:string) => { + const { font_size, size, font_weight } = useDataSource(locale) + return [ + { + key: 'xAxis.show', + text: i18n('Show xAxis', locale), + component: 'switch', + }, + { + key: 'xAxis.axisLine.show', + text: i18n('Show axis line', locale), + component: 'switch', + }, + { + key: 'xAxis.axisLine.color', + text: i18n('Axis line color', locale), + component: 'color', + }, + { + key: 'xAxis.axisLine.size', + text: i18n('Axis line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'xAxis.tickText.show', + text: i18n('Show tick text', locale), + component: 'switch', + }, + { + key: 'xAxis.tickText.size', + text: i18n('Tick text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'xAxis.tickText.weight', + text: i18n('Tick text Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'xAxis.tickText.color', + text: i18n('Tick text color', locale), + component: 'color', + }, + { + key: 'xAxis.tickLine.show', + text: i18n('Show tick line', locale), + component: 'switch', + }, + { + key: 'xAxis.tickLine.size', + text: i18n('Tick line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'xAxis.tickLine.length', + text: i18n('Tick line length', locale), + component: 'select', + dataSource: size + }, + { + key: 'xAxis.tickLine.color', + text: i18n('Tick line color', locale), + component: 'color' + } + ] +} + +export default getXAxisSettings \ No newline at end of file diff --git a/src/widget/setting-modal/settings/yAxis.ts b/src/widget/setting-modal/settings/yAxis.ts new file mode 100644 index 00000000..daefeb5d --- /dev/null +++ b/src/widget/setting-modal/settings/yAxis.ts @@ -0,0 +1,104 @@ +import i18n from '../../../i18n' +import useDataSource from './dataSource' + +const getYAxisSettings = (locale:string) => { + const { size, font_weight, font_size } = useDataSource(locale) + return [ + { + key: 'yAxis.show', + text: i18n('Show yAxis', locale), + component: 'switch', + }, + { + key: 'yAxis.position', + text: i18n('Position', locale), + component: 'select', + dataSource: [ + { key: 'right', text: i18n('Right', locale) }, + { key: 'left', text: i18n('Left', locale) } + ], + }, + { + key: 'yAxis.type', + text: i18n('price_axis_type', locale), + component: 'select', + dataSource: [ + { key: 'normal', text: i18n('normal', locale) }, + { key: 'percentage', text: i18n('percentage', locale) }, + { key: 'log', text: i18n('log', locale) } + ], + }, + { + key: 'yAxis.inside', + text: i18n('Inside', locale), + component: 'switch', + }, + { + key: 'yAxis.reverse', + text: i18n('reverse_coordinate', locale), + component: 'switch', + }, + { + key: 'yAxis.axisLine.show', + text: i18n('Show axis line', locale), + component: 'switch', + }, + { + key: 'yAxis.axisLine.size', + text: i18n('Axis line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'yAxis.axisLine.color', + text: i18n('Axis line color', locale), + component: 'color' + }, + { + key: 'yAxis.tickText.show', + text: i18n('Show tick text', locale), + component: 'switch', + }, + { + key: 'yAxis.tickText.color', + text: i18n('Tick text color', locale), + component: 'color' + }, + { + key: 'yAxis.tickText.size', + text: i18n('Tick text size', locale), + component: 'select', + dataSource: font_size + }, + { + key: 'yAxis.tickText.weight', + text: i18n('Tick text Weight', locale), + component: 'select', + dataSource: font_weight + }, + { + key: 'yAxis.tickLine.show', + text: i18n('Show tick line', locale), + component: 'switch', + }, + { + key: 'yAxis.tickLine.color', + text: i18n('Tick line color', locale), + component: 'color', + }, + { + key: 'yAxis.tickLine.size', + text: i18n('Tick line size', locale), + component: 'select', + dataSource: size + }, + { + key: 'yAxis.tickLine.length', + text: i18n('Tick line length', locale), + component: 'select', + dataSource: size + } + ] +} + +export default getYAxisSettings \ No newline at end of file diff --git a/src/widget/symbol-search-modal/index.tsx b/src/widget/symbol-search-modal/index.tsx index 26f7b173..ccbb6ac6 100644 --- a/src/widget/symbol-search-modal/index.tsx +++ b/src/widget/symbol-search-modal/index.tsx @@ -18,11 +18,11 @@ import { Modal, List, Input } from '../../component' import i18n from '../../i18n' -import { SymbolInfo, Datafeed } from '../../types' +import { SymbolInfo, ChartDataLoaderType } from '../../types/types' export interface SymbolSearchModalProps { locale: string - datafeed: Datafeed + datafeed: ChartDataLoaderType onSymbolSelected: (symbol: SymbolInfo) => void onClose: () => void } diff --git a/src/widget/timeframe-modal/index.less b/src/widget/timeframe-modal/index.less new file mode 100644 index 00000000..5f1ba179 --- /dev/null +++ b/src/widget/timeframe-modal/index.less @@ -0,0 +1,14 @@ +@import (reference) '../../base.less'; + +.@{prefix-cls}-timeframe-modal-input { + margin: 20px 0 10px 0; + height: 40px; + svg { + width: 18px; + height: 18px; + fill: var(--klinecharts-pro-text-second-color); + } + .value { + font-size: 16px; + } +} \ No newline at end of file diff --git a/src/widget/timeframe-modal/index.tsx b/src/widget/timeframe-modal/index.tsx new file mode 100644 index 00000000..a5b9fad0 --- /dev/null +++ b/src/widget/timeframe-modal/index.tsx @@ -0,0 +1,118 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, createSignal } from 'solid-js' + +import { Modal, Input } from '../../component' + +import i18n from '../../i18n' + +import { Period } from '../../types' +import { setInputClass } from '../../component/input' +import { setPeriodModalVisible } from '../../store/chartStore' + +export interface TimeframeModalProps { + locale: string + periods: Period[] + onTimeframeSelected: (period: Period) => void +} + +export const [periodInputValue, setPeriodInputValue] = createSignal('') + +const TimeframeModal: Component = props => { + const defClass = 'klinecharts-pro-input klinecharts-pro-timeframe-modal-input' + const [period, setPeriod] = createSignal(null) + + const handleKeyDown = (event: KeyboardEvent) => { + // Check if the pressed key is "Enter" (key code 13) + if (event.key === "Enter") { + if (period()) { + props.onTimeframeSelected(period()!) + } + closeModal() + // Add your desired action here + } else { + // Get the key code of the pressed key + const keyCode = event.key; + + // Check if the key code corresponds to a number, letter, or Backspace + const isAllowedKey = + /^[a-zA-Z0-9]$/i.test(keyCode) || // Check for letters and numbers + keyCode === "Backspace"; // Check for Backspace + + // If the key is not allowed, prevent default behavior (e.g., typing) + if (isAllowedKey) { + if (keyCode === 'Backspace') + setPeriodInputValue(periodInputValue().slice(0, -1)) + else + setPeriodInputValue(periodInputValue()+event.key) + + performCheck() + } + } + } + + function performCheck() { + const perio = props.periods.find((per) => { + if (periodInputValue() === `${per.span}` && per.text.split(' ')[1].charAt(0) === 'm') { + console.log('1 passed') + return true + } + // if (periodInputValue().slice(0, -1) === `${per.multiplier}` && /\d/.test(periodInputValue().charAt(periodInputValue().length-1))) { + // console.log('2 passed') + // return true + // } + if (periodInputValue().slice(0, -1) === `${per.span}` && ((periodInputValue().charAt(periodInputValue().length - 1) === per.text.split(' ')[1].charAt(0)))) { + console.log('3 passed') + return true + } + return false + }) + console.log(perio) + if (perio) { + setPeriod(perio) + setInputClass(defClass) + } else { + setInputClass(defClass + ' input-error') + setPeriod(null) + } + } + + function closeModal() { + setPeriodInputValue('') + setPeriodModalVisible(false) + } + + performCheck() + + return ( + + + + + } + focus={true} + value="" + onKeyDown={handleKeyDown}/> + + ) +} + +export default TimeframeModal