Im using Chart.js to display a pie chart and its legend.
When a user clicks on a legend label to disable\hide it I want to recalculate the sum total for all visible sections remaining in the pie chart.
I have overridden the default Legend Click Handler and am trying to do this in there - Im not sure if this is the place to do it but it seems logical to do so.
const defaultLegendClickHandler = Chart.defaults.plugins.legend.onClick;
const pieDoughnutLegendClickHandler = Chart.controllers.doughnut.overrides.plugins.legend.onClick;
const newLegendClickHandler = function (e, legendItem, legend) {
const index = legendItem.datasetIndex;
const type = legend.chart.config.type;
if (type === 'pie' || type === 'doughnut') {
pieDoughnutLegendClickHandler(e, legendItem, legend)
} else {
defaultLegendClickHandler(e, legendItem, legend);
}
let ci = legend.chart;
//Iterate through visible values of a data set and sum them...????
};
You can interate throguh the data, check if it is hidden and if not add it to the total:
const defaultLegendClickHandler = Chart.defaults.plugins.legend.onClick;
const pieDoughnutLegendClickHandler = Chart.controllers.doughnut.overrides.plugins.legend.onClick;
const newLegendClickHandler = function(e, legendItem, legend) {
const index = legendItem.datasetIndex;
const type = legend.chart.config.type;
if (type === 'pie' || type === 'doughnut') {
pieDoughnutLegendClickHandler(e, legendItem, legend)
} else {
defaultLegendClickHandler(e, legendItem, legend);
}
let ci = legend.chart;
const sum = ci.data.datasets.reduce((acc, curr) => {
curr.data.forEach((d, i) => {
if (ci.getDataVisibility(i)) {
acc += d;
}
});
return acc
}, 0)
console.log(sum)
// Do whatever you want with the total.
};
const options = {
type: 'doughnut',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"]
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
backgroundColor: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"]
}
]
},
options: {
plugins: {
legend: {
onClick: newLegendClickHandler
}
}
}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.js"></script>
</body>
Related
How to detect click on an axis label with chart.js
In the example bellow, I can only detect click on the graph itself
https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6
You will need to implement a custom plugin that can listen to all the events of the canvas:
const findLabel = (labels, evt) => {
let found = false;
let res = null;
labels.forEach(l => {
l.labels.forEach((label, index) => {
if (evt.x > label.x && evt.x < label.x2 && evt.y > label.y && evt.y < label.y2) {
res = {
label: label.label,
index
};
found = true;
}
});
});
return [found, res];
};
const getLabelHitboxes = (scales) => (Object.values(scales).map((s) => ({
scaleId: s.id,
labels: s._labelItems.map((e, i) => ({
x: e.translation[0] - s._labelSizes.widths[i],
x2: e.translation[0] + s._labelSizes.widths[i] / 2,
y: e.translation[1] - s._labelSizes.heights[i] / 2,
y2: e.translation[1] + s._labelSizes.heights[i] / 2,
label: e.label,
index: i
}))
})));
const plugin = {
id: 'customHover',
afterEvent: (chart, event, opts) => {
const evt = event.event;
if (evt.type !== 'click') {
return;
}
const [found, labelInfo] = findLabel(getLabelHitboxes(chart.scales), evt);
if (found) {
console.log(labelInfo);
}
}
}
Chart.register(plugin);
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'orange'
}
]
},
options: {}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>
Adpatation for Angular with ng2-charts (chart-js v3.7.1)
Just use Chart.register
i.e. put the following functiion into component ngOnInit()
RegisterPlugin() {
Chart.register(
{
id: 'yAxisCustomClick',
afterEvent: (chart: Chart<'bar'>, event: {
event: ChartEvent;
replay: boolean;
changed?: boolean | undefined;
cancelable: false;
inChartArea: boolean
}) => {
const evt = event.event;
if (evt.type === 'click' && evt.x! < Object.values(chart.scales).filter(s => s.id === 'x')[0].getBasePixel()) {
const labelIndex = Object.values(chart.scales).filter(s => s.id === 'y')[0].getValueForPixel(evt.y!);
const label = Object.values(chart.scales).filter(s => s.id === 'y')[0].getTicks()[labelIndex!]?.label;
if (label) {
console.log('Do the stuff for', label)
}
}
}
}
);
}
Example in stackblitz udpated
https://stackblitz.com/edit/ng2-charts-bar-template-qchyz6
I'm currently migrating from 2.9.3 to 3.7.1 and I'm having trouble with migrating a callback function from the options object.
Former location: options.tooltips.callbacks.title
Migrated location: options.plugins.tooltip.callbacks.title
Former function (simplified):
function (tooltipItems, data) {
var tooltipItem = tooltipItems[0];
var currentLabel = data.labels[tooltipItem.index];
var nextLabel = data.labels[tooltipItem.index +1];
return currentLabel + ' - ' + nextLabel;
}
Migrated function:
function (tooltipItems) {
var tooltipItem = tooltipItems[0];
var currentLabel = tooltipItem.label;
var nextLabel = ? // how to get nextLabel?
return currentLabel + ' - ' + nextLabel;
}
tooltipItem.dataset has a label array but that appears empty when i console.log(tooltipItems)
You get access to the chart object and have the data index so you can just get the correct label from the labels array like so:
title: (items) => {
const item = items[0];
const {
chart
} = item;
const nextLabel = chart.data.labels[item.dataIndex + 1] || '';
return `${item.label}, next label: ${nextLabel}`;
}
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'orange'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'pink'
}
]
},
options: {
plugins: {
tooltip: {
callbacks: {
title: (items) => {
const item = items[0];
const {
chart
} = item;
const nextLabel = chart.data.labels[item.dataIndex + 1] || '';
return `${item.label}, next label: ${nextLabel}`;
}
}
}
}
}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.js"></script>
</body>
I have three doughnut charts side by side like so:
The problem is, the number of items is different between the charts, causing the legend have different height and in turn the charts are not in line. Is it possible to align the charts to the top of the container?
You will need to use an custom plugin for this that makes an html legend:
const getOrCreateLegendList = (chart, id) => {
const legendContainer = document.getElementById(id);
let listContainer = legendContainer.querySelector('ul');
if (!listContainer) {
listContainer = document.createElement('ul');
listContainer.style.display = 'flex';
listContainer.style.flexDirection = 'row';
listContainer.style.margin = 0;
listContainer.style.padding = 0;
legendContainer.appendChild(listContainer);
}
return listContainer;
};
const htmlLegendPlugin = {
id: 'htmlLegend',
afterUpdate(chart, args, options) {
const ul = getOrCreateLegendList(chart, options.containerID);
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = chart.options.plugins.legend.labels.generateLabels(chart);
items.forEach(item => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.flexDirection = 'row';
li.style.marginLeft = '10px';
li.onclick = () => {
const {
type
} = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(item.index);
} else {
chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex));
}
chart.update();
};
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = item.fillStyle;
boxSpan.style.borderColor = item.strokeStyle;
boxSpan.style.borderWidth = item.lineWidth + 'px';
boxSpan.style.display = 'inline-block';
boxSpan.style.height = '20px';
boxSpan.style.marginRight = '10px';
boxSpan.style.width = '20px';
// Text
const textContainer = document.createElement('p');
textContainer.style.color = item.fontColor;
textContainer.style.margin = 0;
textContainer.style.padding = 0;
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
});
}
};
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink',
backgroundColor: 'pink'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'orange',
backgroundColor: 'orange'
}
]
},
options: {
plugins: {
legend: {
display: false,
},
htmlLegend: {
// ID of the container to put the legend in
containerID: 'legend-container',
}
}
},
plugins: [htmlLegendPlugin],
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<div id="legend-container"></div>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.js"></script>
</body>
is it possible to call a default on-canvas tooltip at a fixed datalabel position without hovering the mouse?
You can dispatch hover events as in this example, got from this answer:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js#3.0.0/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels#2.0.0"></script>
<button id="a">Hover a</button>
<button id="b">Hover b</button>
<button id="c">Hover c</button>
<button id="d">Hover d</button>
<button id="e">Hover all</button>
<canvas id="chart"></canvas>
<script>
let c = new Chart($("#chart"), {
type: "doughnut",
data: {
labels: ["a", "b", "c", "d"],
datasets: [
{
data: [1, 2, 4, 8],
backgroundColor: ["red", "blue", "green", "orange"]
}
]
}
});
console.log("chart drawn!");
$("#a").on("click", function () {
t(0);
});
$("#b").on("click", function () {
t(1);
});
$("#c").on("click", function () {
t(2);
});
$("#d").on("click", function () {
t(3);
});
$("#e").on("click", function () {
hoverAll();
});
function t(idx) {
var meta = c.getDatasetMeta(0),
rect = c.canvas.getBoundingClientRect(),
point = meta.data[idx].getCenterPoint(),
evt = new MouseEvent("mousemove", {
clientX: rect.left + point.x,
clientY: rect.top + point.y
}),
node = c.canvas;
node.dispatchEvent(evt);
}
function hoverAll() {
for (let i = 0; i < 4; i++) {
console.log(i);
t(i);
}
}
</script>
But, hover events seems to be mutually exclusive, meaning that you have only one at once, because there is only one mouse that can hover over one element once at a time. That is why the added hoverall button does not work, and leaves the last one hovered element.
If you want all the datasets and records of your chart displayed, the datalabels plugin seems the way to go, as suggested in the comments.
You can use the setActiveElements function like so:
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink',
backgroundColor: 'pink'
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderColor: 'orange',
backgroundColor: 'orange'
}
]
},
options: {}
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
const chart = new Chart(ctx, options);
document.getElementById("tt").addEventListener("click", () => {
const {
tooltip,
chartArea
} = chart;
tooltip.setActiveElements([{
datasetIndex: 0,
index: 1
},
{
datasetIndex: 1,
index: 1
}
]);
chart.update();
});
<body>
<button id="tt">
Make tooltip active
</button>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.js"></script>
</body>
I want to set font colour at character level (or word level) in my data-labels in chartjs. See before and after image below.
For example, if my data-label is 0.89 I would like the paint the 0 as yellow, 8 as blue and 9 as red.
Sandbox:
https://codesandbox.io/s/quizzical-hooks-zcg91?file=/src/components/LineChart.jsx
Afaik you can set the individual collors of the letters with the datalabels plugin, you will need to write your own custom plugin for that, what you can do is provide an array of collors to the color property to give each entry a different collor: https://codesandbox.io/s/dazzling-leaf-pxmzm
datalabels: {
display: true,
color: ["black", "green", "blue", "pink", "purple"],
align: "end",
padding: {
right: 2
},
labels: {
padding: { top: 10 },
title: {
font: {
weight: "bold"
}
}
},
formatter: function (value) {
return "\n" + value;
}
}
Edit:
after looking at the code the datalabels plugin uses to render the labels its not possible to color individual characters, if you want that you will need to write your own custom plugin:
const customDatalabalesPlugin = {
id: 'customDatalabels',
afterDatasetsDraw: (chart, args, opts) => {
const {
ctx,
_metasets
} = chart;
_metasets.forEach((meta) => {
meta.data.forEach((datapoint) => {
const lineHeight = ctx.measureText('M').width;
const dpVal = datapoint.parsed.y;
const text = dpVal.toString();
const textWidth = ctx.measureText(text).width;
const color = opts.color || 'black';
if (typeof color === 'string') {
ctx.fillStyle = color;
ctx.fillText(text, (datapoint.x - textWidth / 2), (datapoint.y - lineHeight));
} else if (Array.isArray(color)) {
let x = datapoint.x - textWidth / 2;
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
ctx.fillStyle = color[i % color.length];
ctx.fillText(char, x, (datapoint.y - lineHeight));
x += ctx.measureText(char).width;
}
} else {
console.error('Invalid color type provided');
}
})
})
}
}
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12.4, 19.234, 3.23213, 5.4, 2, 3],
borderColor: 'pink'
}]
},
options: {
plugins: {
customDatalabels: {
color: ['pink', 'green', 'orange'], // Color each character individual collor
// color: 'pink' // Color whole label this collor
}
}
},
plugins: [customDatalabalesPlugin]
}
const ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.js"></script>
</body>