Algorithm for tiling video views - algorithm

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...

Related

2d circle rect collision and reflection doesnt work

I have game with map built by rectangles, darker rectangles (named "closed") mean its place where balls should be able to move, ball should reflect from the lighter rectangles(named "open") border. In future I'll add more balls and they will reflect from each other.
The problem is with new Vector after collision.
I force function circleRectGetCollisionNormal() to return vector(-1,0) what i think its normal for this case (ball is moving in right direction).
Ball is starting with degrees and change it simply to vector, this reflection worked for 45 degrees but when I change angle to 10 degrees ball moved into lighter rectangles(named "open").
Here is how it looks like (Picture)
I'm doing like this:
1-check if ball collided with lighter rectangle,
2-if it collided, I want to change direction so I return vector, for example for right side of ball colliding with rectangle return [-1,0] (I think its normal of vertical line, and its pointing left direction).
3-calculate new ball move Vector from this equation: newMoveVector = oldMoveVector − (2 * dotProduct(oldMoveVector, normalVector) * normalVector)
Here is code for each step:
1.
circleRect(circlePos, circleSize, rectPos, rectSize) {
//its rectRect collision but it doesnt matter because reflection surface is always horizontal or vertical
let r1 = {
left: circlePos.x - circleSize.x/2,
right: circlePos.x + circleSize.x/2,
top: circlePos.y - circleSize.y/2,
bottom: circlePos.y + circleSize.y/2
};
let r2 = {
left: rectPos.x,
right: rectPos.x + rectSize.x,
top: rectPos.y,
bottom: rectPos.y + rectSize.y
};
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
isOnOpenTile(pos: Vector, size: Vector) {
let openTiles = this.getTiles('open');
let result = false;
openTiles.forEach(element => {
if( this.circleRect(pos,size,element.pos,element.size) ){
result = element;
return;
}
});
return result;
}
2.
circleRectGetCollisionNormal(c, r) {
if(c.pos.y <= r.pos.y - (r.size.y/2)) return new Vector(0,-1);
//Hit was from below the brick
if(c.pos.y >= r.pos.y + (r.size.y/2)) return new Vector(0,1);
//Hit was from above the brick
if(c.pos.x < r.pos.x) return new Vector(1,0);
//Hit was on left
if(c.pos.x > r.pos.x) return new Vector(-1,0);
//Hit was on right
return false;
}
3.
getNewMoveVector(moveVector, normalVector) {
normalVector = this.normalize(normalVector);
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
return moveVector.sub(dotProduct.mult(normalVector).mult(new Vector(2,2)));
}
normalize(v) {
let length = Math.sqrt((v.x*v.x) + (v.y*v.y));
return new Vector(v.x/length,v.y/length);
}
And here is main function for this
getMoveVectorOnCollision(circle) {
let coll = this.isOnOpenTile( circle.pos, circle.size );
if( coll != false) {
let vector = this.circleRectGetCollisionNormal(circle, coll);
return this.getNewMoveVector(circle.moveVector, vector);
} else return false;
}
Object Vector always contain 2 values all of function (mult, sub, div, add) work like here.
sub(vector: Vector) {
return new Vector(this.x - vector.x, this.y - vector.y);
}
Please give me advice, actual solution or tell about different way to do this reflection. I wasted more than 3 days trying to solve this, I have to move on.
Yor dot product calculation is erroneous. Change these lines:
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
by this one line:
let dotProduct = (moveVector.x * normalVector.x + moveVector.y * normalVector.y);
Note that dotProduct is scalar value, not vector, so you have to make vector for subtraction as
subvec.x = 2 * dotProduct * normalVector.x
subvec.y = 2 * dotProduct * normalVector.y
and
return moveVector.sub(subvec);

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>

Midpoint Displacement 2D algorithm producing unusual patterns

I'm having difficulties with the Midpoint Displacement Algorithm using Haxe. I am implementing this by following the steps found here.
First, create an array that represents a blank map. You begin by giving the four corners a random value.
In this square, create the middle point by averaging the four corners and adding a small 'error', or random value. Then create the midpoints of the 4 sides by averaging the two corners each is between. After these steps, you are left with 4 squares. Repeat the steps:
Create the middle point by averaging the four corners and adding a small 'error'.
Create the midpoint of each side by averaging the two corners each point is between.
Each iteration, make the range of the RNG smaller. That way the original few points can have pretty large variation, but the later points only get tiny adjustments. This ensures the right amount of detail in an image.
Here is the function I've written to perform these steps and then normalize the values:
public static function generateFloatMatrix(Columns:Int, Rows:Int, RangeModifier:Float = 0.65):Array<Array<Float>>
{
//Blank 2D Array
var matrix:Array<Array<Float>> = InitFloatMatrix(Columns, Rows);
var range:Float = 1;
//Set Values for all four corners
matrix[0][0] = Math.random() * range;
matrix[Rows-1][0] = Math.random() * range;
matrix[0][Columns-1] = Math.random() * range;
matrix[Rows - 1][Columns - 1] = Math.random() * range;
//Calculates the amount of segments in base 2
var length = Math.sqrt((Columns * Columns) + (Rows * Rows));
var power:Int = Std.int(Math.pow(2, Math.ceil(Math.log(length) / Math.log(2))));
//Stores largest calculated value for normalization
var max:Float = 0;
var width:Int = Std.int(Columns);
var height:Int = Std.int(Rows);
var i:Int = 1;
while (i < power)
{
//Segment Size
width = Std.int(Columns / i);
height = Std.int(Rows / i);
for (y in 0...i)
{
for (x in 0...i)
{
//Top Left Coordinates per segment
var left = width * x;
var top = height * y;
//Find Midpoint
var xMid = Math.ceil(left + (width / 2));
var yMid = Math.ceil(top + (height / 2));
//Make sure right and bottom do not go out of bounds
var right:Int = (left + width < Columns ? left + width : Columns - 1);
var bottom:Int = (top + height < Rows ? top + height : Rows - 1);
//Sets midpoint value to average of all four corners.
matrix[yMid][xMid] =
(matrix[top][left] +
matrix[bottom][left] +
matrix[bottom][right] +
matrix[top][right]) / 4;
//trace ("Top: " + top + " - Left: " + left + " - Bottom: " + bottom + " - Right: " + right);
//Adds random value to midpoint
matrix[yMid][xMid] += Math.random() * range;
//Set side values to average of adjacent corners
matrix[top][xMid] = (matrix[top][left] + matrix[top][right]) / 2;
matrix[bottom][xMid] = (matrix[bottom][left] + matrix[bottom][right]) / 2;
matrix[yMid][left] = (matrix[top][left] + matrix[bottom][left]) / 2;
matrix[yMid][right] = (matrix[top][right] + matrix[bottom][right]) / 2;
max = Math.max(matrix[top][left], max);
}
}
//Reduces range
range *= RangeModifier;
i *= 2;
}
//Normalizes all values in matrix
for (y in 0...Rows)
{
for (x in 0...Columns)
{
matrix[y][x] /= max;
}
}
return matrix;
}
These are the images it is producing if I use each value to render each pixel to the specified coordinate. All the pixels that are rendered white have the value 0, black is value 1.
Your problem is that you don't necessarily hit the already populated pixels with your calculations if your map dimensions are not a power of two. For example if your map is 30 units wide, your grid width is 15 in the first pass and 7 in the second pass, where it bases its calculations on the yet untouched unit 14.
A solution is to do all calculations with floating-point arithmetic until you determine the unit indices, which must of course be integer:
while (i < power)
{
var width:Float = Columns / i; // Floating-point division
var height:Float = Rows / i;
for (y in 0...i)
{
for (x in 0...i)
{
var left:Int = Math.floor(width * x);
var top:Int = Math.floor(height * y);
var xMid:Int = Math.floor(width * (x + 0.5));
var yMid:Int = Math.floor(height * (y + 0.5));
var right:Int = Math.floor(width * (x +1));
var bottom:Int = Math.floor(height * (y + 1));
//Make sure right and bottom do not go out of bounds
if (right > Columns - 1) right = Columns - 1;
if (bottom > Rows - 1) bottom = Rows - 1;
// Do offset and interpolation stuff
}
}
}
This should give you a random map, graph-paper effect and all.
(Caveat: I'm not familiar with Haxe, but have tested this in Javascript, which doesn't have an integer type. I've used Math-floor throughout, where you'll want to do it the Haxe way.)
Finally, it looks to me that you do too many passes. I'd base the power on the maximum of the two dimensions instead of the diagonal. You can also skip the last step where wthe width is near one.

Algorithm for finding a bounded image

what would be the most effective and efficient algorithm for finding a solid-color bounded image (an image within a border, for example) given a one-dimensional array of pixel values and a threshold?
I thought of a couple.
For example:
Start at the halfway point of the image dimensions e.g. width / 2 height / 2.
loop through pixels until you hit a pixel not in your threshold. Do this for all four sides and extract dimensions from the indexes.
The problem with this algorithm is if you are given an image that is, for example, only bounded on the right side, and its width is less than half of the containing image... then this wouldn't work.
public static Rect GetBounded(this WriteableBitmap wb, int aRGBThreshold)
{
int[] pixels = wb.Pixels;
int width = wb.PixelWidth;
int height = wb.PixelHeight;
int leftIndex = (height / 2) * width;
int topIndex = width / 2;
int rightIndex = (width * (height / 2 + 1)) - 1;
int bottomIndex = width * height - (width / 2);
int left = 0, top = 0, right = 0, bottom = 0;
int i;
for (i = leftIndex; i <= rightIndex; i++)
{
if (pixels[i] < aRGBThreshold)
break;
left++;
}
for (i = topIndex; i <= bottomIndex; i += width)
{
if (pixels[i] < aRGBThreshold)
break;
top++;
}
for (i = rightIndex; i >= leftIndex; i--)
{
if (pixels[i] < aRGBThreshold)
break;
right++;
}
for (i = bottomIndex; i >= topIndex; i -= width)
{
if (pixels[i] < aRGBThreshold)
break;
bottom++;
}
return new Rect(left, top, width - right - left, height - bottom - top);
}
public static Rect GetBounded(this WriteableBitmap wb, int aThreshold, int rThreshold, int gThreshold, int bThreshold)
{
int argbthreshold = (aThreshold << 24) + (rThreshold << 16) + (gThreshold << 8) + bThreshold;
return wb.GetBounded(argbthreshold);
}
In the case you are looking for a rectangle (as your approach and code suggest), your approach is good. You could improve it by doing a binary search instead of a linear one to find the first and last object points in a row or column. This is similar to the c++ functions std::lower_bound and std::upper_bound (see http://en.cppreference.com/w/cpp/algorithm). This should be faster if your rectangles are far away from the image boundaries.
If the object can have any shape but its components are connected, probably it would be better to find a single pixel that lies in the object and do flood fill later.
If the object can have any shape and does not need to be connected, you have to traverse the whole image and keep the minimum and maximum row and column where the pixel exceeds the threshold. I think it would be enough to scan rows only, from left until you find an object pixel and from right later. If the image is stored in row-major order, it is more efficient to scan rows. If it is in column-major order, scan columns.

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

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();
}

Resources