I am using Morris donut charts and I want to try and ensure that the segments always start at 12 o'clock so there is some consistency when viewing multiple charts together on the same screen.
Previously I used jQuery circliful https://github.com/pguso/jquery-plugin-circliful which worked great but the quality was really fuzzy on Retina screens so I checked out Morris and it does what I want bar this.
I want something that looks like the first where the segment always starts at the top middle:
Does anyone know how to achieve this?
var donut= Morris.Donut({
element: 'donut-example',
data: [
{label: "New clients", value: 35},
{label: "In-Store Sales", value: 65}
],
colors: ['#90c070', '#eeeeee'],
select :0
});
donut.select(0);
http://jsfiddle.net/0t976ez6/
No, as far as I can tell from the source code, there's no API (public or private) in Morris to do that. See Donut's redraw() for how it renders the chart: it always starts from the lowest point and goes in counterclockwise direction.
Now, I would recommend to consider switching to other chart libraries, before proceeding. For example there's a D3.js — not the simplest library out there, but it provides so much freedom to do almost anything, it sometimes may be hard to work with. But there is a ton of examples of every thing you could think of. Like, here's a blog post about creating similar looking donut charts using D3.
But hey, it's a JavaScript, we can patch things, so nothing is impossible!
Warning: patching library code is not the best idea! You must be careful and do that only when it's really the only way.
The easiest (certainly not the simplest) way would be to monkey-patch Morris.Donut.redraw() to start from the Math.PI (middle top) and go in clockwise direction. To do that, the initial value of last local variable should be changed from 0 to Math.PI and logic that finds next starting point tweaked a bit.
Here's is a working example where I've surrounded lines that were patched with special PATCHED comments:
/**
* Careful: patched lib-function!
*
* In case of problems, see original
*/
Morris.Donut.prototype.redraw = function() {
var C, cx, cy, i, idx, last, max_value, min, next, seg, total, value, w, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _results;
this.raphael.clear();
cx = this.el.width() / 2;
cy = this.el.height() / 2;
w = (Math.min(cx, cy) - 10) / 3;
total = 0;
_ref = this.values;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
value = _ref[_i];
total += value;
}
min = 5 / (2 * w);
C = 1.9999 * Math.PI - min * this.data.length;
// ------------ PATCHED: start from the top
last = Math.PI;
// original:
// last = 0
// ------------
idx = 0;
this.segments = [];
_ref1 = this.values;
for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) {
value = _ref1[i];
// ------------ PATCHED: change direction of rendering
next = last - min - C * (value / total);
seg = new Morris.DonutSegment(cx, cy, w * 2, w, next, last, this.data[i].color || this.options.colors[idx % this.options.colors.length], this.options.backgroundColor, idx, this.raphael);
// original:
// next = last + min + C * (value / total);
// seg = new Morris.DonutSegment(cx, cy, w * 2, w, last, next, this.data[i].color || this.options.colors[idx % this.options.colors.length], this.options.backgroundColor, idx, this.raphael);
// ------------
seg.render();
this.segments.push(seg);
seg.on('hover', this.select);
seg.on('click', this.click);
last = next;
idx += 1;
}
this.text1 = this.drawEmptyDonutLabel(cx, cy - 10, this.options.labelColor, 15, 800);
this.text2 = this.drawEmptyDonutLabel(cx, cy + 10, this.options.labelColor, 14);
max_value = Math.max.apply(Math, this.values);
idx = 0;
_ref2 = this.values;
_results = [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
value = _ref2[_k];
if (value === max_value) {
this.select(idx);
break;
}
_results.push(idx += 1);
}
return _results;
};
var donut = Morris.Donut({
element: 'donut-example',
data: [{
label: "In-Store Sales",
value: 55
}, {
label: "New clients",
value: 25
}, {
label: "Reseller stores",
value: 20
}],
colors: ['#90c070', '#ee9090', '#9090ee'],
select: 0
});
donut.select(0);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://rawgit.com/morrisjs/morris.js/master/morris.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<link href="http://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css" rel="stylesheet" />
<div id="donut-example" style="height: 250px;"></div>
Of course, there's may be another way to achieve the same thing: maybe I overlooked some local variable changing sign of which will result in the same rendering.
Related
I'm receiving all distances between a random number of points in a 2 dimensional coordinate system.
How can I visualize this as coordinates on a map in my browser?
In case there are many solutions I just want to see the first possible one that my algorithm can come up with.
So here's an extremely easy example:
PointCount = 3
Distances:
0-1 = 2
0-2 = 4
1-2 = 2
Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement?
I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points.
The above example is simplified -
Real distance values could look like this:
(0) (1) (2) (3)
(0) 0 2344 3333 10000
(1) 0 3566 10333
(2) 0 12520
I'm not sure this is relevant for SO, but anyway...
The way to do this is quite simply to place the points one by one using the data:
Pick a random location for the first point (let's say it's 0,0).
The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0).
The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions.
The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution.
Continue like this until all points have been placed.
Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference.
A quick and dirty implementation (not handling quite a few cases, and tested very little):
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}
// adapted from https://stackoverflow.com/a/12221389/3527940
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h / d);
ry = dx * (h / d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [
[xi, yi],
[xi_prime, yi_prime]
];
}
function generateData(nbPoints) {
var i, j, k;
var originalPoints = [];
for (i = 0; i < nbPoints; i++) {
originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]);
}
var data = [];
var distances;
for (i = 0; i < nbPoints; i++) {
distances = [];
for (j = 0; j < i; j++) {
distances.push(distance(originalPoints[i], originalPoints[j]));
}
data.push(distances);
}
//console.log("original points", originalPoints);
//console.log("distance data", data);
return data;
}
function findPointsForDistances(data, threshold) {
var points = [];
var solutions;
var solutions1, solutions2;
var point;
var i, j, k;
if (!threshold)
threshold = 0.01;
// First point, arbitrarily set at 0,0
points.push([0, 0]);
// Second point, arbitrarily set at d(0,1),0
points.push([data[1][0], 0]);
// Third point, intersection of two circles, pick any solution
solutions = intersection(
points[0][0], points[0][1], data[2][0],
points[1][0], points[1][1], data[2][1]);
//console.log("possible solutions for point 3", solutions);
points.push(solutions[0]);
//console.log("solution for points 1, 2 and 3", points);
found = true;
// Subsequent points, intersections of n-1 circles, use first two to find 2 solutions,
// the 3rd to pick one of the two
// then use others to check it's valid
for (i = 3; i < data.length; i++) {
// distances to points 1 and 2 give two circles and two possible solutions
solutions = intersection(
points[0][0], points[0][1], data[i][0],
points[1][0], points[1][1], data[i][1]);
//console.log("possible solutions for point " + (i + 1), solutions);
// try to find which solution is compatible with distance to point 3
found = false;
for (j = 0; j < 2; j++) {
if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) {
point = solutions[j];
found = true;
break;
}
}
if (!found) {
console.log("could not find solution for point " + (i + 1));
console.log("distance data", data);
console.log("solution for points 1, 2 and 3", points);
console.log("possible solutions for point " + (i + 1), solutions);
console.log("distances to point 3",
distance(solutions[0], points[2]),
distance(solutions[1], points[2]),
data[i][2]
);
break;
}
// We have found a solution, we need to check it's valid
for (j = 3; j < i; j++) {
if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) {
console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1));
found = false;
break;
}
}
if (!found) {
console.log("stopping");
break;
}
points.push(point);
}
if (found) {
//console.log("complete solution", points);
return points;
}
}
console.log(findPointsForDistances([
[],
[2344],
[3333, 3566],
[10000, 10333, 12520],
]));
console.log(findPointsForDistances([
[],
[2],
[4, 2],
]));
console.log(findPointsForDistances([
[],
[4000],
[5000, 3000],
[3000, 5000, 4000]
]));
console.log(findPointsForDistances([
[],
[2928],
[4938, 3437],
[10557, 10726, 13535]
]));
var nbPoints, i;
for (nbPoints = 4; nbPoints < 8; nbPoints++) {
for (i = 0; i < 10; i++) {
console.log(findPointsForDistances(generateData(nbPoints)));
}
}
Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like:
y = canvasHeight - y
If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let scale = 10;
let radius = 10;
function point(x, y) {
ctx.fillRect(x*scale, y*scale, radius, radius);
}
// test
point(10, 15);
point(20, 8);
<html>
<body>
<canvas id="canvas" width=1000 height=1000></canvas>
</body>
</html>
There are plenty of libraries out there.
chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes.
chartJS is another better looking library.
And you can use html5 canvas it's easy and fun but it will take time especially in scaling.
To scale and position, you should use the minimum and maximum values for x and y.
Good luck
I've been working on this problem for some time now with little promising results. I am trying to split up an image into connected regions of similar color. (basically split a list of all the pixels into multiple groups (each group containing the coordinates of the pixels that belong to it and share a similar color).
For example:
http://unsplash.com/photos/SoC1ex6sI4w/
In this image the dark clouds at the top would probably fall into one group. Some of the grey rock on the mountain in another, and some of the orange grass in another. The snow would be another - the red of the backpack - etc.
I'm trying to design an algorithm that will be both accurate and efficient (it needs to run in a matter of ms on midrange laptop grade hardware)
Below is what I have tried:
Using a connected component based algorithm to go through every pixel from top left scanning every line of pixels from left to right (and comparing the current pixel to the top pixel and left pixel). Using the CIEDE2000 color difference formula if the pixel at the top or left was within a certain range then it would be considered "similar" and part of the group.
This sort of worked - but the problem is it relies on color regions having sharp edges - if any color groups are connected by a soft gradient it will travel down that gradient and continue to "join" the pixels as the difference between the individual pixels being compared is small enough to be considered "similar".
To try to fix this I chose to set every visited pixel's color to the color of most "similar" adjacent pixel (either top or left). If there are no similar pixels than it retains it's original color. This somewhat fixes the issue of more blurred boundaries or soft edges because the first color of a new group will be "carried" along as the algorithm progresses and eventually the difference between that color and the current compared color will exceed the "similarity" threashold and no longer be part of that group.
Hopefully this is making sense. The problem is neither of these options are really working. On the image above what is returned are not clean groups but noisy fragmented groups that is not what I am looking for.
I'm not looking for code specifically - but more ideas as to how an algorithm could be structured to successfully combat this problem. Does anyone have ideas about this?
Thanks!
You could convert from RGB to HSL to make it easier to calculate the distance between the colors. I'm setting the color difference tolerance in the line:
if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {...}
If you change 0.3, you can get different results.
See it working.
Please, let me know if it helps.
function hsl_to_rgb(h, s, l) {
// from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function rgb_to_hsl(r, g, b) {
// from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
function color_distance(v1, v2) {
// from http://stackoverflow.com/a/13587077/1204332
var i,
d = 0;
for (i = 0; i < v1.length; i++) {
d += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
return Math.sqrt(d);
};
function round_to_groups(group_nr, x) {
var divisor = 255 / group_nr;
return Math.ceil(x / divisor) * divisor;
};
function pixel_data_to_key(pixel_data) {
return pixel_data[0].toString() + '-' + pixel_data[1].toString() + '-' + pixel_data[2].toString();
}
function posterize(context, image_data, palette) {
for (var i = 0; i < image_data.data.length; i += 4) {
rgb = image_data.data.slice(i, i + 3);
hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]);
key = pixel_data_to_key(hsl);
if (key in palette) {
new_hsl = palette[key];
new_rgb = hsl_to_rgb(new_hsl[0], new_hsl[1], new_hsl[2]);
rgb = hsl_to_rgb(hsl);
image_data.data[i] = new_rgb[0];
image_data.data[i + 1] = new_rgb[1];
image_data.data[i + 2] = new_rgb[2];
}
}
context.putImageData(image_data, 0, 0);
}
function draw(img) {
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
img.style.display = 'none';
var image_data = context.getImageData(0, 0, canvas.width, canvas.height);
var data = image_data.data;
context.drawImage(target_image, 0, 0, canvas.width, canvas.height);
data = context.getImageData(0, 0, canvas.width, canvas.height).data;
original_pixels = [];
for (i = 0; i < data.length; i += 4) {
rgb = data.slice(i, i + 3);
hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]);
original_pixels.push(hsl);
}
group_headers = [];
groups = {};
for (i = 0; i < original_pixels.length; i += 1) {
if (group_headers.length == 0) {
group_headers.push(original_pixels[i]);
}
group_found = false;
for (j = 0; j < group_headers.length; j += 1) {
// if a similar color was already observed
if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {
group_found = true;
if (!(pixel_data_to_key(original_pixels[i]) in groups)) {
groups[pixel_data_to_key(original_pixels[i])] = group_headers[j];
}
}
if (group_found) {
break;
}
}
if (!group_found) {
if (group_headers.indexOf(original_pixels[i]) == -1) {
group_headers.push(original_pixels[i]);
}
if (!(pixel_data_to_key(original_pixels[i]) in groups)) {
groups[pixel_data_to_key(original_pixels[i])] = original_pixels[i];
}
}
}
posterize(context, image_data, groups)
}
var target_image = new Image();
target_image.crossOrigin = "";
target_image.onload = function() {
draw(target_image)
};
target_image.src = "http://i.imgur.com/zRzdADA.jpg";
canvas {
width: 300px;
height: 200px;
}
<canvas id="canvas"></canvas>
You can use "Mean Shift Filtering" algorithm to do the same.
Here's an example.
You will have to determine function parameters heuristically.
And here's the wrapper for the same in node.js
npm Wrapper for meanshift algorithm
Hope this helps!
The process you are trying to complete is called Image Segmentation and it's a well studied area in computer vision, with hundreds of different algorithms and implementations.
The algorithm you mentioned should work for simple images, however for real world images such as the one you linked to, you will probably need a more sophisticated algorithm, maybe even one that is domain specific (are all of your images contains a view?).
I have little experience in Node.js, however from Googling a bit I found the GraphicsMagic library, which as a segment function that might do the job (haven't verified).
In any case, I would try looking for "Image segmentation" libraries, and if possible, not limit myself only to Node.js implementations, as this language is not the common practice for writing vision applications, as opposed to C++ / Java / Python.
I would try a different aproach. Check out this description of how a flood fill algorithm could work:
Create an array to hold information about already colored coordinates.
Create a work list array to hold coordinates that must be looked at. Put the start position in it.
When the work list is empty, we are done.
Remove one pair of coordinates from the work list.
If those coordinates are already in our array of colored pixels, go back to step 3.
Color the pixel at the current coordinates and add the coordinates to the array of colored pixels.
Add the coordinates of each adjacent pixel whose color is the same as the starting pixel’s original color to the work list.
Return to step 3.
The "search approach" is superior because it does not only search from left to right, but in all directions.
You might look at k-means clustering.
http://docs.opencv.org/3.0-beta/modules/core/doc/clustering.html
I'm trying to figure out a way to create a colorwheel similar to this:, in JS. The colorwheel should have ~4096(*) elements the size of a single pixel, with their color set via a CSS background rule.
I know this is not how you're supposed to create a colorpicker, and that normally you should never have so many single-pixel DOM elements for anything. You don't need to tell this to me or try to figure out a different way to accomplish this.
I'd also be interested in having each of the pixel-sized elements be left-aligned, instead of for example absolutely-positioned.
(x): 4096 is the number of all shorthand HEX codes (#XXX), but the colorwheel doesn't have monochrome values, except for white. So the actual number of unique colors would be 4081(?)
This is the code I've managed to come up with (pretty much nothing):
var p = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for(var i = 0; i < p.length; i++)
{
for(var j = 0; j < p.length; j++)
{
for(var k = 0; k < p.length; k++)
{
document.write('<div style="background:#' + p[i] + p[j] + p[k] +'"></div>');
}
}
}
And is the result I get (zoomed 10x), with the following CSS:
div
{
float: left;
width: 10px;
height: 10px;
}
As you can see, it's pretty far from what I want. So any help would be greatly appreciated, since I'm quite lost on how to accomplish this. I've got the colors, but I don't know how to arrange them in a wheel.
EDIT:
Unless someone happens to give me a pretty much complete solution to this, it seems that this is a bit above my skill-level, at the moment. So I'm willing to settle for something that (I assume) would be easier to implement.
Basically another form of output that would be acceptable, would be something like this:
I had previously posted an answer that relied upon the browser converting a colour from the HSL colour-space to the RGB one. Unfortunately, this was an approach that while simple, didn't produce the images shown.
In order to correctly produce the desired outputs, a far easier method is to instead utilise the HSV colour model - one which is quite similar to the HSL one.
When we use the correct colour-space, determining the correct colour for any given pixel is a simple matter of interpolating 3 values - all of which change linearly (the amount of change remains constant. 0 at one end, 1 at the other end will mean 0.5 at a point half-way between them)
First, lets look at your desired outputs and how our HSV inputs change with respect to X and Y coordinates. We'll start of with the easier to visualise and create - the flat strip.
Flat strip
We can observe the following about this image:
The hue ranges from 0 at the left edge to 360 at the right edge.
The sat ranges from 0 at the top edge to 1, half-way between the top
& bottom edges. Beyond the point it reaches 1, it's clamped to 1.
The val ranges from 0 half-way between the top and bottom to 1 at
the bottom. Before the point it is 0, it's clamped to 0.
Now, let's look at the wheel representation of the same picture.
Colour wheel
If you look closely, you'll see that the strip, when wrapped into a circle will produce the colour-wheel. The centre of the wheel corresponds to the top-edge of the strip and the outer edge corresponds to the bottom edge.
This is also how we can show that the original wheel you showed is a somewhat innacurate representation of the colour-space, since it has the red on the left edge. Basically, your image has been flipped horizontally. ;)
Okay, that then shows how the images are related to the HSV colour-space. Next, we really need to be able to create them on the fly. This is fairly straight-forward now we've the plan for how to go about it.
Once this is done, we'll end up with 2 canvases - these were the images I used for the annotations. From there, there's a couple of ways you could go about it.
You could: allow the user to pick any colour they like, before returning to them the closest colour from the set of short-hand hex values.
Or you could: back-up a little, only setting colours on the canvas to those which are in the same set of short-hand values.
One will take longer to calculate the chosen colour, while the other will take longer to calculate the initial images.
I have left that part of the implementation up to you, instead opting to: eschew the idea of so many DOM elements, using just 2 canvas instead and also, to simply pick the colour exactly as chosen, based off the code I linked to # MDN.
function newEl(tag){return document.createElement(tag)}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
var strip = makeCanvas();
strip.addEventListener('mousemove', pick);
document.body.appendChild( strip );
var wheel = makeWheel(256);
wheel.addEventListener('mousemove', pick);
document.body.appendChild( wheel );
}
var hsv2rgb = function(hsv) {
var h = hsv.hue, s = hsv.sat, v = hsv.val;
var rgb, i, data = [];
if (s === 0) {
rgb = [v,v,v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
switch(i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
}
}
return rgb;
};
function clamp(min, max, val)
{
if (val < min) return min;
if (val > max) return max;
return val;
}
function makeCanvas()
{
var can, ctx;
can = newEl('canvas');
ctx = can.getContext('2d');
can.width = 360;
can.height = 100;
var span = newEl('span');
var imgData = ctx.getImageData(0,0,360,100);
var xPos, yPos, index;
var height=imgData.height, width=imgData.width;
for (yPos=0; yPos<height; yPos++)
{
for (xPos=0; xPos<width; xPos++)
{
// this is the point at which the S & V values reach
// the peaks or start to change. 2 means height/2
// so a divisor of 3 would mean the 'break-points'
// were at the 1/3 and 2/3 positions
// while a divisor of 4 would imply 1/4 and 3/4
//
// Have a look at the generated images using the eye-
// dropper tool of an image program (Gimp, Photoshop,
// etc) that allows you to choose the HSV colour
// model, to get a better idea of what I'm saying
// here.
var divisor = 2;
var hue = xPos;
var sat = clamp(0, 1, yPos / (height/divisor) );
var val = clamp(0, 1, (height-yPos) / (height/divisor) );
var rgb = hsv2rgb( {hue:hue, sat:sat, val:val} );
index = 4 * (xPos + yPos*360);
imgData.data[ index + 0 ] = rgb[0] * 255; // r
imgData.data[ index + 1 ] = rgb[1] * 255; // g
imgData.data[ index + 2 ] = rgb[2] * 255; // b
imgData.data[ index + 3 ] = 255; // a
}
}
ctx.putImageData(imgData, 0, 0);
return can;
}
// see the comment in the above function about the divisor. I've
// hard-coded it here, to 2
// diameter/2 corresponds to the max-height of a strip image
function makeWheel(diameter)
{
var can = newEl('canvas');
var ctx = can.getContext('2d');
can.width = diameter;
can.height = diameter;
var imgData = ctx.getImageData(0,0,diameter,diameter);
var maxRange = diameter / 2;
for (var y=0; y<diameter; y++)
{
for (var x=0; x<diameter; x++)
{
var xPos = x - (diameter/2);
var yPos = (diameter-y) - (diameter/2);
var polar = pos2polar( {x:xPos, y:yPos} );
var sat = clamp(0,1,polar.len / ((maxRange/2)));
var val = clamp(0,1, (maxRange-polar.len) / (maxRange/2) );
var rgb = hsv2rgb( {hue:polar.ang, sat:sat, val:val} );
var index = 4 * (x + y*diameter);
imgData.data[index + 0] = rgb[0]*255;
imgData.data[index + 1] = rgb[1]*255;
imgData.data[index + 2] = rgb[2]*255;
imgData.data[index + 3] = 255;
}
}
ctx.putImageData(imgData, 0,0);
return can;
}
function deg2rad(deg)
{
return (deg / 360) * ( 2 * Math.PI );
}
function rad2deg(rad)
{
return (rad / (Math.PI * 2)) * 360;
}
function pos2polar(inPos)
{
var vecLen = Math.sqrt( inPos.x*inPos.x + inPos.y*inPos.y );
var something = Math.atan2(inPos.y,inPos.x);
while (something < 0)
something += 2*Math.PI;
return { ang: rad2deg(something), len: vecLen };
}
function pick(event)
{
var can = this;
var ctx = can.getContext('2d');
var color = document.getElementById('color');
var x = event.layerX;
var y = event.layerY;
var pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data;
var rgba = 'rgba(' + data[0] + ',' + data[1] +
',' + data[2] + ',' + (data[3] / 255) + ')';
color.style.background = rgba;
color.textContent = rgba;
}
canvas
{
border: solid 1px red;
}
<div id="color" style="width: 200px; height: 50px; float: left;"></div>
The problem is that you are mapping a three-axis system (R, G, B) on a two-axis system (x,y). In the colorwheel example the three axis are drawn like a circle, and it is very difficult to create this using only x and y coördinates, especially when the requirement is that the pixels (div's) should be left-aligned.
The best approach to accomplish this is to create a three-dimensional array with the color values, map this onto a two-dimensional array and then loop this two-dimensional array from left to right and top-down.
Like this:
var p = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for(var i = 0; i < p.length; i++)
{
for(var j = 0; j < p.length; j++)
{
for(var k = 0; k < p.length; k++)
{
var x = i - k * 0.5;
var y = j - k * 0.25;
background[x][y] = "#" + p[i] + p[j] + p[k];
}
}
}
for (var l = 0; l < maxX; l++)
{
for (var m = 0; m < maxY; m++)
{
document.write('<div style="background:#' + background[x][y] +'"></div>');
}
}
This is not a complete answer, just a possible first step to solve this. Maybe this can help you to create a good solution.
I am trying to write a script to place 100 circles of varying sizes onto a stage. I've outlined the concise requirements below.
Given the following:
var stage; // contains a "width" and "height" property.
var circle; // the circle class. contains x, y, radius & a unique id property.
var circleArray; // contains 100 circle instances
requirements:
write a function to place 100 circles of varying radius onto the stage.
placements must be random but evenly distributed (no clumping).
placement must be performant - this will be executing on a mobile web browser.
circles must not intersect/overlap other circles.
circle.x >= 0 must be true.
circle.y >= 0 && circle.y <= stage.height must be true.
circles may have any of the following radius sizes (assigned at creation):
150
120
90
80
65
My current attempt is a brute-force method, which does not operate efficiently. If I attempt to insert any more than ~10 circles, the browser hangs. Below is my current implementation, which I am completely OK with throwing away in favor of a more performant / better one.
Here is a live demo (NOTE: there is no actual drawing code, just the logic, but it will still lock up the browser so be warned!!) http://jsbin.com/muhiziduxu/2/edit?js,console
function adjustForOverlap (circleArray) {
// a reference to the circle that is invoking this function.
var _this = this;
// remove this circle from the array we are iterating over.
var arr = circleArray.filter(function (circle){
return circle.id !== _this.id;
});
// while repeat == true, the circle may be overlapping something.
var repeat = true;
while(repeat) {
var hasOverlap = false;
for (var i=0; i<arr.length; i++) {
var other = arr[i];
var dx = _self.x - other.x;
var dy = _self.y - other.y;
var rr = _self.radius + other.radius;
if (dx * dx + dy * dy < rr * rr) {
// if here, then an overlap was detected.
hit = true;
break;
}
}
// if hit is false, the circle didn't overlap anything, so break.
if (hit === false) {
repeat = false;
break;
} else {
// an overlap was detected, so randomize position.
_self.x = Math.random() * (stage.width*2);
_self.y = Math.random() * stage.height;
}
}
}
There are lots of efficient collision detection algorithms. Many of them work by dividing up the space into cells and maintaining a separate data structure with efficient lookup of other objects in the cell. The basic steps are:
Identify a random spot for your new circle
Determine which cells it's in
Look in each of those cells for a collision
If there's a collision, goto 1.
Else, add the new circle to each of the cells it overlaps.
You can use a simple square grid (i.e. a 2-d array) for the cell data structure, or something else like a quadtree. You can also in some cases get a bit of extra speed by trying a cheap-but-coarse collision check first (do the bounding boxes overlap), and if that returns true try the slightly more expensive and exact check.
Update
For quadtrees, check out d3-quadtree, which ought to give you a pretty good implementation, with examples.
For a (very quick, untested) 2-d array implementation:
function Grid(radius, width, height) {
// I'm not sure offhand how to find the optimum grid size.
// Let's use a radius as a starting point
this.gridX = Math.ceil(width / radius);
this.gridY = Math.ceil(height / radius);
// Determine cell size
this.cellWidth = width / this.gridX;
this.cellHeight = height / this.gridY;
// Create the grid structure
this.grid = [];
for (var i = 0; i < gridY; i++) {
// grid row
this.grid[i] = [];
for (var j = 0; j < gridX; j++) {
// Grid cell, holds refs to all circles
this.grid[i][j] = [];
}
}
}
Grid.prototype = {
// Return all cells the circle intersects. Each cell is an array
getCells: function(circle) {
var cells = [];
var grid = this.grid;
// For simplicity, just intersect the bounding boxes
var gridX1Index = Math.floor(
(circle.x - circle.radius) / this.cellWidth
);
var gridX2Index = Math.ceil(
(circle.x + circle.radius) / this.cellWidth
);
var gridY1Index = Math.floor(
(circle.y - circle.radius) / this.cellHeight
);
var gridY2Index = Math.ceil(
(circle.y + circle.radius) / this.cellHeight
);
for (var i = gridY1Index; i < gridY2Index; i++) {
for (var j = gridX1Index; j < gridX2Index; j++) {
// Add cell to list
cells.push(grid[i][j]);
}
}
return cells;
},
add: function(circle) {
this.getCells(circle).forEach(function(cell) {
cell.push(circle);
});
},
hasCollisions: function(circle) {
return this.getCells(circle).some(function(cell) {
return cell.some(function(other) {
return this.collides(circle, other);
}, this);
}, this);
},
collides: function (circle, other) {
if (circle === other) {
return false;
}
var dx = circle.x - other.x;
var dy = circle.y - other.y;
var rr = circle.radius + other.radius;
return (dx * dx + dy * dy < rr * rr);
}
};
var g = new Grid(150, 1000, 800);
g.add({x: 100, y: 100, radius: 50});
g.hasCollisions({x: 100, y:80, radius: 100});
Here's a fully-functional example: http://jsbin.com/cojoxoxufu/1/edit?js,output
Note that this only shows 30 circles. It looks like the problem is often unsolvable with your current radii, width, and height. This is set up to look for up to 500 locations for each circle before giving up and accepting a collision.
When drawing a linechart with gRaphael using milliseconds along the x-axis I commonly get inconsistencies in the placement of the data points. Most commonly the initial data points are to the left of the y-axis (as seen in the fiddle below), sometimes the last data-point will be beyond the right side of the view-box/past the termination of the x-axis.
Does anyone know:
1) Why this occurs,
2) How to prevent it, &/or
3) How to check for it (I can use transform to move the lines/points if I know when it has happened/by how much).
my code:
var r = Raphael("holder"),
txtattr = { font: "12px sans-serif" };
var r2 = Raphael("holder2"),
txtattr2 = { font: "12px sans-serif" };
var x = [], y = [], y2 = [], y3 = [];
for (var i = 0; i < 1e6; i++) {
x[i] = i * 10;
y[i] = (y[i - 1] || 0) + (Math.random() * 7) - 3;
}
var demoX = [[1, 2, 3, 4, 5, 6, 7],[3.5, 4.5, 5.5, 6.5, 7, 8]];
var demoY = [[12, 32, 23, 15, 17, 27, 22], [10, 20, 30, 25, 15, 28]];
var xVals = [1288885800000, 1289929440000, 1290094500000, 1290439560000, 1300721700000, 1359499228000, 1359499308000, 1359499372000];
var yVals = [80, 76, 70, 74, 74, 78, 77, 72];
var xVals2 = [1288885800000, 1289929440000];
var yVals2 = [80, 76];
var lines = r.linechart(10, 10, 300, 220, xVals, yVals, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines.symbols.attr({ r: 3 });
var lines2 = r2.linechart(10, 10, 300, 220, xVals2, yVals2, { nostroke: false, axis: "0 0 1 1", symbol: "circle", smooth: true })
.hoverColumn(function () {
this.tags = r2.set();
for (var i = 0, ii = this.y.length; i < ii; i++) {
this.tags.push(r.tag(this.x, this.y[i], this.values[i], 160, 10).insertBefore(this).attr([{ fill: "#fff" }, { fill: this.symbols[i].attr("fill") }]));
}
}, function () {
this.tags && this.tags.remove();
});
lines2.symbols.attr({ r: 3 });
I do have to use gRaphael and the x-axis has to be in milliseconds (it is labeled later w/customized date strings)
Primary example fiddle: http://jsfiddle.net/kcar/aNJxf/
Secondary example fiddle (4th example on page frequently shows both axis errors):
http://jsfiddle.net/kcar/saBnT/
root cause is the snapEnds function (line 718 g.raphael.js), the rounding it does, while fine in some cases, is adding or subtracting years from/to the date in other cases.
Haven't stepped all the way through after this point, but since the datapoints are misplaced every time the rounding gets crazy and not when it doesn't, I'm going to go ahead and assume this is causing issues with calculating the chart columns, also before being sent to snapEnds the values are spot on just to confirm its not just receiving miscalculated data.
code of that function from g.raphael.js
snapEnds: function(from, to, steps) {
var f = from,
t = to;
if (f == t) {
return {from: f, to: t, power: 0};
}
function round(a) {
return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a);
}
var d = (t - f) / steps,
r = ~~(d),
R = r,
i = 0;
if (r) {
while (R) {
i--;
R = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
}
i ++;
} else {
if(d == 0 || !isFinite(d)) {
i = 1;
} else {
while (!r) {
i = i || 1;
r = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
i++;
}
}
i && i--;
}
t = round(to * Math.pow(10, i)) / Math.pow(10, i);
if (t < to) {
t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
}
f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
return { from: f, to: t, power: i };
},
removed the rounding nonsense from snapEnds and no more issues, not noticed any downside from either axis or any other area of the chart. If you see one I'd love to hear it though.
code of that function from g.raphael.js now:
snapEnds: function(from, to, steps) {
return {from: from, to: to, power: 0};
},
Hi if you comment this:
if (valuesy[i].length > width - 2 * gutter) {
valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
len = width - 2 * gutter;
}
if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
}
in g.line.js, It seems to solve the problem, and it also solves a similar problem with the values in the y axis.
Upgrading from v0.50 to v0.51 fixed the issue for me.
Still not sure why it occurs, adding in a transparent set was not a desirable option.
The simplest way to check for if the datapoints were rendered outside of the graph seems to be getting a bounding box for the axis set and a bounding box for the datapoints and checking the difference between the x and x2 values.
If anyone can help me with scaling the datapoint set, or figure out how to make this not happen at all, I will still happily appreciate/up vote answers
//assuming datapoints is the Raphael Set for the datapoints, axes is the
//Raphael Set for the axis, and datalines is the Raphael Set for the
//datapoint lines
var pointsBBox = datapoints.getBBox();
var axesBBox = axes.getBBox();
var xGapLeft = Math.ceil(axesBBox.x - pointsBBox.x);
//rounding up to integer to simplify, and the extra boost from y-axis doesn't
//hurt, <1 is a negligible distance in transform
var xGapRight = Math.ceil(axesBBox.x2 - pointsBBox.x2);
var xGap = 0;
if(xGapLeft > 0){
datapoints.transform('t' +xGapLeft +',0');
datalines.transform('t' +xGapLeft +',0');
xGap = xGapLeft;
}else if (xGapRight < 0) { //using else if because if it is a scale issue it will
//be too far right & too far left, meaning both are true and using transform will
//just shift it right then left and you are worse off than before, using
//set.transform(scale) works great on dataline but when using on datapoints scales
// symbol radius not placement
if (xGapLeft < 0 && xGapRight < xGapLeft) { xGapRight = xGapLeft; }
//in this case the initial point is right of y-axis, the end point is right of
//x-axis termination, and the difference between last point/axis is greater than
//difference between first point/axis
datapoints.transform('t' +xGapRight +',0');
datalines.transform('t' +xGapRight +',0');
xGap = xGapRight;
}
rehookHoverOverEvent(xGap); //there are so many ways to do this just leaving it
//here as a call to do so, if you don't the hover tags will be over the original
//datapoints instead of the new location, at least they were in my case.