javascriptchartschart.jsradar-chart

How to apply two color on each point labels in chart js?


Im working on displaying radar chart. I need to apply 2 color in each point labels. A image attached below. For example if first label 'Eating 65 / 28' then 65 is in red abd 28 in blue. enter image description here I have applied html style on it something like Eating <span style="color:red;font-weight:bold;">65</span> <span style="color:blue;font-weight:bold;">28</span> but style didn't rendered. Is there anyway to do this. below is the code.

const data = {
  labels: [
    'Eating 65 / 28',
    'Drinking 59 / 48',
    'Sleeping 90 / 40',
    'Designing 81 / 19',
    'Coding 56 / 96',
    'Cycling 55 / 27',
    'Running 48 / 100'
  ],
  datasets: [{
    label: 'My First Dataset',
    data: [65, 59, 90, 81, 56, 55, 40],
    fill: true,
    backgroundColor: 'rgba(255, 99, 132, 0.2)',
    borderColor: 'rgb(255, 99, 132)',
    pointBackgroundColor: 'rgb(255, 99, 132)',
    pointBorderColor: '#fff',
    pointHoverBackgroundColor: '#fff',
    pointHoverBorderColor: 'rgb(255, 99, 132)'
  }, {
    label: 'My Second Dataset',
    data: [28, 48, 40, 19, 96, 27, 100],
    fill: true,
    backgroundColor: 'rgba(54, 162, 235, 0.2)',
    borderColor: 'rgb(54, 162, 235)',
    pointBackgroundColor: 'rgb(54, 162, 235)',
    pointBorderColor: '#fff',
    pointHoverBackgroundColor: '#fff',
    pointHoverBorderColor: 'rgb(54, 162, 235)'
  }]
};

const config = {
  type: 'radar',
  data: data,
  options: {
    elements: {
      line: {
        borderWidth: 3
      }
    }
  },
};

Solution

  • It can be done, but it is far from trivial. Also, we may ask ourselves when having some fancy of styling a chart if it's really useful, does it provide more information, or might it tire or scare away the user who is typically happy to see the same type of data depicted in the same kind style.

    In any case, the problem is technically interesting. The implementation is based on a custom axis, RadialLinearScaleCustomPointLabels, that extends RadialLinearScale, to provide a (scriptable) option pointStyle.draw.callback that allows drawing a label in sequences that can be styled differently. That is done through the context of the call, that provides the function this.drawText(text) to draw a part of the label, preceded by call to either this.setStyle({color, bold}), or this.setDefaultStyle to set its style. Better seen actually used, as in the following example.

    // global variables obtained from the umd script, no modules
    const RadialLinearScale = Chart.RadialLinearScale;
    const {renderText, toFont, color: makeColor} = Chart.helpers;
    
    // // global variables obtained via import, in a module environment
    // import {Chart, RadialLinearScale, registerables} from "./chart.js";
    // Chart.register(...registerables);
    // import {renderText, toFont, color as makeColor} from './helpers.js';
    
    class RadialLinearScaleCustomPointLabels extends RadialLinearScale{
        static id = 'radial_custom_point_labels';
    
        static defaults = {
            ...RadialLinearScale.defaults,
            pointLabels: {
                ...RadialLinearScale.defaults.pointLabels,
                draw: {
                    callback(label){ this.drawText(label); }
                }
            }
        }
    
        drawGrid(){
            const display = this.options.pointLabels.display;
            if(display){
                this.options.pointLabels.display = false;
            }
            super.drawGrid();
            if(display){
                const thisScale = this;
                const {options: {pointLabels: pointLabelsOpts}} = this;
                this._pointLabels.forEach(
                    (pointLabel, i) => {
                        const optsAtIndex = pointLabelsOpts.setContext(thisScale.getPointLabelContext(i));
                        thisScale.customDrawLabel(pointLabel, thisScale._pointLabelItems[i], optsAtIndex);
                    }
                )
            }
            this.options.pointLabels.display = display;
        }
    
        _newRenderContext(ctx, fullText, plFont, x, y, defaultColor, textAlign){
            let _restore;
            const hackCtx = () => {
                _restore = ctx.restore;
                ctx.restore = () => null;
            }
            const restoreCtx = () => {
                ctx.restore = _restore.bind(ctx);
                ctx.restore();
            }
    
            if(textAlign !== 'left'){
                // change horizontal alignment to left, so we can measure the text after it was rendered.
                hackCtx();
                renderText(ctx, '', this._x, y, plFont, {
                    color: 'rgba(0,0,0,0)',
                    textAlign,
                    textBaseline: 'middle'
                });
                const m = ctx.measureText(fullText);
                restoreCtx();
                if(textAlign === 'center'){
                    x -= m.width/2;
                }
                else if(textAlign === 'right'){
                    x -= m.width;
                }
    
                textAlign = 'left';
            }
    
            return {
                _color: defaultColor,
                _userBold: false,
    
                _x: x,
                setDefaultStyle(){
                    this._color = defaultColor;
                    this._userBold = false;
                },
                setStyle({color, bold}){
                    this._color = color;
                    this._userBold = bold;
                },
                drawText(txt){
                    hackCtx();
                    renderText(ctx, txt, this._x, y, plFont, {
                        color: this._color,
                        textAlign,
                        textBaseline: 'middle',
                        ... this._userBold ? {
                            strokeColor: makeColor(this._color).alpha(0.3).rgbString(),
                            strokeWidth: 2
                        } : {}
                    });
                    this._x += ctx.measureText(txt).width;
                    restoreCtx();
                }
            }
        }
    
        customDrawLabel(label, pos, opts){
            if(!pos.visible){
                return;
            }
            const plFont = toFont(opts.font);
            const color = opts.color ?? 'rgb(0,0,0)';
            const { x , y , textAlign  } = pos;
            opts.draw.callback?.call?.(
                 this._newRenderContext(this.ctx, label, plFont, x, y + plFont.lineHeight / 2, color, textAlign), label);
        }
    }
    
    
    Chart.register(RadialLinearScaleCustomPointLabels);
    
    
    const data = {
        labels: [
            'Eating 65 / 28',
            'Drinking 59 / 48',
            'Sleeping 90 / 40',
            'Designing 81 / 19',
            'Coding 56 / 96',
            'Cycling 55 / 27',
            'Running 48 / 100'
        ],
        datasets: [{
            label: 'My First Dataset',
            data: [65, 59, 90, 81, 56, 55, 40],
            fill: true,
            backgroundColor: 'rgba(255, 99, 132, 0.2)',
            borderColor: 'rgb(255, 99, 132)',
            pointBackgroundColor: 'rgb(255, 99, 132)',
            pointBorderColor: '#fff',
            pointHoverBackgroundColor: '#fff',
            pointHoverBorderColor: 'rgb(255, 99, 132)'
        }, {
            label: 'My Second Dataset',
            data: [28, 48, 40, 19, 96, 27, 100],
            fill: true,
            backgroundColor: 'rgba(54, 162, 235, 0.2)',
            borderColor: 'rgb(54, 162, 235)',
            pointBackgroundColor: 'rgb(54, 162, 235)',
            pointBorderColor: '#fff',
            pointHoverBackgroundColor: '#fff',
            pointHoverBorderColor: 'rgb(54, 162, 235)'
        }]
    };
    
    const config = {
        type: 'radar',
        data: data,
        options: {
            elements: {
                line: {
                    borderWidth: 3
                }
            },
            scales:{
                r: {
                    type: 'radial_custom_point_labels',
                    pointLabels: {
                        draw: {
                            callback: function(labelText){
                                const [_, text, number1, slash, number2] = labelText.match(/^(\D+)(\d+)(\s*\/\s*)(\d+)$/);
                                this.setDefaultStyle();
                                this.drawText(text);
                                this.setStyle({color: 'rgb(255, 99, 132)', bold: true});
                                this.drawText(number1);
                                this.setDefaultStyle();
                                this.drawText(slash);
                                this.setStyle({color: 'rgb(54, 162, 235)', bold: true});
                                this.drawText(number2);
                            }
                        }
                    }
                }
            }
        },
    };
    new Chart(document.querySelector('#chart1'), config);
    <div style="min-height: 60vh">
        <canvas id="chart1">
        </canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>