Files
riwa-ionic/src/composables/useTradingView.ts

209 lines
5.5 KiB
TypeScript

import type { Awaitable } from "@vueuse/core";
import type { ChartOptions, DeepPartial, HistogramData, IChartApi, ISeriesApi, LayoutOptions, SeriesDataItemTypeMap, SeriesType, Time } from "lightweight-charts";
import type { ThemeMode } from "./useTheme";
import { AreaSeries, BarSeries, BaselineSeries, CandlestickSeries, ColorType, createChart, HistogramSeries, LineSeries } from "lightweight-charts";
import { cloneDeep, mergeWith } from "lodash-es";
export type Series = "Area" | "Bar" | "Baseline" | "Candlestick" | "Histogram" | "Line";
export type TData = SeriesDataItemTypeMap[SeriesType];
export type WeightChartOptions = DeepPartial<ChartOptions>;
export type TradingViewData = (() => Awaitable<TData[]>) | MaybeRefOrGetter<TData[]>;
export interface VolumeData extends HistogramData {
value: number;
color?: string;
}
export interface TradingViewOptions<T = Series> {
theme?: ThemeMode;
type?: T;
data?: TradingViewData;
volumeData?: MaybeRefOrGetter<VolumeData[]>;
weightChartOptions?: MaybeRefOrGetter<WeightChartOptions>;
autosize?: boolean;
showVolume?: boolean;
}
const lightThemeLayout: Partial<LayoutOptions> = {
textColor: "black",
background: { type: ColorType.Solid, color: "#fff" },
};
const darkThemeLayout: Partial<LayoutOptions> = {
textColor: "white",
background: { type: ColorType.Solid, color: "#000" },
};
const initializeOptions: Required<TradingViewOptions> = {
theme: "dark",
type: "Candlestick",
data: [],
volumeData: [],
weightChartOptions: {
width: 400,
height: 300,
},
autosize: true,
showVolume: true,
};
function getChartSeriesDefinition(type: Series) {
switch (type) {
case "Area":
return AreaSeries;
case "Bar":
return BarSeries;
case "Baseline":
return BaselineSeries;
case "Candlestick":
return CandlestickSeries;
case "Histogram":
return HistogramSeries;
case "Line":
return LineSeries;
default:
return CandlestickSeries;
}
}
const { isDark } = useTheme();
export function useTradingView(target: MaybeRefOrGetter<HTMLElement | null>, options?: TradingViewOptions) {
const opts: Required<TradingViewOptions> = mergeWith(cloneDeep(initializeOptions), cloneDeep(options));
const chart = ref<IChartApi | null>(null);
const chartSeriesDefinition = getChartSeriesDefinition(opts.type);
const chartEl = ref<HTMLElement | null>(null);
const series = ref<ISeriesApi<SeriesType> | null>(null);
const volumeSeries = ref<ISeriesApi<"Histogram"> | null>(null);
function fitContent() {
chart.value?.timeScale().fitContent();
}
function resize() {
if (!chart.value || !chartEl.value)
return;
const dimensions = chartEl.value.getBoundingClientRect();
chart.value.resize(dimensions.width, dimensions.height);
}
function setData(data: TradingViewData) {
if (isRef(data)) {
series.value?.setData(toValue(data));
}
else if (isFunction(data)) {
const result = data();
if (isPromise(result)) {
result.then((data) => {
series.value?.setData(data);
fitContent();
}).catch((err) => {
console.error("Failed to load trading view data:", err);
});
}
else {
series.value?.setData(result);
}
}
else {
series.value?.setData(toValue(data));
}
}
function setVolumeData(data: MaybeRefOrGetter<VolumeData[]>) {
if (!volumeSeries.value)
return;
const volumeData = toValue(data);
volumeSeries.value.setData(volumeData);
}
function changeTheme(theme: ThemeMode) {
if (!chart.value)
return;
const layout = theme === "dark" ? darkThemeLayout : lightThemeLayout;
chart.value.applyOptions({ layout });
}
tryOnMounted(() => {
const el = unrefElement(target);
if (!el)
return;
chartEl.value = el;
const chartOptions = toValue(opts.weightChartOptions);
chart.value = createChart(el, chartOptions);
series.value = chart.value.addSeries(chartSeriesDefinition, {
priceFormat: {
type: "price",
precision: 1,
},
});
// 添加成交量副图
if (opts.showVolume) {
volumeSeries.value = chart.value.addSeries(HistogramSeries, {
priceFormat: {
type: "volume",
},
priceScaleId: "", // 设置为右侧价格轴
});
// 配置成交量图表在底部
chart.value.priceScale("").applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
// 设置成交量数据
if (opts.volumeData) {
setVolumeData(opts.volumeData);
}
}
watch(isDark, (dark) => {
changeTheme(dark ? "dark" : "light");
}, { immediate: true });
setData(opts.data);
fitContent();
if (isRef(opts.data)) {
watch(opts.data, (newData) => {
setData(newData);
}, { deep: true });
}
if (isRef(opts.volumeData)) {
watch(opts.volumeData, (newData) => {
setVolumeData(newData);
}, { deep: true });
}
if (opts.autosize) {
useTimeoutFn(() => {
resize();
}, 0);
window.addEventListener("resize", resize);
}
});
tryOnUnmounted(() => {
chart.value?.remove();
chart.value = null;
window.removeEventListener("resize", resize);
});
watch(() => toValue(opts.weightChartOptions), (newOptions) => {
if (!chart.value)
return;
chart.value.applyOptions(newOptions);
}, { deep: true });
return { chart, series, volumeSeries, fitContent, resize, setVolumeData };
}