如何使用chart.js在刻度尺上的文本旁边添加图标?

rdlzhqv9  于 2023-10-18  发布在  Chart.js
关注(0)|答案(1)|浏览(175)

我在本地文件夹中有一个现有的svg图标。我想用chart.js绘制一个图表。现在我可以画出来了。
Demo
但我想在刻度文本旁边添加图标。在这个例子中,我想添加一个图标的权利,以文本“X轴”的x轴和相同的图标上方的文本“Y轴”。在网上很难找到这样的例子。
我的代码:

import { Component, VERSION } from '@angular/core';
        import { ChartOptions, TooltipItem, Chart } from 'chart.js';
        import { filter } from 'rxjs/operators';

        const labels = [
          'Jan',
          'Feb',
          'Mar',
          'april',
          'may',
          'jun',
          'july',
          'aug',
          'sept'
        ];
        const tooltipPlugin = Chart.registry.getPlugin('tooltip') as any;

        tooltipPlugin.positioners.verticallyCenter = (elements, position) => {
          if (!elements.length) {
            return tooltipPlugin.positioners.average(elements);
          }
          const { x, y, base, width } = elements[0].element;
          const height = (base - y) / 2;
          const offset = x + width / 2;
          return {
            x: offset,
            y: y + height
          };
        };

        const data = {
          labels: labels,
          datasets: [
            {
              maxBarThickness: 40,
              label: '',
              data: [50, 20, 30, 75, 30, 60, 70, 80, 100],
              backgroundColor: 'red'
            }
          ]
        };
        @Component({
          selector: 'my-app',
          templateUrl: './app.component.html',
          styleUrls: ['./app.component.css']
        })
        export class AppComponent {
          name = 'Angular ' + VERSION.major;
          data = data;
          options: ChartOptions = {
            aspectRatio: 2,
            layout: {
              padding: {
                top: 0
              }
            },
            responsive: true,
            maintainAspectRatio: true,
            scales: {
              y: {
                title: {
                  display: true,
                  text: 'Y Axis'
                },
                axis: 'y',
                grid: {
                  display: false,
                  drawTicks: false,
                  tickLength: 0
                },
                max: 100,

                ticks: {
                  major: {
                    enabled: false
                  },
                  padding: 17,
                  stepSize: 25,
                  callback: (value, index, ticks) => {
                    return index === 0 || index === ticks.length - 1 ? '' : `${value}%`;
                  }
                },
                afterTickToLabelConversion: first => {
                  console.log(first);
                }
              },
              x: {
                title: {
                  display: true,
                  text: 'X Axis'
                },
                axis: 'x',
                grid: { drawTicks: false },
                ticks: {
                  padding: 17
                }
              }
            },
            plugins: {
              tooltip: {
                position: 'verticallyCenter' as 'average',
                animation: { duration: 0 },
                callbacks: {
                  title: (context: TooltipItem<'bar'>[]) => {
                    console.log(context[0].label);
                    return context[0].label;
                  }
                }
              },
              legend: {
                display: false,
                position: 'bottom'
              },
              title: {
                display: false
              }
            }
          };
        }
dy2hfwbg

dy2hfwbg1#

在chart.js中绘制svg相对容易,可以这样做:

const imgSVG = new Image();
imgSVG.src = '../assets/svg.svg';
// .... after the image is loaded
ctx.drawImage(imgSVG, x, y, w, h);

其中,对2D上下文ctx的访问作为图表或缩放对象(chart.ctxscale.ctx)的属性在各种chart.js处理程序中可用。
更深入地了解所提出的解决方案的细节,我们定义(对于一个svg图像)一个对象svgDrawPositions,它包含(可能多个)所需渲染的细节:

svgDrawPositions: {[id: string]: {x: number, y: number, rotation: number, h: number}}

绘制位置索引的id s,在这个特定的情况下,是轴的id,因为svg要为两个轴中的每一个显示。
稍后我们将讨论xyrotationh的计算方法。现在,我们将认为它们已经可用。然后,svg的实际绘制可以通过一个简单的插件完成,只需一个afterDraw方法:

let svgLoaded = false;
const imgSVG = new Image();
imgSVG.onerror = function(){
    console.warn('Error loading image');
};
imgSVG.onload = function(){
    svgLoaded = true;
    drawAllPositions(document.getElementById('chart1').getContext('2d'));
};
imgSVG.src = '../assets/svg.svg';
function drawAllPositions(ctx){
    if(!ctx || !svgLoaded){
        return;
    }
    Object.values(svgDrawPositions).forEach(({x, y, rotation, h})=>
    {
        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(rotation);
        const w = imgSVG.width / imgSVG.height * h;
        ctx.drawImage(
            imgSVG, 0, 0, w, h
        );
        ctx.restore();
    })
};

const pluginDrawSVG = {
    id: 'drawSVG',
    afterDraw: (chart) => {
        drawAllPositions(chart.ctx)
    },
};
Chart.register(pluginDrawSVG);

我们只需要svgDrawPositions中svg渲染的高度,因为我们使用实际svg的纵横比来计算宽度。
现在,真实的困难是如何设置渲染的位置。这是因为坐标轴的标题是少数几个其呈现细节未在图表对象中公开的项目之一。坐标轴、刻度、刻度标签、网格线都在图表对象中有其实时信息,但坐标轴标题不在那里。解决这个问题的方法不是很好,它包括复制与轴标题定位相关的chart.js的源代码。
然后,该框架是自定义缩放类的框架:

function scaleDrawTitle(){
    const {
        ctx,
        id,
        options: {position, title},
    } = this;
    
    // code taken from chart.js source to compute the position: x, y, rotation, h

    svgDrawPositions[id] = {x, y, rotation, h};
};

class CategoryScaleTE extends CategoryScale{
    static id = 'category_te';

    drawTitle(){
        super.drawTitle();
        scaleDrawTitle.call(this);
    }
}

class LinearScaleTE extends LinearScale{
    static id = 'linear_te';

    drawTitle(){
        super.drawTitle();
        scaleDrawTitle.call(this);
    }
}

Chart.register(CategoryScaleTE, LinearScaleTE);

this stackblitz fork中的代码使用了原始的chart.js 3.3.2,而下面的代码片段演示(无Angular )基于最新版本4.3.3

const {toFont, toPadding, isArray} = Chart.helpers;
const tooltipPlugin = Chart.registry.getPlugin('tooltip');
const {CategoryScale, LinearScale} = Chart;

tooltipPlugin.positioners.verticallyCenter = (elements) => {
    if(!elements.length){
        return tooltipPlugin.positioners.average(elements);
    }
    const {x, y, base, width} = elements[0].element;
    const height = (base - y) / 2;
    const offset = x + width / 2;
    return {
        x: offset,
        y: y + height,
    };
};

/* ------------
CategoryScaleTE and LinearScaleTE - custom scale classes, that set up the positions where the
svg image will be drawn. Since the axis title is not exposed in the chart object, the following
functions duplicate the source code of chart.js related to positioning of axis title
*/
const offsetFromEdge = (scale, edge, offset) =>
    edge === 'top' || edge === 'left'
        ? scale[edge] + offset
        : scale[edge] - offset;

const _alignStartEnd = (align, start, end) =>
    align === 'start' ? start : align === 'end' ? end : (start + end) / 2;

function titleArgs(scale, offset, position, align){
    const {top, left, bottom, right} = scale;
    let rotation = 0;
    let maxWidth, titleX, titleY;
    if(scale.isHorizontal()){
        titleX = _alignStartEnd(align, left, right);
        titleY = offsetFromEdge(scale, position, offset);
        maxWidth = right - left;
    }
    else{
        titleX = offsetFromEdge(scale, position, offset);
        titleY = _alignStartEnd(align, bottom, top);
        rotation = position === 'left' ? -Math.PI / 2 : Math.PI / 2;
    }
    return {titleX, titleY, maxWidth, rotation};
}

function scaleDrawTitle(){
    const {
        ctx,
        id,
        options: {position, title},
    } = this;
    if(!title.display){
        return;
    }
    const font = toFont(title.font);
    const padding = toPadding(title.padding);
    const align = title.align;
    let offset = font.lineHeight / 2;
    if(position === 'bottom'){
        offset += padding.bottom;
        if(isArray(title.text)){
            offset += font.lineHeight * (title.text.length - 1);
        }
    }
    else{
        offset += padding.top;
    }
    const {titleX, titleY, rotation} = titleArgs(
        this,
        offset,
        position,
        align
    );

    ctx.save();
    ctx.font = title.font;
    const mt = ctx.measureText(title.text);
    ctx.restore();
    const h0 = mt.fontBoundingBoxAscent + mt.fontBoundingBoxDescent,
        h = Math.max(20, h0);

    const dx0 = mt.width / 2 + 4, // 4 for the space between text and svg
        dy0 = h0 / 2 - h - 1, // to have approx the same "underline" as the text
        dx = dx0 * Math.cos(rotation) - dy0 * Math.sin(rotation),
        dy = dx0 * Math.sin(rotation) + dy0 * Math.cos(rotation),
        x = titleX + dx,
        y = titleY + dy;

    svgDrawPositions[id] = {x, y, rotation, h};
};

class CategoryScaleTE extends CategoryScale{
    static id = 'category_te';

    drawTitle(){
        super.drawTitle();
        scaleDrawTitle.call(this);
    }
}

class LinearScaleTE extends LinearScale{
    static id = 'linear_te';

    drawTitle(){
        super.drawTitle();
        scaleDrawTitle.call(this);
    }
}

Chart.register(CategoryScaleTE, LinearScaleTE);

/* ------------
svg image and pluginDrawSVG - actually drawing the swg on canvas
*/
const svgDrawPositions = {};
let svgLoaded = false;

const imgSVG = new Image();
imgSVG.onerror = function(){
    console.warn('Error loading image');
};
imgSVG.onload = function(){
    svgLoaded = true;
    drawAllPositions(document.getElementById('chart1').getContext('2d'));
};
imgSVG.src = 'https://web.archive.org/web/20230602150221if_/https://upload.wikimedia.org/wikipedia/commons/4/4f/SVG_Logo.svg';

function drawAllPositions(ctx){
    if(!ctx || !svgLoaded){
        return;
    }
    Object.values(svgDrawPositions).forEach(({x, y, rotation, h})=>
    {
        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(rotation);
        const w = imgSVG.width / imgSVG.height * h;
        ctx.drawImage(
            imgSVG, 0, 0, w, h
        );
        ctx.restore();
    })
};

const pluginDrawSVG = {
    id: 'drawSVG',
    afterDraw: (chart) => {
        drawAllPositions(chart.ctx)
    },
};
Chart.register(pluginDrawSVG);

const labels = [
    'Jan',
    'Feb',
    'Mar',
    'april',
    'may',
    'jun',
    'july',
    'aug',
    'sept',
];

const data = {
    labels: labels,
    datasets: [
        {
            maxBarThickness: 40,
            label: '',
            data: [50, 20, 30, 75, 30, 60, 70, 80, 100],
            backgroundColor: 'red',
        },
    ],
};

chart = new Chart('chart1', {
    type: "bar",
    data,
    options: {
        aspectRatio: 2,
        layout: {
            padding: {
                top: 0,
            },
        },
        responsive: true,
        maintainAspectRatio: true,
        scales: {
            y: {
                type: 'linear_te',
                title: {
                    display: true,
                    text: 'Y Axis',
                },
                axis: 'y',
                grid: {
                    display: false,
                    drawTicks: false,
                    tickLength: 0,
                },
                max: 100,

                ticks: {
                    major: {
                        enabled: false,
                    },
                    padding: 17,
                    stepSize: 25,
                    callback: (value, index, ticks) => {
                        return index === 0 || index === ticks.length - 1 ? '' : `${value}%`;
                    },
                },
            },
            x: {
                type: 'category_te',
                title: {
                    display: true,
                    text: 'X Axis axis axis',
                },
                axis: 'x',
                grid: {drawTicks: false},
                ticks: {
                    padding: 17,
                },
            },
        },
        plugins: {
            tooltip: {
                position: 'verticallyCenter',
                animation: {duration: 0},
                callbacks: {
                    title: (context) => {
                        return context[0].label;
                    },
                },
            },
            legend: {
                display: false,
                position: 'bottom',
            },
            title: {
                display: false,
            },
        },
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.3/chart.umd.min.js"
        integrity="sha512-mCXCsj30LV3PLPTIuWjZQX84qiQ56EgBZOsPUA+ya5mWmAb8Djdxa976zWzxquOwkh0TxI12KA4eniKpY3yKhA=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div style="max-height:500px">
    <canvas id="chart1"></canvas>
</div>

相关问题