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

问题描述 投票:0回答:1

我在本地文件夹中有一个现有的 svg 图标。我想用 Chart.js 绘制图表。现在我可以绘制它了。

演示

但我想在比例文本旁边添加图标。在示例中,我想在 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
              }
            }
          };
        }
javascript chart.js
1个回答
0
投票

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

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

可以在各种 Chart.js 处理程序中访问 2d 上下文

ctx
作为图表或比例对象的属性(
chart.ctx
scale.ctx
)。

更深入地了解所提出的解决方案的细节,我们(对于一个 svg 图像)定义一个对象,

svgDrawPositions
,其中包含(可能是多个)所需渲染的详细信息:

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

在这种特殊情况下,用于索引绘图位置的

id
是轴的 id,因为要为两个轴中的每一个显示 svg。

稍后我们将讨论

x
y
rotation
h
值的计算方式。目前,我们认为它们已经可用。然后,可以通过一个简单的插件使用
afterDraw
方法来完成 svg 的实际绘制:

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,而下面的代码片段演示(无角度)基于最新版本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>

© www.soinside.com 2019 - 2024. All rights reserved.