Related
I am new to Lottie and am wondering how to play 2 animations side by side at the same time in the same canvas element on the Web.
I have followed this example[1] and then the advice given here[2] with regards to transforming the canvas on every frame to position either animation respectively.
What I would like to achieve is this: 2 red balls bouncing side by side on the same canvas. One playing at x = 0 and one playing at x = 100 on the same canvas.
Here is my approach in a CodePen[3]. I extracted the JS for visibility.
const testjson = {...animation...};
const cvs = document.getElementById("canvas");
const ctx = cvs.getContext("2d");
// Drawing sth on the context to see whether sharing works
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 40, 40);
function renderAnimation(canvasContext, translation) {
const animData = {
renderer: "canvas",
loop: true,
rendererSettings: {
context: canvasContext,
clearCanvas: true,
},
animationData: testjson,
};
const anim = bodymovin.loadAnimation(animData);
// Transform the canvas for the respective animation on enter-frame
anim.addEventListener("enterFrame", () => {
ctx.translate(translation.x, translation.y);
});
// If efective, causes onion-effect
// anim.setSubframe(false);
}
renderAnimation(ctx, { x: 0, y: 0 });
renderAnimation(ctx, { x: 100, y: 0 });
Alas, the way I implemented it does not seem to work.
Does anybody know what I am missing?
Thank you!
[1]: https://codepen.io/RenanSgorlom/pen/orgxyJ
[2]: https://github.com/airbnb/lottie-web/issues/1671
[3]: https://codepen.io/user1207504/pen/MWVYvxd
You don't need that much code to have some balls bouncing on the same canvas ...
Certainly no need for a library to just do that.
The balls we can draw using arc functions:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc
movement is to increase the position on x or y, in the code is the x += vx
bouncing we just change the direction, you can see it in my code vx *= -1
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
class ball {
constructor(data) {
this.data = data
}
draw() {
this.data.x += this.data.vx
this.data.y += this.data.vy
if (this.data.x > canvas.width || this.data.x < 0) this.data.vx *= -1
if (this.data.y > canvas.height || this.data.y < 0) this.data.vy *= -1
ctx.beginPath()
ctx.fillStyle = this.data.color
ctx.arc(this.data.x,this.data.y, this.data.radius, 0, 2*Math.PI);
ctx.fill()
}
}
const balls = [
new ball({x: 10, y: 10, vx: 0, vy: 1, radius: 8, color: "pink" }),
new ball({x: 90, y: 90, vx: 0, vy: -1, radius: 8, color: "red" }),
new ball({x: 5, y: 50, vx: 1, vy: 1.5, radius: 8, color: "cyan" })
]
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
balls.forEach(b => b.draw())
requestAnimationFrame(animate)
}
animate()
<canvas id="canvas" width=100 height=100></canvas>
The problem is that the "position" variable is not beeing updated in order to make the digit display next to each other and not on top of each other.
I also tryed to put it inside the prototype function thinking that if it's beeing updated right before it runs the drawImage function it will work...but it does not and I do not understand what the problem is.
<pre>
var sprite = new Image();
sprite.src = "Flappy_Bird_Sprite.png"
function drawSprite (img, x_crop, y_crop, x_width, y_height, x_pos, y_pos, x_posWidth, y_posHeight) {
this.img = img;
this.x_crop = x_crop;
this.y_crop = y_crop;
this.x_width = x_width;
this.y_height = y_height;
this.x_pos = x_pos;
this.y_pos = y_pos;
this.x_posWidth = x_posWidth;
this.y_posHeight = y_posHeight;
}
var position = 10;
drawSprite.prototype.draw = function () {
ctx.drawImage(this.img, this.x_crop, this.y_crop, this.x_width, this.y_height, this.x_pos, this.y_pos, this.x_posWidth, this.y_posHeight);
}
var arr = [
new drawSprite(sprite, 137, 306, 7, 10, position, 50, 20, 40),
new drawSprite(sprite, 139, 477, 5, 10, position, 50, 15, 40),
new drawSprite(sprite, 292, 160, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 306, 160, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 320, 160, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 334, 160, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 292, 184, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 306, 184, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 320, 184, 12, 18, position, 50, 15, 40),
new drawSprite(sprite, 334, 184, 12, 18, position, 50, 15, 40),
]
var increment = 0;
function animate () {
requestAnimationFrame(animate);
increment++;
if (increment % 10 == 0){
score++;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
var tString = score.toString(); //Makeing the score into a string so I can select each number with the substring function
var length = tString.length; //Finding out the length of the string(in our case is 2)
for(var i = 0; i < length; i++){
var substring = tString.substring(i,i+1); //Selecting each digit of that number in order to draw each of them separately
var toNumber = parseInt(substring); //Setting that "string" back into a number so I can acces that number in the array
if( length == 2 ) { //I am trying to make this work for 2 digit number(10-99) and if this works, then I will add 3 digits and so on, I know that this should work only for numbers from 10-99
if( i == 0){ // Position the first digit of that number at the position ....
position = 180;
}
if( i == 1){ // Position the second digit of that number at the position ....
position = 105;
}
}
arr[toNumber].draw();
}
}
animate();
<code>
Your calls to new drawSprite all use the same value for position, which is fixed at the point you create those objects.
You could try updating the sprite instance before drawing:
arr[toNumber].x_pos = position;
arr[toNumber].draw();
I managed to figure out how to enhance the solutions I've found here in stackoverflow for adding vertical and horizontal lines to a line chart in ChartJS v2+ to apply to a bubble chart ChartJS Bubble w/Lines (I advise skipping to my Update at the end for a better approach)
var originalBubbleDraw = Chart.controllers.bubble.prototype.draw;
Chart.helpers.extend(Chart.controllers.bubble.prototype, {
draw: function() {
originalBubbleDraw.apply(this, arguments);
var chart = this.chart;
var ctx = chart.chart.ctx;
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-0'];
var xvalue = chart.config.data.queryLimits['x'];
var yvalue = chart.config.data.queryLimits['y'];
var xcolor = chart.config.data.queryLimits['xcolor'];
var ycolor = chart.config.data.queryLimits['ycolor'];
var lineThickness = 3;
function drawLine(x1,y1,x2,y2,color) {
console.log("color="+color+", x1="+x1+", x2="+x2+", y1="+y1+", y2="+y2);
ctx.save();
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.strokeStyle = color;
ctx.lineWidth=lineThickness;
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
// draw vertical line
if (xvalue) {
x1 = xaxis.getPixelForValue(xvalue);
x2 = xaxis.getPixelForValue(xvalue);
y1 = yaxis.top;
y2 = yaxis.bottom;
drawLine(x1,y1,x2,y2,xcolor);
}
// draw horizontal line
if (yvalue) {
x1 = xaxis.left;
x2 = xaxis.right;
y1 = yaxis.getPixelForValue(yvalue);
y2 = yaxis.getPixelForValue(yvalue);
drawLine(x1,y1,x2,y2,ycolor);
}
}
});
var config = {
type: 'bubble',
data: {
queryLimits: {x: 42, y: 21, xcolor: '#00FF00', ycolor: '#0000ff'},
datasets: [
{
label: '',
data: [
{x: 20, y: 30, r: 15},
{x: 40, y: 10, r: 10},
{x: 100, y: 15, r: 10},
{x: 50, y: 22, r: 5},
{x: 80, y: 26, r: 3},
{x: 63, y: 28, r: 10},
{x: 71, y: 18, r: 12}
],
backgroundColor:"#FF6384",
hoverBackgroundColor: "#FF6384",
}]
}
};
var ctx = document.getElementById("myChart").getContext("2d");
new Chart(ctx, config);
I need to take this one step further and shade the area/background of the chart on one side of vertical line and above or below the horizontal line a different color (like a light gray or something subtle).
I am not sure if the approach is to try to change a portion of the background or to add rectangles sized and positioned to simulate the background shading.
Ideas?
Here is an example mockup of the goal:
Update
For what it is worth for those viewing this in the future, I ended up discovering a better way to approach both the drawing of lines and rectangles by using the annotation plugin for chartjs found here https://github.com/chartjs/chartjs-plugin-annotation. It is far easier to work with and doesn't have the consequence of firing the code to draw the lines and rectangles more than necessary. Also I ended up able to use another plugin https://github.com/compwright/chartjs-plugin-draggable for dragging annotations created from that first plugin. I am leaving the accepted answer as-is because it does answer the question I had from the context of how to solve the rectangle shaded area following the original extension approach, however I recommend the plugin approach vs that now after learning more about this.
As you did with your queryLimits attribute, you can do the same way an attribute that fills the portion you want.
By adding the following attribute to your chart dataset :
fillBackground: {
// In this porperty, add the string portion you want to fill
// Inputs are : "tr" for top-right
// "tl" for top-left
// "br" for bottom-right
// "bl" for bottom-left
pos: ["tr", "bl", "br"],
// A single color will be used in all the portions
// But you can also set an array of colors which must have the same length as the pos
// i.e color: ["rgba(0, 0, 0, 0.1)", "rgba(30, 30, 30, 0.3)", "rgba(60, 60, 60, 0.5)"]
color: "rgba(30, 30, 30, 0.15)"
}
Then adding the following piece of code in your draw() function :
function drawRect(x1, y1, x2, y2, color) {
ctx.save();
ctx.fillStyle = color;
ctx.fillRect(x1, y1, x2, y2, color);
ctx.restore();
}
// Checks if you have the attribute in your dataset
if (chart.config.data.fillBackground) {
// Make sure you have portions in your chart
if (!xvalue || !yvalue) return;
var pos = chart.config.data.fillBackground.pos;
var color = chart.config.data.fillBackground.color;
// For every position in your array ..
for (p in pos) {
// Based on the string code, fills the right portion
switch (pos[p]) {
case "tl":
drawRect(xaxis.left, yaxis.top, xaxis.getPixelForValue(xvalue) - lineThickness / 2 - xaxis.left, yaxis.getPixelForValue(yvalue) - lineThickness / 2 - yaxis.top, (Array.isArray(color)) ? color[p] : color);
break;
case "tr":
drawRect(xaxis.getPixelForValue(xvalue) + lineThickness / 2, yaxis.top, xaxis.right, yaxis.getPixelForValue(yvalue) - lineThickness / 2 - yaxis.top, (Array.isArray(color)) ? color[p] : color);
break;
case "bl":
drawRect(xaxis.left, yaxis.getPixelForValue(yvalue) + lineThickness / 2, xaxis.getPixelForValue(xvalue) - lineThickness / 2 - xaxis.left, yaxis.bottom - (yaxis.getPixelForValue(yvalue) + lineThickness / 2), (Array.isArray(color)) ? color[p] : color);
break
case "br":
drawRect(xaxis.getPixelForValue(xvalue) + lineThickness / 2, yaxis.getPixelForValue(yvalue) + lineThickness / 2, xaxis.right, yaxis.bottom - (yaxis.getPixelForValue(yvalue) + lineThickness / 2), (Array.isArray(color)) ? color[p] : color);
break;
}
}
}
This should solve your problem.
You can check your example working with these functions in this fiddle, and here is the result :
When i draw circles of the same color one by one using fill() for each circle. It comes that when they are overlapped, common are color is darker (saturated).
See example 1 (left) in JS Bin.
If i create long path and then use fill() for this common path, it has strange artifacts (yep, of course it's confused by complex path and don't know what i'm trying to draw)
See example 2 (right) in JS Bin.
How i can achieve that common area of circles with the same color was not saturated, it shoud be not darker than other circles with the same color. (exactly what i have in JS Bin right side, but without crazy artifacts)
If circles with different colors has common areas, color should be saturated.
Regarding your .getImageData solution ...
It's faster to use compositing rather than .getImageData to blend your colors.
Here's a function to combine overlapping semi-transparent circles without having the overlap darken.
Draw all identically colored circles on a 2nd canvas in an opaque color.
Set context.globalCompositeOperation='source-in' which causes new drawings to replace existing pixels.
Fill the 2nd canvas with the desired semi-transparent color for this set of indentically colored circles.
The result is a set of overlapping circles without the darkening effect.
function uniformColorCircles(circles){
var PI2=Math.PI*2;
tempctx.globalCompositeOperation='source-over';
tempctx.clearRect(0,0,cw,ch);
tempctx.beginPath();
for(var i=0;i<circles.length;i++){
var c=circles[i];
tempctx.arc(c.x,c.y,c.radius,0,PI2);
}
tempctx.fillStyle='black';
tempctx.fill();
tempctx.globalCompositeOperation='source-in';
tempctx.fillStyle=circles[0].rgba;
tempctx.fill();
}
And here is example code and a Demo involving multiple sets of semi-transparent circles:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
ctx.fillRect(0,0,120,220);
var tempCanvas=canvas.cloneNode();
var tempctx=tempCanvas.getContext('2d');
var c1={x:100,y:200,radius:50,rgba:'rgba(255,0,0,0.5)'};
var c2={x:100,y:240,radius:35,rgba:'rgba(255,0,0,0.5)'};
var c3={x:140,y:200,radius:50,rgba:'rgba(0,255,255,0.5)'};
var c4={x:140,y:240,radius:35,rgba:'rgba(0,255,255,0.5)'};
var c5={x:120,y:140,radius:50,rgba:'rgba(255,255,0,0.5)'};
uniformColorCircles([c1,c2]);
ctx.drawImage(tempCanvas,0,0);
uniformColorCircles([c3,c4]);
ctx.drawImage(tempCanvas,0,0);
uniformColorCircles([c5]);
ctx.drawImage(tempCanvas,0,0);
function uniformColorCircles(circles){
var PI2=Math.PI*2;
tempctx.globalCompositeOperation='source-over';
tempctx.clearRect(0,0,cw,ch);
tempctx.beginPath();
for(var i=0;i<circles.length;i++){
var c=circles[i];
tempctx.arc(c.x,c.y,c.radius,0,PI2);
}
tempctx.fillStyle='black';
tempctx.fill();
tempctx.globalCompositeOperation='source-in';
tempctx.fillStyle=circles[0].rgba;
tempctx.fill();
}
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=512 height=512></canvas>
I have solved the problem using helper canvases and canvas image data processing.
There are data array, which contains coordinates and value which is used to determine which color we need to use.
I draw each circle color in it's own layer, then process each layer with preventSaturation function. Then add all layers into original canvas.
Please if anyone know the better way let me know.
If someone didn't understand what i needed to do it was:
1) i had this
2) i tried to have this
var canvas = document.getElementById('circles');
var ctx = canvas.getContext('2d');
var radius = 30;
var opacity = .7;
var data = [
{
x: 200,
y: 200,
v: 10
},
{
x: 230,
y: 230,
v: 20
},
{
x: 250,
y: 210,
v: 30
},
{
x: 270,
y: 190,
v: 40
},
{
x: 300,
y: 220,
v: 100
},
{
x: 300,
y: 260,
v: 200
},
{
x: 320,
y: 210,
v: 300
},
{
x: 300,
y: 160,
v: 200
},
{
x: 380,
y: 160,
v: 3000
},
{
x: 380,
y: 110,
v: 3000
},
{
x: 320,
y: 190,
v: 3000
}
];
var styles = {
blue: {
edgeValue: 0,
color: [0, 0, 255, 0.7]
},
green: {
edgeValue: 100,
color: [0, 255, 0, 0.7]
},
red: {
edgeValue: 1000,
color: [255, 0, 0, 0.7]
}
};
var layers = {};
for (var prop in styles) {
if(styles.hasOwnProperty(prop)) {
var c = document.createElement('canvas');
c.width = canvas.width;
c.height = canvas.height;
var cx = c.getContext('2d');
var cc = document.createElement('canvas');
cc.width = radius * 2;
cc.height = radius * 2;
var ccx = cc.getContext('2d');
var cColor = styles[prop].color;
ccx.fillStyle = 'rgba(' + cColor[0] + ',' + cColor[1] + ',' + cColor[2] + ',' + cColor[3] + ')';
ccx.beginPath();
ccx.arc(radius, radius, radius, 0, Math.PI * 2, true);
ccx.closePath();
ccx.fill();
layers[prop] = {
color: styles[prop].color,
edgeValue: styles[prop].edgeValue,
canvas: c,
ctx: cx,
canvasC: cc,
ctxC: ccx,
objects: []
};
}
}
data.forEach(function(o) {
var layer = o.v < styles.green.edgeValue ? layers.blue : o.v < styles.red.edgeValue ? layers.green : layers.red;
layer.ctx.drawImage(layer.canvasC, o.x, o.y);
layer.objects.push(o);
});
for(prop in layers) {
if(layers.hasOwnProperty(prop)) {
var image = layers[prop].ctx
.getImageData(0, 0, layers[prop].canvas.width, layers[prop].canvas.height);
preventColorSaturation(image.data, layers[prop].color);
layers[prop].ctx.putImageData(image, 0, 0);
ctx.drawImage(layers[prop].canvas, 0, 0);
}
}
function preventSaturation (d, s) {
var rgb255 = raRGBA255(s);
for (var i = 0; i < d.length; i += 4) {
d[i] = Math.min(d[i], rgb255[0]);
d[i + 1] = Math.min(d[i + 1], rgb255[1]);
d[i + 2] = Math.min(d[i + 2], rgb255[2]);
d[i + 3] = Math.min(d[i + 3], rgb255[3]);
}
}
function raRGBA255 (s) {
return [
s[0],
s[1],
s[2],
255 * s[3]
];
}
function raHexToRGB (s) {
var hexREGEXP = /^#([0-9A-Za-z]{3,6})$/;
var parsedHEX = s.match(hexREGEXP);
if (!parsedHEX) {
return [0, 0, 0];
}
return [
parseInt(parsedHEX[1].slice(0, 2), 16),
parseInt(parsedHEX[1].slice(2, 4), 16),
parseInt(parsedHEX[1].slice(4), 16)
];
}
I have labels ranging from 50-90, and every number in between in displayed.
I would like to list the labels by 5 or 10 because currently they are all crunched together.
It also make the left part of the y-axis cut off.
EDIT 2: Ok so i actually needed functionality like this in a project I am working on so i have made a custom build of chart.js to include this functionality.http://jsfiddle.net/leighking2/mea767ss/ or https://github.com/leighquince/Chart.js
It's a combination of the two solutions below but tied into the core of CHart.js so no need to specify custom scale and charts.
Both line and bar charts have a new option called
labelsFilter:function(label, index){return false;)
by default this will just return false so all labels on the x-axis will display but if a filter is passed as an option then it will filter the labels
so here is an example with both bar and line
var ctx = document.getElementById("chart").getContext("2d");
var data = {
labels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
datasets: [{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.75)",
highlightStroke: "rgba(220,220,220,1)",
data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
}]
};
var myLineChart = new Chart(ctx).Line(data, {
labelsFilter: function (value, index) {
return (index + 1) % 5 !== 0;
}
});
<script src="http://quincewebdesign.com/cdn/Chart.js"></script>
<canvas id="chart" width="1200px"></canvas>
ORIGINAL ANSWER
You can override the scale draw function to achieve this. Only thing i don;t like about this is it will apply to all your graphs so the other option is to have a custom graph type that makes use of the overridden draw.
EDIT 1: just realized the same affect can be achieved by using the index value rather than the label and this can then be applied to all label types not just numerical ones, this applies for both examples and could be easily changed. Here is the second example using the index rather than the label http://jsfiddle.net/leighking2/n9c8jx55/
1st - Overriding the scale draw function http://jsfiddle.net/leighking2/96grgz0d/
Only change here is before drawing the x-axis label we test if the label is a number and if its remainder when divided by 5 is not equal to 0 (so any number not dividable by 5)
if it matches both those criteria we do not draw the label
Chart.Scale = Chart.Scale.extend({
draw : function(){
console.log(this);
var helpers = Chart.helpers;
var each = helpers.each;
var aliasPixel = helpers.aliasPixel;
var toRadians = helpers.radians;
var ctx = this.ctx,
yLabelGap = (this.endPoint - this.startPoint) / this.steps,
xStart = Math.round(this.xScalePaddingLeft);
if (this.display){
ctx.fillStyle = this.textColor;
ctx.font = this.font;
each(this.yLabels,function(labelString,index){
var yLabelCenter = this.endPoint - (yLabelGap * index),
linePositionY = Math.round(yLabelCenter);
ctx.textAlign = "right";
ctx.textBaseline = "middle";
if (this.showLabels){
ctx.fillText(labelString,xStart - 10,yLabelCenter);
}
ctx.beginPath();
if (index > 0){
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
linePositionY += helpers.aliasPixel(ctx.lineWidth);
ctx.moveTo(xStart, linePositionY);
ctx.lineTo(this.width, linePositionY);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
ctx.beginPath();
ctx.moveTo(xStart - 5, linePositionY);
ctx.lineTo(xStart, linePositionY);
ctx.stroke();
ctx.closePath();
},this);
each(this.xLabels,function(label,index){
//================================
//test to see if we draw the label
//================================
if(typeof label === "number" && label%5 != 0){
return;
}
var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
// Check to see if line/bar here and decide where to place the line
linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
isRotated = (this.xLabelRotation > 0);
ctx.beginPath();
if (index > 0){
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
ctx.moveTo(linePos,this.endPoint);
ctx.lineTo(linePos,this.startPoint - 3);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
// Small lines at the bottom of the base grid line
ctx.beginPath();
ctx.moveTo(linePos,this.endPoint);
ctx.lineTo(linePos,this.endPoint + 5);
ctx.stroke();
ctx.closePath();
ctx.save();
ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
ctx.rotate(toRadians(this.xLabelRotation)*-1);
ctx.textAlign = (isRotated) ? "right" : "center";
ctx.textBaseline = (isRotated) ? "middle" : "top";
ctx.fillText(label, 0, 0);
ctx.restore();
},this);
}
}
});
then we can use the graphs like normal. Declare data
var ctx = document.getElementById("chart").getContext("2d");
var data = {
labels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
datasets: [{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [65, 34, 21, 11, 11, 34, 34, 12, 24, 45, 65, 34, 21, 11, 11, 34, 34, 12, 24, 45, 65, 34, 21, 11, 11, 34, 34, 12, 24, 45]
}, ]
};
draw graph
var myLineChart = new Chart(ctx).Line(data);
2nd custom graph + custom scale + filter function http://jsfiddle.net/leighking2/6xej5ek3/
In this method we still need to create a custom scale object but instead of having this applied to all charts we create we can choose to only apply it to those that we have declared. Also in this example we can also have the filter be a function that gets applied at run time so we can have each graph filter the labels differently
first the scale object
Chart.CustomScale = Chart.Scale.extend({
draw: function () {
console.log(this);
var helpers = Chart.helpers;
var each = helpers.each;
var aliasPixel = helpers.aliasPixel;
var toRadians = helpers.radians;
var ctx = this.ctx,
yLabelGap = (this.endPoint - this.startPoint) / this.steps,
xStart = Math.round(this.xScalePaddingLeft);
if (this.display) {
ctx.fillStyle = this.textColor;
ctx.font = this.font;
each(this.yLabels, function (labelString, index) {
var yLabelCenter = this.endPoint - (yLabelGap * index),
linePositionY = Math.round(yLabelCenter);
ctx.textAlign = "right";
ctx.textBaseline = "middle";
if (this.showLabels) {
ctx.fillText(labelString, xStart - 10, yLabelCenter);
}
ctx.beginPath();
if (index > 0) {
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
linePositionY += helpers.aliasPixel(ctx.lineWidth);
ctx.moveTo(xStart, linePositionY);
ctx.lineTo(this.width, linePositionY);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
ctx.beginPath();
ctx.moveTo(xStart - 5, linePositionY);
ctx.lineTo(xStart, linePositionY);
ctx.stroke();
ctx.closePath();
}, this);
each(this.xLabels, function (label, index) {
//======================================================
//apply the filter the the label if it is a function
//======================================================
if (typeof this.labelsFilter === "function" && this.labelsFilter(label)) {
return;
}
var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
// Check to see if line/bar here and decide where to place the line
linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
isRotated = (this.xLabelRotation > 0);
ctx.beginPath();
if (index > 0) {
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
ctx.moveTo(linePos, this.endPoint);
ctx.lineTo(linePos, this.startPoint - 3);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
// Small lines at the bottom of the base grid line
ctx.beginPath();
ctx.moveTo(linePos, this.endPoint);
ctx.lineTo(linePos, this.endPoint + 5);
ctx.stroke();
ctx.closePath();
ctx.save();
ctx.translate(xPos, (isRotated) ? this.endPoint + 12 : this.endPoint + 8);
ctx.rotate(toRadians(this.xLabelRotation) * -1);
ctx.textAlign = (isRotated) ? "right" : "center";
ctx.textBaseline = (isRotated) ? "middle" : "top";
ctx.fillText(label, 0, 0);
ctx.restore();
}, this);
}
}
});
now the custom graph that will make use of this scale, rather annoyingly we have to override the whole of the buildscale function
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
//======================================================
//ensure the new option is part of the options
//======================================================
this.options.labelsFilter = data.labelsFilter || null;
Chart.types.Line.prototype.initialize.apply(this, arguments);
},
buildScale: function (labels) {
var helpers = Chart.helpers;
var self = this;
var dataTotal = function () {
var values = [];
self.eachPoints(function (point) {
values.push(point.value);
});
return values;
};
var scaleOptions = {
templateString: this.options.scaleLabel,
height: this.chart.height,
width: this.chart.width,
ctx: this.chart.ctx,
textColor: this.options.scaleFontColor,
fontSize: this.options.scaleFontSize,
//======================================================
//pass this new options to the scale object
//======================================================
labelsFilter: this.options.labelsFilter,
fontStyle: this.options.scaleFontStyle,
fontFamily: this.options.scaleFontFamily,
valuesCount: labels.length,
beginAtZero: this.options.scaleBeginAtZero,
integersOnly: this.options.scaleIntegersOnly,
calculateYRange: function (currentHeight) {
var updatedRanges = helpers.calculateScaleRange(
dataTotal(),
currentHeight,
this.fontSize,
this.beginAtZero,
this.integersOnly);
helpers.extend(this, updatedRanges);
},
xLabels: labels,
font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
lineWidth: this.options.scaleLineWidth,
lineColor: this.options.scaleLineColor,
gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
showLabels: this.options.scaleShowLabels,
display: this.options.showScale
};
if (this.options.scaleOverride) {
helpers.extend(scaleOptions, {
calculateYRange: helpers.noop,
steps: this.options.scaleSteps,
stepValue: this.options.scaleStepWidth,
min: this.options.scaleStartValue,
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
});
}
//======================================================
//Use the new Custom Scal that will make use of a labelsFilter function
//======================================================
this.scale = new Chart.CustomScale(scaleOptions);
}
});
then we can use it like normal. Declare the data but this time pass a new option for labelsFilter that is a function to apply the filtering of the x labels
var ctx = document.getElementById("chart").getContext("2d");
var data = {
labels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
labelsFilter: function (label) {
//return true if this label should be filtered out
return label % 5 !== 0;
},
datasets: [{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [65, 34, 21, 11, 11, 34, 34, 12, 24, 45, 65, 34, 21, 11, 11, 34, 34, 12, 24, 45, 65, 34, 21, 11, 11, 34, 34, 12, 24, 45]
}, ]
};
then draw the graph using our new custom graph name
var myLineChart = new Chart(ctx).LineAlt(data);
Overall even though it is a bit more involved i prefer the second method as it means that a custom filter can be applied to each graph i declare.
I updated the provided code snippet to prevent the rotation of the x-axis labels. Also some parameters were not being passed on in the constructor. Check the //Mike Walder comments.
//Code to manually set the interval of X-Axis Labels: From http://jsfiddle.net/leighking2/n9c8jx55/
Chart.CustomScale = Chart.Scale.extend({
draw: function () {
var helpers = Chart.helpers;
var each = helpers.each;
var aliasPixel = helpers.aliasPixel;
var toRadians = helpers.radians;
var ctx = this.ctx,
yLabelGap = (this.endPoint - this.startPoint) / this.steps,
xStart = Math.round(this.xScalePaddingLeft);
if (this.display) {
ctx.fillStyle = this.textColor;
ctx.font = this.font;
each(this.yLabels, function (labelString, index) {
var yLabelCenter = this.endPoint - (yLabelGap * index),
linePositionY = Math.round(yLabelCenter);
ctx.textAlign = "right";
ctx.textBaseline = "middle";
if (this.showLabels) {
ctx.fillText(labelString, xStart - 10, yLabelCenter);
}
ctx.beginPath();
if (index > 0) {
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
linePositionY += helpers.aliasPixel(ctx.lineWidth);
ctx.moveTo(xStart, linePositionY);
ctx.lineTo(this.width, linePositionY);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
ctx.beginPath();
ctx.moveTo(xStart - 5, linePositionY);
ctx.lineTo(xStart, linePositionY);
ctx.stroke();
ctx.closePath();
}, this);
each(this.xLabels, function (label, index) {
//======================================================
//apply the filter to the index if it is a function
//======================================================
if (typeof this.labelsFilter === "function" && this.labelsFilter(index)) {
return;
}
//Hack by Mike Walder to enforce X-Labels are Written horizontally
var xLabelRot = this.xLabelRotation;
this.xLabelRotation = 0;
//End of Hack
var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
// Check to see if line/bar here and decide where to place the line
linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
//Mike Walder: isRotated nees original Roation Value to display the X-Label in the RollOver Area of a Datapoint
isRotated = true;(xLabelRot > 0);
ctx.beginPath();
if(this.scaleShowVerticalLines){
if (index > 0) {
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
ctx.moveTo(linePos, this.endPoint);
ctx.lineTo(linePos, this.startPoint - 3);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
// Small lines at the bottom of the base grid line
ctx.beginPath();
ctx.moveTo(linePos, this.endPoint);
ctx.lineTo(linePos, this.endPoint + 5);
ctx.stroke();
ctx.closePath();
ctx.save();
ctx.translate(xPos, (isRotated) ? this.endPoint + 12 : this.endPoint + 8);
ctx.rotate(toRadians(this.xLabelRotation) * -1);
//Mike Walder added center here, because it looks better if the label designator is in the center of the smal line
ctx.textAlign = "center";
ctx.textBaseline = (isRotated) ? "middle" : "top";
ctx.fillText(label, 0, 0);
ctx.restore();
}, this);
}
}
});
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
//======================================================
//ensure the new option is part of the options
//======================================================
this.options.labelsFilter = data.labelsFilter || null;
Chart.types.Line.prototype.initialize.apply(this, arguments);
},
buildScale: function (labels) {
var helpers = Chart.helpers;
var self = this;
var dataTotal = function () {
var values = [];
self.eachPoints(function (point) {
values.push(point.value);
});
return values;
};
var scaleOptions = {
// Mike Walder: added this configuration option since it is overridden in the new code
scaleShowVerticalLines: this.options.scaleShowVerticalLines,
templateString: this.options.scaleLabel,
height: this.chart.height,
width: this.chart.width,
ctx: this.chart.ctx,
textColor: this.options.scaleFontColor,
fontSize: this.options.scaleFontSize,
//======================================================
//pass this new options to the scale object
//======================================================
labelsFilter: this.options.labelsFilter,
fontStyle: this.options.scaleFontStyle,
fontFamily: this.options.scaleFontFamily,
valuesCount: labels.length,
beginAtZero: this.options.scaleBeginAtZero,
integersOnly: this.options.scaleIntegersOnly,
calculateYRange: function (currentHeight) {
var updatedRanges = helpers.calculateScaleRange(
dataTotal(),
currentHeight,
this.fontSize,
this.beginAtZero,
this.integersOnly);
helpers.extend(this, updatedRanges);
},
xLabels: labels,
font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
lineWidth: this.options.scaleLineWidth,
lineColor: this.options.scaleLineColor,
gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
showLabels: this.options.scaleShowLabels,
display: this.options.showScale
};
if (this.options.scaleOverride) {
helpers.extend(scaleOptions, {
calculateYRange: helpers.noop,
steps: this.options.scaleSteps,
stepValue: this.options.scaleStepWidth,
min: this.options.scaleStartValue,
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
});
}
//======================================================
//Use the new Custom Scal that will make use of a labelsFilter function
//======================================================
this.scale = new Chart.CustomScale(scaleOptions);
}
});