Related
I'm looking for a way to zoom from place to place on a globe in D3 v4 (v4 is important). What I'm looking for is basically exactly this: https://www.jasondavies.com/maps/zoom/ My problem is that Jason Davies obfuscated his code, so I can't read it, and I can't find a bl.ock containing that project or anything similar to it. I'll provide a link to what I've got here: http://plnkr.co/edit/0mjyR3ovTfkDXB8FTG0j?p=preview
The relevant is probably inside the .tween():
.tween("rotate", function () {
var p = d3.geoCentroid(points[i]),
r = d3.geoInterpolate(projection.rotate(), [-p[0], -p[1]]);
return function (t) {
projection.rotate(r(t));
convertedLongLats = [projection(points[0].coordinates), projection(points[1].coordinates)]
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
for (var j = 0; j < points.length; j++) {
var textCoords = projection(points[j].coordinates);
c.fillStyle = textColors, c.textAlign = "center", c.font = "18px FontAwesome", c.fillText(points[j].icon, textCoords[0], textCoords[1]);
textCoords[0] += 15;
c.textAlign = "left", c.font = " 12px Roboto", c.fillText(points[j].location, textCoords[0], textCoords[1]);
}
c.strokeStyle = textColors, c.lineWidth = 4, c.setLineDash([10, 10]), c.beginPath(), c.moveTo(convertedLongLats[0][0], convertedLongLats[0][1]), c.lineTo(convertedLongLats[1][0], convertedLongLats[1][1]), c.stroke();
};
})
Basically, I want what I've got now but I want it to zoom in, pretty much exactly like it is in the Animated World Zoom example I provided above. I'm not really looking for code, I'd rather someone point me in the right direction with an example or something (it's worth noting that I'm fairly new to d3 and that this project is heavily based on World Tour by mbostock, so it uses canvas). Thank you all in advance!
Based on your plunker and comment, a challenge in zooming out between two points in a transition is that the interpolator will only interpolate between two values. The solution in your plunker relies on two interpolators, one for zooming in and zooming out. This method has added un-needed complexity and somewhere along the line, as you note, it jumps to an incorrect scale. You could simplify this:
Take an interpolator that interpolates between -1 and 1, and weight each scale according to the absolute value of the interpolator. At zero, the zoom should be out all the way, while at -1,1, you should be zoomed in:
var s = d3.interpolate(-1,1);
// get the appropriate scale:
scale = Math.abs(0-s(t))*startEndScale + (1-Mat.abs(0-s(t)))*middleScale
This is a little clunky as it goes from zooming out to zooming in rather abruptly, so you could ease it with a sine type easing:
var s = d3.interpolate(0.0000001,Math.PI);
// get the appropriate scale:
scale = (1-Math.abs(Math.sin(s(t))))*startEndScale + Math.abs(Math.sin(s(t)))*middleScale
I've applied this to your plunker here.
For a simple and minimal example using the example that I suggested and your two points and path (and using your plunkr as a base), stripping out the animated line and icons, I would probably put together something like (plunker, snippet below best viewed on full screen):
var width = 600,
height = 600;
var points = [{
type: "Point",
coordinates: [-74.2582011, 40.7058316],
location: "Your Location",
icon: "\uF015"
}, {
type: "Point",
coordinates: [34.8887969, 32.4406351],
location: "Caribe Royale Orlando",
icon: "\uF236"
}];
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var point = points[0].coordinates;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(width / 2)
.clipAngle(90)
.precision(0.6)
.rotate([-point[0], -point[1]]);
var path = d3.geoPath()
.projection(projection)
.context(c);
var colorLand = "#4d4f51",
colorGlobe = "#2e3133",
textColors = "#fff";
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function (error, world) {
if (error) throw error;
var sphere = { type: "Sphere" };
var land = topojson.feature(world, world.objects.land);
var i = 0;
var scaleMiddle = width/2;
var scaleStartEnd = width * 2;
loop();
function loop() {
d3.transition()
.tween("rotate",function() {
var flightPath = {
type: 'Feature',
geometry: {
type: "LineString",
coordinates: [points[i++%2].coordinates, points[i%2].coordinates]
}
};
// next point:
var p = points[i%2].coordinates;
// current rotation:
var currentRotation = projection.rotate();
// next rotation:
var nextRotation = projection.rotate([-p[0],-p[1]]).rotate();
// Interpolaters:
var r = d3.geoInterpolate(currentRotation,nextRotation);
var s = d3.interpolate(0.0000001,Math.PI);
return function(t) {
// apply interpolated values
projection.rotate(r(t))
.scale( (1-Math.abs(Math.sin(s(t))))*scaleStartEnd + Math.abs(Math.sin(s(t)))*scaleMiddle ) ;
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
c.beginPath(), path(flightPath), c.globalAlpha = 0.5, c.shadowColor = "#fff", c.shadowBlur = 5, c.lineWidth = 0.5, c.strokeStyle = "#fff", c.stroke(), c.shadowBlur = 0, c.globalAlpha = 1;
}
})
.duration(3000)
.on("end", function() { loop(); })
}
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
I'm using Chart.js 2.6 and I have implemented the horizontalLine plugin to show an average value on my bar charts. It works fine, however when the tooltip displays in the spot where it intersects with the line, it is partially covered by the horizontal line itself. I'm trying to figure out how to make the tooltip draw ABOVE the horizontal line.
I understand the tooltip is part of the canvas element, and therefore does not have a z-index property. How can I accomplish this?
Here is what I'm using for my horizontal line plugin.
var horizonalLinePlugin = {
afterDraw: function(chartInstance) {
var yScale = chartInstance.scales["y-axis-0"];
var canvas = chartInstance.chart;
var ctx = canvas.ctx;
var index, line, style, width;
if (chartInstance.options.horizontalLine) {
for (index = 0; index < chartInstance.options.horizontalLine.length; index++) {
line = chartInstance.options.horizontalLine[index];
style = (line.style) ? line.style : "rgba(169,169,169, .6)";
yValue = (line.y) ? yScale.getPixelForValue(line.y) : 0 ;
ctx.lineWidth = (line.width) ? line.width : 3;
if (yValue) {
ctx.beginPath();
ctx.moveTo(chartInstance.chartArea.left, yValue);
ctx.lineTo(canvas.width, yValue);
ctx.strokeStyle = style;
ctx.stroke();
}
if (line.text) {
ctx.fillStyle = style;
ctx.fillText(line.text, 0, yValue + ctx.lineWidth);
}
}
return;
}
}
};
Chart.pluginService.register(horizonalLinePlugin);
... and then I add it to the bar chart options using the following
options: {
...standard option stuff...
"horizontalLine": [{
"y": averageValue,
"style" : colorOfTheLine
}]
}
Which generates a chart that looks like the one below.
..however when you hover on a segment of the chart to display the tooltip, and the tooltip is in the path of the horizontal line, it causes the issue seen below.
Attach your plugin to the afterDatasetDraw hook, instead of afterDraw . This will make the horizontal line to be drawn before the tooltip.
var horizonalLinePlugin = {
afterDatasetDraw: function(chartInstance) {
var yScale = chartInstance.scales["y-axis-0"];
var canvas = chartInstance.chart;
var ctx = canvas.ctx;
var index, line, style, width;
if (chartInstance.options.horizontalLine) {
for (index = 0; index < chartInstance.options.horizontalLine.length; index++) {
line = chartInstance.options.horizontalLine[index];
style = (line.style) ? line.style : "rgba(169,169,169, .6)";
yValue = (line.y) ? yScale.getPixelForValue(line.y) : 0;
ctx.lineWidth = (line.width) ? line.width : 3;
if (yValue) {
ctx.beginPath();
ctx.moveTo(chartInstance.chartArea.left, yValue);
ctx.lineTo(canvas.width, yValue);
ctx.strokeStyle = style;
ctx.stroke();
}
if (line.text) {
ctx.fillStyle = style;
ctx.fillText(line.text, 0, yValue + ctx.lineWidth);
}
}
return;
}
}
};
Chart.pluginService.register(horizonalLinePlugin);
new Chart(canvas, {
type: 'bar',
data: {
labels: ["January", "February"],
datasets: [{
label: "Dataset 1",
data: [80, 50]
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
},
horizontalLine: [{
y: 50,
style: 'red'
}]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.min.js"></script>
<canvas id="canvas"></canvas>
I'm using Chart.js to draw a series of charts on my site and I've written a helper method to draw different charts easily:
drawChart(ctxElement, ctxType, ctxDataLabels, ctxDataSets, midLabel) {
var ctx = ctxElement;
var data = {
labels: ctxDataLabels,
datasets: ctxDataSets
};
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = midLabel,
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(text, textX, textY);
ctx.save();
}
});
var chart = new Chart(ctx, {
type: ctxType,
data: data,
options: {
legend: {
display: false
},
responsive: true
}
});
}
The last parameter for the drawChart() method contains the label that should be added in the middle of the chart. The Chart.pluginService.register part is the code that draws the label. The problem is that when I execute the drawChart method multiple times (in my case three times) and supply the label of each chart in the method executions, all three labels are shown on top of each other on each chart. I need to display each label in the corresponding chart. All other parameters are handled correctly, except for the label.
How do I achieve that?
A simple workaround is to add another parameter to your function to differentiate your charts from each other.
I chose to use the id of a chart for this, so that you are sure you won't affect another one.
You first need to edit a little bit your function :
// !!
// Don't forget to change the prototype
// !!
function drawChart(ctxElement, ctxType, ctxDataLabels, ctxDataSets, midLabel, id) {
var ctx = ctxElement;
var data = {
labels: ctxDataLabels,
datasets: ctxDataSets
};
Chart.pluginService.register({
afterDraw: function(chart) {
// Makes sure you work on the wanted chart
if (chart.id != id) return;
// From here, it is the same as what you had
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
// ...
}
});
// ...
}
From now, when you call your function, don't forget about the id :
// ids need to be 0, 1, 2, 3 ...
drawChart(ctxElement, ctxType, ctxDataLabels, ctxDataSets, "Canvas 1", 0);
drawChart(ctxElement, ctxType, ctxDataLabels, ctxDataSets, "Canvas 2", 1);
drawChart(ctxElement, ctxType, ctxDataLabels, ctxDataSets, "Canvas 3", 2);
You can see a fully working example on this fiddle (with 3 charts), and here is a preview :
I am using Chart.js for drawing pie chart in my php page.I found tooltip as showing each slice values.
But I wish to display those values like below image.
I do not know how to do this with chart.js.
Please help me.
My Javascript code:
function drawPie(canvasId,data,legend){
var ctx = $("#pie-canvas-" + canvasId).get(0).getContext("2d");
var piedata = [];
$.each(data,function(i,val){
piedata.push({value:val.count,color:val.color,label:val.status});
});
var options =
{
tooltipTemplate: "<%= Math.round(circumference / 6.283 * 100) %>%",
}
var pie = new Chart(ctx).Pie(piedata,options);
if(legend)document.getElementById("legend").innerHTML = pie.generateLegend();
}
php code:
printf('<table><tr>');
echo '<td style="text-align: right;"><canvas id="pie-canvas-'
. $canvasId
. '" width="256" height="256" ></canvas></td><td style="text-align: left;width:360px;height:auto" id="legend" class="chart-legend"></td></tr></table>';
echo '<script type="text/javascript">drawPie('
. $canvasId
. ', '
. $data3
.', '
. $legend
. ');</script>';
For Chart.js 2.0 and up, the Chart object data has changed. For those who are using Chart.js 2.0+, below is an example of using HTML5 Canvas fillText() method to display data value inside of the pie slice. The code works for doughnut chart, too, with the only difference being type: 'pie' versus type: 'doughnut' when creating the chart.
Script:
Javascript
var data = {
datasets: [{
data: [
11,
16,
7,
3,
14
],
backgroundColor: [
"#FF6384",
"#4BC0C0",
"#FFCE56",
"#E7E9ED",
"#36A2EB"
],
label: 'My dataset' // for legend
}],
labels: [
"Red",
"Green",
"Yellow",
"Grey",
"Blue"
]
};
var pieOptions = {
events: false,
animation: {
duration: 500,
easing: "easeOutQuart",
onComplete: function () {
var ctx = this.chart.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
this.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
total = dataset._meta[Object.keys(dataset._meta)[0]].total,
mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius)/2,
start_angle = model.startAngle,
end_angle = model.endAngle,
mid_angle = start_angle + (end_angle - start_angle)/2;
var x = mid_radius * Math.cos(mid_angle);
var y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
if (i == 3){ // Darker text color for lighter background
ctx.fillStyle = '#444';
}
var percent = String(Math.round(dataset.data[i]/total*100)) + "%";
//Don't Display If Legend is hide or value is 0
if(dataset.data[i] != 0 && dataset._meta[0].data[i].hidden != true) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
}
});
}
}
};
var pieChartCanvas = $("#pieChart");
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pieOptions
});
HTML
<canvas id="pieChart" width=200 height=200></canvas>
jsFiddle
I found an excellent Chart.js plugin that does exactly what you want:
https://github.com/emn178/Chart.PieceLabel.js
From what I know I don't believe that Chart.JS has any functionality to help for drawing text on a pie chart. But that doesn't mean you can't do it yourself in native JavaScript. I will give you an example on how to do that, below is the code for drawing text for each segment in the pie chart:
function drawSegmentValues()
{
for(var i=0; i<myPieChart.segments.length; i++)
{
// Default properties for text (size is scaled)
ctx.fillStyle="white";
var textSize = canvas.width/10;
ctx.font= textSize+"px Verdana";
// Get needed variables
var value = myPieChart.segments[i].value;
var startAngle = myPieChart.segments[i].startAngle;
var endAngle = myPieChart.segments[i].endAngle;
var middleAngle = startAngle + ((endAngle - startAngle)/2);
// Compute text location
var posX = (radius/2) * Math.cos(middleAngle) + midX;
var posY = (radius/2) * Math.sin(middleAngle) + midY;
// Text offside to middle of text
var w_offset = ctx.measureText(value).width/2;
var h_offset = textSize/4;
ctx.fillText(value, posX - w_offset, posY + h_offset);
}
}
A Pie Chart has an array of segments stored in PieChart.segments, we can look at the startAngle and endAngle of these segments to determine the angle in between where the text would be middleAngle. Then we would move in that direction by Radius/2 to be in the middle point of the chart in radians.
In the example above some other clean-up operations are done, due to the position of text drawn in fillText() being the top right corner, we need to get some offset values to correct for that. And finally textSize is determined based on the size of the chart itself, the larger the chart the larger the text.
Fiddle Example
With some slight modification you can change the discrete number values for a dataset into the percentile numbers in a graph. To do this get the total value of the items in your dataset, call this totalValue. Then on each segment you can find the percent by doing:
Math.round(myPieChart.segments[i].value/totalValue*100)+'%';
The section here myPieChart.segments[i].value/totalValue is what calculates the percent that the segment takes up in the chart. For example if the current segment had a value of 50 and the totalValue was 200. Then the percent that the segment took up would be: 50/200 => 0.25. The rest is to make this look nice. 0.25*100 => 25, then we add a % at the end. For whole number percent tiles I rounded to the nearest integer, although can can lead to problems with accuracy. If we need more accuracy you can use .toFixed(n) to save decimal places. For example we could do this to save a single decimal place when needed:
var value = myPieChart.segments[i].value/totalValue*100;
if(Math.round(value) !== value)
value = (myPieChart.segments[i].value/totalValue*100).toFixed(1);
value = value + '%';
Fiddle Example of percentile with decimals
Fiddle Example of percentile with integers
You can make use of PieceLabel plugin for Chart.js.
{ pieceLabel: { mode: 'percentage', precision: 2 } }
Demo |
Documentation
The plugin appears to have a new location (and name): Demo Docs.
#Hung Tran's answer works perfect. As an improvement, I would suggest not showing values that are 0. Say you have 5 elements and 2 of them are 0 and rest of them have values, the solution above will show 0 and 0%. It is better to filter that out with a not equal to 0 check!
var val = dataset.data[i];
var percent = String(Math.round(val/total*100)) + "%";
if(val != 0) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
Updated code below:
var data = {
datasets: [{
data: [
11,
16,
7,
3,
14
],
backgroundColor: [
"#FF6384",
"#4BC0C0",
"#FFCE56",
"#E7E9ED",
"#36A2EB"
],
label: 'My dataset' // for legend
}],
labels: [
"Red",
"Green",
"Yellow",
"Grey",
"Blue"
]
};
var pieOptions = {
events: false,
animation: {
duration: 500,
easing: "easeOutQuart",
onComplete: function () {
var ctx = this.chart.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
this.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
total = dataset._meta[Object.keys(dataset._meta)[0]].total,
mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius)/2,
start_angle = model.startAngle,
end_angle = model.endAngle,
mid_angle = start_angle + (end_angle - start_angle)/2;
var x = mid_radius * Math.cos(mid_angle);
var y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
if (i == 3){ // Darker text color for lighter background
ctx.fillStyle = '#444';
}
var val = dataset.data[i];
var percent = String(Math.round(val/total*100)) + "%";
if(val != 0) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
}
});
}
}
};
var pieChartCanvas = $("#pieChart");
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pieOptions
});
For Chart.js 3
I've modified "Hung Tran"'s Code.
animation: {
onProgress: function() {
// console.error('this', this);
const ctx = this.ctx;
// ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
let dataSum = 0;
if(this._sortedMetasets.length > 0 && this._sortedMetasets[0].data.length > 0) {
const dataset = this._sortedMetasets[0].data[0].$context.dataset;
dataSum = dataset.data.reduce((p, c) => p + c, 0);
}
if(dataSum <= 0) return;
this._sortedMetasets.forEach(meta => {
meta.data.forEach(metaData => {
const dataset = metaData.$context.dataset;
const datasetIndex = metaData.$context.dataIndex;
const value = dataset.data[datasetIndex];
const percent = (Math.round(value / dataSum * 1000) / 10) + '%';
const mid_radius = metaData.innerRadius + (metaData.outerRadius - metaData.innerRadius) * 0.7;
const start_angle = metaData.startAngle;
const end_angle = metaData.endAngle;
if(start_angle === end_angle) return; // hidden
const mid_angle = start_angle + (end_angle - start_angle) / 2;
const x = mid_radius * Math.cos(mid_angle);
const y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
ctx.fillText(percent, metaData.x + x, metaData.y + y + 15);
});
});
}
}
Give the option for pie chart
onAnimationProgress: drawSegmentValues
like:
var pOptions = {
onAnimationProgress: drawSegmentValues
};
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pOptions
});
Easiest way to do this with Chartjs. Just add below line in options:
pieceLabel: {
fontColor: '#000'
}
Best of luck
I need to change the fill color (internal area) in a Line Chart.js when the point is negative.
The code is simple and basic:
$(document).ready(function(){
var ctx = $("#myChart").get(0).getContext("2d");
var data = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
//fillColor : "rgba(60,91,87,1)",
// String - the color to fill the area under the line with if fill is true
backgroundColor: "rgba(75,192,192,0.4)",
strokeColor : "rgba(60,91,87,1)",
pointColor : "rgba(60,91,87,1)",
pointStrokeColor : "#58606d",
// The actual data
data: [65, 59, 80, -81, 56, 55, -40],
// String - If specified, binds the dataset to a certain y-axis. If not specified, the first y-axis is used. First id is y-axis-0
yAxisID: "y-axis-0",
}
]
};
var options = {
scales: {
yAxes: [{
display: true,
ticks: {
suggestedMin: 0, // minimum will be 0, unless there is a lower value.
// OR //
beginAtZero: true // minimum value will be 0.
}
}]
}
};
var myLineChart = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
// myLineChart.data.datasets[0].metaDataset._points[3]._model.backgroundColor = "red";
// if (myLineChart.datasets[0].points[4].value < 0) {
// myLineChart.datasets[0].points[4].fillColor = "red";
// myLineChart.update();
// }
})
I'm trying to get this result:
You can extend the line chart to do this.
Preview
Script
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[0].data);
var max = Math.max.apply(null, this.chart.data.datasets[0].data);
var yScale = this.getScaleForId(this.getDataset().yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(75,192,192,0.4)');
gradient.addColorStop(ratio, 'rgba(75,192,192,0.4)');
gradient.addColorStop(ratio, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
this.chart.data.datasets[0].backgroundColor = gradient;
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
and then
...
var myLineChart = new Chart(ctx, {
type: 'NegativeTransparentLine',
data: {
...
Fiddle - http://jsfiddle.net/g2r2q5Lu/
To get #potatopeelings code above to work with chart.js 2.5.x you need to add yAxisID : 'y-axis-0' into your datasets, as below.
var myLineChart = new Chart(ctx, {
type: 'NegativeTransparentLine',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
yAxisID : 'y-axis-0',
....
i update the method to work with multiple datasets.
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
for(let i=0; i< this.chart.data.datasets.length; i++) {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[i].data);
var max = Math.max.apply(null, this.chart.data.datasets[i].data);
var yScale = this.getScaleForId(this.chart.data.datasets[i].yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(247,100,120,0.4)');
gradient.addColorStop(1, 'rgba(247,100,120,0.4)');
this.chart.data.datasets[i].backgroundColor = gradient;
}
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
Tested on chart.js 2.8.0 on Angular 8
import { Component, OnInit, ViewChild } from '#angular/core';
import { Chart, ChartDataSets, ChartOptions } from 'chart.js';
import { Color, Label } from 'ng2-charts';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public lineChartData: ChartDataSets[] = [
{ data: [89, 0, -80, 81, 56, -55, 40], label: 'Series A', yAxisID: 'y-axis-0' },
{ data: [-890, 0, 800, -810, -560, 550, -400], label: 'Series B', yAxisID: 'y-axis-0' },
];
public lineChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
public lineChartOptions: (ChartOptions & { annotation: any }) = {
responsive: true,
};
public lineChartColors: Color[] = [
{
backgroundColor: 'rgba(255,0,0,0.3)',
},
{
backgroundColor: 'rgba(0,255,0,0.3)',
},
];
public lineChartLegend = true;
public lineChartType = 'line';
public lineChartPlugins = [];
constructor() {
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
for(let i=0; i< this.chart.data.datasets.length; i++) {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[i].data);
var max = Math.max.apply(null, this.chart.data.datasets[i].data);
var yScale = this.getScaleForId(this.chart.data.datasets[i].yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
gradient.addColorStop(0, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(55,210,99,0.4)');
gradient.addColorStop(ratio, 'rgba(247,100,120,0.4)');
gradient.addColorStop(1, 'rgba(247,100,120,0.4)');
this.chart.data.datasets[i].backgroundColor = gradient;
}
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});
this.lineChartType = 'NegativeTransparentLine';
}
ngOnInit() {
}
}
<div style="display: block;">
<canvas baseChart width="400" height="400"
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[colors]="lineChartColors"
[legend]="lineChartLegend"
[chartType]="lineChartType"
[plugins]="lineChartPlugins">
</canvas>
</div>
This is derived from this post. It works for Chart.js v2.9.4 and doesn't require any external code or creating a custom chart type. Simply add this plugins object to your chart options. (note that the plugins object is separate from the options object. If you put the plugins object inside of the options object, it won't work.)
new Chart(document.querySelector(`canvas`), {
type: 'line',
data: {
labels: your_labels,
datasets: [{
data: your_data
}]
},
options: {
maintainAspectRatio: false, //allow the graph to resize to its container
scales: {
yAxes: [{
ticks: {
beginAtZero: true //make sure zero line exists on the graph
}
}]
}
}, //<-make sure plugins is outside of the options object
plugins: [{
beforeRender: function(graph) {
let gradient = graph.ctx.createLinearGradient(0, 0, 0, graph.height), //create a gradient for the background
zero_line = graph.scales[`y-axis-0`].getPixelForValue(0) / graph.height; //calculate where the zero line is plotted on the graph
gradient.addColorStop(0, `rgba(0,200,0,.2)`); //good color faded out
gradient.addColorStop(zero_line, `rgba(0,200,0,.8)`); //good color at zero line
gradient.addColorStop(zero_line, `rgba(200,0,0,.8)`); //bad color at zero line
gradient.addColorStop(1, `rgba(200,0,0,.2)`); //bad color faded out
graph.data.datasets[0]._meta[0].$filler.el._model.backgroundColor = gradient; //set the graphs background to the gradient we just made
}
}]
});
Obviously for more complex graphs you'll need to update dataset indexes and axis names, but for simple graphs, it's this simple.
#potatopeelings code will work if your dataset data format is in [1,2,3,...] form
If your data format is in [{x: 1 , y: 1},...] form, you need to change var min and var max to:
var min = this.chart.data.datasets[0].data.reduce((min, p) => p.y < min ? p.y : min, this.chart.data.datasets[0].data[0].y);
var max = this.chart.data.datasets[0].data.reduce((max, p) => p.y > max ? p.y : max, this.chart.data.datasets[0].data[0].y);
Tested on ChartJS 2.7.3
#potatopeelings The gradient messed up if all data was negative or positive, here's how I fixed it. (Changed the gradient colours but the fix is still there)
Chart.defaults.NegativeTransparentLine = Chart.helpers.clone(Chart.defaults.line);
Chart.controllers.NegativeTransparentLine = Chart.controllers.line.extend({
update: function () {
// get the min and max values
var min = Math.min.apply(null, this.chart.data.datasets[0].data);
var max = Math.max.apply(null, this.chart.data.datasets[0].data);
var yScale = this.getScaleForId(this.getDataset().yAxisID);
// figure out the pixels for these and the value 0
var top = yScale.getPixelForValue(max);
var zero = yScale.getPixelForValue(0);
var bottom = yScale.getPixelForValue(min);
// build a gradient that switches color at the 0 point
var ctx = this.chart.chart.ctx;
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
var ratio = Math.min((zero - top) / (bottom - top), 1);
if(ratio < 0){
ratio = 0;
gradient.addColorStop(1, 'rgba(0,255,0,1)');
}else if(ratio == 1){
gradient.addColorStop(1, 'rgba(255,0,0,1)');
}else{
gradient.addColorStop(0, 'rgba(255,0,0,1)');
gradient.addColorStop(ratio, 'rgba(255,0,0,1)');
gradient.addColorStop(ratio, 'rgba(0,255,0,1)');
gradient.addColorStop(1, 'rgba(0,255,0,1)');
}
console.log(ratio)
this.chart.data.datasets[0].backgroundColor = gradient;
return Chart.controllers.line.prototype.update.apply(this, arguments);
}
});