简体   繁体   中英

Highchart Legend with Checkbox, events, hover style, symbol order change

I currently have Highcharts implemented in a Chart component in my application, but I need to make some changes to the Legend, went through most of the documentation, created some functions with Highcharts.wrap() .

First, the Legend was simple, each legend item being

[Symbol] [Label] . 在此处输入图像描述

But now I need to change it into:

[Checkbox] [Label] [Symbol] 在此处输入图像描述

Here is what I got so far:

[Checkbox] [Symbol] [Label] 在此处输入图像描述

And with the click on the checkbox replicating the click on the Legend (symbol, label), which shows/hide the series line.

how? with this: (showing only the important parts)

const defaultOptions: Highcharts.Options = {
    ...,
    legend: {
        borderColor: "transparent",
        verticalAlign: "top",
        align: "left",
        x: 14,
        itemCheckboxStyle: {
            cursor: "pointer",
            border: "1px solid #62737a",
        },
    },
    ...,
    plotOptions: {
        series: {
            ...,
            showCheckbox: true,
            selected: true,
            events: {
                checkboxClick: function () {
                    this.setVisible(!this.visible);
                },
            },
        },
        ...,
    },
...,
}

If we only use showCheckbox: true , the checkbox will be far on the right side of each label, not ideal. So this is needed: ( If possible I also would like tips on how to avoid the any error on TS in this case, without the comments ).

Highcharts.wrap(Highcharts.Legend.prototype, "positionCheckboxes", legendCheckboxPosition);
function legendCheckboxPosition(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    this: any,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    p: any,
    scrollOffset: number
) {
    const alignAttr = this.group.alignAttr;
    const clipHeight = this.clipHeight || this.legendHeight;
    let translateY: number;

    if (alignAttr) {
        translateY = alignAttr.translateY;
        Highcharts.each(
            this.allItems,
            function (item: {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                checkbox: any;
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                legendItem: { getBBox: (arg0: boolean) => any };
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                checkboxOffset: any;
            }) {
                const checkbox = item.checkbox;
                const bBox = item.legendItem.getBBox(true);
                let top;

                if (checkbox) {
                    top = translateY + checkbox.y + (scrollOffset || 0) + 2;
                    Highcharts.css(checkbox, {
                        left:
                            alignAttr.translateX +
                            item.checkboxOffset +
                            checkbox.x -
                            100 -
                            bBox.width +
                            17 +
                            "px",
                        top: top + "px",
                        display: top > translateY - 6 && top < translateY + clipHeight - 6 ? "" : "none",
                    });
                }
            }
        );
    }
}

But with this done, I still need to make some changes, which are:

  1. Change the order of Symbol and Label There is supposed to be a rtl property inside the legends options, which is supposed to change the order of Symbol and Label , but if I do that, it reverses, but it also reverse the order of the legends somehow, I'll show: -> Without rtl : 没有 rtl 选项为真

-> With rtl: true inside the legends options: 在此处输入图像描述

The checkbox distance I understand, because it will need to change my legendCheckboxPosition function, my real problem here is the order of the legends being changed, like if I used legend.reversed: true .. I found out that I can use the reversed property to fix this, but I was wondering if this was a bug with something else..because in the documentation the rtl property only changes the order of Symbol and Label , not the legends order.

This is what I need: 在此处输入图像描述

  1. I need to put a style in the :hover of the checkbox, I tried using the legend.itemCheckboxStyle but that doesn't allow me to add hover effects... (I need to place a box-shadow when hovering the checkbox)
  • ONE ISSUE SOLVED : Another issue is when clicking the legend item (which is separated of the checkbox) When clicking the legend item, it shows/hide the series, but it doesn't change the checkbox selection. I know that the checkbox selection is determined by the series.selected property, and that I have the legendItemClick event inside the plotOptions.series.events , but inside that I don't have a this.setSelected function, only this.setVisible function. I tried using that, but it seems to freeze the chart, not doing anything. How to change the checkbox selection when clicking only in the legend item?

Edit : Managed to solve this by adding this event to options.plotOptions.series.events :

legendItemClick: function () {
    const seriesIndex = this.index;
    this.chart.series[seriesIndex].select();
},

Well.. that is my problem, with the hope that you guys can help me solve it.

A possible way to arranging the elements of legend would be to edit the legend in legend.labelFormatter and add a Unicode line character. To have the checkbox on the right also you can style it in legend.itemCheckboxStyle .

  legend: {
    symbolWidth: 0,
    useHTML: true,
    labelFormatter: function() {
      return `<div>${this.name}<span style="color: green;">&#9473;</span></div>`;
    },
    itemDistance: 50,
    itemCheckboxStyle: {
      "width": "13px",
      "height": "13px",
      "margin-left": "-130px",
      "position": "absolute"
    }
  },

API References:

https://api.highcharts.com/highcharts/legend.itemCheckboxStyle

Demo: https://jsfiddle.net/BlackLabel/sbcL7rp9/3/

After a lot of research and experimenting, I managed to have it all working.

Options (default options and events, also toggling visibility on load)

const series = GetSeries(opts, null);
...
// Check if Series is supposed to be hidden from load (from the checkbox selected prop), if it is, hide it
series.forEach((serie) => {
    if (serie.selected !== undefined && serie.selected === false) serie.visible = false;
});

const defaultOptions: Highcharts.Options = {
    ...,
    legend: {
        borderColor: "transparent",
        verticalAlign: "top",
        align: "left",
        x: 14,
    },
    ...,
    plotOptions: {
        series: {
            ...
            showCheckbox: true,
            selected: true,
            events: {
                checkboxClick: function () {
                    this.setVisible(!this.visible);
                },
                legendItemClick: function () {
                    const seriesIndex = this.index;
                    this.chart.series[seriesIndex].select();
                },
            },
        },
        ...
    },
    ...
};

return mergeObjects(defaultOptions, opts);
}

And a lot of resolved extending Highcharts :

// Adjust position of the Highchart Legend Checkbox, and switch label and symbol in the legend
function legendPositionAdjustments(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    this: any,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    p: any,
    scrollOffset: number
) {
    const pixelsToREM = (value: number) => {
        return value / 16;
    };
    const alignAttr = this.group.alignAttr;
    const clipHeight = this.clipHeight || this.legendHeight;
    let translateY: number;
const legendMainElement = this.box.parentGroup.element;
// Adjust the main legend element to be closer to the checkbox
if (legendMainElement) {
    Highcharts.attr(legendMainElement, "transform", "translate(19, 10)");
}
if (alignAttr) {
    translateY = alignAttr.translateY;
    this.allItems.forEach(function (item: {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        checkbox: any;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        legendItem: { getBBox: (arg0: boolean) => any };
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        checkboxOffset: any;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        legendGroup: any;
    }) {
        const bBox = item.legendItem.getBBox(true);

        // Change position of Label and Symbol in the Highcharts Legend
        const legendItemElement = item.legendGroup.element;
        const legendItemPath = legendItemElement.querySelector("path.highcharts-graph");
        const legendItemPoint = legendItemElement.querySelector("path.highcharts-point");
        const legendItemText = legendItemElement.querySelector("text");
        if (legendItemPath) {
            Highcharts.attr(legendItemPath, "transform", `translate(${bBox.width + 3}, 0)`);
        }
        if (legendItemPoint) {
            Highcharts.attr(legendItemPoint, "transform", `translate(${bBox.width + 3}, 0)`);
        }
        if (legendItemText) {
            Highcharts.attr(legendItemText, "x", 0);
        }

        // Adjust the position of the checkbox to the left side of the Highcharts Legend
        const checkbox = item.checkbox;
        let top;
        let left;
        if (checkbox) {
            top = translateY + checkbox.y + (scrollOffset || 0) + 4;
            left = alignAttr.translateX + item.checkboxOffset + checkbox.x - 100 - bBox.width + 17;
            Highcharts.css(checkbox, {
                left: pixelsToREM(left) + "rem",
                top: pixelsToREM(top) + "rem",
                display: top > translateY - 6 && top < translateY + clipHeight - 6 ? "" : "none",
            });
        }
    });
}
}

// This function is called when triggering show/hide of series, always calling with visibility = true
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
function colorizeLegendRegardlessOfVisibility(this: any, proceed: any, item: any, visible: any) {
    proceed.call(this, item, true);
}

This will:

  • Avoid changing the color of the legend and symbol when series is hidden or legend is out of focus
  • Correct position the checkbox to left side of the legend
  • Switch positions of the symbol and label, which are in the SVG

Hope to help someone who encounters similar problems in the future.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM