vue3 를 사용하여 기본적인 트레이딩뷰챠트(lightweight-charts) 를 띄우려고 한다.
KB star 에서 사용하던 vue3 프로젝트를 기본으로 한다.
트레이딩뷰챠트를 사용하기 위해 가장 먼저 해야 할 일은 lightweight-charts 에서 설치하는 것입니다 .
npm install --save lightweight-charts
componests 구조
vue3 컴포넌트 참고
https://stackblitz.com/edit/vitejs-vite-r4bbai?file=src%2Fmain.js
vue ref() 참고
https://www.danvega.dev/blog/2020/02/12/vue3-ref-vs-reactive/
App.vue
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import { ref } from 'vue';
/*
* There are example components in both API styles: Options API, and Composition API
*
* Select your preferred style from the imports below:
*/
import LWChart from './components/composition-api/LWChart.vue';
// import LWChart from './components/options-api/LWChart.vue';
/**
* Generates sample data for the lightweight chart
* @param {Boolean} ohlc Whether generated dat should include open, high, low, and close values
* @returns {Array} sample data
*/
function generateSampleData(ohlc) {
const randomFactor = 25 + Math.random() * 25;
function samplePoint(i) {
return (
i *
(0.5 +
Math.sin(i / 10) * 0.2 +
Math.sin(i / 20) * 0.4 +
Math.sin(i / randomFactor) * 0.8 +
Math.sin(i / 500) * 0.5) +
200
);
}
const res = [];
let date = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
const numberOfPoints = ohlc ? 100 : 500;
for (var i = 0; i < numberOfPoints; ++i) {
const time = date.getTime() / 1000;
const value = samplePoint(i);
if (ohlc) {
const randomRanges = [
-1 * Math.random(),
Math.random(),
Math.random(),
].map((i) => i * 10);
const sign = Math.sin(Math.random() - 0.5);
res.push({
time,
low: value + randomRanges[0],
high: value + randomRanges[1],
open: value + sign * randomRanges[2],
close: samplePoint(i + 1),
});
} else {
res.push({
time,
value,
});
}
date.setUTCDate(date.getUTCDate() + 1);
}
return res;
}
const chartOptions = ref({});
const data = ref(generateSampleData(false));
const seriesOptions = ref({
color: 'rgb(45, 77, 205)',
});
const chartType = ref('line');
const lwChart = ref();
function randomShade() {
return Math.round(Math.random() * 255);
}
const randomColor = (alpha = 1) => {
return `rgba(${randomShade()}, ${randomShade()}, ${randomShade()}, ${alpha})`;
};
const colorsTypeMap = {
area: [
['topColor', 0.4],
['bottomColor', 0],
['lineColor', 1],
],
bar: [
['upColor', 1],
['downColor', 1],
],
baseline: [
['topFillColor1', 0.28],
['topFillColor2', 0.05],
['topLineColor', 1],
['bottomFillColor1', 0.28],
['bottomFillColor2', 0.05],
['bottomLineColor', 1],
],
candlestick: [
['upColor', 1],
['downColor', 1],
['borderUpColor', 1],
['borderDownColor', 1],
['wickUpColor', 1],
['wickDownColor', 1],
],
histogram: [['color', 1]],
line: [['color', 1]],
};
// Set a random colour for the series as an example of how
// to apply new options to series. A similar appraoch will work on the
// option properties.
const changeColors = () => {
const options = {};
const colorsToSet = colorsTypeMap[chartType.value];
colorsToSet.forEach((c) => {
options[c[0]] = randomColor(c[1]);
});
seriesOptions.value = options;
};
const changeData = () => {
const candlestickTypeData = ['candlestick', 'bar'].includes(chartType.value);
const newData = generateSampleData(candlestickTypeData);
data.value = newData;
if (chartType.value === 'baseline') {
const average =
newData.reduce((s, c) => {
return s + c.value;
}, 0) / newData.length;
seriesOptions.value = { baseValue: { type: 'price', price: average } };
}
};
const changeType = () => {
const types = [
'line',
'area',
'baseline',
'histogram',
'candlestick',
'bar',
].filter((t) => t !== chartType.value);
const randIndex = Math.round(Math.random() * (types.length - 1));
chartType.value = types[randIndex];
changeData();
// call a method on the component.
lwChart.value.fitContent();
};
</script>
<template>
<div class="chart-container">
<LWChart
:type="chartType"
:data="data"
:autosize="true"
:chart-options="chartOptions"
:series-options="seriesOptions"
ref="lwChart"
/>
</div>
<button type="button" @click="changeColors">Set Random Colors</button>
<button type="button" @click="changeType">Change Chart Type</button>
<button type="button" @click="changeData">Change Data</button>
</template>
<style scoped>
.chart-container {
margin: auto;
width: 95%;
height: 300px;
}
</style>
main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
style.css
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.5em 1em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #e9e9e9;
cursor: pointer;
transition: border-color 0.25s;
margin-left: 0.5em;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
#app {
height: 100vh;
width: 100vw;
}
LWChart.vue
<script setup>
import {
ref,
onMounted,
onUnmounted,
watch,
defineExpose,
defineProps,
} from 'vue';
import { createChart } from 'lightweight-charts';
const props = defineProps({
type: {
type: String,
default: 'line',
},
data: {
type: Array,
required: true,
},
autosize: {
default: true,
type: Boolean,
},
chartOptions: {
type: Object,
},
seriesOptions: {
type: Object,
},
timeScaleOptions: {
type: Object,
},
priceScaleOptions: {
type: Object,
},
});
// Function to get the correct series constructor name for current series type.
function getChartSeriesConstructorName(type) {
return `add${type.charAt(0).toUpperCase() + type.slice(1)}Series`;
}
// Lightweight Chart instances are stored as normal JS variables
// If you need to use a ref then it is recommended that you use `shallowRef` instead
let series;
let chart;
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 seriesConstructor = getChartSeriesConstructorName(props.type);
series = chart[seriesConstructor](props.seriesOptions);
series.setData(props.data);
};
onMounted(() => {
// Create the Lightweight Charts Instance using the container ref.
chart = createChart(chartContainer.value, props.chartOptions);
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);
}
});
onUnmounted(() => {
if (chart) {
chart.remove();
chart = null;
}
if (series) {
series = null;
}
});
/*
* 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 (series && chart) {
chart.removeSeries(series);
}
addSeriesAndData(props);
}
);
watch(
() => props.data,
(newData) => {
if (!series) return;
series.setData(newData);
}
);
watch(
() => props.chartOptions,
(newOptions) => {
if (!chart) return;
chart.applyOptions(newOptions);
}
);
watch(
() => props.seriesOptions,
(newOptions) => {
if (!series) return;
series.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>
참고
https://tradingview.github.io/lightweight-charts/tutorials/vuejs/wrapper