I am using the d3 library to create Bar Charts. The values on the axes are strings (for example, some names). I have the ability to drag the axis to change the size of the graphs. Resizing the graph causes a recalculation of the line that fit in the previous empty space. Since I'm using d3 (that's working with svg), I can't use the css property to add '...' if the string doesn't fit, I have to do it manually. The function I am using is given below:
protected truncateLabel(
truncatedLabel: Selection<SVGTextElement, unknown, null, undefined>,
textToFit: string,
freeSpacePx: number,
) {
truncatedLabel.node().textContent = textToFit;
const averageWidthOfCharInPx = truncatedLabel.node().textLength.baseVal.value / textToFit.length;
if (truncatedLabel.node().textLength.baseVal.value <= freeSpacePx){
return truncatedLabel;
}
const numberOfCharsThatWillFit = freeSpacePx / averageWidthOfCharInPx;
truncatedLabel.node().textContent = textToFit.slice(0, numberOfCharsThatWillFit - 2) + '...';
return truncatedLabel;
}
Everything works well if there are few bars, but if there are many bars, resizing starts to slow down a lot because of this function.
Is there a better way to check if a string fits and add '...'?
Or any way to optimize my function?
Related
I am implementing a set of custom elements that will be used like this:
<om-root>
...
<om-node id="node1">
...
<om-node id="node2">
...
</om-node>
...
</om-node>
...
<om-root>
That is, my <om-node> elements will be mixed in with arbitrary HTML, which may have positioning and/or CSS transform applied.
The purpose of these <om-node> elements is to apply CSS affine transformations to their content based on various conditions. But regardless of its position in the hierarchy, each om-node computes a transformation relative to the root node.
I can't just apply the computed transformation to the node, because the browser will combine that with the transformations of all its ancestor elements: if I rotate node1 by 30 degrees, then node2 will also be rotated by 30 degrees before its own transformation is applied.
Ideally, what I want is something that works like Element.getClientRects(), but returns a matrix rather than just a bounding box. Then I could do some math to compensate for the difference between the coordinate systems of the <om-node> and <om-root> elements.
This question is similar to mine, but doesn't have a useful answer. The question mentions using getComputedStyle(), but that doesn't do what is claimed – getComputedStyle(elt).transform returns a transformation relative to the element's containing block, not the viewport. Plus, the result doesn't include the effects of "traditional" CSS positioning (in fact it doesn't have a value at all for traditionally-positioned elements).
So:
Is there a robust way to get the transformation matrix for an element relative to the viewport?
The layout engine obviously has this info, and I'd prefer not to do a complicated (and expensive) tree-walking process every time anything changes.
Having thought some more about the question, it occurred to me that, in fact, you can solve the problem using getBoundingClientRect().
Of course, getBoundingClientRect() on its own does not tell you how an element has been transformed, because the same bounding box describes many possible transformations:
However, if we add three child elements, with a known size and position relative to the parent, then we can figure out more information by comparing each of their bounding boxes. The figure below shows where I have placed these three "gauge" elements (which in practice are empty and invisible):
The vectors u̅ and v̅ are orthogonal unit vectors of the parent element's untransformed coordinate system. After the element has been transformed by various CSS positioning and transform properties, we first need to find the transformed unit vectors u̅' and v̅'. We can do that by comparing the bounding boxes of the three gauge elements – the diagram below shows the process with two different example transformations:
the vector from box 1 to box 2 is equivalent to u̅'
the vector from box 1 to box 3 is equivalent to v̅'
the midpoint between [the top left of box 3] and [the bottom right of box 2] gives us point P: this is the transformed position of the parent element's origin
From these three values u̅', v̅' and P we can directly construct a 2D affine transformation matrix T:
This matrix T represents all the transformations affecting the parent element – not just CSS transform rules, but also "traditional" positioning, the effects of margins and borders, etc. And, because it's calculated from getBoundingClientRect(), it is always relative to the viewport – you can compare any two elements directly, regardless of their relationship within the DOM hierarchy.
Note: all this assumes we are only dealing with 2D affine transformations, such as transform:rotate(30deg) or left:120px. Dealing with 3D CSS transforms would be more complicated, and is left as an exercise for the reader.
Putting the above into code form:
class WonderDiv extends HTMLElement {
constructor () {
super();
this.gauges = [null, null, null];
}
connectedCallback () {
this.style.display = "block";
this.style.position = "absolute";
}
createGaugeElement (i) {
let g = document.createElement("div");
// applying the critical properties via a style
// attribute makes them harder to override by accident
g.style = "display:block; position:absolute;"
+ "margin:0px; width:100px; height:100px;"
+ "left:" + ( ((i+1)%2) ? "-100px;" : "0px;")
+ "top:" + ( (i<2) ? "-100px;" : "0px;");
this.appendChild(g);
this.gauges[i] = g;
return g;
}
getClientTransform () {
let r = [];
let i;
for (i=0; i<3; i++) {
// this try/catch block creates the gauge elements
// dynamically if they are missing, so (1) they aren't
// created where they aren't needed, and (2) they are
// restored automatically if someone else removes them.
try { r[i] = this.gauges[i].getBoundingClientRect(); }
catch { r[i] = this.createGaugeElement(i).getBoundingClientRect(); }
}
// note the factor of 100 here - we've used 100px divs
// instead of 1px divs, on a hunch that might be safer
return DOMMatrixReadOnly.fromFloat64Array(new Float64Array([
(r[1].left - r[0].left) / 100,
(r[1].top - r[0].top) / 100,
(r[2].left - r[0].left) / 100,
(r[2].top - r[0].top) / 100,
(r[1].right + r[2].left) /2,
(r[1].top + r[2].bottom) /2
]));
}
}
customElements.define("wonder-div", WonderDiv);
– the custom <wonder-div> element extends <div> to have a getClientTransform() method, which works like getClientBoundingRect() except that it returns a DOMMatrix instead of a DOMRect.
CSS Transformations are actually relatively heavy operations and do come with some gotchas.. (they TRANSFORM elements) so you may not be able to avoid traversing the nodes without implementing an intelligent state system, for example, storing all your objects + transformation in your javascript class..
That said, one easy workaround for small use cases is to disable transform on all the parent elements using something like 'inline' but this is not suitable for all cases..
<div id="outside">
<div id="inside">Absolute</div>
</div>
document.getElementById('outside').style.display = "inline";
The more robust approach is to retrieve and parse the computedStyles dynamically ...
function getTranslateXY(element) {
const style = window.getComputedStyle(element)
const matrix = new DOMMatrixReadOnly(style.transform)
return {
translateX: matrix.m41,
translateY: matrix.m42
}
}
Then you can dynamically set new transformations on any node by adding/subtracting from the current transformation state.
My problem: i would like to find an automatic way to center labels in donut chart cells. In my case each cells contains an array of complex objects, what i want is to show the number of those objects.
See:
Playing with radius, allowed me to find those values:
First layer: -28
Second layer: -20
Third layer: -10
Fourth layer: -8
I applied it as a quick fix but i don't like this solution, as it's fixed for 4 layers (what if I need to add an other layer ? etc....) and using "magic numbers" is unmaintenable...
Do you have a better solution ?
You can test it here: https://jsfiddle.net/aliz/gwz7om9e/
Line 40:
pieSeries.labels.template.radius = am4core.percent(positionRadiusInPie);
Note:
Using those attributes didn't work: "horizontalCenter", "VerticalCenter", "textAlign", "align".
EDIT: response to Bhavik Kalariya
This is what I get if I force one radius for all layers.
You can at least get it down to just using one base constant of your choosing by using an adapter approach on the label's radius to calculate the value you want for each series that is added to the chart using whatever formula you choose. Here's a basic formula that gave good results for me with your setup, where BASE_INNER_LABEL_RADIUS is set to -45.
pieSeries.labels.template.adapter.add('radius', function(radius, target) {
var chart = target.baseSprite; //chart stored in baseSprite of the AxisCircularLabel
var index = chart.series.indexOf(target.dataItem.component); //corresponding series stored in the dataItem's component
return am4core.percent(BASE_INNER_LABEL_RADIUS / (index + 2)); //Uses singular constant defined elsewhere, which is used in all series
});
This will adjust itself according to the number of series you add to the chart. You can make this as robust as you want by making further tweaks if you have fewer series and want to make the labels even more centered.
Updated fiddle
Edit
If you want to be even more generic, you can get at the slice sprite directly through the target.dataItem.sprites array (typically the first element, though you can loop through and look for an object whose className is "Slice" if you want to be super safe about it) and calculate your desired radius value using any of the numeric properties it has, such as innerRadius.
pieSeries.labels.template.adapter.add('radius', function(radius, target) {
var chart = target.baseSprite; //chart stored in baseSprite of the AxisCircularLabel
var index = chart.series.indexOf(target.dataItem.component); //corresponding series stored in the dataItem's component
return -(target.dataItem.sprites[0].innerRadius) / (index + 3);
});
You'll want to adjust this accordingly, of course.
Fiddle
All I want to do is set a simple Global Option that formats numbers with commas for Y AXIS and Tooltips. I have tried a million examples and I can not get this to work.
Version: Chart.js/2.2.2
I would like to format all numbers with commas for Y axis and tooltip values using a simple global option. The reason why this would be easier using global is that I am generating and dynamically sending in the JSON data and adding more options to that would be a pain, dynamically when all the numbers need to just behave the same.
After talking to the developer I have a really nice global method for setting y axis and tooltip number formatting. I hope this can help someone else too!
You can definitely use global options to control that.
For instance, here's how you would update so that all linear scales (the default Y axis) will format using dollar signs and nice numbers
Chart.scaleService.updateScaleDefaults('linear', {
ticks: {
callback: function(tick) {
return '$' + tick.toLocaleString();
}
}
});
Here's how you could also do the same for a tooltip
Chart.defaults.global.tooltips.callbacks.label = function(tooltipItem, data) {
var dataset = data.datasets[tooltipItem.datasetIndex];
var datasetLabel = dataset.label || '';
return datasetLabel + ": $" + dataset.data[tooltipItem.index].toLocaleString();
};
Then I asked about including simple flags for formatting within the axes and dataset settings:
I've hesitated to push to include these in the core because there are so many different options and trying to support each and every one would dramatically increase the size of the already large library. It may make sense to encourage someone to create a library of formatters that essentially automates the creation of these functions to speed up development.
References:
https://github.com/chartjs/Chart.js/issues/3294#issuecomment-246532871
https://jsfiddle.net/ChrisGo/k4Lxncvm/
I am plotting two lines on a graph in Matlab, and converting it to plot.ly using the Matlab library. When I use the 'strip' = false json property, it preserves the Matlab layout. However, it removes the nice feature where by you get all the data when you hover over one line. When 'strip' = false, you only get data pertaining to the line you hover over.
Does anyone know how to use 'strip' = false and yet retain all the hover overs?
Sample code in Matlab:
X = linspace(0,2*pi,50)';
Y = [cos(X), 0.5*sin(X)];
figure
plot(X,Y)
Then generate two plot.ly plots:
fig2plotly(gcf, 'strip', 0);
fig2plotly(gcf, 'strip', 1);
These can be respectively found at:
https://plot.ly/~alexdp/0
https://plot.ly/~alexdp/2
Note the difference in the hover over behaviour.
When you convert a matlab figure to Plotly Figure with strip=false, the hovermode attribute is set to closest by default, hence it only shows data pertaining to nearest curve on hovering. To override this behaviour:
X = linspace(0,2*pi,50);
Y = [cos(X), 0.5*sin(X)];
figure
plot(X,Y)
% Convert the chart..
plotly_fig = fig2plotly(gcf, 'strip', 0)
% Set hovermode to blank (basically disable the attribute)
plotly_fig.layout.hovermode=''
% Send the updated figure to plotly:
resp = plotly(plotly_fig)
url = resp.url
Every example I have found shows all of the scatter plot points to be of random radii. Is it possible to have them all the same size? If I try to statically set the radius all of the circles will be very small (I'm assuming the default radius). However, if I use Math.random() as in most examples there are circles large and small. I want all the circles to be large. Is there a way to do that? Here's the code snippet forming the graph data using Math.random() (this works fine for some reason):
function scatterData(xData, yData)
{
var data = [];
for (var i = 0; i < seismoNames.length; i++)
{
data.push({
key: seismoNames[i],
values: []
});
var xVals=""+xData[i];
xVals=xVals.split(",");
var yVals=""+yData[i];
yVals=yVals.split(",");
for (var j = 0; j < xVals.length; j++)
{
data[i].values.push({
x: xVals[j],
y: yVals[j],
size: Math.random()
});
}
}
return data;
}
Math.random() spits out values between 0 and 1 such as 0.164259538891095 and 0.9842195005008699. I have tried putting these as static values in the 'size' attribute, but no matter what the circles are always really small. Is there something I'm missing?
Update: The NVD3 API has changed, and now uses pointSize, pointSizeDomain, etc. instead of just size. The rest of the logic for exploring the current API without complete documentation still applies.
For NVD3 charts, the idea is that all adjustments you make can be done by calling methods on the chart function itself (or its public components) before calling that function to draw the chart in a specific container element.
For example, in the example you linked too, the chart function was initialized like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range());
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
The .showDistX() and .showDistY() turn on the tick-mark distribution in the axes; .color() sets the series of colours you want to use for the different categories. The next too lines access the default axis objects within the chart and set the number format to be a two-digit decimal. You can play around with these options by clicking on the scatterplot option from the "Live Code" page.
Unfortunately, the makers of the NVD3 charts don't have a complete documentation available yet describing all the other options you can set for each chart. However, you can use the javascript itself to let you find out what methods are available.
Inspecting a NVD3.js chart object to determine options
Open up a web page that loads the d3 and nvd3 library. The live code page on their website works fine. Then open up your developer's console command line (this will depend on your browser, search your help pages if you don't know how yet). Now, create a new nvd3 scatter chart function in memory:
var testChart = nv.models.scatterChart();
On my (Chrome) console, the console will then print out the entire contents of the function you just created. It is interesting, but very long and difficult to interpret at a glance. And most of the code is encapsulated so you can't change it easily. You want to know which properties you can change. So run this code in the next line of your console:
for (keyname in testChart){console.log(keyname + " (" + typeof(testChart[keyname]) + ")");}
The console should now print out neatly the names of all the methods and objects that you can access from that chart function. Some of these will have their own methods and objects you can access; discover what they are by running the same routine, but replacing the testChart with testChart.propertyName, like this:
for (keyname in testChart.xAxis){console.log(keyname + " (" + typeof(testChart.xAxis[keyname]) + ")");}
Back to your problem. The little routine I suggested above doesn't sort the property names in any order, but skimming through the list you should see three options that relate to size (which was the data variable that the examples were using to set radius)
size (function)
sizeDomain (function)
sizeRange (function)
Domain and range are terms used by D3 scales, so that gives me a hint about what they do. Since you don't want to scale the dots, let's start by looking at just the size property. If you type the following in the console:
testChart.size
It should print back the code for that function. It's not terribly informative for what we're interested in, but it does show me that NVD3 follows D3's getter/setter format: if you call .property(value) you set the property to that value, but if you call .property() without any parameters, it will return back the current value of that property.
So to find out what the size property is by default, call the size() method with no parameters:
testChart.size()
It should print out function (d) { return d.size || 1}, which tells us that the default value is a function that looks for a size property in the data, and if it doesn't exist returns the constant 1. More generally, it tells us that the value set by the size method determines how the chart gets the size value from the data. The default should give a constant size if your data has no d.size property, but for good measure you should call chart.size(1); in your initialization code to tell the chart function not to bother trying to determine size from the data and just use a constant value.
Going back to the live code scatterplot can test that out. Edit the code to add in the size call, like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range())
.size(1);
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
Adding that extra call successfully sets all the dots to the same size -- but that size is definitely not 1 pixel, so clearly there is some scaling going on.
First guess for getting bigger dots would be to change chart.size(1) to chart.size(100). Nothing changes, however. The default scale is clearly calculating it's domain based on the data and then outputting to a standard range of sizes. This is why you couldn't get big circles by setting the size value of every data element to 0.99, even if that would create a big circle when some of the data was 0.01 and some was 0.99. Clearly, if you want to change the output size, you're going to have to set the .sizeRange() property on the chart, too.
Calling testChart.sizeRange() in the console to find out the default isn't very informative: the default value is null (nonexistent). So I just made a guess that, same as the D3 linear scale .range() function, the expected input is a two-element array consisting of the max and min values. Since we want a constant, the max and min will be the same. So in the live code I change:
.size(1);
to
.size(1).sizeRange([50,50]);
Now something's happening! But the dots are still pretty small: definitely not 50 pixels in radius, it looks closer to 50 square pixels in area. Having size computed based on the area makes sense when sizing from the data, but that means that to set a constant size you'll need to figure out the approximate area you want: values up to 200 look alright on the example, but the value you choose will depend on the size of your graph and how close your data points are to each other.
--ABR
P.S. I added the NVD3.js tag to your question; be sure to use it as your main tag in the future when asking questions about the NVD3 chart functions.
The radius is measured in pixels. If you set it to a value less than one, yes, you will have a very small circle. Most of the examples that use random numbers also use a scaling factor.
If you want all the circles to have a constant radius you don't need to set the value in the data, just set it when you add the radius attribute.
Not sure which tutorials you were looking at, but start here: https://github.com/mbostock/d3/wiki/Tutorials
The example "Three little circles" does a good step-by-step of the different things you can do with circles:
http://mbostock.github.io/d3/tutorial/circle.html