Related
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>
Here is my HTML div:
<canvas id="mycanvas" width="290" height="140"></canvas>
JavaScript:
$(document).ready(function() {
var ctx = $("#mycanvas").get(0).getContext("2d");
var data = [{
value: 923864,
color: "#58508d",
highlight: "#003f5c",
label: "Dr. John",
},
{
value: 720988,
color: "#3292b0",
highlight: "#6fefff",
label: "Alex"
},
{
value: 179539,
color: "orange",
highlight: "darkblue",
label: "Other",
},
];
var piechart = new Chart(ctx).Pie(data);
});
First at all, you should upgrade to the most stable version of Chart.js, which currently is v2.9.4.
The dataset accepts number of properties, that can be defined for styling the border. Its color and width are controlled through the following ones.
borderColor arc border color
borderWidth arc border width (in pixels).
In order to see a shadow, you can use the Plugin Core API. The API offers a range of hooks that may be used for performing custom code. In the beforeDraw for example, you can draw a circle with shadow through CanvasRenderingContext2D.arc().
Please take a look at your amended code below and see how it works.
new Chart('canvas', {
type: 'pie',
plugins: [{
beforeDraw: chart => {
var ctx = chart.chart.ctx;
ctx.save();
ctx.beginPath();
ctx.fillStyle = 'white';
ctx.shadowColor = 'black';
ctx.shadowBlur = 20;
ctx.shadowOffsetX = -10;
ctx.shadowOffsetY = 0;
const x = chart.chart.width / 2;
const y = chart.chart.height / 2 + 15;
ctx.arc(x, y, 95, 0, Math.PI*2, false);
ctx.fill();
ctx.restore();
}
}],
data: {
labels: ['Dr. John', 'Alex', 'Other'],
datasets: [{
data: [923864, 720988, 179539],
backgroundColor: ['#58508d', '#3292b0', 'orange'],
hoverBackgroundColor: ['#003f5c', '#6fefff', 'darkblue'],
borderWidth: 0
}],
},
options: {
responsive: false,
layout: {
padding: {
top: 15,
bottom: 20
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<canvas id="canvas" height="260"></canvas>
Using the following code to save a Google chart. But it is getting downloaded as a file and not an image, it was throwing following
"Resource interpreted as Document but transferred with MIME type image/octet-stream: "data:image/octet-stream;base64,..."
And the code:
function getImgData(chartContainer) {
var chartArea = chartContainer.getElementsByTagName('div')[1];
var svg = chartArea.innerHTML;
var doc = chartContainer.ownerDocument;
var canvas = doc.createElement('canvas');
canvas.setAttribute('width', chartArea.offsetWidth);
canvas.setAttribute('height', chartArea.offsetHeight);
canvas.setAttribute(
'style',
'position: absolute; ' +
'top: ' + (-chartArea.offsetHeight * 2) + 'px;' +
'left: ' + (-chartArea.offsetWidth * 2) + 'px;');
doc.body.appendChild(canvas);
canvg(canvas, svg);
var imgData = canvas.toDataURL("image/png");
canvas.parentNode.removeChild(canvas);
return imgData;
}
function saveAsImg(chartContainer) {
var imgData = getImgData(chartContainer);
window.location = imgData.replace("image/png", "image/octet-stream");
}
you can use an anchor tag with a download attribute to assign the file name
and most charts have a getImageURI method
also, don't see the need to replace("image/png", "image/octet-stream")
see following working snippet...
google.charts.load('current', {
callback: function () {
var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
google.visualization.events.addListener(chart, 'ready', function () {
// set anchor tag
var saveLink = document.getElementById('saveLink');
saveLink.href = chart.getImageURI();
// cause download to occur
saveLink.click();
});
chart.draw(google.visualization.arrayToDataTable([
['Task', 'Hours'],
['A', 19.2],
['B', 30.8],
['C', 50.0]
]), {
height: 200,
chartArea: {
top: 24
},
legend: 'none',
pieHole: 0.4,
theme: 'maximized',
width: 200
});
},
packages: ['corechart']
});
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>
<a id="saveLink" download="chart.png">Save Chart</a>
We are using nnnick chart.js (open source chart) in our application for reporting purpose.There is a requirement to show the Custom tool-tip in the line chart.
As of now , Normal chart tooltip is showing based on the X-axis and Y axis dataset values. But Here we want to show the Dynamic additional data in the Tooltip.
For Example ,
Let us take a Student Enrollment .
here
X Axis Value - Enrollment Month (Jan,Feb,Mar....etc)
Y Axis Value - Number of Enrollments (10,20,30...ect)
After the Line chart is plotted , Now it is displaying (Jan ,10) in the tooltip.
We have to show the Number of Male & Female student details in the tool tip On mouse over the data point Jan 10 (i.e) (Jan,10, Male:5 , Female 5 ).
If you see the above screen shot , Green color is toop-tip is the normal one which is a built-in option. Red Color highlighted tool-tip is the one we are expecting.
Please provide any suggestion on this .
So you can achieve this using the custom tool tip function in the newer (not sure when it was included) version of chart js. You can have it display anything you want in place of a normal tooltip so in this case i have added a tooltip and a tooltip-overview.
The really annoying thing is though in this function you are not told which index you are currently showing a tooltip for. Two ways to solve this, override the showToolTip function so it actually passes this information or do a quick little hack to extract the label from the tooltip text and get the index from the labels array (hacky but quicker so i went for this one in the example)
So here is a quick example based upon the samples in chartjs samples folder. This is just a quick example so you would prob need to play around with the positioning and stuff until its what you need.
Chart.defaults.global.pointHitDetectionRadius = 1;
Chart.defaults.global.customTooltips = function(tooltip) {
// Tooltip Element
var tooltipEl = $('#chartjs-tooltip');
var tooltipOverviewEl = $('#chartjs-tooltip-overview');
// Hide if no tooltip
if (!tooltip) {
tooltipEl.css({
opacity: 0
});
tooltipOverviewEl.css({
opacity: 0
});
return;
}
//really annoyingly we don;t get told which index this comes from so going to have
//to extract the label from the text :( and then find the index based on that
//other option here is to override the the whole showTooltip in chartjs and have the index passed
var label = tooltip.text.substr(0, tooltip.text.indexOf(':'));
var labelIndex = labels.indexOf(label);
var maleEnrolmentNumber = maleEnrolments[labelIndex];
var femaleEnrolmentNumber = FemaleEnrolments[labelIndex];
// Set caret Position
tooltipEl.removeClass('above below');
tooltipEl.addClass(tooltip.yAlign);
// Set Text
tooltipEl.html(tooltip.text);
//quick an ddirty could use an actualy template here
var tooltipOverviewElHtml = "<div> Overall : " + (maleEnrolmentNumber + femaleEnrolmentNumber) + "</div>";
tooltipOverviewElHtml += "<div> Male : " + (maleEnrolmentNumber) + "</div>";
tooltipOverviewElHtml += "<div> Female : " + (femaleEnrolmentNumber) + "</div>";
tooltipOverviewEl.html(tooltipOverviewElHtml);
// Find Y Location on page
var top;
if (tooltip.yAlign == 'above') {
top = tooltip.y - tooltip.caretHeight - tooltip.caretPadding;
} else {
top = tooltip.y + tooltip.caretHeight + tooltip.caretPadding;
}
// Display, position, and set styles for font
tooltipEl.css({
opacity: 1,
left: tooltip.chart.canvas.offsetLeft + tooltip.x + 'px',
top: tooltip.chart.canvas.offsetTop + top + 'px',
fontFamily: tooltip.fontFamily,
fontSize: tooltip.fontSize,
fontStyle: tooltip.fontStyle,
});
tooltipOverviewEl.css({
opacity: 1,
fontFamily: tooltip.fontFamily,
fontSize: tooltip.fontSize,
fontStyle: tooltip.fontStyle,
});
};
var maleEnrolments = [5, 20, 15, 20, 20, 30, 50]; // Integer array for male each value is corresponding to each month
var FemaleEnrolments = [5, 0, 15, 20, 30, 30, 20]; // Integer array for Female each value is corresponding to each month
var labels = ["Jan", "February", "March", "April", "May", "June", "July"]; //Enrollment by Month
var lineChartData = {
labels: labels,
datasets: [{
label: "Student Details",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [10, 20, 30, 40, 50, 60, 70], //enrollement Details overall (Male + Female)
}]
};
var ctx2 = document.getElementById("chart2").getContext("2d");
window.myLine = new Chart(ctx2).Line(lineChartData, {
responsive: true
});
#canvas-holder1 {
width: 300px;
margin: 20px auto;
}
#canvas-holder2 {
width: 50%;
margin: 20px 25%;
position:relative;
}
#chartjs-tooltip-overview {
opacity: 0;
position: absolute;
background: rgba(0, 0, 0, .7);
color: white;
padding: 3px;
border-radius: 3px;
-webkit-transition: all .1s ease;
transition: all .1s ease;
pointer-events: none;
-webkit-transform: translate(-50%, 0);
transform: translate(-50%, 0);
left:200px;
top:0px
}
#chartjs-tooltip {
opacity: 1;
position: absolute;
background: rgba(0, 0, 0, .7);
color: white;
padding: 3px;
border-radius: 3px;
-webkit-transition: all .1s ease;
transition: all .1s ease;
pointer-events: none;
-webkit-transform: translate(-50%, 0);
transform: translate(-50%, 0);
}
.chartjs-tooltip-key {
display:inline-block;
width:10px;
height:10px;
}
<script src="https://raw.githack.com/nnnick/Chart.js/master/Chart.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="canvas-holder2">
<div id="chartjs-tooltip-overview"></div>
<div id="chartjs-tooltip"></div>
<canvas id="chart2" width="600" height="600" />
</div>
I have created a donut chart from Google Charts API. When clicking on each slice, it should increase by 10 units and decrease the adjacent slice (clockwise) by 10 units. What I have thus far is a alert popup that explains this, but I would like to redraw the chart with the new values.
Here is my code:
<html>
<head>
<!--Load the AJAX API-->
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
// Load the Visualization API and the piechart package.
google.load('visualization', '1.0', {'packages':['corechart']});
// Set a callback to run when the Google Visualization API is loaded.
google.setOnLoadCallback(drawChart);
// Callback that creates and populates a data table,
// instantiates the pie chart, passes in the data and
// draws it.
function drawChart() {
// Create the data table.
var data = new google.visualization.DataTable();
data.addColumn('string', 'Option');
data.addColumn('number', 'Value');
data.addRows([
['Option A', 40],
['Option B', 30],
['Option C', 30]
]);
// Set chart options
var options = {
height: 300,
fontName: 'Lato, sans-serif',
title: 'Values per option',
titleTextStyle: {
color: '#5a5a5a',
fontSize: 20,
bold: true,
align: 'center'
},
pieHole: 0.6,
slices: {
0: {color: 'red'},
1: {color: 'blue'},
2: {color: 'green'}
},
legend: {
position: 'bottom',
textStyle: {
color: '#5a5a5a',
fontSize: 14
}
},
enableInteractivity: true,
pieSliceText: 'none'
};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
function selectHandler() {
var selectedItem = chart.getSelection()[0];
if (selectedItem && selectedItem.row <2) {
var activeTrait = data.getValue(selectedItem.row, 0);
activePerc = data.getValue(selectedItem.row, 1);
activePercNew = parseInt(activePerc)+10
adjaceTrait = data.getValue(selectedItem.row+1, 0);
adjacePerc = data.getValue(selectedItem.row+1, 1);
adjacePercNew = parseInt(adjacePerc)-10
alert(activeTrait + ' has a value of ' + activePerc + '%. The new value will now be set to ' + activePercNew + '% and ' + adjaceTrait + ' will be corrected to ' + adjacePercNew + '%.');
}
if (selectedItem && selectedItem.row == 2) {
var activeTrait = data.getValue(selectedItem.row, 0);
activePerc = data.getValue(selectedItem.row, 1);
activePercNew = parseInt(activePerc)+10
adjaceTrait = data.getValue(selectedItem.row-2, 0);
adjacePerc = data.getValue(selectedItem.row-2, 1);
adjacePercNew = parseInt(adjacePerc)-10
alert(activeTrait + ' has a value of ' + activePerc + '%. The new value will now be set to ' + activePercNew + '% and ' + adjaceTrait + ' will be corrected to ' + adjacePercNew + '%.');
}
}
google.visualization.events.addListener(chart, 'select', selectHandler);
chart.draw(data, options);
}
</script>
</head>
<body>
<!--Div that will hold the pie chart-->
<div id="chart_div" style="width:800; height:300"></div>
</body>
</html>
I would just like to resize the selected and adjacent slices by clicking on a single slice. Not sure if I should create a var newdata with the changed values and use chart.draw(newdata, option)?
Yeah you're pretty much there, and you're definitely on the right track with your answer. You can use data.setValue() to adjust your values then you would have something like this in your second "if" statement:
data.setValue(selectedItem.row,1, activePercNew);
data.setValue(selectedItem.row-2,1, adjacePercNew);
chart.draw(data, options);
// the thing we just clicked on was redrawn so it lost its handler, reinstate it:
google.visualization.events.addListener(chart, 'select', selectHandler);
And the same in the first but with selectedItem.row+1 instead of selectedItem.row-2. Or, ideally, tidy that section up a little so the two if statements figure out which things you're referring to and then one bit of code does the redraw. For example, here's an adjusted handler function which also doesn't rely on there being 3 sections:
function selectHandler() {
var selectedItem = chart.getSelection()[0];
var numRows = data.getNumberOfRows();
// verify the selection isn't inexplicibly invalid
if (selectedItem && selectedItem.row < numRows && selectedItem.row >= 0) {
// find the two items we're looking at
var curItem = selectedItem.row;
// we either want selected.row + 1 or we want 0 if the selected item was the last one
var otherItem = selectedItem.row == numRows - 1 ? 0 : selectedItem.row + 1;
// calculate the new values
var activePerc = data.getValue(curItem , 1);
var activePercNew = parseInt(activePerc)+10;
var adjacePerc = data.getValue(otherItem , 1);
var adjacePercNew = parseInt(adjacePerc )-10;
// update the chart
data.setValue(curItem,1, activePercNew);
data.setValue(otherItem,1, adjacePercNew);
chart.draw(data, options);
// the thing we just clicked on was redrawn so it lost its handler, reinstate it:
google.visualization.events.addListener(chart, 'select', selectHandler);
}
}
You might want to then also consider what should happen if a value is forced right to zero - with this solution it'll disappear from the chart, and then the next click will force an invalid negative value.