Search code examples
javascripthtmlcsschartschart.js

Chart.Js. point style as a linear gradient


I am trying to make the points different colors: by default it should be background: linear-gradient(243.46deg, #FFC700 -1%, #F52525 131.66%); If the next value is smaller than the previous, it should show be background: linear-gradient(55.98deg, #E83C3C 11.73%, #9D3C3C 72.51%);

I've found some tutorials about linear-gradient on canvas but still all my points are black

var trendPriceArray = [
  0,
  109119,
  103610,
  112561,
  0,
  0,
  0,
  101852,
  0,
  99736,
  134382,
  110018
];

var trendPercentArray = [-5,
  8.6, -9.5, -2.1,
  34.7, -18.1
];

var monthLabels = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December'
];

// Filter out data points with trendPriceArray values of 0
var filteredData = [];
for (var i = 0; i < trendPriceArray.length; i++) {
  if (trendPriceArray[i] !== 0) {
    filteredData.push({
      x: monthLabels[i], // X coordinate as month label
      y: trendPriceArray[i], // Y coordinate
      label: trendPercentArray[i] + '%', // Display label above the point
    });
  }
}

// Initialize Chart.js
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: monthLabels, // Use the month labels
    datasets: [{
      label: 'Price Trend',
      data: filteredData, // Use filtered data
      fill: false,
      borderColor: '#4e4e4e', // Set the line graph color to #4e4e4e
      backgroundColor: 'rgba(75, 192, 192, 0.2)',
      borderWidth: 1,
      pointBackgroundColor: [], // Empty array to be filled with point background colors
      pointRadius: 13, // Set the point radius to 13 pixels
      pointHoverRadius: 13, // Set the point hover radius to 13 pixels
    }]
  },
  options: {
    scales: {
      x: {
        type: 'category', // Use category scale with the month labels
        title: {
          display: true,
          text: 'Month',
          font: {
            family: 'Montserrat', // Set the font family to Montserrat
          },
          color: '#4e4e4e', // Set the axis label text color to #4e4e4e
        },
        ticks: {
          font: {
            family: 'Montserrat', // Set the font family to Montserrat
          },
          color: '#4e4e4e', // Set the tick text color to #4e4e4e
        },
      },
      y: {
        display: true,
        title: {
          display: true,
          text: 'Price',
          marginBottom: '10px',
          font: {
            family: 'Montserrat', // Set the font family to Montserrat
          },
          color: '#4e4e4e', // Set the axis label text color to #4e4e4e
        },
        ticks: {
          font: {
            family: 'Montserrat', // Set the font family to Montserrat
          },
          color: '#4e4e4e', // Set the tick text color to #4e4e4e
        },
      }
    },
    plugins: {
      tooltip: {
        enabled: true,
        mode: 'index',
        intersect: false,
      },
      legend: {
        labels: {
          font: {
            family: 'Montserrat', // Set the font family to Montserrat
          },
          color: '#4e4e4e', // Set the legend text color to #4e4e4e
        },
      },
    }
  }
});

function createDefaultGradient(ctx) {
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  gradient.addColorStop(0, 'rgba(75, 192, 192, 0.2)');
  gradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)');
  return gradient;
}

function createRedGradient(ctx) {
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  gradient.addColorStop(0, 'rgba(255, 0, 0, 0.2)');
  gradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)');
  return gradient;
}

function createGradientPointStyle(gradient) {
  return function(context) {
    const chart = context.chart;
    const {
      ctx,
      borderWidth
    } = chart;
    const {
      x,
      y
    } = context.p0;
    const pointRadius = 6; // You can adjust this value as needed

    ctx.save();
    ctx.beginPath();
    ctx.arc(x, y, pointRadius, 0, Math.PI * 2);
    ctx.fillStyle = gradient;
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  };
}


// Initialize an empty pointStyle array
myChart.data.datasets[0].pointStyle = [];


for (var i = 0; i < filteredData.length; i++) {
  var gradient = createDefaultGradient(ctx); // Default gradient
  if (i < filteredData.length - 1 && filteredData[i + 1].y < filteredData[i].y) {
    gradient = createRedGradient(ctx); // Red gradient for smaller values
  }
  myChart.data.datasets[0].pointStyle.push(createGradientPointStyle(gradient));
}

myChart.update(); // Update the chart
<!DOCTYPE html>
<html>

<head>
  <!-- Include Chart.js library -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
  <!-- Include Montserrat font from Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
</head>

<body>
  <!-- Create a container for the chart -->
  <div>
    <canvas id="myChart" width="400" height="200"></canvas>
  </div>
</body>

</html>


Solution

  • There are theoretically two different avenues to draw custom styled data points, and you combined them the wrong way:

    1. drawing yourself the point on the main canvas; this can only be done through a plugin, in its afterDraw function; otherwise, anything you draw is covered by the default drawing of the chart
    2. using pointStyle, to which you may assign a newly created (small) canvas or image on which you draw the (reusable) point symbol.

    Since the second, standard method is good enough for your requirements, we can forget about the first, which should only be employed in exceptional cases, not covered by the other one.

    Also, you should consider the moments/order when each part of your code gets executed. In particular, since you are using a scriptable pointStyle (i.e., a function that is called when the point style is needed and returns the canvas), you don't need to set the pointStyle in a second stage, after the chart was first rendered, and neither the chart.update call.

    The very point of scriptable options is to enable you to use just-computed dynamic values (like your filtered y values) to avoid redrawing the whole chart. The fact is that the point values are available before the points are drawn, so through the arguments provided to the pointStyle function you'll find the required y values.

    Here's a corrected version of your code, with minimal change:

    function createDefaultGradient(ctx, max) {
        const gradient = ctx.createLinearGradient(0, 0, 0, max);
        gradient.addColorStop(0, 'rgba(75, 192, 192, 0.2)');
        gradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)');
        return gradient;
    }
    
    function createRedGradient(ctx, max) {
        const gradient = ctx.createLinearGradient(0, 0, 0, max);
        gradient.addColorStop(0, 'rgba(255, 0, 0, 0.2)');
        gradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)');
        return gradient;
    }
    
    function createGradientPointStyle(gradientCreator, pointRadius) {
        const canvas = document.createElement('canvas');
        canvas.width = 2*pointRadius+2;
        canvas.height = 2*pointRadius+2;
        const ctx = canvas.getContext("2d");
        ctx.beginPath();
        ctx.arc(pointRadius+1, pointRadius+1, pointRadius, 0, Math.PI * 2);
        ctx.fillStyle = gradientCreator(ctx, 2*pointRadius+2);
        ctx.closePath();
        ctx.fill();
        return canvas;
    }
    
    function pointStyle(context, opt){
        var i = context.dataIndex,
            data = context.dataset.data;
        var gradientCreator = createDefaultGradient;
        if (i < data.length - 1 && data[i + 1].y < data[i].y) {
            gradientCreator = createRedGradient; // Red gradient for smaller values
        }
        var radius = context.active ? opt.hoverRadius : opt.radius;
        return createGradientPointStyle(gradientCreator, radius)
    }
    
    var trendPriceArray = [
        0,
        109119,
        103610,
        112561,
        0,
        0,
        0,
        101852,
        0,
        99736,
        134382,
        110018
    ];
    
    var trendPercentArray = [-5,
        8.6, -9.5, -2.1,
        34.7, -18.1
    ];
    
    var monthLabels = [
        'January',
        'February',
        'March',
        'April',
        'May',
        'June',
        'July',
        'August',
        'September',
        'October',
        'November',
        'December'
    ];
    
    // Filter out data points with trendPriceArray values of 0
    var filteredData = [];
    for (var i = 0; i < trendPriceArray.length; i++) {
        if (trendPriceArray[i] !== 0) {
            filteredData.push({
                x: monthLabels[i], // X coordinate as month label
                y: trendPriceArray[i], // Y coordinate
                label: trendPercentArray[i] + '%', // Display label above the point
            });
        }
    }
    
    // Initialize Chart.js
    var ctx = document.getElementById('myChart').getContext('2d');
    var myChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: monthLabels, // Use the month labels
            datasets: [{
                label: 'Price Trend',
                data: filteredData, // Use filtered data
                // fill: false,
                borderColor: '#4e4e4e', // Set the line graph color to #4e4e4e
                //backgroundColor: 'rgba(75, 192, 192, 0.2)',
                borderWidth: 1,
                // pointBackgroundColor: [], // Empty array to be filled with point background colors
                pointRadius: 13, // Set the point radius to 13 pixels
                pointHoverRadius: 23, // Set the point hover radius to 13 pixels
                pointStyle
            }]
        },
        options: {
            scales: {
                x: {
                    type: 'category', // Use category scale with the month labels
                    title: {
                        display: true,
                        text: 'Month',
                        font: {
                            family: 'Montserrat', // Set the font family to Montserrat
                        },
                        color: '#4e4e4e', // Set the axis label text color to #4e4e4e
                    },
                    ticks: {
                        font: {
                            family: 'Montserrat', // Set the font family to Montserrat
                        },
                        color: '#4e4e4e', // Set the tick text color to #4e4e4e
                    },
                },
                y: {
                    display: true,
                    title: {
                        display: true,
                        text: 'Price',
                        marginBottom: '10px',
                        font: {
                            family: 'Montserrat', // Set the font family to Montserrat
                        },
                        color: '#4e4e4e', // Set the axis label text color to #4e4e4e
                    },
                    ticks: {
                        font: {
                            family: 'Montserrat', // Set the font family to Montserrat
                        },
                        color: '#4e4e4e', // Set the tick text color to #4e4e4e
                    },
                }
            },
            plugins: {
                tooltip: {
                    enabled: true,
                    mode: 'index',
                    intersect: false,
                },
                legend: {
                    labels: {
                        font: {
                            family: 'Montserrat', // Set the font family to Montserrat
                        },
                        color: '#4e4e4e', // Set the legend text color to #4e4e4e
                    },
                },
            }
        }
    });
    <div style="min-height: 60vh">
        <canvas id="myChart"></canvas>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>