kendoChart: Is there any way to display multiple series of differing value scales using a single valueAxis? - telerik

I'm using a single kendoChart to display up to 10 lines of data.
Each line represents process data that may have widely different context and min/max ranges, but all lines are related in time, the categoryAxis. When displayed, each valueAxis correctly shows the scale for the corresponding line.
However, with 10 lines, the 10 valueAxes take up far too much of the screen to be usable for my requirements.
I tried hiding all axes except one with the expectation that the chart would expand to fill up the space taken by the hidden axes, but that does
not happen. I get a lone axis surrounded by blank space and the chart's plot area remains the same size.
I tried setting all of the series to use the same valueAxis and then varying the valueAxis min/max per the active channel as chosen by clicking
a legend item. This expands the plot area as needed, but removes the ability to see all lines since the scale is specific to one line.
Is it possible for kendoChart to show multiple plots independently from a single valueAxis (e.g. a line with values between 0.5 and 0.7 would appear scaled to the full chart area, and so would a line with values between 25 and 100, but the valueAxis might display either scale.)

The solution I used for this problem is more code than I expected to need. Perhaps Telerik's other products have an API for this.
Essentially, I maintain a structure outside of the kendoChart that stores the real data for each series, and this real data is mapped to the expected scale of the currently visible valueAxis. The mapping function is the standard transform from one scale into another.
The valueAxis is 'swapped' depending on which legend item is clicked, and that event triggers a redraw on the chart where all the series data is mapped to the 'active' axis.
Some code snippets. A series is also described as a channel.
// The data structure.
this._channelDescriptors.push({
fullName: ch.fullName || "",
axisTitle: (ch.fullName + axisEUString) || "",
axisFont: ch.axisFont || "",
axisColor: ch.color || "#000000",
realData: [],
minData: Number.MAX_VALUE,
maxData: Number.MIN_VALUE
});
// This event causes the switching of valueAxis for all members of the series.
$("#" + chartID).kendoChart({
// Other kendoChart configurations
//
legendItemClick: function (e) {
var idx = e.seriesIndex;
sncTrender.updateAxis(idx);
e.preventDefault();
},
tooltip: {
visible: true,
template: "#=series.name# : #=kendo.format('{0:N4}', dataItem.realValue)#<br />#=kendo.format('{0:MM-dd HH:mm:ss.fff}', dataItem.Time)#",
},
//
// Other kendoChart configurations
});
// All code snippets are members of a wrapper object.
updateAxis: function (ch) {
if (this.series[ch].visible) {
this.setAxis(ch);
}
},
// Every series is set to the same valueAxis via the selected series' valueAxis.name property.
setAxis: function (ch) {
var i,
channel = this._channelDescriptors[ch];
this._currentChannel = ch;
for (i = 0; i < this.series.length; i++) {
this.series[i].axis = this._channelDescriptors[ch].fullName;
}
// Set the active valueAxis properties. This is the only axis visible maintained for the chart.
this.valueAxis.name = channel.fullName;
this.valueAxis.title.text = channel.axisTitle;
this.valueAxis.title.font = channel.axisFont;
this.valueAxis.line.color = channel.axisColor;
},
// The mapping occurs here, and the transform calculation is this line
// Y: (yRange * (chDesc.realData[k].realValue - newMin) / newRange) + this.valueAxis.min,
//
updateChart: function (allTrends) {
// ...
timeStamps = trendDataResponse.curve.Timestamp;
t1 = trendArgs.t1;
t2 = trendArgs.t2;
xValues = trendDataResponse.curve.X;
yValues = trendDataResponse.curve.Y;
pointCount = xValues.length;
min = Number.MAX_VALUE;
max = Number.MIN_VALUE;
categoryTimes = [pointCount];
newData = [];
for (l = 0; l < pointCount; l++) {
min = Math.min(min, yValues[l]);
max = Math.max(max, yValues[l]);
ts = new Date(timeStamps[l]);
categoryTimes[l] = ts;
// The Y data will be plotted on the chart, but the cursor tooltip will
// use the realValue data. In this way, the series can be visible regardless of
// the valueAxis scaling, but the actual data is also available. Refer to the
// tooltip template.
newData.push({ X: xValues[l], Y: yValues[l], realValue: yValues[l], Time: ts });
}
// Real data for each channel is stored in channelDescriptors.
chDesc = this._channelDescriptors[channelID];
chDesc.realData = newData;
chDesc.minData = min;
chDesc.maxData = max;
// The valueAxis min/max is set only for the 'active' series.
if (this._currentChannel === channelID) {
this.categoryAxis.categories = categoryTimes;
yRange = max - min;
scaleAdjustment = yRange * SNC.CONST_yAxisScaleAdjustmentFactor;
this.valueAxis.min = min - scaleAdjustment;
this.valueAxis.max = max + scaleAdjustment;
}
}
// Scale curves to current axis.
// Use real data for the current series.
for (j = 0; j < this.series.length; ++j) {
chDesc = this._channelDescriptors[j];
if (j === this._currentChannel) {
this.series[j].data = chDesc.realData;
continue;
}
// Use mapped data for all other series.
recalcData = [];
newMin = chDesc.minData;
newMax = chDesc.maxData;
newRange = newMax - newMin;
rangeAdjustment = newRange * SNC.CONST_yAxisScaleAdjustmentFactor;
newMin = newMin - rangeAdjustment;
newMax = newMax + rangeAdjustment;
for (k = 0; k < chDesc.realData.length; ++k) {
recalcData.push({
X: chDesc.realData[k].X,
Y: (yRange * (chDesc.realData[k].realValue - newMin) / newRange) + this.valueAxis.min,
realValue: chDesc.realData[k].realValue,
Time: chDesc.realData[k].Time,
});
}
this.series[j].data = recalcData;
}
chart.redraw();
}

Related

Algorithm for tiling video views

Im making an app with video chat, and need to layout the participants in a zoom/teams like screen, filling a rectangle completely. Im locking the rotation to landscape, so I expect most video will be around 16/9 aspect ration, but this CAN be cropped, so its just something to aim for.
So given n tiles and an x times y rectangle, return a list of n rectangles with position and size which will together fill completely the outer rectangle.
Hoping someone knows about an algorithm which can do this while preserving aspect ratio as good as possible!
(I tried making a simple algorithm just progressively adding a column or a row, depending on which will make tiles aspect ratio match 16/9 closest, until there is enough sub-tiles, and then "joining" unused tiles afterwards, but it came out more complex and not as good as I hoped for...)
public static List<Tile> GetTilePartitionResult(
double width, double height,
int partitions, double preferredAspectRatio = 16d/9d)
{
var columns = 1;
var rows = 1;
var lastAddedRow = false;
while (columns * rows < partitions)
{
// Find out if we should add a row or a column
var rowAddedAspect = GetAspectRatio(width, height, rows + 1, columns);
var columnAddedAspect = GetAspectRatio(width, height, rows, columns + 1);
var rowAddedDiffFromIdeal = Math.Abs(preferredAspectRatio - rowAddedAspect);
var columnAddedDiffFromIdeal = Math.Abs(preferredAspectRatio - columnAddedAspect);
if (rowAddedDiffFromIdeal < columnAddedDiffFromIdeal)
{
rows++;
lastAddedRow = true;
}
else
{
columns++;
lastAddedRow = false;
}
}
// Since after adding the "last" divider we might have an excess number of cells
// So trim the "other" dimension until there is just enough tiles
if (lastAddedRow)
{
while (((columns - 1) * rows) >= partitions) columns--;
}
else
{
while (((rows - 1) * columns) >= partitions) rows--;
}
// Assume we have the optimal grid/column setup, now distribute
// the tiles over this grid
var tileHeight = height / rows;
var tileWidth = width / columns;
var tiles = new List<Tile>();
for (var row = 0; row < rows; row++)
{
for (var column = 0; column < columns; column++)
{
var newTile = new Tile
{
Height = tileHeight,
Width = tileWidth,
XOffSet = column * tileWidth,
YOffSet = row * tileHeight,
GridX = column,
GridY = row
};
tiles.Add(newTile);
// Was this the last tile:
if (tiles.Count == partitions)
{
// Yes -> check if there is free space on this column
var extraColumns = columns - 1 - column;
if (extraColumns > 0)
{
// this extra space can be used in 2 ways,
// either expand current tile with, or expand
// height of previous row columns(the cells that are "above" the empty space)
// We decide which is best by choosing the resulting aspect ratio which
// most closely matches desired aspect ratio
var newWidthIfExpandingHorizontally = newTile.Width + (extraColumns * tileWidth);
var newHeightIfExpandingVertically = height * 2;
var aspectRatioIfExpandingHorizontally =
GetAspectRatio(newWidthIfExpandingHorizontally, height, 1, 1);
var aspectRationIfExpandingVertically =
GetAspectRatio(width, newHeightIfExpandingVertically, 1, 1);
if (Math.Abs(aspectRatioIfExpandingHorizontally - preferredAspectRatio) <
Math.Abs(aspectRationIfExpandingVertically - preferredAspectRatio))
{
// TODO: Should consider widening multiple "right" places tiles
// and move some down if extra cells > 1 .... Next time...
newTile.Width = newWidthIfExpandingHorizontally;
}
else
{
// Find all tiles in previous row above empty space and change height:
var tilesToExpand = tiles.Where(t => t.GridY == row - 1 && t.GridX > column);
foreach (var tile in tilesToExpand)
{
tile.Height = newHeightIfExpandingVertically;
}
}
}
// Nothing else to do on this column(we filled it...)
break;
}
}
}
return tiles;
}
P.S. My code is in C#, but this is really a generic algorithm-question...

How do I tweak binning for dc.js and crossfilter? Is that the performance bottleneck?

I'm trying to make a generic cross filter that can take in a csv and build a dashboard. Here are working examples:
https://ubershmekel.github.io/gfilter/?dl=https://ubershmekel.github.io/csvData/spent.csv
https://ubershmekel.github.io/gfilter/?dl=https://ubershmekel.github.io/csvData/Sacramentorealestatetransactions.csv
But for some reason the flight data is slow and unresponsive. Compare these 2 which analyze the same data:
https://ubershmekel.github.io/gfilter/?dl=https://ubershmekel.github.io/csvData/flights-3m.csv
https://github.com/square/crossfilter
I think it's because the histogram binning is too detailed but I can't find a good way to tweak that in the api reference. #gordonwoodhull mentioned:
If the binning is wrong you really want to look at the way you've set up crossfilter - dc.js just uses what it is given.
How do I tweak the binning of crossfilter? I've tried messing with the xUnits, dimension and group rounding to no avail.
This is the problem code I suspect is slow/wrong:
var dim = ndx.dimension(function (d) { return d[propName]; });
if (isNumeric(data[0][propName])) {
var theChart = dc.barChart("#" + chartId);
var countGroup = dim.group().reduceCount();
var minMax = d3.extent(data, function (d) { return +d[propName] });
var min = +minMax[0];
var max = +minMax[1];
theChart
.width(gfilter.width).height(gfilter.height)
.dimension(dim)
.group(countGroup)
.x(d3.scale.linear().domain([min, max]))
.elasticY(true);
theChart.yAxis().ticks(2);
You can adjust binning by passing a function that adjusts values to the group() method. For example, this group would create integer bins:
var countGroup = dim.group(function (v) { return Math.floor(v); });
And this one would create bins of 20 units a piece:
var countGroup = dim.group(function(d) { return Math.floor(d / 20) * 20 });
Factoring out a variable for bin size:
var bin = 20; // or any integer
var countGroup = dim.group(function(d) { return Math.floor(d / bin) * bin });
If you use binning, you'll also likely want your bars to be of a width matching your bin size. To do so, add a call to xUnits() on your bar chart. xUnits() sets the number of points on the axis:
.xUnits(function(start, end, xDomain) { return (end - start) / bin; })
See the documentation for crossfilter dimension group(), dc.js xUnits()
You can check out the results at:
https://ubershmekel.github.io/gfilter/?dl=testData/Sacramentorealestatetransactions.csv
This worked for me. I had to avoid 3 pitfalls: the group() function needed to round to the bar locations, xUnits needed the amount of bars, and making the domain (x axis) show the max value.
var numericValue = function (d) {
if (d[propName] === "")
return NaN;
else
return +d[propName];
};
var dimNumeric = ndx.dimension(numericValue);
var minMax = d3.extent(data, numericValue);
var min = minMax[0];
var max = minMax[1];
var barChart = dc.barChart("#" + chartId);
// avoid very thin lines and a barcode-like histogram
var barCount = 30;
var span = max - min;
lastBarSize = span / barCount;
var roundToHistogramBar = function (d) {
if (isNaN(d) || d === "")
d = NaN;
if (d == max)
// This fix avoids the max value always being in its own bin (max).
// I should figure out how to make the grouping equation better and avoid this hack.
d = max - lastBarSize;
var res = min + span * Math.floor(barCount * (d - min) / span) / barCount;
return res;
};
var countGroup = dimNumeric.group(roundToHistogramBar);
barChart.xUnits(function () { return barCount; });
barChart
.width(gfilter.width).height(gfilter.height)
.dimension(dimNumeric)
.group(countGroup)
.x(d3.scale.linear().domain([min - lastBarSize, max + lastBarSize]).rangeRound([0, 500]))
.elasticY(true);
barChart.yAxis().ticks(2);

Compose an image with floating point layers in webgl

I have trying to render an image in the browser which is built like this:
A bunch of rectangles are each filled with a radial gradient (ideally Gaussian, but can be approximated with a few stopping points
Each rectangle is rotated and translated before being deposited on a drawing area
The image is flattened by summing all the intensities of the rectangles (and cropping to the drawing area's dimensions )
The intensity is rescaled so that the highest intensity is 255 and the lowest 0 (ideally I can apply some sort of gamma correction too)
Finally an image is drawn where the color of each pixel is taken from a palette of 256 colors.
The reason I cannot do this easily with a canvas object is that I need to be working in floating points or I'll lose precision. I do not know in advance what the maximum intensity and minimum intensity will be, so I cannot merely draw transparent rectangles and hope for the best.
Is there a way to do this in webgl? If so, how would I go about it?
You can use the regular canvas to perform this task :
1) check min/max of your rects, so you can build a mapping function double -> [0-255] out of that range.
2) draw the rects in 'lighter' mode == add the component values.
3) you might have a saturation when several rects overlaps : if so, double the mapping range and go to 2).
Now if you don't have saturation just adjust the range to use the full [0-255] range of the canvas, and you're done.
Since this algorithm makes use of getImageData, it might not reach 60 fps on all browsers/devices. But more than 10fps on desktop/Chrome seems perfectly possible.
Hopefully the code below will clarify my description :
//noprotect
// boilerplate
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
// rectangle collection
var rectCount = 30;
var rects = buildRandRects(rectCount);
iterateToMax();
// --------------------------------------------
function iterateToMax() {
var limit = 10; // loop protection
// initialize min/max mapping based on rects min/max
updateMapping(rects);
//
while (true) {
// draw the scene using current mapping
drawScene();
// get the max int value from the canvas
var max = getMax();
if (max == 255) {
// saturation ?? double the min-max interval
globalMax = globalMin + 2 * (globalMax - globalMin);
} else {
// no sauration ? Just adjust the min-max interval
globalMax = globalMin + (max / 255) * (globalMax - globalMin);
drawScene();
return;
}
limit--;
if (limit <= 0) return;
}
}
// --------------------------------------------
// --------------------------------------------
// Oriented rectangle Class.
function Rect(x, y, w, h, rotation, min, max) {
this.min = min;
this.max = max;
this.draw = function () {
ctx.save();
ctx.fillStyle = createRadialGradient(min, max);
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.scale(w, h);
ctx.fillRect(-1, -1, 2, 2);
ctx.restore();
};
var that = this;
function createRadialGradient(min, max) {
var gd = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
var start = map(that.min);
var end = map(that.max);
gd.addColorStop(0, 'rgb(' + start + ',' + start + ',' + start + ')');
gd.addColorStop(1, 'rgb(' + end + ',' + end + ',' + end + ')');
return gd;
}
}
// Mapping : float value -> 0-255 value
var globalMin = 0;
var globalMax = 0;
function map(value) {
return 0 | (255 * (value - globalMin) / (globalMax - globalMin));
}
// create initial mapping
function updateMapping(rects) {
globalMin = rects[0].min;
globalMax = rects[0].max;
for (var i = 1; i < rects.length; i++) {
var thisRect = rects[i];
if (thisRect.min < globalMin) globalMin = thisRect.min;
if (thisRect.max > globalMax) globalMax = thisRect.max;
}
}
// Random rect collection
function buildRandRects(rectCount) {
var rects = [];
for (var i = 0; i < rectCount; i++) {
var thisMin = Math.random() * 1000;
var newRect = new Rect(Math.random() * 400, Math.random() * 400, 10 + Math.random() * 50, 10 + Math.random() * 50, Math.random() * 2 * Math.PI, thisMin, thisMin + Math.random() * 1000);
rects.push(newRect);
}
return rects;
}
// draw all rects in 'lighter' mode (=sum values)
function drawScene() {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < rectCount; i++) {
var thisRect = rects[i];
thisRect.draw();
}
ctx.restore();
}
// get maximum value for r for this canvas
// ( == max r, g, b value for a gray-only drawing. )
function getMax() {
var data = ctx.getImageData(0, 0, cv.width, cv.height).data;
var max = 0;
for (var i = 0; i < data.length; i += 4) {
if (data[i] > max) max = data[i];
if (max == 255) return 255;
}
return max;
}
<canvas id='cv' width = 400 height = 400></canvas>

How can I draw an autoscaling D3.js graph that plots a mathematical function?

I have a working jsfiddle that I made using JSXGraph, a graphing toolkit for mathematical functions. I'd like to port it to D3.js for personal edification, but I'm having a hard time getting started.
The jsfiddle graphs the value of -ke(-x/T) + k, where x is an independent variable and the values of k and t come from sliders.
board.create('functiongraph',
[
// y = -k * e(-x/t) + k
function(x) { return -k.Value()*Math.exp(-x/t.Value()) + k.Value(); },
0
]
);
The three things I'm most stumped on:
Actually drawing the graph and its axes - it's not clear to me which of the many parts of the D3 API I should be using, or what level of abstraction I should be operating at.
Re-rendering the graph when a slider is changed, and making the graph aware of the value of the sliders.
Zooming out the graph so that the asymptote defined by y = k is always visible and not within the top 15% of the graph. I do this now with:
function getAestheticBoundingBox() {
var kMag = k.Value();
var tMag = t.Value();
var safeMinimum = 10;
var limit = Math.max(safeMinimum, 1.15 * Math.max(k.Value(), t.Value()));
return [0, Math.ceil(limit), Math.ceil(limit), 0];
}
What's the right way for me to tackle this problem?
I threw this example together really quick, so don't ding me on the code quality. But it should give you a good starting point for how you'd do something like this in d3. I implemented everything in straight d3, even the sliders.
As #LarKotthoff says, the key is that you have to loop your function and build your data:
// define your function
var func = function(x) {
return -sliders.k() * Math.exp(-x / sliders.t()) + sliders.k();
},
// your step for looping function
step = 0.01;
drawPlot();
function drawPlot() {
// avoid first callback before both sliders are created
if (!sliders.k ||
!sliders.t) return;
// set your limits
var kMag = sliders.k();
var tMag = sliders.t();
var safeMinimum = 10;
var limit = Math.max(safeMinimum, 1.15 * Math.max(kMag, tMag));
// generate your data
var data = [];
for (var i = 0; i < limit; i += step) {
data.push({
x: i,
y: func(i)
})
}
// set our axis limits
y.domain(
[0, Math.ceil(limit)]
);
x.domain(
[0, Math.ceil(limit)]
);
// redraw axis
svg.selectAll("g.y.axis").call(yAxis);
svg.selectAll("g.x.axis").call(xAxis);
// redraw line
svg.select('.myLine')
.attr('d', lineFunc(data))
}

Flot - display labels on top of stacked bars

I am using the code snippet from this stackoverflow question to label my flot data points. So far this has served me well, but now I have to label the overall values of stacked bars. There are two data series and so far I've managed to calculate the sums, but I can't seem to work out a proper positioning for my labels. I'd like to place them on top of the stacks, but pointOffset only gives me the offsets based on non-stacked bars.
This is the code I am currently using, it places the labels where the second series' data points would be, if the bars weren't stacked, which puts them somewhere in the top bars.
$.each(p.getData()[1].data, function(i, el){
var series0 = p.getData()[0].data;
sum = el[1] + series0[i][2]
var o = p.pointOffset({x: el[0], y: el[1]});
$('<div class="data-point-label">' + sum + '</div>').css( {
position: 'absolute',
left: o.left - 5,
top: o.top ,
display: 'none'
}).appendTo(p.getPlaceholder()).fadeIn('slow');
});
Edit #1: So far I've tried using c2p/p2c, calculating the top value using the single data points' top values and finding more documentation on the stack plugin. I am afraid none of this has helped me much.
Edit #2: I've also tried the code given in this stackoverflow answer but it doesn't work for me. I suspect the author is using some label plugin ...
The solution to put the labels in the top of the bars in stack usinjg canvas is that you have to calculate the xPoint in base of the sum of the values in the complete stack.
Here is a example of code
var sumaArr = [];
for (var u = 0; u < p.getData().length; u++) {
$.each(p.getData()[u].data, function (i, el) {
sumaArr[i] > 0 ? sumaArr[i] = sumaArr[i] + el[1] : sumaArr[i] = el[1];
});
}
var ctx = p.getCanvas().getContext("2d");
var data = p.getData()[p.getData().length - 1].data;
var xaxis = p.getXAxes()[0];
var yaxis = p.getYAxes()[0];
var offset = p.getPlotOffset();
ctx.font = "12px 'Segoe UI'";
ctx.fillStyle = "gray";
for (var i = 0; i < data.length; i++) {
var text = sumaArr[i];
var metrics = ctx.measureText(text);
var xPos = (xaxis.p2c(data[i][0]) + offset.left) - metrics.width / 2;
var yPos = yaxis.p2c(sumaArr[i]) + offset.top - 5;
ctx.fillText(text, xPos, yPos);
}
The var yPos use the sume of the values that make the complete stack.

Resources