Files
chart/src/components/LWChart.vue
2025-08-24 23:44:43 +09:00

432 lines
8.9 KiB
Vue

<script setup>
import {
ref,
onMounted,
onUnmounted,
watch,
defineExpose,
defineProps,
} from 'vue';
import {
createChart,
LineSeries,
AreaSeries,
BarSeries,
CandlestickSeries,
HistogramSeries,
BaselineSeries,
} from 'lightweight-charts';
const props = defineProps({
type: {
type: String,
default: 'line',
},
data: {
type: Array,
required: true,
},
ema: {
type: Array,
required: true,
},
macd: {
type: Object,
},
bb: {
type: Object,
},
rsi: {
type: Object,
},
autosize: {
default: true,
type: Boolean,
},
chartOptions: {
type: Object,
},
emaOptions: {
type: Object,
},
candleOptions: {
type: Object,
},
macdLineOptions: {
type: Object,
},
macdSignalOptions: {
type: Object,
},
macdHistogramOptions: {
type: Object,
},
rsiLineOptions: {
type: Object,
},
upperBbOptions: {
type: Object,
},
middleBbOptions: {
type: Object,
},
lowerBbOptions: {
type: Object,
},
timeScaleOptions: {
type: Object,
},
priceScaleOptions: {
type: Object,
},
lazyLock: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['lazyLoad', 'mouseMove'])
function getChartSeriesDefinition(type) {
switch (type.toLowerCase()) {
case 'line':
return LineSeries;
case 'area':
return AreaSeries;
case 'bar':
return BarSeries;
case 'candlestick':
return CandlestickSeries;
case 'histogram':
return HistogramSeries;
case 'baseline':
return BaselineSeries;
}
return LineSeries;
}
// Lightweight Charts™ instances are stored as normal JS variables
// If you need to use a ref then it is recommended that you use `shallowRef` instead
let emaLine;
let candlestick;
let chart;
let macdLine;
let macdSignalLine;
let macdHistogram;
let macdPane;
let rsiPane;
let upperBb;
let middleBb;
let lowerBb;
let rsiLine;
const chartContainer = ref();
const fitContent = () => {
if (!chart) return;
chart.timeScale().fitContent();
};
const getChart = () => {
return chart;
};
defineExpose({fitContent, getChart});
// Auto resizes the chart when the browser window is resized.
const resizeHandler = () => {
if (!chart || !chartContainer.value) return;
const dimensions = chartContainer.value.getBoundingClientRect();
chart.resize(dimensions.width, dimensions.height);
};
// Creates the chart series and sets the data.
const addSeriesAndData = props => {
const candleStickDefinition = getChartSeriesDefinition(props.type);
candlestick = chart.addSeries(candleStickDefinition, props.candleOptions);
let oldT = 0;
let list = []
props.data.forEach(item => {
if (item.t !== oldT) {
list.push({
time: Number(item.t),
open: Number(item.o),
high: Number(item.h),
low: Number(item.l),
close: Number(item.c),
volume: Number(item.v),
})
}
oldT = item.t
})
candlestick.setData(list);
const seriesDefinition = getChartSeriesDefinition('line')
emaLine = chart.addSeries(seriesDefinition, props.emaOptions)
emaLine.setData(props.ema)
macdLine = macdPane.addSeries(seriesDefinition, props.macdLineOptions)
macdSignalLine = macdPane.addSeries(seriesDefinition, props.macdSignalOptions)
macdHistogram = macdPane.addSeries(getChartSeriesDefinition('histogram'), props.macdHistogramOptions)
if(Object.keys(props.macd).length) {
macdLine.setData(props.macd.m)
macdHistogram.setData(props.macd.h)
macdSignalLine.setData(props.macd.s)
}
upperBb = chart.addSeries(seriesDefinition, props.upperBbOptions);
middleBb = chart.addSeries(seriesDefinition, props.middleBbOptions);
lowerBb = chart.addSeries(seriesDefinition, props.lowerBbOptions);
if(Object.keys(props.macd).length) {
upperBb.setData(props.bb.u);
middleBb.setData(props.bb.m);
lowerBb.setData(props.bb.l);
}
rsiLine = rsiPane.addSeries(seriesDefinition, props.rsiLineOptions)
if(Object.keys(props.macd).length) {
rsiLine.setData(props.rsi)
}
// console.log(3)
};
onMounted(() => {
// Create the Lightweight Charts Instance using the container ref.
chart = createChart(chartContainer.value, props.chartOptions);
macdPane = chart.addPane();
macdPane.setStretchFactor(0.5)
rsiPane = chart.addPane();
rsiPane.setStretchFactor(0.4)
addSeriesAndData(props);
if (props.priceScaleOptions) {
chart.priceScale().applyOptions(props.priceScaleOptions);
}
if (props.timeScaleOptions) {
chart.timeScale().applyOptions(props.timeScaleOptions);
}
chart.timeScale().fitContent();
if (props.autosize) {
window.addEventListener('resize', resizeHandler);
}
chart.timeScale().subscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChanged);
chart.subscribeCrosshairMove(onCrosshairMove)
chart.subscribeClick(onClick);
});
function onClick(dd) {
console.log(dd)
}
function onVisibleLogicalRangeChanged(newVisibleLogicalRange) {
const barsInfo = candlestick.barsInLogicalRange(newVisibleLogicalRange);
// if there less than 50 bars to the left of the visible area
if (barsInfo !== null && barsInfo.barsBefore < 500 && !props.lazyLock) {
// try to load additional historical data and prepend it to the series data
emits('lazyLoad', props.data[0].t - 5000);
}
}
function onCrosshairMove(param) {
if (param.point === null || param.time === undefined) {
return;
}
emits('mouseMove', param.seriesData.get(candlestick))
//
// // 현재 포인터 아래의 캔들 데이터 가져오기
// const data = param.seriesData.get(candleSeries);
// console.log(data)
}
onUnmounted(() => {
if (chart) {
chart.remove();
chart = null;
}
if (candlestick) {
candlestick = null;
}
window.removeEventListener('resize', resizeHandler);
});
/*
* Watch for changes to any of the component properties.
* If an options property is changed then we will apply those options
* on top of any existing options previously set (since we are using the
* `applyOptions` method).
*
* If there is a change to the chart type, then the existing series is removed
* and the new series is created, and assigned the data.
*
*/
watch(
() => props.autosize,
enabled => {
if (!enabled) {
window.removeEventListener('resize', resizeHandler);
return;
}
window.addEventListener('resize', resizeHandler);
}
);
watch(
() => props.type,
newType => {
if (candlestick && chart) {
chart.removeSeries(candlestick);
}
addSeriesAndData(props);
}
);
watch(
() => props.data,
newData => {
if (!candlestick) return;
let oldT = 0;
let list = []
newData.forEach(item => {
if (item.t !== oldT) {
list.push({
time: item.t,
open: Number(item.o),
high: Number(item.h),
low: Number(item.l),
close: Number(item.c),
volume: Number(item.v),
})
}
oldT = item.t
})
candlestick.setData(list);
}
);
watch(
() => props.ema,
newData => {
if (!emaLine) return;
emaLine.setData(newData);
}
);
watch(
() => props.macd,
newData => {
if (!macdLine || !macdHistogram || !macdSignalLine) return;
macdLine.setData(newData.m);
macdHistogram.setData(newData.h);
macdSignalLine.setData(newData.s);
}
);
watch(
() => props.bb,
newData => {
if (!upperBb || !middleBb || !lowerBb) return;
upperBb.setData(newData.u);
middleBb.setData(newData.m);
lowerBb.setData(newData.l);
}
);
watch(
() => props.rsi,
newData => {
if (!rsiLine) return;
rsiLine.setData(props.rsi);
}
);
watch(
() => props.chartOptions,
newOptions => {
if (!chart) return;
chart.applyOptions(newOptions);
}
);
watch(
() => props.candleOptions,
newOptions => {
if (!candlestick) return;
candlestick.applyOptions(newOptions);
}
);
watch(
() => props.emaOptions,
newOptions => {
if (!emaLine) return;
emaLine.applyOptions(newOptions);
}
);
watch(
() => props.macdLineOptions,
newOptions => {
if (!macdLine) return;
macdLine.applyOptions(newOptions);
}
);
watch(
() => props.macdSignalOptions,
newOptions => {
if (!macdSignalLine) return;
macdSignalLine.applyOptions(newOptions);
}
);
watch(
() => props.macdHistogramOptions,
newOptions => {
if (!macdHistogram) return;
macdHistogram.applyOptions(newOptions);
}
);
watch(
() => props.priceScaleOptions,
newOptions => {
if (!chart) return;
chart.priceScale().applyOptions(newOptions);
}
);
watch(
() => props.timeScaleOptions,
newOptions => {
if (!chart) return;
chart.timeScale().applyOptions(newOptions);
}
);
</script>
<template>
<div class="lw-chart" ref="chartContainer"></div>
</template>
<style scoped>
.lw-chart {
height: 100%;
}
</style>