코딩/Spring Boot

ECharts로 켄들차트 그리기

skyoon 2025. 1. 17. 19:49

Git Code 버전 : 0.0.6-SNAPSHOT

  주식데이터를 켄들차트로 그려보겠다. Apache ECharts에서는 많은 형식의 그래프를 지원하며 예제도 잘되어있다.

https://echarts.apache.org/en/index.html

 

Apache ECharts

ECharts: A Declarative Framework for Rapid Construction of Web-based Visualization You are welcomed to cite the following paper whenever you use ECharts in your R&D projects, products, research papers, technical reports, news reports, books, presentations,

echarts.apache.org

 

라이브러리 포함하는 법은 간단하다. 아래와 같이 echart CDN을 HTML에 포함해 주면 된다.

<!-- ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>

 

코드는 아래와 같이 작성한다.

  • stock.html
더보기
<!-- 상단 입력 메뉴 코드-->

<div class="row">
    <div class="mt-5">
        <div class="md-12"></div>
        <div id="myChart" style="height: 600px"></div>
    </div>
</div>

<!-- 하단 DataTable 표시 코드-->

{ 중 략 } 
<script type="text/javascript">
    // { 중 략 }

    function getStockPriceInfo(table) {
        if ($("#itmsNm").val() === "") {
            alert("종목명을 입력해주세요!");
            return;
        }
        let jsonData = {
            itmsNm: $("#itmsNm").val(),
            numOfRows: $("#numOfRows").val(),
            pageNo: 1,
        };

        $.ajax({
            url: "/stock/getStockPriceInfo",
            type: "GET",
            data: jsonData,
            success: function (items) {
                table.clear().draw();

                // items 배열을 순회하며 HTML 문자열 생성
                items.forEach(function (item) {
                    table.row
                        .add([
                            item.basDt,
                            item.itmsNm,
                            item.clpr,
                            item.vs,
                            item.mkp,
                            item.hipr,
                            item.lopr,
                            item.mrktTotAmt,
                        ])
                        .draw();
                });

				// 주식정보 가져온 것을 차트 그리는 함수를 호출한다.
                drawCandleChart(items); // items를 사용하여 차트를 다시 그립니다.

                $("#itemTable").DataTable();
            },
            error: function (xhr, status, error) {
                alert("데이터를 가져오는 데 실패했습니다.");
            },
        });
    }
	// { 중 략 }

	// 하단 코드를 추가한다.
    function drawCandleChart(stockData) {
        const upColor = "#FF0000";
        const downColor = "#0000FF";
        // 10일 주식 데이터 샘플 (날짜, 시가, 종가, 저가, 고가, 거래량)

        let chartDom = $("#myChart")[0];
        let myChart = echarts.init(chartDom, null, {
            renderer: "canvas",
            useDirtyRecy: false,
        });
        let data = splitData(stockData);
        let getDataLen = data.categoryData.length;

        myChart.setOption(
            (option = {
                animation: false,
                legend: {
                    bottom: 10,
                    left: "center",
                    data: [$("#itmsNm").val(), "MA5", "MA10", "MA20", "MA30"],
                },
                tooltip: {
                    trigger: "axis",
                    axisPointer: { type: "cross" },
                    borderWidth: 1,
                    borderColor: "#ccc",
                    padding: 10,
                    textStyle: { color: "#000" },
                    position: function (pos, params, el, elRect, size) {
                        const obj = { top: 10 };
                        if (pos[0] < size.viewSize[0] / 2) {
                            obj["left"] = 30;
                        } else {
                            obj["right"] = 30;
                        }
                        return obj;
                    },
                },
                axisPointer: {
                    link: [{ xAxisIndex: "all" }],
                    label: { backgroundColor: "#777" },
                },
                toolbox: {
                    feature: {
                        dataZoom: { yAxisIndex: false },
                        brush: { type: ["lineX", "clear"] },
                    },
                },
                brush: {
                    xAxisIndex: "all",
                    brushLink: "all",
                    outOfBrush: { colorAlpha: 0.1 },
                },
                visualMap: {
                    show: false,
                    seriesIndex: 5,
                    dimension: 2,
                    pieces: [
                        {
                            value: 1,
                            color: downColor,
                        },
                        {
                            value: -1,
                            color: upColor,
                        },
                    ],
                },
                grid: [
                    {
                        left: "10%",
                        right: "8%",
                        height: "50%",
                    },
                    {
                        left: "10%",
                        right: "8%",
                        top: "63%",
                        height: "16%",
                    },
                ],
                xAxis: [
                    {
                        type: "category",
                        data: data.categoryData,
                        boundaryGap: false,
                        axisLine: { onZero: false },
                        splitLine: { show: false },
                        min: "dataMin",
                        max: "dataMax",
                        axisPointer: { z: 100 },
                    },
                    {
                        type: "category",
                        gridIndex: 1,
                        data: data.categoryData,
                        boundaryGap: false,
                        axisLine: { onZero: false },
                        axisTick: { show: false },
                        splitLine: { show: false },
                        axisLabel: { show: false },
                        min: "dataMin",
                        max: "dataMax",
                    },
                ],
                yAxis: [
                    {
                        scale: true,
                        splitArea: { show: true },
                    },
                    {
                        scale: true,
                        gridIndex: 1,
                        splitNumber: 2,
                        axisLabel: { show: false },
                        axisLine: { show: false },
                        axisTick: { show: false },
                        splitLine: { show: false },
                    },
                ],
                dataZoom: [
                    {
                        type: "inside",
                        xAxisIndex: [0, 1],
                        start: 50,
                        end: 100,
                    },
                    {
                        show: true,
                        xAxisIndex: [0, 1],
                        type: "slider",
                        top: "85%",
                        start: 50,
                        end: 100,
                    },
                ],
                series: [
                    {
                        name: $("#itmsNm").val(),
                        type: "candlestick",
                        data: data.values,
                        itemStyle: {
                            color: upColor,
                            color0: downColor,
                            borderColor: undefined,
                            borderColor0: undefined,
                        },
                    },
                    {
                        name: "MA5",
                        type: "line",
                        data: calculateMA(5, data),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5,
                        },
                    },
                    {
                        name: "MA10",
                        type: "line",
                        data: calculateMA(10, data),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5,
                        },
                    },
                    {
                        name: "MA20",
                        type: "line",
                        data: calculateMA(20, data),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5,
                        },
                    },
                    {
                        name: "MA30",
                        type: "line",
                        data: calculateMA(30, data),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5,
                        },
                    },
                    {
                        name: "Volume",
                        type: "bar",
                        xAxisIndex: 1,
                        yAxisIndex: 1,
                        data: data.volumes,
                    },
                ],
            }),
            true
        );
        /*
        myChart.dispatchAction({
            type: "brush",
            areas: [
                {
                    brushType: "lineX",
                    coordRange: [data.categoryData[getDataLen - 2], data.categoryData[getDataLen - 1]],
                    xAxisIndex: 0,
                },
            ],
        });*/
    }

    // 캔들 데이터 추출 함수
    function splitData(items) {
        let categoryData = [];
        let values = [];
        let volumes = [];
        items.sort(arrOrder("basDt"));

        for (let i = 0; i < items.length; i++) {
            categoryData.push(items[i].basDt);
            values.push([
                Number(items[i].mkp),
                Number(items[i].clpr),
                Number(items[i].lopr),
                Number(items[i].hipr),
                Number(items[i].mrktTotAmt),
            ]);
            volumes.push([i, items[i].mrktTotAmt, Number(items[i].mkp) > Number(items[i].clpr) ? 1 : -1]); // 거래량 데이터 추가
        }

        return {
            categoryData: categoryData,
            values: values,
            volumes: volumes,
        };
    }

    function arrOrder(key) {
        return (a, b) => a[key] - b[key];
    }

    // 이동 평균선 계산 함수
    function calculateMA(dayCount, data) {
        let result = new Array(data.values.length).fill("-");
        let sum = 0;

        // 첫 번째 `dayCount` 값의 초기 합계를 계산합니다.
        for (let i = 0; i < dayCount; i++) {
            sum += data.values[i][1]; // 고가 데이터 사용
        }

        // 첫 번째 MA 값을 저장합니다.
        result[dayCount - 1] = (sum / dayCount).toFixed(1);

        // 슬라이딩 윈도우를 사용하여 나머지 MA 값을 계산합니다.
        for (let i = dayCount; i < data.values.length; i++) {
            sum += data.values[i][1]; // 새로운 값을 더합니다.
            sum -= data.values[i - dayCount][1]; // 슬라이드 아웃되는 값을 뺍니다.
            result[i] = (sum / dayCount).toFixed(1);
        }

        return result;
    }
</script>

  켄들차트 이외 이동 평균선 함수까지 같이 포함된 코드이다. 수행해 보면 아래와 같이 표시된다.