I have seen many posts on this topic, but it doesn't seem the issue has ever been properly addressed.
We have a large scatter with about 30 points on it (nothing overwhelming). But in certain cases, the dots will be very close together or overlapping (not much we can really do about that, I guess).
The main problem is that we want the data labels visible at all times, and these data labels are overlapping when the points are close to each other.
We have tried allowOverlap: false, but that's not really what we need/want. Our ideal outcome is allowing all datalabels to be displayed on screen inside the scatter while still being able to read each one at all times.
Do we fix this by adjusting the separation of the dots or by adjusting the separation/padding of the datalabels? Any suggestions? Thank you.
I haven't found a working configuration solution of this problem from Highcharts (although I cannot guarantee there isn't one in latest version). However there are some algorithms for acceptable randomization of the labels coordinates that split data labels.
Here are some useful links that could help you with the algorithm:
wordcloud package in R (cloud.R is the file containing the algorithm)
direct labels package in R
And some dummy pseudo code translation in JavaScript of the R code would be:
splitLabels: function() {
// Create an array of x-es and y-es that indicate where your data lie
var xArr = getAllDataX();
var yArr = getAllDataY();
var labelsInfo = {};
this.chartSeries.forEach(function(el) {
var text = el.data.name;
labelsInfo[el.data.id] = {
height: getHeight(text),
width: getWidth(text),
text: text
};
}, this);
var sdx = getStandardDeviation(xArr);
var sdy = getStandardDeviation(yArr);
if(sdx === 0) sdx = 1;
if(sdy === 0) sdy = 1;
var boxes = [];
var xlim = [], ylim = [];
xlim[0] = this.chart.xAxis[0].getExtremes().min;
xlim[1] = this.chart.xAxis[0].getExtremes().max;
ylim[0] = this.chart.yAxis[0].getExtremes().min;
ylim[1] = this.chart.yAxis[0].getExtremes().max;
for (var i = 0; i < data.length; i++) {
var pointX = data[i].x;
var pointY = data[i].y;
if (pointX<xlim[0] || pointY<ylim[0] || pointX>xlim[1] || pointY>ylim[1]) continue;
var theta = Math.random() * 2 * Math.PI,
x1 = data[i].x,
x0 = data[i].x,
y1 = data[i].y,
y0 = data[i].y,
width = labelsInfo[data[i].id].width,
height = labelsInfo[data[i].id].height ,
tstep = Math.abs(xlim[1] - xlim[0]) > Math.abs(ylim[1] - ylim[0]) ? Math.abs(ylim[1] - ylim[0]) / 100 : Math.abs(xlim[1] - xlim[0]) / 100,
rstep = Math.abs(xlim[1] - xlim[0]) > Math.abs(ylim[1] - ylim[0]) ? Math.abs(ylim[1] - ylim[0]) / 100 : Math.abs(xlim[1] - xlim[0]) / 100,
r = 0;
var isOverlapped = true;
while(isOverlapped) {
if((!hasOverlapped(x1-0.5*width, y1-0.5*height, width, height, boxes)
&& x1-0.5*width>xlim[0] && y1-0.5*height>ylim[0] && x1+0.5*width<xlim[1] && y1+0.5*height<ylim[1]) )
{
boxes.push({
leftX: x1-0.5*width,
bottomY: y1-0.5*height,
width: width,
height: height,
icon: false,
id: data[i].id,
name: labelsInfo[data[i].id].text
});
data[i].update({
name: labelsInfo[data[i].id].text,
dataLabels: {
x: (x1 - data[i].x),
y: (data[i].y - y1)
}
}, false);
isOverlapped = false;
} else {
theta = theta+tstep;
r = r + rstep*tstep/(2*Math.PI);
x1 = x0+sdx*r*Math.cos(theta);
y1 = y0+sdy*r*Math.sin(theta);
}
}
}
// You may have to redraw the chart here
},
You can call this function on redraw or optimized to call it less often.
Please note that if you have some big points or shapes or icons indicating where your data items lie you will have to check if any of the proposed solutions does not interfere(overlap) with the icons as well.
You can try to adapt this algorithm:
function StaggerDataLabels(series) {
sc = series.length;
if (sc < 2) return;
for (s = 1; s < sc; s++) {
var s1 = series[s - 1].points,
s2 = series[s].points,
l = s1.length,
diff, h;
for (i = 0; i < l; i++) {
if (s1[i].dataLabel && s2[i].dataLabel) {
diff = s1[i].dataLabel.y - s2[i].dataLabel.y;
h = s1[i].dataLabel.height + 2;
if (isLabelOnLabel(s1[i].dataLabel, s2[i].dataLabel)) {
if (diff < 0) s1[i].dataLabel.translate(s1[i].dataLabel.translateX, s1[i].dataLabel.translateY - (h + diff));
else s2[i].dataLabel.translate(s2[i].dataLabel.translateX, s2[i].dataLabel.translateY - (h - diff));
}
}
}
}
}
//compares two datalabels and returns true if they overlap
function isLabelOnLabel(a, b) {
var al = a.x - (a.width / 2);
var ar = a.x + (a.width / 2);
var bl = b.x - (b.width / 2);
var br = b.x + (b.width / 2);
var at = a.y;
var ab = a.y + a.height;
var bt = b.y;
var bb = b.y + b.height;
if (bl > ar || br < al) {
return false;
} //overlap not possible
if (bt > ab || bb < at) {
return false;
} //overlap not possible
if (bl > al && bl < ar) {
return true;
}
if (br > al && br < ar) {
return true;
}
if (bt > at && bt < ab) {
return true;
}
if (bb > at && bb < ab) {
return true;
}
return false;
}
http://jsfiddle.net/menXU/6/
Related
I'm encountering a big problem when using the number 0 (zero) as a factor for the colors to generate scales, the numbers close to 0 (zero) end up becoming almost white, impossible to see a difference.
The idea is that above 0 (zero) it starts green and gets even stronger and below 0 (zero) starting with a red one and getting stronger.
I really need any number, even if it's 0.000001 already has a visible green and the -0.000001 has a visible red.
Link to SpreadSheet:
https://docs.google.com/spreadsheets/d/1uN5rDEeR10m3EFw29vM_nVXGMqhLcNilYrFOQfcC97s/edit?usp=sharing
Note to help with image translation and visualization:
Número = Number
Nenhum = None
Valor Máx. = Max Value
Valor Min. = Min Value
Current Result / Expected Result
After reading your new comments I understand that these are the requisites:
The values above zero should be green (with increased intensity the further beyond zero).
The values below zero should be red (with increased intensity the further beyond zero).
Values near zero should be coloured (not almost white).
Given those requisites, I developed an Apps Script project that would be useful in your scenario. This is the full project:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu("Extra").addItem("Generate gradient", "parseData").addToUi();
}
function parseData() {
var darkestGreen = "#009000";
var lighestGreen = "#B8F4B8";
var darkestRed = "#893F45";
var lighestRed = "#FEBFC4";
var range = SpreadsheetApp.getActiveRange();
var data = range.getValues();
var biggestPositive = Math.max.apply(null, data);
var biggestNegative = Math.min.apply(null, data);
var greenPalette = colourPalette(darkestGreen, lighestGreen, biggestPositive);
var redPalette = colourPalette(darkestRed, lighestRed, Math.abs(
biggestNegative) + 1);
var fullPalette = [];
for (var i = 0; i < data.length; i++) {
if (data[i] > 0) {
var cellColour = [];
cellColour[0] = greenPalette[data[i] - 1];
fullPalette.push(cellColour);
} else if (data[i] < 0) {
var cellColour = [];
cellColour[0] = redPalette[Math.abs(data[i]) - 1];
fullPalette.push(cellColour);
} else if (data[i] == 0) {
var cellColour = [];
cellColour[0] = null;
fullPalette.push(cellColour);
}
}
range.setBackgrounds(fullPalette);
}
function colourPalette(darkestColour, lightestColour, colourSteps) {
var firstColour = hexToRGB(darkestColour);
var lastColour = hexToRGB(lightestColour);
var blending = 0.0;
var gradientColours = [];
for (i = 0; i < colourSteps; i++) {
var colour = [];
blending += (1.0 / colourSteps);
colour[0] = firstColour[0] * blending + (1 - blending) * lastColour[0];
colour[1] = firstColour[1] * blending + (1 - blending) * lastColour[1];
colour[2] = firstColour[2] * blending + (1 - blending) * lastColour[2];
gradientColours.push(rgbToHex(colour));
}
return gradientColours;
}
function hexToRGB(hex) {
var colour = [];
colour[0] = parseInt((removeNumeralSymbol(hex)).substring(0, 2), 16);
colour[1] = parseInt((removeNumeralSymbol(hex)).substring(2, 4), 16);
colour[2] = parseInt((removeNumeralSymbol(hex)).substring(4, 6), 16);
return colour;
}
function removeNumeralSymbol(hex) {
return (hex.charAt(0) == '#') ? hex.substring(1, 7) : hex
}
function rgbToHex(rgb) {
return "#" + hex(rgb[0]) + hex(rgb[1]) + hex(rgb[2]);
}
function hex(c) {
var pool = "0123456789abcdef";
var integer = parseInt(c);
if (integer == 0 || isNaN(c)) {
return "00";
}
integer = Math.round(Math.min(Math.max(0, integer), 255));
return pool.charAt((integer - integer % 16) / 16) + pool.charAt(integer % 16);
}
First of all the script will use the Ui class to show a customised menu called Extra. That menu calls the main function parseData, that reads the whole selection data with getValues. That function holds the darkest/lightest green/red colours. I used some colours for my example, but I advise you to edit them as you wish. Based on those colours, the function colourPalette will use graphical linear interpolation between the two colours (lightest and darkest). That interpolation will return an array with colours from darkest to lightest, with as many in-betweens as the maximum integer in the column. Please notice how the function uses many minimal functions to run repetitive tasks (converting from hexadecimal to RGB, formatting, etc…). When the palette is ready, the main function will create an array with all the used colours (meaning that it will skip unused colours, to give sharp contrast between big and small numbers). Finally, it will apply the palette using the setBackgrounds method. Here you can see some sample results:
In that picture you can see one set of colours per column. Varying between random small and big numbers, numerical series and mixed small/big numbers. Please feel free to ask any doubt about this approach.
A very small improvement to acques-Guzel Heron
I made it skip all non numeric values, beforehand it just errored out.
I added an option in the menu to use a custom range.
Thank you very much acques-Guzel Heron
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Extra')
.addItem('Generate gradient', 'parseData')
.addItem('Custom Range', 'customRange')
.addToUi();
}
function parseData(customRange = null) {
const darkestGreen = '#009000';
const lighestGreen = '#B8F4B8';
const darkestRed = '#893F45';
const lighestRed = '#FEBFC4';
let range = SpreadsheetApp.getActiveRange();
if (customRange) {
range = SpreadsheetApp.getActiveSpreadsheet().getRange(customRange);
}
const data = range.getValues();
const biggestPositive = Math.max.apply(null, data.filter(a => !isNaN([a])));
const biggestNegative = Math.min.apply(null, data.filter(a => !isNaN([a])));
const greenPalette = colorPalette(darkestGreen, lighestGreen, biggestPositive);
const redPalette = colorPalette(darkestRed, lighestRed, Math.abs(biggestNegative) + 1);
const fullPalette = [];
for (const datum of data) {
if (datum > 0) {
fullPalette.push([greenPalette[datum - 1]]);
} else if (datum < 0) {
fullPalette.push([redPalette[Math.abs(datum) - 1]]);
} else if (datum == 0 || isNaN(datum)) {
fullPalette.push(['#ffffff']);
}
}
range.setBackgrounds(fullPalette);
}
function customRange() {
const ui = SpreadsheetApp.getUi();
result = ui.prompt("Please enter a range");
parseData(result.getResponseText());
}
function colorPalette(darkestColor, lightestColor, colorSteps) {
const firstColor = hexToRGB(darkestColor);
const lastColor = hexToRGB(lightestColor);
let blending = 0;
const gradientColors = [];
for (i = 0; i < colorSteps; i++) {
const color = [];
blending += (1 / colorSteps);
color[0] = firstColor[0] * blending + (1 - blending) * lastColor[0];
color[1] = firstColor[1] * blending + (1 - blending) * lastColor[1];
color[2] = firstColor[2] * blending + (1 - blending) * lastColor[2];
gradientColors.push(rgbToHex(color));
}
return gradientColors;
}
function hexToRGB(hex) {
const color = [];
color[0] = Number.parseInt((removeNumeralSymbol(hex)).slice(0, 2), 16);
color[1] = Number.parseInt((removeNumeralSymbol(hex)).slice(2, 4), 16);
color[2] = Number.parseInt((removeNumeralSymbol(hex)).slice(4, 6), 16);
return color;
}
function removeNumeralSymbol(hex) {
return (hex.charAt(0) == '#') ? hex.slice(1, 7) : hex;
}
function rgbToHex(rgb) {
return '#' + hex(rgb[0]) + hex(rgb[1]) + hex(rgb[2]);
}
function hex(c) {
const pool = '0123456789abcdef';
let integer = Number.parseInt(c, 10);
if (integer === 0 || isNaN(c)) {
return '00';
}
integer = Math.round(Math.min(Math.max(0, integer), 255));
return pool.charAt((integer - integer % 16) / 16) + pool.charAt(integer % 16);
}
So I am working on creating a docker image that contains the AI for Connect 4. I have tried looking up tutorials and tried doing code but I am stuck. If you are in the defi space, you might have heard of golem network, I am currently just trying to implement Connect 4 into it. I would truly be happy with any help. Thx
Here is my code for the AI (this is currently not a docker image)
C4.AI = function(_game, _player, _strength) {
var _rack = _game.rack;
var _columns = _rack.length;
var _rows = _rack[0].length;
var _best_col = 0;
_player.human = false;
function findAndPlayMove() {
if (_game.current === _player) {
// Give the previous move's drop animation some time to finish
setTimeout(function() {
var best = alphabeta(_strength, -Infinity, Infinity, _player);
var r = _game.util.getDropRow(_best_col);
_game.trigger('drop', { col_index : _best_col });
_best_col = 0;
}, 500);
}
}
function alphabeta(depth, alpha, beta, player) {
var value = evaluateBoard(player);
// If maximum search depth is reached or this board is a win/loss we
// don't need to look further
if (depth === 0 || value === Infinity || value === -Infinity) {
return value;
}
// Calculate moves for this AI player
if (player === _player) {
var scores = [];
// For each column calculate the max possible score
// (player tries to maximize)
for (var c = 0; c < _columns; c++) {
var r = _game.util.getDropRow(c);
// This column is already full of coins
if (r === -1) continue;
// Temporarily store move
_rack[c][r] = player;
// Recursively calculate the best result this move could have
// for this player
alpha = Math.max(alpha, alphabeta(depth - 1, alpha, beta, player.opponent));
// Undo the move
delete _rack[c][r];
scores[c] = alpha;
if (beta <= alpha) break;
}
if (depth === _strength) {
var max_score = -Infinity;
var last_valid = null;
for (var i = 0; i < scores.length; i++) {
// TODO: find out why >= screws up AI
var score = scores[i];
if (score > max_score) {
max_score = score;
_best_col = i;
}
if (score) last_valid = i;
}
// All moves by player will lead to loss
// Just pick a valid column
if (max_score === -Infinity) {
_best_col = last_valid;
}
}
return alpha;
} else {
// For each column calculate the min possible score
// (opponent tries to minimize)
for (var c = 0; c < _columns; c++) {
var r = _game.util.getDropRow(c);
// This column is already full
if (r === -1) continue;
// Temporarily store move
_rack[c][r] = player;
// Recursively calculate the best result this move could have
// for oppponent
beta = Math.min(beta, alphabeta(depth - 1, alpha, beta, player.opponent));
// Undo the move
delete _rack[c][r];
// If the opponent's score for this column is <= to our player's
// maximum there's no need to further explore this branch: it
// would never lead to a better score
if (beta <= alpha) {
break;
}
}
return beta;
}
}
/* ################################################################################
UTILITIES
################################################################################ */
function evaluateBoard(player) {
// Some pre-calculated values for each rack position, based on the
// number of possible 4-in-a-rows a position could theoretically be part of
var values = [
[ 3, 4, 5, 5, 4, 3 ],
[ 4, 6, 8, 8, 6, 4 ],
[ 5, 8, 11, 11, 8, 5 ],
[ 7, 10, 13, 13, 10, 7 ],
[ 5, 8, 11, 11, 8, 5 ],
[ 4, 6, 8, 8, 6, 4 ],
[ 3, 4, 5, 5, 4, 3 ]
];
var patterns = {
'2 connected, empty on the left': {
rx: /__#{2}[^#_]/g,
value: 10
},
'2 connected, empty on the right': {
rx: /[^#_]#{2}__/g,
value: 10
},
'2 connected, empty on both sides': {
rx: /_#{2}_/g,
value: 20
},
'3 connected, empty on the left': {
rx: /_#{3}/g,
value: 50
},
'3 connected, empty on the right': {
rx: /#{3}_/g,
value: 50
},
'3 connected, empty middle left': {
rx: /#_#{2}/g,
value: 50
},
'3 connected, empty middle right': {
rx: /#{2}_#/g,
value: 50
},
'3 connected, empty on both sides': {
rx: /_#{3}_/g,
value: 100
}
};
var views = [
getSouthView(player),
getEastView(player),
getSouthWestView(player),
getSouthEastView(player)
];
var score = 0;
$.each(views, function(i, view) {
var player_view = view.replace(/X/g, '#');
var opponent_view = view.replace(/O/g, '#');
if (opponent_view.match(/#{4}/)) {
score = -Infinity;
return false;
}
if (player_view.match(/#{4}/)) {
score = Infinity;
return false;
}
$.each(patterns, function(name, pattern) {
var matches = player_view.match(pattern.rx);
if (matches) {
score += matches.length * pattern.value;
}
matches = opponent_view.match(pattern.rx);
if (matches) {
score -= matches.length * pattern.value;
}
});
});
return score;
}
function isCell(c, r) {
return 0 <= c && c < _columns && 0 <= r && r < _rows;
}
function getCellChar(c, r, player) {
var cell = _rack[c][r];
if (cell === _player) {
return 'X';
} else if (cell) {
return 'O';
}
return '_';
}
/* ################################################################################
VIEWS
################################################################################ */
function getEastView(player) {
var a = [];
for (var r = 0; r < _rows; r++) {
a.push('\n');
for (var c = 0; c < _columns; c++) {
a.push(getCellChar(c, r, player));
}
}
return a.join('') + '\n';
}
function getSouthView(player) {
var a = [];
for (var c = 0; c < _columns; c++) {
a.push('\n');
for (var r = 0; r < _rows; r++) {
a.push(getCellChar(c, r, player));
}
}
return a.join('') + '\n';
}
function getSouthWestView(player) {
var c = 0;
var r = 0;
var max = _columns * _rows;
var counter = 0;
var a = [];
a.push('\n');
while (counter != max) {
if (isCell(c, r)) {
var cell = _rack[c][r];
a.push(getCellChar(c, r, player));
counter++;
c++;
r--;
} else if (r < 0) {
a.push('\n');
r = c;
c = 0;
} else {
c++;
r--;
}
}
return a.join('') + '\n';
}
function getSouthEastView(player) {
var c = _columns - 1;
var r = 0;
var max = _columns * _rows;
var counter = 0;
var a = [];
a.push('\n');
while (counter != max) {
if (isCell(c, r)) {
var cell = _rack[c][r];
a.push(getCellChar(c, r, player));
counter++;
c--;
r--;
} else if (r < 0) {
a.push('\n');
r = _columns - c - 1;
c = _columns - 1;
} else {
c--;
r--;
}
}
return a.join('') + '\n';
}
_game.on('waitingForDrop', findAndPlayMove);
};
ok so the reason for this question is that i am trying to deal with multiple konva shapes at a time. in the original project the shapes are being selected by drawing a momentary rectangle around the shapes that you want selected (rectangular selection). I have seen some of the other post about this, but they only seem to deal with the selection itself, i have that working.
Here is a codepen example that illustrates the problem.
link
Instructions:
click the select button to have the two shapes put in a group and a transformer applied
Rotate and scale the selected shapes.
click the deselect button to have the shapes moved back onto the layer.
The parts that is interresting is after line 92, where i am exploring different methods of moving the shapes back onto the layer.
children.toArray().forEach(e => {
// Need to apply transformations correctly before putting back on layer
//Method 1
if (method === 1) {
let newTransforms = e.getAbsoluteTransform();
let localTransforms = e.getTransform();
let m = newTransforms.getMatrix();
let matrices = getMatrix(e);
console.log("matrix before : ");
console.log(matrices);
e.rotation(selectionGroupRotation);
e.skew({ x: m[1], y: m[2] });
e.scale({ x: m[0], y: m[3] });
e.position({ x: m[4], y: m[5] })
m = newTransforms.getMatrix();
matrices = getMatrix(e);
console.log("matrix after : ");
// console.log(m);
console.log(matrices);
}
//Method 2
if (method === 2) {
let groupPos = selectionGroup.position();
let point = { x: groupPos.x, y: groupPos.y };
let groupScale = selectionGroup.scale();
let groupRotation = selectionGroup.rotation();
let configGroupMatrix = selectionGroup.getTransform();
let newpos = configGroupMatrix.point(point);
e.rotation(selectionGroupRotation + e.rotation());
e.scaleX(groupScale.x * e.scaleX());
e.scaleY(groupScale.y * e.scaleY());
let finalpos = {
x: groupPos.x + e.x(),
y: groupPos.y + e.y()
}
e.x(finalpos.x);
e.y(finalpos.y);
}
e.moveTo(layer);
})
The frustrating part is that the function getAbsoluteTransform() seem to give a transformed matrix, but you can't set the transformation matrix of a shape directly. But the solution might be as simple as setting the shapes matrix to the one returned from getAbsoluteTransform()
Currently, there are no methods to in Konva core to calculate attributes from the matrix. But you can easily find them online.
https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
extract rotation, scale values from 2d transformation matrix
From the answers, I made this function to get attrs:
function decompose(mat) {
var a = mat[0];
var b = mat[1];
var c = mat[2];
var d = mat[3];
var e = mat[4];
var f = mat[5];
var delta = a * d - b * c;
let result = {
x: e,
y: f,
rotation: 0,
scaleX: 0,
scaleY: 0,
skewX: 0,
skewY: 0,
};
// Apply the QR-like decomposition.
if (a != 0 || b != 0) {
var r = Math.sqrt(a * a + b * b);
result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r);
result.scaleX = r;
result.scaleY = delta / r;
result.skewX = Math.atan((a * c + b * d) / (r * r));
result.scleY = 0;
} else if (c != 0 || d != 0) {
var s = Math.sqrt(c * c + d * d);
result.rotation =
Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s));
result.scaleX = delta / s
result.scaleY = s;
result.skewX = 0
result.skewY = Math.atan((a * c + b * d) / (s * s));
} else {
// a = b = c = d = 0
}
result.rotation *= 180 / Math.PI;
return result;
}
Then you can use that function to calculate attributes from the absolute transform.
Demo: https://codepen.io/lavrton/pen/dwGPBz?editors=1010
I created in webgl a javascript file that needs to draw a cone. If I choose a low circle division, it work perfectly, and all the lines are displayed. But at high one, high from 255, it breaks. It seems not all of the vertices are linked. I can not understand why the difference.
case 'cone':{
var CONE_DIV = 255;
var angolo = 360 / CONE_DIV;
var altezza = 1.0;
// Coordinates
var vertices = [];
vertices.push(0.0); //v0t X
vertices.push(0.0); //v0t Y
vertices.push(0.0); //v0t Z
vertices.push(0.0); //v0 X
vertices.push(altezza); //v0 Y
vertices.push(0.0); //v0 Z
for (var i = 0; i < CONE_DIV; i++) {
var ai = i * 2 * Math.PI / CONE_DIV;
var si = Math.sin(ai);
var ci = Math.cos(ai);
vertices.push(ci); //coordinate X
vertices.push(0.0); //coordinate Y
vertices.push(si); //coordinate Z
}
// Colors
var colors = [];
for (var k = 0; k < CONE_DIV; k++) {
for (var t = 0; t < 2; t++) {
colors.push(a);
colors.push(b);
colors.push(c);
}
}
// Indices of the vertices
var indices = [];
//index high vertex
for (var j = 1; j <= CONE_DIV; j++) {
indices.push(1);
indices.push(j+1);
var l = j + 2; //last vertex base - return to first
if (l == CONE_DIV + 2) {
indices.push(2);
} else {
indices.push(l);
}
}
//index base
for (var j = 1; j <= CONE_DIV; j++) {
indices.push(0);
indices.push(j+1);
var l = j+2;
if (l == CONE_DIV + 2) { //last vertex base - return to first
indices.push(2);
} else {
indices.push(l);
}
}
I need to change size of bubbles(points) by supplying 4th value in data points in Highcharts' 3D scatter chart. I couldn't find any way how to do this. Can anyone help ?
It seems that it is not supported out of the box. Although in this Thread in the Highcharts-Forum, a wrapper is shown that allows a 4th w value to be used as the size of the bubble (see http://jsfiddle.net/uqLfm1k6/1/):
(function (H) {
H.wrap(H.seriesTypes.bubble.prototype, 'getRadii', function (proceed, zMin, zMax, minSize, maxSize) {
var math = Math,
len,
i,
pos,
zData = this.zData,
wData = this.userOptions.data.map( function(e){ return e.w }), // ADDED
radii = [],
options = this.options,
sizeByArea = options.sizeBy !== 'width',
zThreshold = options.zThreshold,
zRange = zMax - zMin,
value,
radius;
// Set the shape type and arguments to be picked up in drawPoints
for (i = 0, len = zData.length; i < len; i++) {
// value = zData[i]; // DELETED
value = this.chart.is3d()? wData[i] : zData[i]; // ADDED
// When sizing by threshold, the absolute value of z determines the size
// of the bubble.
if (options.sizeByAbsoluteValue && value !== null) {
value = Math.abs(value - zThreshold);
zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
zMin = 0;
}
if (value === null) {
radius = null;
// Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size
} else if (value < zMin) {
radius = minSize / 2 - 1;
} else {
// Relative size, a number between 0 and 1
pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
if (sizeByArea && pos >= 0) {
pos = Math.sqrt(pos);
}
radius = math.ceil(minSize + pos * (maxSize - minSize)) / 2;
}
radii.push(radius);
}
this.radii = radii;
});
}(Highcharts));