Search code examples
javascriptreactjshighcharts

highcharts in react customization with plotlines and custom marker only on new data point


I am using highcharts and highcharts-react-official to create chart for eth Realtime price. I also show user currentround details that i am getting in startedRoundData from partent component and it render like this (image attached) also added plotlines for startTime and endTime. as you can see plotline text is corpped. enter image description here

This is the exact chart I want to copy with all details and functionality. enter image description here

Here is my code

import React, { useEffect, useState } from "react";
import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import moment from "moment";

const Chart = ({ data, startedRoundData }) => {
  const [chartOptions, setChartOptions] = useState({});

  useEffect(() => {
    const pricesData = data?.prices || [];
    const chartData = pricesData
      .slice(0, -1)
      .map((item) => [item.timestamp * 1000, parseFloat(item.price)]);

    const lastTimestamp = chartData[chartData.length - 1]?.[0] || 0;
    const futureTimestamp = lastTimestamp + 10000; // 10 seconds in the future

    const options = {
      chart: {
        type: "spline",
        backgroundColor: "#131722",
        height: "400px",
        marginRight: 10,
        marginLeft: 0,
        marginBottom: 30,
        marginTop: 10,
        animation: Highcharts.svg,
      },
      title: null,
      xAxis: {
        type: "datetime",
        lineColor: "#2a2e39",
        tickColor: "#2a2e39",
        labels: {
          style: { color: "#787b86", fontSize: "10px" },
          y: 20,
          formatter: function () {
            return moment(this.value).format("HH:mm:ss");
          },
        },
        tickLength: 0,
        minPadding: 0,
        maxPadding: 0,
        gridLineWidth: 1,
        gridLineColor: "#2a2e39",
        tickInterval: 10000, // 10 seconds interval
        tickAmount: 6,
        min: lastTimestamp - 50000, // Show 50 seconds of past data
        max: futureTimestamp, // Extend to 10 seconds in the future
        plotLines: [
          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.startTime
              ? moment(startedRoundData?.startTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              useHTML: true, // Allows for custom HTML
              align: "center",
              y: 0,
              x: 0,
              formatter: function () {
                return `
                      <div class="custom-plotline-label">
                        <span class="label-text">Current</span>
                      </div>
                    `;
              },
            },
            events: {
              render: function () {
                // Access and style after rendering
                const plotLineLabel = this.plotLinesAndBands[0].label.element;
                plotLineLabel.style.transform = "rotate(270deg)";
                plotLineLabel.style.transformOrigin = "100% 12px";
                plotLineLabel.style.position = "absolute";
                plotLineLabel.style.left = "150px";
                plotLineLabel.style.top = "10px";
              },
            },
          },

          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.lockTime
              ? moment(startedRoundData?.lockTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              text: "End",
              align: "center",
              style: { color: "#ffffff", fontSize: "12px" },
              y: 15,
            },
          },
        ],
      },
      yAxis: {
        title: null,
        labels: {
          align: "right",
          x: 45,
          style: { color: "#787b86", fontSize: "10px" },
          formatter: function () {
            return this.value.toFixed(2);
          },
        },
        gridLineColor: "#2a2e39",
        gridLineWidth: 1,
        tickAmount: 5,
        opposite: true,
        plotLines: [
          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.startTime
              ? moment(startedRoundData?.startTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              text: startedRoundData?.startPrice
                ? startedRoundData?.startPrice
                : null,
              align: "center",
              style: {
                color: "#ffffff",
                fontSize: "12px",
                whiteSpace: "nowrap", // To ensure the text doesn't break
              },
              y: 15,
            },
          },
        ],
      },
      legend: { enabled: false },
      series: [
        {
          name: "Price",
          data: chartData,
          color: "#00ff00",
          lineWidth: 2,
          marker: {
            enabled: false,
          },
        },
      ],
      tooltip: {
        enabled: false,
      },
      credits: { enabled: false },
      plotOptions: {
        spline: {
          animation: {
            duration: 1000,
            easing: "easeOutQuart",
          },
        },
      },
    };

    setChartOptions(options);
  }, [data]);

  useEffect(() => {
    if (chartOptions.series) {
      const chart = Highcharts.charts[0];
      if (chart) {
        const series = chart.series[0];
        const lastPoint = series.data[series.data.length - 1];
        if (lastPoint) {
          lastPoint.update(
            {
              marker: {
                enabled: true,
                radius: 5,
                symbol: "circle",
                fillColor: "#00ff00",
                lineColor: "#00ff00",
                lineWidth: 2,
                states: {
                  hover: {
                    enabled: true,
                    radius: 5,
                    fillColor: "#00ff00",
                    lineColor: "#00ff00",
                    lineWidth: 2,
                  },
                },
              },
            },
            false
          );
          chart.redraw();
        }
      }
    }
  }, [chartOptions]);

  return (
    <div className="bg-[#131722] p-4 rounded-lg h-[485px]">
      <HighchartsReact highcharts={Highcharts} options={chartOptions} />
    </div>
  );
};

export default Chart;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>


Solution

  • The best solution will be to use annotations with the custom shape and plot lines without labels. For example:

    Highcharts.SVGRenderer.prototype.symbols.customArrow = function (x, y, w, h) {
      return ['M', x, y, 'L', x + w, y, x + w, y + h, x, y + h, x - 10, y +h / 2, 'z'];
    };
    

      annotations: [{
        crop: false,
        events: {
          add: function(){
            const rotateLabel = (label) => {
              const {
                width,
                height
              } = label.graphic.getBBox();
              label.attr({
                rotation: -90,
                rotationOriginX: width / 2,
                rotationOriginY: height / 2
              });
            }
    
            rotateLabel(this.labels[0]);
            rotateLabel(this.labels[1]);
          }
        },
        labels: [{
          shape: 'customArrow',
          text: 'Current',
          ...
        }, ...]
      }]
    

    Live demo: https://stackblitz.com/edit/react-cfh7pt?file=index.js,style.css

    API Reference: https://api.highcharts.com/highcharts/annotations

    Docs: https://www.highcharts.com/docs/advanced-chart-features/annotations-module