Display D3.js label text into two lines - javascript

I am trying to create a funnel chart using the D3Funnel library . I am able to render the chart and display my data but since I am drawing it in a very small size widthwise, the text labels for the categories often overflow out of the funnel. Which doesn't look very pleasing. So now my idea is to display the labels in two lines; in the first line will be the cateogry and in the second line I want to display the amount.
To be able to do so, I tried following the various examples available here to text wrap the labels/text but I haven't been able to make any progress. So it will be great if someone can help me out here.
http://jsfiddle.net/NewMonk/s76oap3d/1/
I feel that the relevant place where I need to make the change is somewhere in the below part of the code but not able to figure out how shall I go about it:
{
key: '_addBlockLabel',
value: function _addBlockLabel(group, index) {
var paths = this.blockPaths[index];
var label = this._getBlockData(index)[0].formatted;
var fill = this.data[index][3] || this.label.fill;
var x = this.width / 2; // Center the text
var y = this._getTextY(paths);
group.append('text').text(label).attr({
'x': x,
'y': y,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'fill': fill,
'pointer-events': 'none'
}).style('font-size', this.label.fontSize);
}
/**
* Returns the y position of the given label's text. This is determined by
* taking the mean of the bases.
*
* #param {Array} paths
*
* #return {Number}
*/
}, {
key: '_getTextY',
value: function _getTextY(paths) {
if (this.isCurved) {
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length;
}
return (paths[1][1] + paths[2][1]) / 2;
}
}]);
Regards

You can append two text elements instead of one.
Playing with the code it seems that you have access to both the label and the value, so just need to append them separately, with a dy property in order to separate them vertically:
key: '_addBlockLabel',
value: function _addBlockLabel(group, index) {
var paths = this.blockPaths[index];
var text = this.blocks[index].label.raw;
var value = this.blocks[index].value;
var fill = this.blocks[index].label.color;
var x = this.width / 2; // Center the text
var y = this._getTextY(paths);
group.append('text').text(text).attr({
x: x,
y: y,
dy: -10,
fill: fill,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'pointer-events': 'none'
}).style('font-size', this.label.fontSize);
group.append('text').text(value).attr({
x: x,
y: y,
dy: 10,
fill: fill,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'pointer-events': 'none'
}).style('font-size', this.label.fontSize);
}
Working fiddle: http://jsfiddle.net/38kkctqh/

Related

How to vertically center align Multi-Line-Text on Canvas (in NodeJS)

I am trying to vertically center align multi-line text on a canvas, using canvas-multiline-text on top of the npm module canvas. I cannot figure out how to (programmatically / automatically) vertically center the text.
The text might be 1 word or it might be a sentence with 10+ words. Is there a way to figure out its height and calculate the y position accordingly?
Note: I am not in a browser, I am using node.js.
const fs = require('fs')
const { registerFont, createCanvas, loadImage } = require('canvas')
const drawMultilineText = require('canvas-multiline-text')
const width = 2000;
const height = 2000;
let fontSize = 250;
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
//Filling the square
context.fillStyle = '#fff'
context.fillRect(0, 0, width, height)
//Text Styles
context.font = "normal 700 250px Arial";
context.textAlign = 'center'
context.textBaseline = 'middle';
context.fillStyle = '#000'
//Drawing Multi Line Text
const fontSizeUsed = drawMultilineText(
context,
"Hello World, this is a test",
{
rect: {
x: 1000,
y: 1000, // <-- not sure what to put here / how to calculate this?
width: context.canvas.width - 20,
height: context.canvas.height - 20
},
font: 'Arial',
verbose: true,
lineHeight: 1.4,
minFontSize: 100,
maxFontSize: 250
}
)
I put this as an answer as I can't comment yet, but the module you are using has no support for this. What you can do is modify the module (or just copy the function into your own code). The function has a variable y which stores the height of the text. You can initialize the variable outside of the for loop so that you can use it when drawing the text. Then you can subtract opts.rect.height from your new y variable and divide by two to get the correct offset, now the text will be centered in that y variable you specify when calling the function. Oh and finally get "context.textBaseline = 'middle';" as the module you use makes it calculations assuming a bottom baseLine. Here is the final code for the drawMultiLineText function
...
const words = require('words-array')(text)
if (opts.verbose) opts.logFunction('Text contains ' + words.length + ' words')
var lines = []
let y; //New Line
...
var x = opts.rect.x;
y = fontSize; //modified line
lines = []
...
if (opts.verbose) opts.logFunction("Font used: " + ctx.font);
const offset = opts.rect.y + (opts.rect.height - y) / 2; //New line, calculates offset
for (var line of lines)
// Fill or stroke
if (opts.stroke)
ctx.strokeText(line.text.trim(), line.x, line.y + offset) //modified line
else
ctx.fillText(line.text.trim(), line.x, line.y + offset) //modified line
...
This module you use isn't exactly the cleanest code (uses var instead of let, has a dependency just to split a string into words), so I would highly recommend you rewrite this function yourself. But those should be the required changes in order for it to center the text.
EDIT: After looking more closely at this dependency's code, it looks like there are more mistakes then I initially thought, the code treats height like the bottom of the rectangle, not like height. To fix this remove opts.rect.y from the line that assigns y, and add it to the offset variable (I already made these changes to the code)

Snap.svg & JavaScript: Creating Shapes and Animating Each on Delay inside a For Loop

first-time/long-time (quack, quack).
I'm a bit frustrated, and just about stumped, by this riddle I can't quite solve in Snap.svg. It's probably an oversight that I'll kick myself for missing, but I'm not seeing it at this point.
I have x & y data that I draw from a DOM element and store into a series of arrays, filter based on certain values in certain columns, and eventually create multiple instances of the ChartLine object in js. Basically, it sorts a certain column by quantity, assigns each value's row a color from a RainbowVis.js object, pushes all the relevant values from each row into an array for y, and draws a path on the line chart where y is the value and x is a steadily-increasing integer in a For loop.
What I'm currently doing here, in the draw() function, is: for each relevant column, create a <circle> with the variable "dot" with the object's x & y variables, assign the attributes, animate the radius from 0 to 8 in a quarter-second, and add the x & y values of i to a string to be used in a <path> I create right after the For loop. I then animate the path, etc., etc.
Without the setTimeout(), it works well. The circles and paths all animate simultaneously on load. However, I want to add a delay to each .animate with the number of milliseconds increasing by polyDelayInterval in each iteration, so each "dot" animates as the line arrives at it. At the VERY least, I want to animate all of the "dots" after the path is done animating.
The problem is, no matter what I've tried so far, I can only get the last set of "dots" (at the highest x value for each line) to animate; the rest stay at r:0. I've read several somewhat-similar posts, both here and elswhere; I've searched up and down the Snap.svg's docs on their site. I just cannot find what I'm doing wrong. Thanks in advance!
var svgMFLC = Snap('svg#ElementID');
function ChartLine(x, y, color, row) {
this.x = x;
this.y = y;
this.color = color;
this.row = row;
var propNames = Object.keys(this.row);
var yAdjust;
var resetX = this.x;
for (var i = 0; i < propNames.length; i++) { // get only the calculated score columns and their values
if (propNames[i].toLowerCase().includes(calcKeyword)) {
yAdjustedToChartArea = chartBottom - (this.row[propNames[i]] * yInterval);
this.y.push(yAdjustedToChartArea); // returns the value of that score column and pushes it to the y array
}
}
this.draw = function () {
var points = "M"; // the string that will determine the coordinates of each line
var dotShadow = svgMFLC.filter(Snap.filter.shadow(0, 0, 2, "#000000", 0.4));
var polyTime = 1500; // in milliseconds
var dot;
var polyDelayInterval = polyTime / (semesterCols.length - 1);
for (var i = 0; i < semesterCols.length; i++) { // for each data point, create a "dot"
dot = svgMFLC.circle(this.x, this.y[i], 0);
dot.attr({
fill: this.color,
stroke: "none",
filter: dotShadow,
class: "chartPointMFLC"
});
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
points += this.x + " " + this.y[i] + " L";
this.x = this.x + xInterval;
}
points = points.slice(0, -2); // take away the excessive " L" from the end of the points string
var poly = svgMFLC.path(points);
var polyLength = poly.getTotalLength();
poly.attr({
fill: "none",
stroke: this.color,
class: "chartLineMFLC",
strokeDasharray: polyLength + " " + polyLength, // setting the strokeDash attributes will help create the "drawing the line" effect when animated
strokeDashoffset: polyLength
});
poly.animate({ strokeDashoffset: 0.00 }, polyTime);
this.x = resetX;
}
}
I can't put a tested solution up without the full code to test, but the problem is almost certainly that you at least need to get a closure for your 'dot' element.
So this line...
setTimeout(function () {
dot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i);
When it comes to call that function, 'dot' will be the last 'dot' from the loop. So you need to create a closure (create functional scope for dot).
So something a bit like...
(function() {
var laterDot = dot;
setTimeout(function () {
laterDot.animate({ r: 8 }, 250, mina.easeout);
}, polyDelayInterval * i)
})();

Chart.js 2 - How will I vertically insert the dataset values inside the bar graph?

I want to vertically insert the dataset values to their corresponding bar graph.
What should I do?
A sample code would be greatly appreciated.
Current part of Code
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.filter(dataset => !dataset._meta[Object.keys(dataset._meta)[0]].hidden).forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
scale_max = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
ctx.fillStyle = '#000';
var y_pos = model.y - 5;
// Make sure data value does not get overflown and hidden
// when the bar's value is too close to max value of scale
// Note: The y value is reverse, it counts from top down
if ((scale_max - model.y) / scale_max >= 0.70)
y_pos = model.y + 20;
ctx.fillText(addCommas(dataset.data[i]), model.x, y_pos);
}
});
}
}
I just got the code from this site so I'm not sure how to play or modify it. As far as I know this kind of feature is not discussed in the documentation of chart js.
expected output
if the value is 35,400 then it should be:
3
5,
4
0
0 (vertically)
After going through the requirement, I wrote this generic function for creating vertical labels, here is a demo of the vertical labeling function in action.
JSFiddle Demo
The parameter details of the vertical label function is described in detail below.
function verticalLabel(ctx, data, dataToSplit, topOffset, spaceBetween, commaGap) {
var i = 0, prev = 0;
for (x of dataToSplit.toString().split("").reverse()) {
if (x.indexOf(",") > -1) {
ctx.fillText(x, data.x + commaGap, prev - topOffset);
} else {
ctx.fillText(x, data.x, data.y - spaceBetween * i - topOffset);
i++;
}
var prev = data.y - spaceBetween * i;
}
}
Calling the function:
this.datasets.forEach(function(dataset) {
dataset.points.forEach(function(points) {
console.log(points);
verticalLabel(ctx, points, addCommas(points.value), 30, 10, 4);
});
})
Parameters:
ctx - The variable containing the line (var ctx = document.getElementById("myChart1").getContext("2d"))
data - This is the variable containing the label, value, x and y properties of the individual points in the chart.
dataToSplit - Before passing in the data into the function, we might do some string manipulations to the data. We can pass the final manipulated string variable into this parameter and this will get converted into the vertical label. So in our above example, we pass the value of bar(which is nothing but data parameter of our function), through another function called addCommas() which will just added number separators into the value, finally this is passed to the main function which show the output of this result as a vertical number.
topOffset - This parameter is used to set the distance of the number from the top of the point, this can have any number provided its of numerical datatype and its an integer!
spaceBetween - This parameter is used to set the distance between the characters of the points value(data), this can have any number provided its of numerical datatype and its an integer! This parameter does not apply to the comma in the data.
commaGap - This parameter is used to set the distance between the characters before the comma and the actual comma, you can think of it as increasing the legibility of the comma by increasing the space before the comma (E.g 9, can be represented as 9 ,), this can have any number provided its of numerical datatype and its an integer! This parameter does not apply to the comma in the data.

create random rectangles with random colors without overlapping

How can i create something like this in QML using javascript?
Actually I know how to create rectangles in QML but want to do something like this. QML canvas can be of any size but whenever QML section is loaded multiple squares are generated with random sizes and colors without overlapping. When I'm trying to do this rectangles are generated in a list form.
I'm a web developer(ruby on rails oriented) but new to such javascript stuff. Any help will be appreciated.
As #ddriver already noticed, the simpliest decision is to loop through all children to find a room to a new rectangle.
Rectangle {
id: container
anchors.fill: parent
property var items: [];
Component {
id: rect
Rectangle {
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1);
border.width: 1
border.color: "#999"
width: 50
height: 50
}
}
Component.onCompleted: {
var cnt = 50;
for(var i = 0;i < cnt;i ++) {
for(var t = 0;t < 10;t ++) {
var _x = Math.round(Math.random() * (mainWindow.width - 200));
var _y = Math.round(Math.random() * (mainWindow.height - 200));
var _width = Math.round(50 + Math.random() * 150);
var _height = Math.round(50 + Math.random() * 150);
if(checkCoord(_x,_y,_width,_height)) {
var item = rect.createObject(container,{ x: _x, y: _y, width: _width, height: _height });
container.items.push(item);
break;
}
}
}
}
function checkCoord(_x,_y,_width,_height) {
if(container.items.length === 0)
return true;
for(var j = 0;j < container.items.length;j ++) {
var item = container.children[j];
if(!(_x > (item.x+item.width) || (_x+_width) < item.x || _y > (item.y+item.height) || (_y+_height) < item.y))
return false;
}
return true;
}
}
Yes, this is not so wise solution but it still can be improved.
If you want efficiency, it will come at the cost of complexity - you will have to use some space partition algorithm. Otherwise, you could just generate random values until you get enough that are not overlapping.
Checking whether two rectangles overlap is simple - if none of the corners of rectangle B is inside rectangle A, then they don't overlap. A corner/point is inside a rectangle if its x and y values are in the range of the rectangle's x and width and y and height respectively.
In JS Math.random() will give you a number between 0 and 1, so if you want to make a random value for example between 50 and 200, you can do that via Math.random() * 150 + 50.
Have an array, add the initial rectangle value to it, then generate new rectangle values, check if they overlap with those already in the array, if not - add them to the array as well. Once you get enough rectangle values, go ahead and create the actual rectangles. Since all your rectangles are squares, you can only go away with 3 values per square - x, y and size.

How to animate both rotation and transformation in Raphaël

I'm trying to do something I thought would be rather simple. I've an object that I move around stepwise, i.e. I receive messages every say 100 milliseconds that tell me "your object has moved x pixels to the right and y pixels down". The code below simulates that by moving that object on a circle, but note that it is not known in advance where the object will be heading in the next step.
Anyway, that is pretty simple. But now I want to also tell the object, which is actually a set of subobjects, that it is being rotated.
Unfortunately, I am having trouble getting Raphaël to do what I want. I believe the reason is that while I can animate both translation and rotation independently, I have to set the center of the rotation when it starts. Obviously the center of the rotation changes as the object is moving.
Here's the code I'm using and you can view a live demo here. As you can see, the square rotates as expected, but the arrow rotates incorrectly.
// c&p this into http://raphaeljs.com/playground.html
var WORLD_SIZE = 400,
rect = paper.rect(WORLD_SIZE / 2 - 20, 0, 40, 40, 5).attr({ fill: 'red' }),
pointer = paper.path("M 200 20 L 200 50"),
debug = paper.text(25, 10, ""),
obj = paper.set();
obj.push(rect, pointer);
var t = 0,
step = 0.05;
setInterval(function () {
var deg = Math.round(Raphael.deg(t));
t += step;
debug.attr({ text: deg + '°' });
var dx = ((WORLD_SIZE - 40) / 2) * (Math.sin(t - step) - Math.sin(t)),
dy = ((WORLD_SIZE - 40) / 2) * (Math.cos(t - step) - Math.cos(t));
obj.animate({
translation: dx + ' ' + dy,
rotation: -deg
}, 100);
}, 100);
Any help is appreciated!
If you want do a translation and a rotation too, the raphael obj should be like that
obj.animate({
transform: "t" + [dx , dy] + "r" + (-deg)
}, 100);
Check out http://raphaeljs.com/animation.html
Look at the second animation from the top on the right.
Hope this helps!
Here's the code:
(function () {
var path1 = "M170,90c0-20 40,20 40,0c0-20 -40,20 -40,0z",
path2 = "M270,90c0-20 40,20 40,0c0-20 -40,20 -40,0z";
var t = r.path(path1).attr(dashed);
r.path(path2).attr({fill: "none", stroke: "#666", "stroke-dasharray": "- ", rotation: 90});
var el = r.path(path1).attr({fill: "none", stroke: "#fff", "stroke-width": 2}),
elattrs = [{translation: "100 0", rotation: 90}, {translation: "-100 0", rotation: 0}],
now = 0;
r.arrow(240, 90).node.onclick = function () {
el.animate(elattrs[now++], 1000);
if (now == 2) {
now = 0;
}
}; })();

Categories