Search code examples
angulartypescriptchartsapexchartsng-apexcharts

How to apply Apex charts multiple gradient fills?


I am using Angular 18 and this is the chart That I want to create: Orignal Chart image

My ts configuration for chart is given below:

// chart--------------
  public chartData: {
    series: ApexAxisChartSeries;
    chart: ApexChart;
    yaxis: ApexYAxis;
    xaxis: ApexXAxis;
    annotations: ApexAnnotations;
    fill: ApexFill;
    
    stroke: ApexStroke;
    markers: ApexMarkers;
    dataLabels:ApexDataLabels
    grid:ApexGrid
    plotOptions:ApexPlotOptions
    colors:string[]
  };
  @ViewChild("chart") chart!: ChartComponent;
constructor(private renderer: Renderer2) {
    const seriesData = [50, 40, 35, -10, -40, 60, -70];
    this.chartData = {
      series: [
        {
          name: 'Profit/Loss',
          data: seriesData,
        },
      ],
      chart: {
        type: 'area',
        height: 400,
        foreColor: '#2f2f2f',
        toolbar:{
          show:false
        }
      },
      yaxis: {
        opposite: true,
        min: -100,
        max: 100,
        tickAmount: 20, // Creates gaps of 10%
        labels: {
          formatter: (value) => {
            return value >= 0 ? `${value}%` : `${value}%`; // Return the value
          },
          style: {
            // Apply the class based on value
            colors:[ '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600','#565656','#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD',]
          }
        }
      },
      xaxis: {
        labels: {
          show: true, // Hide the x-axis labels
        },
        axisBorder: {
          show: false,
        },
        axisTicks: {
          show: false,
        },
      },
      annotations: {
        yaxis: [
          {
            y: 0,
            borderColor: '#000',
            strokeDashArray: 5, // Create the dotted line at 0%
          },
        ],
        points: [
          {
            x: 360, // End of the data series
            y: -70, // Example value of profit/loss at the end of the line
            label: {
              text: 'Profit: 70%',
              borderColor: '#00E396',
              style: {
                color: '#fff',
                background: '#00E396',
              },
            },
          },
        ],
      },
      fill: {
        type: 'gradient',
        gradient: {
          type:'horizontal',
          shadeIntensity: 1,
          inverseColors: false,
          opacityFrom: 0.7,
          opacityTo: 0.9,
          gradientToColors:undefined,
          stops: [0, 50, 100],
          colorStops: [
              {
                offset: 0,
                color: "#19FB9B", // Green color at 0%
                opacity: 0.5
              },
              {
                offset: 50,
                color: "#19FB9B", // Green color transition
                opacity: 0.3
              },
              {
                offset: 50,
                color: "#EE1600", // Red color transition
                opacity: 0.3
              },
              {
                offset: 100,
                color: "#EE1600", // Red color at 100%
                opacity: 0.5
              }
            ]
        },
      },
      
      stroke: {
        curve: 'straight',
        width: 3,
        fill: {
          type: 'gradient',
          gradient: {
            type:'vertical',
            shadeIntensity: 1,
            
            colorStops: [
              {
                offset: 0,
                color: "#19FB9B", // Green color at 0%
                opacity: 1
              },
              {
                offset: 50,
                color: "#19FB9B", // Green color transition
                opacity: 1
              },
              {
                offset: 50,
                color: "#EE1600", // Red color transition
                opacity: 1
              },
              {
                offset: -100,
                color: "#EE1600", // Red color at 100%
                opacity: 1
              }
            ]
          }
        }
      },
      colors: ['#00FFAF'],
      
      markers: {
        size: 0, // Set the marker size to 0 to hide the points
      },
      dataLabels: {
        enabled: false
      },
      grid: {
        row: {
          colors: ["transparent", "transparent"],
          opacity: 0.5,
          
        },
        borderColor:'#2f2f2f'
      },
      plotOptions:{
        area: {
          fillTo: 'end'
        }
      }
    };
   }

  // chart--------------

But After all this configuration the result I got is not accurate: output of this code

Expectations: I want a graph that has a vertical y-axis and it's y-axis should contain a zero dotted line annotation. The area chart's fill should be of gradient type and the gradient of fill should be of horizontal type but it's color changes based on zero line(annotation) of vertical axis. It means that If the line is above than the zero line then that part should be green filled but the other part which is below the zero line should be red filled.


Solution

    1. Since the gradient is horizontal, its colorSteps is also horizontal, that is the offset you set should specifies a horizontal position from 0 to the left margin of the chart area to 100 at its right margin.

      In order to set these horizontal offsets, you have to compute them as the x components of the intersection points of the chart line with the horizontal line (in this case the x axis, y = 0) that should set the change from the green to the red zones and vice versa.

      The intersections of the line with the x axis are to be computed as the intersection of each segment of the line with the x axis, which is rather simple as long as our chart line is not smoothed out:

      const zeroYIntersections = [];
      const nIntervals = seriesData.length - 1; // equidistant intervals for category type axis
      for(let i = 0; i < nIntervals; i++){
         const prod = seriesData[i] * seriesData[i+1];
         if(prod <= 0){ // if the sign changes
            zeroYIntersections.push(100*(i +  (seriesData[i]/(seriesData[i] - seriesData[i+1])))/nIntervals);
            // by 100 * x / nIntervals we set the "units" of these coordinates as percent from 0 to the left to 100 at the right 
         }
      }
      

      Once we have these intersections, defining the gradient is just setting these points as color stops in the option object:

      const colorPlus = "#19FB9B",
         colorMinus = "#EE1600",
         otherColor = (col) => col === colorPlus ? colorMinus : colorPlus;
      let currentColor =  seriesData.find(x => x !== 0) > 0 ? colorPlus : colorMinus;
      const colorStops = [{
            offset: 0, 
            color: currentColor,
            opacity: 0.5
         }];
      for(let intP of zeroYIntersections){
         colorStops.push({
            offset: intP,
            color: currentColor,
            opacity: 0.3
         });
         currentColor = otherColor(currentColor);
         colorStops.push({
            offset: intP,
            color: currentColor,
            opacity: 0.3
         });
      }
      colorStops.push({
         offset: 100,
         color: currentColor,
         opacity: 0.5
      });
      
    2. For the vertical line gradient, the library doesn't consider 0 to be the top of the chart area and 100 the bottom of the chart area, but rather 0 the highest point of the line and 100 the lowest point of the line. Thus, if you want the color change to happen at y = 0, the color stop should not be at 50, but at

      100 * Math.max(...seriesData)/(Math.max(...seriesData) - Math.min(...seriesData))
      

      However, since the vertical cut of the line might not match perfectly the horizontal cut of the filled area, you may consider using the same horizontal gradient + color stop for the line too, with different opacities, of course:

      option.stroke.fill.gradient.type = "horizontal";
      option.stroke.fill.gradient.colorStops = colorStops.map(cs=>({...cs, opacity: 1}));
      
    3. And finally, if you want the opacity to vary linearly from say 0.5 at the (left and right) margins to 0.3 at the center, you have to compute the opacities proportionally at the existing color stops and also add a new color stop at offset 50 where you set the opacity to 0.3.

      You may loop through the actual colorStops array to change the opacities by the linear formula, and also identify in which zone the 50 percent offset will be.

    Here's the (required part of the) original code with these three changes, in a runnable snippet:

    const seriesData = [50, 40, 35, -10, -40, 60, -70];
    const verticalOffset = 100 * Math.max(...seriesData)/(Math.max(...seriesData) - Math.min(...seriesData));
    const option = {
       series: [
          {
             name: 'Profit/Loss',
             data: seriesData,
          },
       ],
       chart: {
          type: 'area',
          height: 400,
          foreColor: '#2f2f2f',
          toolbar:{
             show:false
          }
       },
       yaxis: {
          opposite: true,
          min: -100,
          max: 100,
          tickAmount: 20, // Creates gaps of 10%
          labels: {
             formatter: (value) => {
                return value >= 0 ? `${value}%` : `${value}%`; // Return the value
             },
             style: {
                // Apply the class based on value
                colors:[ '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600', '#EE1600','#565656','#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD', '#00FFBD',]
             }
          }
       },
       xaxis: {
          labels: {
             show: true, // Hide the x-axis labels
          },
          axisBorder: {
             show: false,
          },
          axisTicks: {
             show: false,
          },
       },
       annotations: {
          yaxis: [
             {
                y: 0,
                borderColor: '#000',
                strokeDashArray: 5, // Create the dotted line at 0%
             },
          ],
          points: [
             {
                x: 360, // End of the data series
                y: -70, // Example value of profit/loss at the end of the line
                label: {
                   text: 'Profit: 70%',
                   borderColor: '#00E396',
                   style: {
                      color: '#fff',
                      background: '#00E396',
                   },
                },
             },
          ],
       },
       fill: {
          type: 'gradient',
          gradient: {
             type:'horizontal',
             // will set colorStops later
          },
       },
    
       stroke: {
          curve: 'straight',
          width: 3,
          fill: {
             type: 'gradient',
             gradient: {
                type:'vertical',
                shadeIntensity: 1,
    
                colorStops: [
                   {
                      offset: 0,
                      color: "#19FB9B", // Green color at 0%
                      opacity: 1
                   },
                   {
                      offset: verticalOffset,
                      color: "#19FB9B", // Green color transition
                      opacity: 1
                   },
                   {
                      offset: verticalOffset,
                      color: "#EE1600", // Red color transition
                      opacity: 1
                   },
                   {
                      offset: -100,
                      color: "#EE1600", // Red color at 100%
                      opacity: 1
                   }
                ]
             }
          }
       },
       colors: ['#00FFAF'],
    
       markers: {
          size: 0, // Set the marker size to 0 to hide the points
       },
       dataLabels: {
          enabled: false
       },
       grid: {
          row: {
             colors: ["transparent", "transparent"],
             opacity: 0.5,
    
          },
          borderColor:'#2f2f2f'
       },
       plotOptions:{
          area: {
             fillTo: 'end'
          }
       }
    };
    
    
    const zeroYIntersections = [];
    const nIntervals = seriesData.length - 1; // equidistant intervals for category type axis
    for(let i = 0; i < nIntervals; i++){
       const prod = seriesData[i] * seriesData[i+1];
       if(prod <= 0){ // if the sign changes
          zeroYIntersections.push(100*(i +  (seriesData[i]/(seriesData[i] - seriesData[i+1])))/nIntervals);
       }
    }
    
    const colorPlus = "#19FB9B",
       colorMinus = "#EE1600",
       otherColor = (col) => col === colorPlus ? colorMinus : colorPlus;
    let currentColor =  seriesData.find(x => x !== 0) > 0 ? colorPlus : colorMinus;
    const colorStops = [{
          offset: 0,
          color: currentColor,
          opacity: 0.5
       }];
    for(let intP of zeroYIntersections){
       colorStops.push({
          offset: intP,
          color: currentColor,
          opacity: 0.3
       });
       currentColor = otherColor(currentColor);
       colorStops.push({
          offset: intP,
          color: currentColor,
          opacity: 0.3
       });
    }
    colorStops.push({
       offset: 100,
       color: currentColor,
       opacity: 0.5
    });
    
    
    // use the horizontal gradient for stroke too
    //option.stroke.fill.gradient.type = "horizontal";
    //option.stroke.fill.gradient.colorStops = colorStops.map(cs=>({...cs, opacity: 1}));
    
    
    // set the opacities to vary linearly from 0.5 at the margins to 0.3 at the center
    const opacityMargins = 0.5;
    const opacityCenter = 0.3;
    
    let indexCenter = -1, colorCenter = '';
    colorStops.forEach((colorStop, index) => {
       const {offset, color} = colorStop;
       colorStop.opacity = opacityMargins + Math.min(offset, 100 - offset) / 50 * (opacityCenter - opacityMargins);
       if(indexCenter === -1 && offset > 50){
          indexCenter = index;
          colorCenter = color;
       }
    });
    colorStops.splice(indexCenter, 0, {
       offset: 50,
       color: colorCenter,
       opacity: opacityCenter
    });
    
    option.fill.gradient.colorStops = colorStops;
    
    const chart = new ApexCharts(document.querySelector("#chart"), option);
    chart.render();
    <div id="chart" style="background-color: rgb(0, 0, 0)"></div>
    <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>