I have a custom doughnut ChartJS (with rounded edges), but cannot find the right way to:
make it work under version 2.6.0 (it works just fine under ChartJS 2.0.2, but not under 2.6.0)
add a static red circle under the green with same radius, but with half the lineWidth (like an axis on which the green circle plots) - like this
Here is the Plunker
Chart.defaults.RoundedDoughnut = Chart.helpers.clone(Chart.defaults.doughnut);
Chart.controllers.RoundedDoughnut = Chart.controllers.doughnut.extend({
draw: function (ease) {
var ctx = this.chart.chart.ctx;
var easingDecimal = ease || 1;
Chart.helpers.each(this.getDataset().metaData, function (arc, index) {
arc.transition(easingDecimal).draw();
var vm = arc._view;
var radius = (vm.outerRadius + vm.innerRadius) / 2;
var thickness = (vm.outerRadius - vm.innerRadius) / 2;
var angle = Math.PI - vm.endAngle - Math.PI / 2;
ctx.save();
ctx.fillStyle = vm.backgroundColor;
ctx.translate(vm.x, vm.y);
ctx.beginPath();
ctx.arc(radius * Math.sin(angle), radius * Math.cos(angle), thickness, 0, 2 * Math.PI);
ctx.arc(radius * Math.sin(Math.PI), radius * Math.cos(Math.PI), thickness, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.restore();
});
},
});
var deliveredData = {
labels: [
"Value"
],
datasets: [
{
data: [85, 15],
backgroundColor: [
"#3ec556",
"rgba(0,0,0,0)"
],
borderWidth: [
0, 0
]
}]
};
var deliveredOpt = {
cutoutPercentage: 88,
animation: {
animationRotate: true,
duration: 3000
},
legend: {
display: false
},
tooltips: {
enabled: false
}
};
var chart = new Chart($('#myChart'), {
type: 'RoundedDoughnut',
data: deliveredData,
options: deliveredOpt
});
For question #1 the issue is with the this.getDataset().metaData inside the draw function. It is returning undefined and so the function under Chart.helpers.each doesn't execute
Try this.getMeta().data instead.
EDIT:
For question #2, you can refer to this other Stack Overflow question:
Charts.js: thin donut chart background
Related
I have this map code for MapBox here: How to get click handler on animated canvas dot on MapBox map?
import { useEffect } from 'react'
import * as mapboxgl from 'mapbox-gl'
import locations from 'seeds/sample-location-list.json'
import styles from './Map.module.css'
export default function Map({ scrollable, id, style, onClick }) {
useEffect(() => {
const map = new mapboxgl.Map({
accessToken: '<token>',
container: id ?? 'map',
center: [16.572149246748594, 19.06111670348622],
zoom: 1.5,
style: style ?? 'mapbox://styles/<co>/<style>',
attributionControl: false,
})
// .addControl(new mapboxgl.AttributionControl({
// compact: true
// }), 'top-left')
var size = 120;
// This implements `StyleImageInterface`
// to draw a pulsing dot icon on the map.
var pulsingDot = {
width: size,
height: size,
data: new Uint8ClampedArray(size * size * 4),
context: null,
// When the layer is added to the map,
// get the rendering context for the map canvas.
onAdd: function () {
var canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
},
// Call once before every frame where the icon will be used.
render: function () {
var duration = 1000;
var t = (performance.now() % duration) / duration;
var radius = (size / 2) * 0.3;
var outerRadius = (size / 2) * 0.7 * t + radius;
var context = this.context;
// Draw the outer circle.
context.clearRect(0, 0, this.width, this.height);
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
outerRadius,
0,
Math.PI * 2
);
context.fillStyle = 'rgba(253, 253, 150,' + (1 - t) + ')';
context.fill();
// Draw the inner circle.
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
radius,
0,
Math.PI * 2
);
context.fillStyle = 'rgba(253, 253, 150, 1)';
context.strokeStyle = 'rgb(119, 221, 119)';
context.lineWidth = 2 + 4 * (1 - t);
context.fill();
context.stroke();
// Update this image's data with data from the canvas.
this.data = context.getImageData(
0,
0,
this.width,
this.height
).data;
// Continuously repaint the map, resulting
// in the smooth animation of the dot.
map.triggerRepaint();
// Return `true` to let the map know that the image was updated.
return true;
}
};
if (!scrollable) map.scrollZoom.disable()
map.on('load', function () {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });
locations.forEach(location => {
map.addSource(location.id.seed, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [location.lng, location.lat] // icon position [lng, lat]
},
properties: {},
}
]
}
})
map.addLayer({
'id': location.id.seed,
'type': 'symbol',
'source': location.id.seed,
'layout': {
'icon-image': 'pulsing-dot'
}
});
})
map.on('mousemove', 'earthquakes-viz', (e) => {
// console.log
})
});
if (onClick) map.on('click', onClick)
return () => map.remove()
}, [])
return (
<div id={id ?? "map"} className={styles.map}></div>
)
}
It animates the pulsing of around 20 dots on a map canvas layer. I checked it after some time and it was at 80MB memory usage (my whole React app, of which the map is a small part part), yet my CPU was screaming, fans going off like crazy. How do I fix it so it's not so processor intensive?
Chrome says:
Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
on .getImageData
// update this image's data with data from the canvas
this.data = context.getImageData(
The code is still in the Mapbox example but on first sight looks outdated and not very performance optimised. Did you found a solution to this?
Heres the issue:
https://github.com/mapbox/mapbox-gl-js/pull/12397
I am currently in the process of upgrading a chart.js line chart from v1 to v2 and came across a hurdle.
The chart data includes customOptions of an array of booleans under the featured property.
var chartData = {
labels: [
"14th Jan",
"15th Jan",
"16th Jan",
"17th Jan",
"18th Jan",
"19th Jan"
],
datasets: [{
label: "Listing Popularity",
data: [
1199,
575,
407,
313,
181,
268
],
}],
customOptions: {
featured: [
false,
false,
false,
false,
false,
true
]
}
}
var ctx = document.getElementById('chartjs-chart').getContext('2d');
var myChart = new Chart(ctx, {
type: 'PPLine',
data: chartData,
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
});
I have created an extended version of a line chart that loops over the featured array in the charts customOptions and adds a path if the featured boolean returns true
Chart.controllers.PPLine = Chart.controllers.line.extend({
draw: function () {
var that = this;
var helpers = Chart.helpers;
var xScale = that._xScale;
var ctx = that.chart.ctx;
// Draw chart
Chart.controllers.line.prototype.draw.call(that, arguments);
helpers.each(that.chart.data.customOptions.featured, function (featured, index) {
var meta = that.chart.getDatasetMeta(0);
var linePos = meta.data[index]._model.x,
endLinePos = that.chart.options.elements.point.borderWidth + (meta.data[index + 1] ? meta.data[index + 1]._model.x : linePos);
// Draw featured sections
if (featured) {
var centerX = meta.data[index]._model.x;
var centerY = meta.data[index]._model.y;
var radius = 30;
// Draw boost circle
ctx.save();
ctx.beginPath();
ctx.fillStyle = that.chart.data.customOptions.bumpedColour;
ctx.lineWidth = 10;
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
ctx.fill();
ctx.restore();
// Add text
ctx.save();
ctx.beginPath();
ctx.fillStyle = "#fff";
ctx.font = "10px " + that.chart.config.options.defaultFontFamily;
ctx.fillText("Boost", centerX - (ctx.measureText('Boost').width / 2), centerY + 3);
ctx.fill();
ctx.restore();
}
});
}
});
The issue comes when the last item in the featured array is true and the path gets added to the point, visually, the path gets cut off, so I need to add some padding to the graph to prevent this.
In V1, I was able to do the following inside the extended line chart...
var that = this;
that.scale.xScalePaddingRight = 20;
However, scale is not a property within the v2 object. There is a _xScale property with a paddingRight property, but doing the following does not seem to add the desired padding, so that the path does not get cut off.
var that = this;
that._xScale.paddingRight = 20;
Here is a CodePen with the issue.
I don't want to add padding right in every instance, just when the last point is featured and the path gets drawn.
Any help would be greatly appreciated.
Add it in options:
layout: {
padding: {
right: 20
}
}
I made a line chart using Chart.js version 2.1.3.
var canvas = $('#gold_chart').get(0);
var ctx = canvas.getContext('2d');
var fillPatternGold = ctx.createLinearGradient(0, 0, 0, canvas.height);
fillPatternGold.addColorStop(0, '#fdca55');
fillPatternGold.addColorStop(1, '#ffffff');
var goldChart = new Chart(ctx, {
type: 'line',
animation: false,
data: {
labels: dates,
datasets: [{
label: '',
data: prices,
pointRadius: 0,
borderWidth: 1,
borderColor: '#a97f35',
backgroundColor: fillPatternGold
}]
},
title: {
position: 'bottom',
text: '\u7F8E\u5143 / \u76CE\u53F8'
},
options: {
legend: {
display: false
},
tooltips: {
callback: function(tooltipItem) {
return tooltipItem.yLabel;
}
},
scales: {
xAxes: [{
ticks: {
maxTicksLimit: 8
}
}]
}
}
});
The output is as follow:
As you can see, I limited the maximum count of ticks to 8 via maxTicksLimit. However, the distribution is not even. How can I make the ticks distribute evenly?
p.s. there are always 289 records in the dataset, and the data is recorded every 5 minutes. Sample values of prices variable are:
[
{"14:10", 1280.3},
{"14:15", 1280.25},
{"14:20", 1282.85}
]
I tried different values of maxTicksLimit, and the results are still not distributed evenly.
Chart.js uses an integral skipRatio (to figure out how many labels to skip). With Chart.js v2.1.x, you can write your own plugin to use a fractional skipRatio
Preview
Script
Chart.pluginService.register({
afterUpdate: function (chart) {
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
// store the original maxTicksLimit
xScale.options.ticks._maxTicksLimit = xScale.options.ticks.maxTicksLimit;
// let chart.js draw the first and last label
xScale.options.ticks.maxTicksLimit = (xScale.ticks.length % xScale.options.ticks._maxTicksLimit === 0) ? 1 : 2;
var originalXScaleDraw = xScale.draw
xScale.draw = function () {
originalXScaleDraw.apply(this, arguments);
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
var helpers = Chart.helpers;
var tickFontColor = helpers.getValueOrDefault(xScale.options.ticks.fontColor, Chart.defaults.global.defaultFontColor);
var tickFontSize = helpers.getValueOrDefault(xScale.options.ticks.fontSize, Chart.defaults.global.defaultFontSize);
var tickFontStyle = helpers.getValueOrDefault(xScale.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle);
var tickFontFamily = helpers.getValueOrDefault(xScale.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily);
var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily);
var tl = xScale.options.gridLines.tickMarkLength;
var isRotated = xScale.labelRotation !== 0;
var yTickStart = xScale.top;
var yTickEnd = xScale.top + tl;
var chartArea = chart.chartArea;
// use the saved ticks
var maxTicks = xScale.options.ticks._maxTicksLimit - 1;
var ticksPerVisibleTick = xScale.ticks.length / maxTicks;
// chart.js uses an integral skipRatio - this causes all the fractional ticks to be accounted for between the last 2 labels
// we use a fractional skipRatio
var ticksCovered = 0;
helpers.each(xScale.ticks, function (label, index) {
if (index < ticksCovered)
return;
ticksCovered += ticksPerVisibleTick;
// chart.js has already drawn these 2
if (index === 0 || index === (xScale.ticks.length - 1))
return;
// copy of chart.js code
var xLineValue = this.getPixelForTick(index);
var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines);
if (this.options.gridLines.display) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xLineValue, yTickStart);
this.ctx.lineTo(xLineValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xLineValue, chartArea.top);
this.ctx.lineTo(xLineValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}
if (this.options.ticks.display) {
this.ctx.save();
this.ctx.translate(xLabelValue + this.options.ticks.labelOffset, (isRotated) ? this.top + 12 : this.options.position === "top" ? this.bottom - tl : this.top + tl);
this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1);
this.ctx.font = tickLabelFont;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "top";
this.ctx.fillText(label, 0, 0);
this.ctx.restore();
}
}, xScale);
}
};
}
},
});
Fiddle - http://jsfiddle.net/bh63pe1v/
A simpler solution until this is permanently fixed by the Chart JS contributors is to include a decimal in maxTicksLimit.
For example:
maxTicksLimit: 8,
produces a huge gap at the end.
maxTicksLimit: 8.1,
Does not produce a huge gap at the end.
Depending on what you want to set your maxTicksLimit to, you need to play around with different decimals to see which one produces the best result.
Just do this:
yAxes: [{
ticks: {
stepSize: Math.round((Math.max.apply(Math, myListOfyValues) / 10)/5)*5,
beginAtZero: true,
precision: 0
}
}]
10 = the number of ticks
5 = rounds tick values to the nearest 5 - all y values will be incremented evenly
Similar will work for xAxes too.
Chart.js (http://www.chartjs.org/docs/) can fill the color below line charts using the "fillColor" attribute (filling the region between the line chart itself and the x-axis).
What I'd like to know is whether Chart.js can be configured to create shaded regions such as the one shown below:
http://peltiertech.com/Excel/pix5/HorizBands09.png
Thank you.
Shaded Regions for Line Charts
You can extend the chart to do this.
Preview
or
Script
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
Chart.types.Line.prototype.initialize.apply(this, arguments);
var ranges = [
{
start: 100,
end: 75,
color: 'rgba(250,0,0,0.5)'
},
{
start: 75,
end: 50,
color: 'rgba(0,250,0,0.5)'
},
{
start: 50,
end: 25,
color: 'rgba(0,0,250,0.5)'
},
{
start: 25,
end: 0,
color: 'rgba(250,250,0,0.5)'
}
];
var scale = this.scale;
var rangesStart = scale.calculateY(ranges[0].start);
var rangesEnd = scale.calculateY(ranges[ranges.length - 1].end);
var gradient = this.chart.ctx.createLinearGradient(0, rangesStart, 0, rangesEnd);
ranges.forEach(function (range) {
gradient.addColorStop((scale.calculateY(range.start) - rangesStart) / (rangesEnd - rangesStart), range.color);
gradient.addColorStop((scale.calculateY(range.end) - rangesStart) / (rangesEnd - rangesStart), range.color);
})
this.datasets[0].fillColor = gradient;
}
});
and then
...
new Chart(ctx).LineAlt(data);
If you want to shade the whole background use
var originalDraw = scale.draw;
scale.draw = function() {
originalDraw.apply(this, arguments);
ctx.save();
ctx.fillStyle = gradient;
ctx.fillRect(scale.calculateX(0), scale.startPoint, scale.width, scale.endPoint - scale.startPoint);
ctx.restore();
}
instead of this.datasets[0].fillColor = gradient;
Fiddle (below line) - http://jsfiddle.net/61vg048r/
Fiddle (whole background) - http://jsfiddle.net/u4Lk7xns/
I currently have a pie chart successfully displaying on my reporting dashboard. However, a business request was made to retain a chart outline and display the 'noData' message in the center when all series are empty.
The business did not like the look of a floating label on the page when the chart was empty. Using an existing chart object, would it be possible to essentially fabricate a chart outline and display a noData message?
It is possible to add custom shape, e.g. circle, that will be showing in case there is no data. Using chart's events load and redraw you can update shape to fit in chart and be placed in center or remove when data is added to chart.
API reference for renderer.circle: http://api.highcharts.com/highcharts#Renderer.circle
Example: http://jsfiddle.net/v8n1159o/1/
chart: {
events: {
load: function () {
var chart = this;
if (!chart.hasData()) {
var r = Math.min(chart.plotWidth / 2, chart.plotHeight / 2),
y = chart.plotHeight / 2 + chart.plotTop,
x = chart.plotWidth / 2 + chart.plotLeft;
chart.pieOutline = chart.renderer.circle(x, y, r).attr({
fill: '#ddd',
stroke: 'black',
'stroke-width': 1
}).add();
}
},
redraw: function () {
var chart = this;
if (chart.pieOutline && chart.pieOutline.element) {
if (chart.hasData()) {
chart.pieOutline.destroy();
} else {
var r = Math.min(chart.plotWidth / 2, chart.plotHeight / 2),
y = chart.plotHeight / 2 + chart.plotTop,
x = chart.plotWidth / 2 + chart.plotLeft;
chart.pieOutline.attr({
cx: x,
cy: y,
r: r
});
}
} else if(!chart.hasData()) {
var r = Math.min(chart.plotWidth / 2, chart.plotHeight / 2),
y = chart.plotHeight / 2 + chart.plotTop,
x = chart.plotWidth / 2 + chart.plotLeft;
chart.pieOutline = chart.renderer.circle(x, y, r).attr({
fill: '#ddd',
stroke: 'black',
'stroke-width': 1
}).add();
}
}
},
...