Legends for line charts in Chart.js - chart.js

I'd like to customize a legend for line data so that the legend graphic is a line (styled like the actually data line) rather than a box.
As far as I can tell from the source, the graphic can be a point or a box, and the height of the box is fixed to the font size. The 'generateLabels' option does not seem to allow for extending around these contraints.
Version 2.2.1.
Thanks for any help.

NOTE: This solution only works if you have a local version of Chart.js since it needs to edit a function in the source code of the library, which can't be done if you import it form a CDN.
To achieve what you want, you will need to edit the drawLegendBox function (link to source here).
First, as if you wanted to do a pointStyle legend, add the useLineStyle and set it to true like this :
options: {
legend: {
labels : {
useLineStyle: true
}
}
}
Then you need to go to your local version of Chart.js (obvisouly, you cannot edit it if you import it from a CDN) and search for the function drawLegendBox (on Chart.js v2.2.1, it is roughly line 6460; in Chart.js v2.9.4 search for labelOpts && labelOpts.usePointStyle).
Scroll down a little bit to see something like this :
if (opts.labels && opts.labels.usePointStyle) {
// Recalulate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
var radius = fontSize * Math.SQRT2 / 2;
var offSet = radius / Math.SQRT2;
var centerX = x + offSet;
var centerY = y + offSet;
// Draw pointStyle as legend symbol
Chart.canvasHelpers.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY);
}
// --- NEW CONDITION GOES HERE ---
else {
// Draw box as legend symbol
ctx.strokeRect(x, y, boxWidth, fontSize);
ctx.fillRect(x, y, boxWidth, fontSize);
}
And add this between the two conditions :
else if (opts.labels && opts.labels.useLineStyle) {
ctx.beginPath();
ctx.moveTo(x, y + fontSize * 0.45);
ctx.lineTo(x + boxWidth, y + fontSize * 0.45);
ctx.stroke();
}
With this edit, everytime you will set useLineStyle to true, legend boxes will be drawn as lines, as the following screenshot :

I was able to use pointStyle: line, in the dataset and then under options use labels: {usePointStyle: true,},

Just to improve on this solution from tektiv.
If you want to show a dashed line too use this code in the same spot.
(chartJs 2.7.2 around Line 16289):
if (opts.labels && opts.labels.usePointStyle) {
// CHARTJS CODE
} else if (opts.labels && opts.labels.useLineStyle) {
if (legendItem.borderDash) {
ctx.setLineDash(legendItem.borderDash);
}
ctx.beginPath();
ctx.moveTo(x, y + fontSize / 2);
ctx.lineTo(x + boxWidth, y + fontSize / 2);
ctx.stroke();
} else {
// CHARTJS CODE
}

chart.js v3
For this version, none of the previously mentioned built-in configurations work. You can set boxHeight: 0 on the legend labels in order to get a line instead of a box:
{
legend: {
labels: {
boxHeight: 0
}
}
}

You can make line legend by changing width of legend box (for example 2px), it will be vertical line but it's looks nice too
plugins: {
legend: {
display: true,
labels: {
boxWidth: 2
}
}
}

Related

Horizontal line syncing across multiple charts when hovering with Chart.js in Typescript-react

When hovering over anywhere inside of the chart, I currently am drawing a horizontal line all the way across the chart. I want the ability to be able to sync that across multiple charts on the same page. I am currently doing it with the following code-snippet in my options. I would like for whatever point is being hovered in one chart creating a line, to also have the same hover line across multiple charts. You can see in the image I would be hovering right above the 400 ms mark.
const options= {
onHover:(context:any) => {
const yAxis = context.y;
context.chart.clear();
const ctx = context.chart.ctx;
ctx.beginPath();
ctx.moveTo(45, yAxis);
ctx.lineTo(10000, yAxis);
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';
ctx.stroke();
}
}
I was able to solve this and even make it so it could apply to only certain charts on the page, and even have multiple different groups of synced up charts. The afterEvent sets the X value I want, and the className is set to tell which report group I would like to apply this hover to. If it detects it mousedOut, it sets the x value to 0 and therefore hides it. (Note that I ended up going with a vertical line instead of a horizontal, but the same thing could be done by flipping the x and y values and grabbing e.event.y).
const [xVal, setXVal] = useState(0)
const [currentClassName, setCurrentClassName] = useState('')
const verticalHover = {
id:"verticalHover",
afterEvent: (chart:any, e:any) => {
if(e.event.type === 'mousemove') {
setXVal(e.event.x)
setCurrentClassName(chart.canvas.className)
}
else if(e.event.type === 'mouseout') {
setXVal(0)
setCurrentClassName(chart.canvas.className)
}
},
afterDraw: (chartInstance: any) => {
if(xVal !==0 && chartInstance.canvas.className ===currentClassName){
const ctx = chartInstance.ctx;
ctx.strokeStyle = "#6f6f6f";
ctx.lineWidth = 2;
ctx.beginPath();
//Hardcoded yValues for now
ctx.moveTo(xVal, 40);
ctx.lineTo(xVal,265);
ctx.stroke();
}
}
}
Chart.register(verticalHover);

How to align ticks label in chartJS 3.x?

is possible with chartJS chenge the align position of tick labels for scaleY and for scaleX?
In the image attached i want 300,150,60, 0 on top of the grid line and 9:56 aligned on the right of the first tick.
I try to modify the code in this answer, but without success.
After some research i adapted the method showned here to works with ChartJS 3.x:
plugins: [{
afterDraw: function (c) {
var yScale = c.scales['y'];
yScale.ticks.forEach(function (o, i) {
// top tick label indent
var yT_O = yScale.getPixelForTick(i) - 9;
// left tick label indent
var xT_O = c.width - 35;
c.ctx.fillStyle = "#707070"
c.ctx.fillText(o.label, xT_O, yT_O);
});
}
}]
Actually i'm not sure this is the best plugin event where do that.
I also had to put "display: false" on the ticks options in order to hide the originals tick labels.

How to create a line that has two arrows in Cesium?

Cesium has various polyline materials (PolylineArrow, PolylineDash, PolylineGlow etc.) but
I want to draw a polyline consisting of lines that has an arrow in their head and an arrow in their center.
I think it can be done by adding GLSL code to PolylineArrowMaterial.
Only one idea that I have is adding section of polyline with arrow header instead on single polyline like this :
points.forEach((x, i) => {
if (i < points.length - 1) {
polylines.add({
positions: [points[i], points[i + 1]],
width: 12.0,
material: Cesium.Material.fromType('PolylineArrow', {
color: new Cesium.Color.fromBytes(255, 255, 0, 185)
})
});
}
});

Constrain RaphaelJS print to boundaries and position

I am looking for a more efficient way to constrain/set text in Raphael.
I have text that can be written in a box. That text should be centered (based on a number that could change if user wanted to shift text left/right) and the text cannot go beyond the boundaries of the paper.
This is what I do now and its not manageable performance wise
// Build a path
var path = this.paper.print(x, x, text, font, size, 'middle')
// Center line by getting bounding box and shifting to half of that
var bb = path.getBBox()
path.transform('...T' + [-bb.width / 2, 0])
// Compare paper size vs bb
// if it goes beyond I adjust X and Y accordingly and redo the above
So ideally I would like to predict the size of the text before it prints - I am not sure this is possible though as it is probably font dependent. I have looked for a command to contrain text but do not see one?
The other thought I had was to create some kind of shadow paper that does not print to screen and use that to determine size before I render to user. I am not sure where the lag is though - if it's in the screen rendering good but if its in the general logic of creating the svg then that wont help.
I'd appreciate suggestions
You can use opentype.js for measuring text, also you can get svg path by using Path.toPathData method. There is no need of cufon compiled js fonts.
For text measure, create a canvas element somewhere in your DOM.
<canvas id="canvas" style="display: none;"></canvas>
this function will load text and count width, height
function measureText(text, font_name, size) {
var dfd = $.Deferred();
opentype.load('/fonts/' + font_name + '.ttf', function (err, font) {
if (err) {
dfd.reject('Font is not loaded');
console.log('Could not load font: ' + err);
} else {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
var tmp_path = font.getPath(text, 100, 100, size);
tmp_path.draw(ctx); // Draw path on the canvas
var ascent = 0;
var descent = 0;
var width = 0;
var scale = 1 / font.unitsPerEm * size;
var glyphs = font.stringToGlyphs(text_string);
for (var i = 0; i < glyphs.length; i++) {
var glyph = glyphs[i];
if (glyph.advanceWidth) {
width += glyph.advanceWidth * scale;
}
if (i < glyphs.length - 1) {
var kerningValue = font.getKerningValue(glyph, glyphs[i + 1]);
width += kerningValue * scale;
}
ascent = Math.max(ascent, glyph.yMax);
descent = Math.min(descent, glyph.yMin);
}
return dfd.resolve({
width: width,
height: ascent * scale,
actualBoundingBoxDescent: descent * scale,
fontBoundingBoxAscent: font.ascender * scale,
fontBoundingBoxDescent: font.descender * scale
});
}
});
return dfd.promise();
},
then use this function to get text dimensions:
$.when(measureText("ABCD", "arial", 48)).done(function(res) {
var height = res.height,
width = res.width;
...
});

raphaeljs: drag and apply transformation to Paper.set()

I started to play a little bit with raphaeljs, however I'm having a small problem when dragging and applying a transformation to a Paper.set()
Here is my example: http://jsfiddle.net/PQZmp/2/
1) Why is the drag event added only to the marker and not the slider?
2) The transformation is supposed to be relative(i.e. translate by and not translate to), however if I drag the marker twice, the second dragging starts from the beginning and not from the end of the first.
EDIT:
After the response of Zero, I created a new JSFiddle example: http://jsfiddle.net/9b9W3/1/
1) It would be cool if this referenced the set instead of the first element of the set. Can't this be done with dragger.apply(slider)? I tried it, but only works on the first execution of the method (perhaps inside Raphael it is already being done but to the first element inside the set instead of the set)
2) According to Raphael docs the transformation should be relative to the object position (i.e. translate by and not translate to). But it is not what is happening according to the jsfiddle above (check both markers drag events).
3) So 2) above creates a third question. If a transform("t30,0") is a translation by 30px horizontally, how is the origin calculated? Based on attr("x") or getBBox().x?
The drag event is actually being added to both the marker and the slider -- but your slider has a stroke-width of 1 and no fill, so unless you catch the exact border, the click "falls through" to the canvas.
Behind that is another issue: the drag is being applied to both elements, but this in your drag handler references a specific element, not the set -- so both elements will drag independently from each other.
Lastly: the reason that each drag is starting from the initial position is because the dx, dy parameters in dragger are relative to the coordinates of the initial drag event, and your transform does not take previous transforms into account. Consider an alternative like this:
var r = new Raphael(0, 0, 400, 200);
var marker = r.path("M10,0L10,100").attr({"stroke-width": 5});
var button = r.rect(0, 0, 20, 20, 1).attr( { 'stroke-width': 2, fill: 'white' } );
var slider = r.set( marker, button );
var startx, starty;
var startDrag = function(){
var bbox = slider.getBBox();
startx = bbox.x;
starty = bbox.y;
console.log(this);
}, dragger = function(dx, dy){
slider.transform("t" + ( startx + dx ) + "," + starty );
}, endDrag = function(){
};
slider.drag(dragger, startDrag, endDrag);
To address your updates:
I believe you can specify the context in which the drag function will be executed as optional fourth, fifth, and six parameters to element.drag. I haven't tried this myself, but it looks like this should work great:
slider.drag( dragger, startDrag, endDrag, slider, slider, slider );
The transformation is relative to the object position. This works great for the first slider because its starting position is 0, but not so great for the second slider because...
...the transformation for min/max sliders should actually be relative to the scale, not the individual markers. Thus you will notice that your max slider (the red one) returns to its initial position just as you drag the mouse cursor back over the zero position. Make sense?
var position;
var rect = paper.rect(20, 20, 40, 40).attr({
cursor: "move",
fill: "#f00",
stroke: "#000"
});
t = paper.text(70,70, 'test').attr({
"font-size":16,
"font-family":
"Arial, Helvetica, sans-serif"
});
var st = paper.set();
st.push(rect, t);
rect.mySet = st;
rect.drag(onMove, onStart, onEnd);
onStart = function () {
positions = new Array();
this.mySet.forEach(function(e) {
var ox = e.attr("x");
var oy = e.attr("y");
positions.push([e, ox, oy]);
});
}
onMove = function (dx, dy) {
for (var i = 0; i < positions.length; i++) {//you can use foreach but I want to
// show that is a simple array
positions[i][0].attr({x: positions[i][1] + dx, y: positions[i][2] + dy});
}
}
onEnd = function() {}