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



2 comments:
  1. gosh, I do agree that you code would work but I want to understand why you have called all those methods and why you have set specifically those variables of d3js, so that I could build my own d3JS, please make some tutorial video or explain here why u have used those specific methods and how will I learn to use them ??

    ReplyDelete
    Replies
    1. Hi Arth, thank you for visiting, it really great idea about to make a video tutorial. Till then I can give you little insights on the above code. First thing you should note that D3Js is not just a charting library you can do anything that you can do with SVG and JS to draw and animate images (many sites use d3js for animating the views or drawing abstract svg images), that is also a scalable. If you even want to create your own representation of the data you can do that definitely. So in my opinion D3Js gives us endless possibilities for data visualization.

      variable x - defines the x axis scale based on the data. Which is the smallest and the biggest date in the input data. to define the start and end of x axis.

      variable y - defines the y axis scale based on the data. Which is the smallest and the biggest share value in the input data. to define the min and max of y axis.

      xAxis - defines the tick values of the x axis based on the zoom level using transform. basically the distance between two marker points on the x axis. like 0, 2, ...n.

      yAxis - defines the linear scale of the axis basically the distance between two marker points of the x-axis this can range from days, months, hours based on the zoom values.

      then there is some code to draw the lines to show the stock values. some date formatting and displaying titles etc. Now if you back to the code you will know what is happening there. Hope this helps! Thanks for visiting!

      Delete

Hi there, comments on this site are moderated, you might need to wait until your comment is published. Spam and promotions will be deleted. Sorry for the inconvenience but 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