Chart.js Line chart fixed tooltip - chart.js

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>

Related

Chart.js bar chart category labels onClick event [duplicate]

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

How to integrate basic ChartJs customizations when using react-chartjs-2?

I'm trying to integrate this tooltip-based-on-mouse-position customization into a react-js-2 bar chart. I see where to put the new options:
options: {
tooltips: {
mode: 'index',
position: 'cursor',
intersect: false
}
}
in my existing options block declaration. But I don't understand how/where in the app do I integrate the related function?
Chart.Tooltip.positioners.cursor = function(chartElements, coordinates) {
return coordinates;
};
I've tried simply declaring it as Chart.Tooltip.positioners.cursor = ... or ChartJS.Tooltip.positioners.cursor = ... or Tooltip.positioners.cursor = ... in a few different places but it either causes an error or has no effect. I'm on ChartJS v3.8.0 and react-js-2 v4.1.0. Thank you.
Your options are wrong, you need to define the tooltip options in the options.plugins.tooltip namespace and not the options.tooltips namespace.
Example:
Chart.Tooltip.positioners.mouse = function(items, evtPos) {
return evtPos
};
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: {
plugins: {
tooltip: {
intersect: false,
position: 'mouse',
}
}
}
}
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>
React import:
import { Chart, registerables, Tooltip } from "chart.js";
import { Chart as ReactChartJs } from "react-chartjs-2";
Chart.register(...registerables);
Tooltip.positioners.mouse = function(items, evtPos) {
return evtPos
};
const options = {
plugins: {
tooltip: {
position: 'mouse',
intersect: false
}
}
}
<ReactChartJs type="line" data={data} options={options} />

Chart.js - Pie chart calculate sum of visible values after legend click

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>

ChartJS 3.7.1 tooltip callback, get label value for the next index

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>

Align doughnut/pie charts vertically in the container

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>