I read all docs I could and searched all over the internet attempting to achieve the following (attached image) on ChartJS 3.9.1. Is it even possible to have each axis on a radar chart labeled on ChartJS?
radar chart
I had the same problem as you and found this solution:
Define the array of base64 images you want as labels
const labelImages = [
/* Image 2 */ 'data:image/png;base64,iVBORw0KGg......',
/* Image 1 */ 'data:image/png;base64,iVBORw0KGgoAA.....'
]
Then use the plugin:
plugins: [
{
id: 'Label images',
afterDraw: (chart) => {
for (const i in chart.scales.r._pointLabelItems) {
const point = chart.scales.r._pointLabelItems[i];
var image = new Image();
image.src = labelImages[i];
// you I had 20x20 images thats why I use 20x20
chart.ctx.drawImage(image, point.x - 10, point.y, 20, 20);
}
}
}
]
Draw image from context options
This solution draws images where the labels are (where they start, i think), if you want only images and no text then you should hide the labels. I did it like this:
options: {
// just so that the images are not outside the canvas
layout: {
padding: 20
},
scales: {
y: {
display: false
},
x: {
display: false
},
r: {
pointLabels: {
// Hides the labels around the radar chart but leaves them in the tooltip
callback: function (label, index) {
return ' ';
}
}
}
}
}
I hope this helps
Related
Here is a codepen that I am using to solve this problem. What I would like to do is get the length of the horizontal bars to determine if the label should be plotted inside or outside of the bar. Currently, what I have happening:
{
datalabels: {
color: function(context) {
return [0, 3].includes(context.dataIndex) ? 'black' : 'white'
},
anchor: 'start',
align: 'right',
offset: function(context) {
const chart = context.chart;
const area = chart.chartArea;
const meta = chart.getDatasetMeta(context.datasetIndex);
const model = meta.data[context.dataIndex];
// model.width is NaN
// is there a way to get this value
// after the animation is complete?
console.log(model, model.width)
return 4;
},
font: {
size: 9
}
}
When you run the codepen you notice that model.width prints as NaN but when you look at the object itself model.width is there. If I introduce a setTimeout to log that value it exists (not NaN). When I turn the animation off model.width is available in the function.
Therefore, I think the way to make this happen is to get the values after the animation renders. Is there a way to do that in the offset function for datalabels or is there another way of doing that?
You can use the getProps on the model to get the width after the animations are over like so:
offset: function(context) {
const chart = context.chart;
const area = chart.chartArea;
const meta = chart.getDatasetMeta(context.datasetIndex);
const model = meta.data[context.dataIndex];
const {
width
} = model.getProps(['width'], true);
console.log(width)
return 4;
},
Updated codepen: https://codepen.io/leelenaleee/pen/MWQGbdM?editors=1010
I might have been thinking about it the wrong way. By playing around with the values I realized the value itself is a pretty good indication of whether it should be inside or outside of the bar. What I've done instead is evaluate if the value is greater than 30. If so the color is white and the anchor is set to start. If it less than 30 the color is black and the anchor is set to end:
https://codepen.io/thoughtassassin/pen/rNJvOrj
plugins: {
datalabels: {
color: (context) => getValue(context) > 30 ? '#fff' : '#000',
anchor: (context) => getValue(context) > 30 ? 'start' : 'end',
align: 'right',
offset: 5,
font: {
size: 9
}
},
}
I have the following code running:
var options = {
chart: {
type: 'donut',
fontFamily: 'Lato Light'
},
series: [1,2,3,4,5],
labels: ['1','2','3','4','5'],
theme: {
monochrome: {
enabled: true,
color: '#b19254',
shadeTo: 'dark',
shareIntensity: 0.15
}
},
//colors: ['#b19254', '#9f834c', '#8e7543', '#7c663b', '#b99d65', '#c8b387'],
legend: {
position: 'bottom'
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
show: false
},
value: {
offsetY: -1,
show: true
},
total: {
show: false,
showAlways: false,
formatter: function (w) { return String(Math.round(chart.w.globals.seriesTotals.reduce((a,b) => { return a+b}, 0) * 100) / 100) + ' ' + $currency}
}
}
}
}
},
}
var chart = new ApexCharts(document.querySelector("#investment-chart-wrapper"), options);
chart.render();
var $chartData = chart.dataURI();
$chartData.then(
(result) => {
document.querySelector('#chartimg').setAttribute('src',result.imgURI);
});
The bit I am fighting with is the promise result of the dataURI() method from here.
For some reason, the chart I get has all the information including the series labels, but the color for the series does not show, leaving me with this. The color is used for the legend at the bottom, however.
I am sure I am missing something here. Please let me know what.
I was running into this problem as well today. It was because the animation of the chart has not taken place yet. You have to get the dataURI() after it has fully rendered or turn off the chart animation.
I was able to get this working by setting the rendered chart to a variable at the top of my js file and then using it in a function like this:
function SetChartImage() {
chartHistoricalPCTArea.dataURI().then(({ imgURI }) => {
var image = document.querySelector('#HistoricalPCTImage');
image.src = imgURI;
})
}
I am open to learning that there is already a way (via configuration, or developing a plugin) to hook into the rendering of the label of an axis, such that I could control aspects of the font used to render each line of a multiline label (e.g., what I need to render would be similar visually to a label and sub-label below it, with the primary label being bolded and a larger font size, while the sub-label directly beneath it would be normal font weight and a smaller size).
I am using ChartJs version 3.5.1 to render a horizontal barchart (meaning that the dataset labels on the left are really configured under the y axis), and have tried a few different things already:
Hooking into the tick callback - but I can't even use this function to duplicate default functionality (the value coming into that function isn't the label text; instead it is the index/ordinal of the data row?). Even if I could get this to work as shown in examples, it appears like this would be more for the content of the label than any of the configuration options themselves.
Setting the font configuration for ticks to be an array - but this only serves to allow me to change the font between data rows (e.g., I can make the label of the top row in my horizontal bar chart be size 22, the second label 10, etc. - but not change font attributes within lines of a given label)
Using a plugin like afterDraw to try to go tweak things - but again, the configuration at that point seems to only consider all of the lines together as one label.
Tried looking through past PRs to the project (mostly centered around adding multiline label support, as well as bug fixes specific to that area) to get any additional insight
If there isn't a way currently (via plugins or existing configuration), does anyone have a good feel for where to start attacking this sort of a change as a new PR?
UPDATE
As was shared as a response to my corresponding ChartJs feature request and as the accepted answer below, a custom plugin seems to be the only way currently to accomplish what I wanted for now.
Here are the key bits from my configuration (admittedly much more "one time use only" than the accepted answer, as I moved some of the configuration inside of the plugin as hard-coded values given my relatively narrow use case):
// this will be passed into the chart constructor...
const options = {
//...
scales: {
//...
// I wanted to impact the lefthand side of a horizontal bar chart
y: {
ticks: {
// make the original labels white for later painting over with custom sub-labels
color: "white",
// we still want this here to be able to take up the same space as the eventual label we will stick here
font: {
size: 22,
weight: "bold"
}
}
},
//...
}
};
// This is my plugin, also later passed into the chart constructor
const customSubLabelsPlugin = {
id: "customSubLabels",
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
// I only cared about altering one specific axis
scales: { y }
} = chart;
const labelItems = y._labelItems;
const fontStringSubTitle = "16px Helvetica,Arial,sans-serif";
const fontStringMain = "bold 22px Helvetica,Arial,sans-serif";
// loop over each dataset label
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
// For purposes of redrawing, we are going to always assume that each label is an array - because we make it that way if we need to
const label = Array.isArray(labelItem.label)
? labelItem.label
: [labelItem.label];
// Draw new text on canvas
let offset = 0;
label.forEach((el) => {
let elTextMetrics = ctx.measureText(el);
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain;
} else {
ctx.font = fontStringSubTitle;
}
ctx.save();
ctx.fillStyle = "#546a6f";
ctx.fillText(
el,
labelItem.translation[0],
labelItem.translation[1] + labelItem.textOffset + offset
);
ctx.restore();
offset +=
elTextMetrics.fontBoundingBoxAscent +
elTextMetrics.fontBoundingBoxDescent;
});
}
}
};
You can use a plugin to redraw the ticks for you, might need some finetuning for your specific needs:
var options = {
type: 'line',
data: {
labels: [
["Red", "subTitle"],
["Blue", "subTitle"],
["Yellow", "subTitle"],
["Green", "subTitle"],
["Purple", "subTitle"],
["Orange", "subTitle"]
],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'red',
backgroundColor: 'red'
}]
},
options: {
plugins: {
customTextColor: {
color: 'blue',
boxColor: 'white',
fontStringSubTitle: 'italic 12px Comic Sans MS',
fontStringMain: ''
}
}
},
plugins: [{
id: 'customTextColor',
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
scales: {
y,
x
}
} = chart;
const labelItems = x._labelItems;
const {
color,
boxColor,
fontStringMain,
fontStringSubTitle
} = opts;
const defaultFontString = '12px "Helvetica Neue", Helvetica, Arial, sans-serif';
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
if (!Array.isArray(labelItem.label)) {
continue;
}
let metrics = ctx.measureText(labelItem.label);
let labelWidth = metrics.width;
let labelHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
//Draw box over old labels so they are inviseble
ctx.save();
ctx.fillStyle = boxColor || '#FFFFFF';
ctx.fillRect((labelItem.translation[0] - labelWidth / 2), labelItem.translation[1], labelWidth, labelHeight * labelItem.label.length);
ctx.restore();
// Draw new text on canvas
let offset = 0;
labelItem.label.forEach(el => {
let elTextMetrics = ctx.measureText(el);
let elWidth = elTextMetrics.width;
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain || defaultFontString;
} else {
ctx.font = fontStringSubTitle || defaultFontString;
}
ctx.save();
ctx.fillStyle = color || Chart.defaults.color
ctx.fillText(el, (labelItem.translation[0] - elWidth / 2), labelItem.translation[1] + labelItem.textOffset + offset);
ctx.restore();
offset += elTextMetrics.fontBoundingBoxAscent + elTextMetrics.fontBoundingBoxDescent;
});
}
// Draw white box over old label
}
}]
}
var 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.1/chart.js"></script>
</body>
I have a line chart with 4 datasets, I want to show different tooltip backgrounds for all 4 lines. but tooltip doesn't support dynamic background color, anyway how to do this?
Working "dynamic" example.
***keep in mind the access to data related to your data structure.
Using tooltip-model
https://www.chartjs.org/docs/2.7.3/configuration/tooltip.html#tooltip-model
"hello world" example - Change all tooltips background to red:
tooltips: {
custom: function(tooltipModel) {
tooltipModel.backgroundColor = "red";
}
},
Code Snippet:
var data = {
labels: ["Africa", "Asia", "Europe", "America"],
datasets: [{
/* data */
label: "Population (millions)",
backgroundColor: ["red", "blue","green", 'purple'],
data: [1000,1500,2000, 2200]
}]
};
var options = {
title: {
text: 'Dynamically change tooltip background example',
display: true
},
tooltips: {
titleFontSize: 20,
borderWidth: 2,
borderColor: "white",
displayColors: false, /* if true, color boxes are shown in the tooltip */
/*########### Custom model ###########*/
custom: function(tooltipModel) {
/* if data & datasets not empty & tooltip available */
if (tooltipModel.opacity !== 0 && data.labels.length && data.datasets.length) {
/* get dataPoints index */
var index = tooltipModel.dataPoints[0].index;
/* get dataPoints datasetIndex */
var dataSetIndex = tooltipModel.dataPoints[0].datasetIndex;
/* get the current color on index and datasetIndex position */
var color = data.datasets[dataSetIndex].backgroundColor[index];
/* set backgroundColor */
tooltipModel.backgroundColor = color;
};
}
},
scales: {
xAxes: [{
stacked: true
}],
yAxes: [{
stacked: true
}]
}
};
var myChart = new Chart(document.getElementById("chart"), {
type: 'bar',
data: data,
options: options
});
<canvas id="chart" width="800" height="450"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.8.0"></script>
The outline of the code
"if" (To avoid console errors):
/*########### Custom model ###########*/
custom: function(tooltipModel) {
/* if data & datasets not empty & tooltip available */
if (tooltipModel.opacity !== 0 && data.labels.length && data.datasets.length) {
/* do something */
console.log(tooltipModel.dataPoints[0]); /* return object */
console.log Return object:
Object {
datasetIndex: 0,
index: 1,
label: "Asia",
value: "1500",
x: 338.6845703125,
xLabel: "Asia",
y: 215.28,
yLabel: 1500
}
Than we use dot (.) notation to access the object values.
console.log(tooltipModel.dataPoints[0].index); /* return 1 */
We use this index "anchor" to get the correct index for backgroundColor array:
console.log(data.datasets[dataSetIndex].backgroundColor[index]); /* return "blue"
Last step is to use this color value:
/* set backgroundColor */
tooltipModel.backgroundColor = color;
UI
Usefull to hide color boxes:
displayColors: false, /* if true, color boxes are shown in the tooltip */
https://www.chartjs.org/docs/2.7.3/configuration/tooltip.html#tooltip-configuration
codepen:
https://codepen.io/ezra_siton/pen/dyoQeGe?editors=1011
I also had same problem today.Solution is implementing custom tooltip method but you dont need to create custom tooltip from scratch.
colorArray=["blue","red","green"];
tooltips: {
custom: function(tooltipModel) {
tooltipModel.backgroundColor=this.colorArray[tooltipModel.dataPoints[0].index];
},
},
This code worked for me.Depends on which tooltip you click it will bring the index of color from colorArray.
I need help to put the number of the pie chart in the legend
Chart Image
If i hover the chart with mouse i can see the number relative to each item
i want to display it in the legend either
the important code so far:
var tempData = {
labels: Status,
datasets: [
{
label: "Status",
data: Qtd,
backgroundColor: randColor
},
]
};
var ctx = $("#pieStatus").get(0).getContext("2d");
var chartInstance = new Chart(ctx, {
type: 'pie',
data: tempData,
options: {
title: {
display: true,
fontsize: 14,
text: 'Total de Pedidos por Situação'
},
legend: {
display: true,
position: 'bottom',
},
responsive: false
}
});
"Qtd","randColor" are "var" already with values
You need to edit the generateLabels property in your options :
options: {
legend: {
labels: {
generateLabels: function(chart) {
// Here
}
}
}
}
Since it is quite a mess to create on your own a great template. I suggest using the same function as in the source code and then edit what is needed.
Here are a small jsFiddle, where you can see how it works (edited lines - from 38 - are commented), and its result :
Maybe this is a hacky solution, but for me seems simpler.
The filter parameter
ChartJS legend options have a filter parameter. This is a function that is called for each legend item, and that returns true/false whether you want to show this item in the legend or not.
filter has 2 arguments:
legendItem : The legend item to show/omit. Its properties are described here
data : The data object passed to the chart.
The hack
Since JS passes objects by reference, and filter is called for each legend item, then you can mutate the legendItem object to show the text that you want.
legend : {
labels: {
filter: (legendItem, data) => {
// First, retrieve the data corresponding to that label
const label = legendItem.text
const labelIndex = _.findIndex(data.labels, (labelName) => labelName === label) // I'm using lodash here
const qtd = data.datasets[0].data[labelIndex]
// Second, mutate the legendItem to include the new text
legendItem.text = `${legendItem.text} : ${qtd}`
// Third, the filter method expects a bool, so return true to show the modified legendItem in the legend
return true
}
}
}
Following on from tektiv's answer, I've modified it for ES6 which my linter requires;
options: {
legend: {
labels: {
generateLabels: (chart) => {
const { data } = chart;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const meta = chart.getDatasetMeta(0);
const ds = data.datasets[0];
const arc = meta.data[i];
const custom = (arc && arc.custom) || {};
const { getValueAtIndexOrDefault } = Chart.helpers;
const arcOpts = chart.options.elements.arc;
const fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
const stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
const bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
const value = chart.config.data.datasets[arc._datasetIndex].data[arc._index];
return {
text: `${label}: ${value}`,
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: Number.isNaN(ds.data[i]) || meta.data[i].hidden,
index: i,
};
});
}
return [];
},
},
},
},
I wanted to let the user select from 100+ data sets, but rather than adding/removing them from my Chart I decided to set the showLine: false on any dataset that I want hidden. Unfortunately the default legend would show all 100+. So in my solution I generate the legend manually, filtering out any dataset that has showLine: false.
Your settings will have this:
legend: {
labels: {
generateLabels: (a) => {
return a.data.labels
}
}
And you'll generate your own labels with a helper function:
function updateAllLabels() {
const myNewLabels = [];
myChart.data.datasets.forEach((element) => {
if (element.showLine) {
myNewLabels.push(generateLabel(element));
}
});
myChart.data.labels = myNewLabels;
}
And you'll generate the label with another function:
function generateLabel(data) {
return {
fillStyle: data.borderColor,
lineWidth: 1,
strokeStyle: data.borderColor,
text: data.countyName, // I attach countryName to my datasets for convenience
}
}
Now just don't forget to call the function whenever updating your chart:
updateAllLabels();
myChart.update();
Happy graphing!