Forcetrails: D3.js Candlesticks chart in Lightning web component

 Hello Friends! In this post, I will show you how to build a candlestick charts in the Lightning web component using the D3.js charts library.

candlestick-chart-lwc-d3js


What is D3.js?

D3Js is the data visualization library in Javascript. With the help of that, we can build different types of charts based on the data. D3js uses HTML, CSS, SVG, and Javascript to display the data visualizations. Learn more about D3.js.

For this exercise, we will see how to build candlestick charts using D3JS. We are going to display stock prices on this chart. If you don't know what is candlestick chart then follow this link.



Prerequisites

  • Create one custom object to store the stock prices.
    • Object Name: Stock Data (Stock_Data__c)
    • Fields:
      • Close Price (Close_Price__c) - Currency
      • Date (Date__c) - Date/Time
      • High Price (High_Price__c) - Currency
      • Low Price (Low_Price__c) - Currency
      • Open Price (Open_Price__c) - Currency
      • Symbol (Symbol__c) - Text
  • Download the D3js code zip file from here.
  • Upload the downloaded D3.zip file in static resource with Name D3Js.
  • Upload the sample data.


Implementation

Create an apex class with auraEnabled method to query the stock price data.

CandleStickChartController.cls

public class CandleStickChartController {
    @AuraEnabled(cacheable=true)
    public static List<Stock_Data__c> getStocksData() {
        return [
            SELECT
                Close_Price__c,
                Date__c,
                High_Price__c,
                Low_Price__c,
                Open_Price__c
            FROM Stock_Data__c
            ORDER BY Date__c
            LIMIT 50
        ];
    }
}


Create a lightning web component with the name candleStickChartD3js and copy-paste the following code in its respective files.

candleStickChartD3js.js

import { LightningElement, wire, api } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadScript, loadStyle } from "lightning/platformResourceLoader";

// import d3js from static resource
import D3Js from "@salesforce/resourceUrl/D3Js";

import getStocksData from "@salesforce/apex/CandleStickChartController.getStocksData";

export default class CandleStickChartD3js extends LightningElement {
    @api svgWidth = 1000;
    @api svgHeight = 400;

    d3Initialized = false;
    stocksData;

    renderedCallback() {
        if (this.d3Initialized) {
            return;
        }

        Promise.all([loadScript(this, D3Js + "/d3.min.js")])
            .then(() => {
                this.d3Initialized = true;
                this.initializeD3();
            })
            .catch((error) => {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "Error loading D3",
                        message: error.message,
                        variant: "error"
                    })
                );
            });
    }

    @wire(getStocksData)
    wiredGetStocksData({ error, data }) {
        if (error) {
            console.log(
                "error while getting stocks data",
                JSON.stringify(error)
            );
        } else if (data) {
            this.stocksData = data.map((item) => {
                let newItem = {
                    low: item.Low_Price__c,
                    open: item.Open_Price__c,
                    symbol: item.Symbol__c,
                    close: item.Close_Price__c,
                    high: item.High_Price__c,
                    date: new Date(item.Date__c)
                };
                return newItem;
            });

            this.initializeD3();
        }
    }

    initializeD3() {
        if (!this.d3Initialized || !this.stocksData) {
            return;
        }
        let height = this.svgHeight;
        let width = this.svgWidth;

        let data = this.stocksData;
        let margin = { top: 20, right: 30, bottom: 30, left: 40 };

        // X scale
        let x = d3
            .scaleBand()
            .domain(
                d3.utcDay.range(data[0].date, +data[data.length - 1].date + 1)
            )
            .range([margin.left, width - margin.right])
            .padding(0.2);

        // y scale
        let y = d3
            .scaleLog()
            .domain([d3.min(data, (d) => d.low), d3.max(data, (d) => d.high)])
            .rangeRound([height - margin.bottom, margin.top]);

        // x axis
        let xAxis = (g) =>
            g
                .attr("transform", `translate(0,${height - margin.bottom})`)
                .style("font-size", "0.8rem")
                .call(
                    d3
                        .axisBottom(x)
                        .tickValues(
                            d3.utcMonday
                                .every(width > 720 ? 1 : 2)
                                .range(data[0].date, data[data.length - 1].date)
                        )
                        .tickFormat(d3.utcFormat("%-m/%-d/%Y"))
                )
                .call((g) => g.select(".domain").remove());

        // y axis
        let yAxis = (g) =>
            g
                .attr("transform", `translate(${margin.left},0)`)
                .style("font-size", "0.8rem")
                .call(
                    d3
                        .axisLeft(y)
                        .tickFormat(d3.format("$~f"))
                        .tickValues(d3.scaleLinear().domain(y.domain()).ticks())
                )
                .call((g) =>
                    g
                        .selectAll(".tick line")
                        .clone()
                        .attr("stroke-opacity", 0.2)
                        .attr("x2", width - margin.left - margin.right)
                )
                .call((g) => g.select(".domain").remove());

        // format date
        let formatDate = d3.utcFormat("%B %-d, %Y");

        function formatChange() {
            const f = d3.format("+.2%");
            return (y0, y1) => f((y1 - y0) / y0);
        }

        const svg = d3.select(this.template.querySelector("svg.d3"));
        svg.attr("viewBox", [0, 0, width, height]);
        svg.append("g").call(xAxis);

        svg.append("g").call(yAxis);

        const g = svg
            .append("g")
            .attr("stroke-linecap", "round")
            .attr("stroke", "black")
            .selectAll("g")
            .data(data)
            .join("g")
            .attr("transform", (d) => {
                return `translate(${x(d.date)},0)`;
            });

        g.append("line")
            .attr("y1", (d) => y(d.low))
            .attr("y2", (d) => y(d.high));

        g.append("line")
            .attr("y1", (d) => y(d.open))
            .attr("y2", (d) => y(d.close))
            .attr("stroke-width", x.bandwidth())
            .attr("stroke", (d) =>
                d.open > d.close
                    ? d3.schemeSet1[0]
                    : d.close > d.open
                    ? d3.schemeSet1[2]
                    : d3.schemeSet1[8]
            );

        g.append("title").text(
            (d) =>
                `${formatDate(d.date)} \n` +
                `Open: ${d.open}\n` +
                `Close: ${d.close} (${formatChange()(d.open, d.close)})\n` +
                `Low: ${d.low}\n` +
                `High: ${d.high}`
        );
    }
}

candleStickChartD3js.html

<template>
    <lightning-card title="Candlestick Chart Using D3Js" icon-name="custom:custom19">
        <div class="slds-m-around_medium slds-align_absolute-center">
            <svg class="d3" width={svgWidth} height={svgHeight} lwc:dom="manual"></svg>
        </div>
    </lightning-card>
</template>

candleStickChartD3js.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>50.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

The Lightning Component tab

Lastly, create the Lightning Component tab and add the new component in that so you can access the chart from the tab. If needed you can also add that to the lightning app or record page.


Final Notes

If you face any issue check the below things -
  • if the static resource is loaded and the script is loaded successfully.
  • if the data is loaded correctly and it does not contain any null values.
  • if any API method from D3 is no working try putting version v6.3.1 of d3 in the static resource.
  • if the data types are correct.
This is the basic demonstration of how we can do this, you can add more advanced features like -
  • option to query and display specific data, for example from specific dates.
  • zoom and pan features
  • click and expand to show more details from records.


References




You may also like



No comments :
Post a Comment

Hi there, comments on this site are moderated, you might need to wait until your comment is published. Spam and promotions will be deleted. We have moderated the comments for the safety of this website users. If you have any concern, or if you are not able to comment for some reason, reach email us at rahul@forcetrails.com