Suppose one attempts to plot the complex valued function $f:\mathhbb{C} \to \mathhbb{C}$ as $f(z) =z$ in jsx graph. It may not be complicated as it appears. What one needs is two connected planes. The point (x, y) in domain planr gets mapped to the point (x, y) in codomain plane. As one drags point in domain plane, corresponding changes takes place in the point in co domain plane. So the only question is how to connect two planes. It is matter of 2 dimensions only. If something similar to the following can be added to jsx graph, it would be great addition to jsx graph. Many properties of complex valued function can then be studied.
Here is the link.
http://www.jimrolf.com/java/complexTool/bookComplexTool.html
Two boards board1, board2 can be connected with board1.addChild(board2). This means, every update in board1 triggers an update in board2.
Here is a basic example, see https://jsfiddle.net/zfbrsdwh/ :
const board1 = JXG.JSXGraph.initBoard('jxgbox1', {
boundingbox: [-5, 5, 5, -5], axis:true
});
var p = board1.create('point', [1,2], {name:'Drag me'});
const board2 = JXG.JSXGraph.initBoard('jxgbox2', {
boundingbox: [-5, 5, 5, -5], axis:true
});
var q = board2.create('point', [function() { return [-p.Y(), p.X()]; }],
{name:'image'});
board1.addChild(board2);
Update in reply to the first comment: Visualizing conformal maps in the complex plane can be done by applying the map to a quadrangle. It is necessary to define the edges of the quadrangle by a curve:
var p0 = board1.create('point', [2, -2]);
var p1 = board1.create('point', [2, 2]);
var p2 = board1.create('point', [-2, 2]);
var p3 = board1.create('point', [-2, -2]);
// Draw the quadrangle through p0, p1, p2, p3 as curve
// defined by [fx, fy]
var fx = function(x) {
if (x < 0 || x > 4) { return NaN; }
if (x < 1) {
return (p1.X() - p0.X()) * x + p0.X();
} else if (x < 2) {
return (p2.X() - p1.X()) * (x - 1) + p1.X();
} else if (x < 3) {
return (p3.X() - p2.X()) * (x - 2) + p2.X();
} else if (x < 4) {
return (p0.X() - p3.X()) * (x - 3) + p3.X();
}
};
var fy = function(x) {
if (x < 0 || x > 4) { return NaN; }
if (x < 1) {
return (p1.Y() - p0.Y()) * x + p0.Y();
} else if (x < 2) {
return (p2.Y() - p1.Y()) * (x - 1) + p1.Y();
} else if (x < 3) {
return (p3.Y() - p2.Y()) * (x - 2) + p2.Y();
} else if (x < 4) {
return (p0.Y() - p3.Y()) * (x - 3) + p3.Y();
}
};
var graph1 = board1.create('curve', [fx, fy, 0, 4]);
Then it should be easy to define a conformal map and plot the composition of the two maps in the second board:
// Conformal complex map z -> 1/z
var map = function(x, y) {
var s = x*x+y*y;
return [x / s, -y/s];
};
// Draw the image of the quadrangle under the map
f2x = function(x) {
return map(fx(x), fy(x))[0];
};
f2y = function(x) {
return map(fx(x), fy(x))[1];
};
var graph2 = board2.create('curve', [f2x, f2y, 0, 4]);
The full mathlet is at https://jsfiddle.net/Lmy60f4g/2/
Related
I have co-ordinates for the points by taking which I draw a polygon. I can add points dynamically on the edges of the polygon and when I drag any point it should drag only the connected lines. As points can be added later on the edges so the point co-ordinates need to be ordered/sorted and the polygon should be redrawn by taking the ordered/sorted points so that on dragging any point the lines connected to the dragged point only should be dragged/updated. So to order/sort the points I am sorting the co-ordinates(2D-points) clockwise using Graham Scan/ sorting by polar angle.
My sorting code is
I find the center of the polygon like
function findCenter(points) {
let x = 0,
y = 0,
i,
len = points.length;
for (i = 0; i < len; i++) {
x += Number(points[i][0]);
y += Number(points[i][1]);
}
return { x: x / len, y: y / len }; // return average position
}
Then I sort the points by finding angles of each point from the center like
function findAngle(points) {
const center = findCenter(points);
// find angle
points.forEach((point) => {
point.angle = Math.atan2(point[1] - center.y, point[0] - center.x);
});
}
//arrVertexes is the array of points
arrVertexes.sort(function (a, b) {
return a.angle >= b.angle ? 1 : -1;
});
But the problem I am facing is if I drag any point more towards opposite side and add a new point on the edges afterward and drag the newly added point the sorting of co-ordinates is not ordered exactly because of which there is a flickering while dragging.
Here is a pictorial view of the problem I am facing for quick understanding.
Initially my svg looks like
After this I add a point and dragged like
Then I added one more point like
once I drag the added point towards down, it redraws the polygon something like (is not it weird ?)
Actually It should be like
NOTE: I really don't know what logic should I apply to get the desire functionality. Seeking help from the community leads.
Demo App
So I am looking for a solution that won't give me weird redrawing of the lines. Only the connected lines to the dragged point should be dragged.
EDIT
I came up with MUCH BETTER solution. The only problem with this approach is, When I try to add a new point on left-vertical line and If I try to move it, that newly added point moves to top-horizontal line
Updated-Demo
I've fixed this bug with left line. Take a look: codepen.
I changed getClosestPointOnLines function (actually refactored a little):
as I understood, the result here is to get i - the index for the new point in array, so I moved the algorithm to new function getI
I changed getI to use not only n (current index), but just 2 any indexes: n1 and n2: const getI = (n1, n2) => {
So all your aXys[n] is now a1 and aXys[n - 1] is now a2.
the result of getI is return i; - this is what we want from this function
I added new function-helper updateI. It calls getI and check if there any positive result.
const updateI = (n1, n2) => {
const newI = getI(n1, n2);
if (newI !== undefined) {
i = newI;
return true;
}
};
So your loop over points is now:
for (let n = 1; n < aXys.length; n++) {
updateI(n, n - 1);
}
But we need to check "left" line separately (because it connects begin and end of the array):
if (updateI(aXys.length - 1, 0)) i = aXys.length;
Sorry, but I disabled part of your code. I did not check where do you use it:
if (i < aXys.length) {
let dx = aXys[i - 1][0] - aXys[i][0];
let dy = aXys[i - 1][1] - aXys[i][1];
x = aXys[i - 1][0] - dx * fTo;
y = aXys[i - 1][1] - dy * fTo;
}
So the final version of getClosestPointOnLines now looks like this:
function getClosestPointOnLines(pXy, aXys) {
var minDist;
var fTo;
var fFrom;
var x;
var y;
var i;
var dist;
if (aXys.length > 1) {
const getI = (n1, n2) => {
let i;
const a1 = aXys[n1];
const a2 = aXys[n2];
if (a1[0] != a2[0]) {
let a = (a1[1] - a2[1]) / (a1[0] - a2[0]);
let b = a1[1] - a * a1[0];
dist = Math.abs(a * pXy[0] + b - pXy[1]) / Math.sqrt(a * a + 1);
} else dist = Math.abs(pXy[0] - a1[0]);
// length^2 of line segment
let rl2 = Math.pow(a1[1] - a2[1], 2) + Math.pow(a1[0] - a2[0], 2);
// distance^2 of pt to end line segment
let ln2 = Math.pow(a1[1] - pXy[1], 2) + Math.pow(a1[0] - pXy[0], 2);
// distance^2 of pt to begin line segment
let lnm12 = Math.pow(a2[1] - pXy[1], 2) + Math.pow(a2[0] - pXy[0], 2);
// minimum distance^2 of pt to infinite line
let dist2 = Math.pow(dist, 2);
// calculated length^2 of line segment
let calcrl2 = ln2 - dist2 + lnm12 - dist2;
// redefine minimum distance to line segment (not infinite line) if necessary
if (calcrl2 > rl2) dist = Math.sqrt(Math.min(ln2, lnm12));
if (minDist == null || minDist > dist) {
if (calcrl2 > rl2) {
if (lnm12 < ln2) {
fTo = 0; //nearer to previous point
fFrom = 1;
} else {
fFrom = 0; //nearer to current point
fTo = 1;
}
} else {
// perpendicular from point intersects line segment
fTo = Math.sqrt(lnm12 - dist2) / Math.sqrt(rl2);
fFrom = Math.sqrt(ln2 - dist2) / Math.sqrt(rl2);
}
minDist = dist;
i = n1;
}
return i;
};
const updateI = (n1, n2) => {
const newI = getI(n1, n2);
if (newI !== undefined) {
i = newI;
return true;
}
};
for (let n = 1; n < aXys.length; n++) {
updateI(n, n - 1);
}
if (updateI(aXys.length - 1, 0)) i = aXys.length;
if (i < aXys.length) {
let dx = aXys[i - 1][0] - aXys[i][0];
let dy = aXys[i - 1][1] - aXys[i][1];
x = aXys[i - 1][0] - dx * fTo;
y = aXys[i - 1][1] - dy * fTo;
}
}
console.log(aXys[i - 1]);
return { x: x, y: y, i: i, fTo: fTo, fFrom: fFrom };
}
Working example on codepen.
You should not allow any point to be added that is not close to a line.
When the user clicks, use the distance from a point to a line algorithm to check each line to see if the click is within an acceptable distance of the line. Perhaps a few pixels. If more than one line is within an acceptable distance, perhaps choose the one that is closest.
You now know where in the array to insert the new point. It will be between the first and second points of the line that just matched.
If you do that, the shape drawing should just work.
Im trying to create an Image Cloud component like the one in the image.
I've tried using a simple d3 alorithm with rects and force to center the images and to spread them with collisions. The problem is that it looks like this:
The images look too far apart one of each other, and even with lower collition radius the cloud doesn't use all avaliable space (fit smaller images between gaps). Is it there a way to make this component with the layouts given by d3? should I modify some other layout? I know I can implement an square collition force, but the results doesnt look as the one desired.
Any help is appreciated. Thank you very much.
EDIT: Hello again. I finally made it look somewhat similar to the desired result.
Sadly, now I have more problems. The first and most important one: the big images should be close one of each other and should be as centered as possible.
The second one: Im using React, and my data comes from calling the exported render function from outside the file (see first code snippet), the problem is that when I update the data, the image just dissapears and it doesn't adjust the layout again. How can i make it update it? A temporary hack that I made is to remove every image and rect from the DOM before rendering.
Again, thank you for your time.
Edit: I managed to get everything like I wanted. There is still room for improvement but this is functional for now. I managed to get an observer for the div and a somewhat good state management with React. Someday I will upload the react component to github
// specify svg width and height;
const width = 500, height = 500;
const listenTo = Math.min(width, height);
// create svg and g DOM elements;
let svg = d3.select('body')
.append('svg')
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('width', listenTo)
.attr('height', listenTo)
.append('g')
// move 0,0 to the center
.attr('transform', `translate(${width >>1}, ${height>>1})`);
var images = [], maxImages = 100, maxWeight = 50, minWeight = 1, padding=3;
for(let i = 0; i< maxImages -1; i++){
const weight = (Math.random() *(maxWeight - minWeight)) + minWeight;
images.push({
url:`https://via.placeholder.com/100?text=${Math.ceil(weight)}`,
weight
})
}
// make one image with a weight 3 times bigger for visualization testing propouses
images.push({
url: `https://via.placeholder.com/100?text=${maxWeight * 3}`,
weight: maxWeight * 3,
fx: 0,
fy: 0
})
images.sort((a, b) => b.weight - a.weight);
// make it so the biggest images is equal to 10% of canvas, and thre smallest one 1%
const scl = ((100 / maxImages) / 100);
console.log(scl);
const maxImageSize = listenTo * 0.1;
const minImageSize = listenTo * scl;
// function to scale the images
const scaleSize = d3.scaleLinear().domain([minWeight, maxWeight*3]).range([minImageSize, maxImageSize]).clamp(true);
// append the rects
let vizImages = svg.selectAll('.image-cloud-image')
.data(images)
.enter()
.append('svg:image')
.attr('class', '.image-cloud-image')
.attr('height', d => scaleSize(d.weight))
.attr('width', d => scaleSize(d.weight))
.attr('id', d => d.url)
.attr('xlink:href', d => d.url);
vizImages.exit().remove();
// create the collection of forces
const simulation = d3.forceSimulation()
// set the nodes for the simulation to be our images
.nodes(images)
// set the function that will update the view on each 'tick'
.on('tick', ticked)
.force('center', d3.forceCenter())
.force('cramp', d3.forceManyBody().strength(listenTo / 100))
// collition force for rects
.force('collide', rectCollide().size(d=> {
const s = scaleSize(d.weight);
return [s + padding, s + padding];
}));
// update the position to new x and y
function ticked() {
vizImages.attr('x', d => d.x).attr('y', d=> d.y);
}
// Rect collition algorithm. i don't know exactly how it works
// https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
function rectCollide() {
var nodes, sizes, masses
var size = constant([0, 0])
var strength = 1
var iterations = 1
function force() {
var node, size, mass, xi, yi
var i = -1
while (++i < iterations) { iterate() }
function iterate() {
var j = -1
var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)
while (++j < nodes.length) {
node = nodes[j]
size = sizes[j]
mass = masses[j]
xi = xCenter(node)
yi = yCenter(node)
tree.visit(apply)
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data
var xSize = (size[0] + quad.size[0]) / 2
var ySize = (size[1] + quad.size[1]) / 2
if (data) {
if (data.index <= node.index) { return }
var x = xi - xCenter(data)
var y = yi - yCenter(data)
var xd = Math.abs(x) - xSize
var yd = Math.abs(y) - ySize
if (xd < 0 && yd < 0) {
var l = Math.sqrt(x * x + y * y)
var m = masses[data.index] / (mass + masses[data.index])
if (Math.abs(xd) < Math.abs(yd)) {
node.vx -= (x *= xd / l * strength) * m
data.vx += x * (1 - m)
} else {
node.vy -= (y *= yd / l * strength) * m
data.vy += y * (1 - m)
}
}
}
return x0 > xi + xSize || y0 > yi + ySize ||
x1 < xi - xSize || y1 < yi - ySize
}
function prepare(quad) {
if (quad.data) {
quad.size = sizes[quad.data.index]
} else {
quad.size = [0, 0]
var i = -1
while (++i < 4) {
if (quad[i] && quad[i].size) {
quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
}
}
}
}
}
function xCenter(d) { return d.x + d.vx + sizes[d.index][0] / 2 }
function yCenter(d) { return d.y + d.vy + sizes[d.index][1] / 2 }
force.initialize = function (_) {
sizes = (nodes = _).map(size)
masses = sizes.map(function (d) { return d[0] * d[1] })
}
force.size = function (_) {
return (arguments.length
? (size = typeof _ === 'function' ? _ : constant(_), force)
: size)
}
force.strength = function (_) {
return (arguments.length ? (strength = +_, force) : strength)
}
force.iterations = function (_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}
return force
}
function constant(_) {
return function () { return _ }
}
Here's a fiddle for future people
I have some points in a BufferGeometry. They arrange themselves into a regular 1D/2D/3D grid. I'm doing index mapping to higher dimensions, and moving the vertices dynamically so they end up in a proper spot relative to their neighbors (specifically, I'm visualizing a self-organizing map).
I want to draw the connections between vertices, like the above picture. Doing that for 1D is straightforward enough because it's just a new Line(myBufferGeometry), but how can the same be achieved for 2D and 3D? Do I have to create and update separate geometries for this, like make lots of
LineSegments? How can this be done efficiently? Or maybe is there some "magic" I can do, like with the index property?
I figured this out thanks to prisoner849's comment - this isn't explicitly mentioned in the docs and kinda hidden away in examples, but this is exactly what the index property is for. When LineSegments is provided with a GeometryBuffer that has the property, the lines are based on pairs of indices rather than pairs of points in the position property.
Here's a complete solution for a n x n x n cube:
let nn = n * n;
let nnn = n * n * n;
function mapTo3D(index) {
let x = index % n;
let y = Math.floor(index / n) % n;
let z = Math.floor(index / nn);
return { x: x, y: y, z: z };
}
function mapFrom3D(x, y, z) {
return x + y * n + z * nn;
}
// add nnn points to the position attribute of your myGeometryBuffer...
let indices3D = [];
for (let i = 0; i < nnn; i++) {
var p = mapTo3D(i);
if (p.x + 1 < n) {
indices3D.push(i);
indices3D.push(mapFrom3D(p.x + 1, p.y, p.z));
}
if (p.y + 1 < n) {
indices3D.push(i);
indices3D.push(mapFrom3D(p.x, p.y + 1, p.z));
}
if (p.z + 1 < n) {
indices3D.push(i);
indices3D.push(mapFrom3D(p.x, p.y, p.z + 1));
}
}
myBufferGeometry.setIndex(indices3D);
let lines = new THREE.LineSegments(myBufferGeometry);
I need to plot a graph in a canvas. But how can I use an algebra equation as input, and based on the equation, draw the curve, using javascript?
For example:
x2+5y=250
The equation plots a graph with both positive and negative values.
<!DOCTYPE html>
<html>
<head>
<title>Interactive Line Graph</title>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.1.min.js"></script>
<script>
var graph;
var xPadding = 30;
var yPadding = 30;
var data = { values:[
{ X: "1", Y: 15 },
{ X: "2", Y: 35 },
{ X: "3", Y: 60 },
{ X: "4", Y: 14 },
{ X: "5", Y: 20 },
{ X: "6", Y: 95 },
]};
// Returns the max Y value in our data list
function getMaxY() {
var max = 0;
for(var i = 0; i < data.values.length; i ++) {
if(data.values[i].Y > max) {
max = data.values[i].Y;
}
}
max += 10 - max % 10;
return max;
}
// Return the x pixel for a graph point
function getXPixel(val) {
return ((graph.width() - xPadding) / data.values.length) * val + (xPadding * 1.5);
}
// Return the y pixel for a graph point
function getYPixel(val) {
return graph.height() - (((graph.height() - yPadding) / getMaxY()) * val) - yPadding;
}
$(document).ready(function() {
graph = $('#graph');
var c = graph[0].getContext('2d');
c.lineWidth = 2;
c.strokeStyle = '#333';
c.font = 'italic 8pt sans-serif';
c.textAlign = "center";
// Draw the axises
c.beginPath();
c.moveTo(xPadding, 0);
c.lineTo(xPadding, graph.height() - yPadding);
c.lineTo(graph.width(), graph.height() - yPadding);
c.stroke();
// Draw the X value texts
for(var i = 0; i < data.values.length; i ++) {
c.fillText(data.values[i].X, getXPixel(i), graph.height() - yPadding + 20);
}
// Draw the Y value texts
c.textAlign = "right"
c.textBaseline = "middle";
for(var i = 0; i < getMaxY(); i += 10) {
c.fillText(i, xPadding - 10, getYPixel(i));
}
c.strokeStyle = '#f00';
// Draw the line graph
c.beginPath();
c.moveTo(getXPixel(0), getYPixel(data.values[0].Y));
for(var i = 1; i < data.values.length; i ++) {
c.lineTo(getXPixel(i), getYPixel(data.values[i].Y));
}
c.stroke();
// Draw the dots
c.fillStyle = '#333';
for(var i = 0; i < data.values.length; i ++) {
c.beginPath();
c.arc(getXPixel(i), getYPixel(data.values[i].Y), 4, 0, Math.PI * 2, true);
c.fill();
}
});
</script>
</head>
<body>
<canvas id="graph" width="200" height="150">
</canvas>
</body>
</html>
[i am add one example ploter in math.js ] i want to how to full screen plot the graph and mouse are cilck in graph any point to show the details in x&y value.so how to change please help me.
Parsing linear equation.
Or maybe it is the Parsing of the equation that the question is about.
This answer shows how to parse a simple linear equation.
User inputs x2+5y=230 and you need to solve and plot for y for f(x) which would be the function function(x) { return (3 * x -230) / -5; }
Will assume the equation is always in the same form with x and y and some scalars and constants scalar * x + const + scalar * y = const
Define the rules
Rules
Only x and y will be considered variables.
A term is a scalar and a variable 2x or a constant +1.
All additional characters will be ignored including *,/,%
Numbers can have decimal places. Valid numbers 1 +1 0.2 -2 10e5
Scalars must be adjacent to variables 3y2 becomes 6y 3y-2 stays as is.
Parsing
To parse a equation we must break it down into unambiguous easy to manipulate units. In this case a unit I call a term and will have 3 properties.
scalar A number
variable the name of the variable x,y or null for constants
side which side of the equation the term is Left or right
An example equation
2x + 2 + 3y = 4x - 1y
First parsed to create
terms
// shorthand not code
{2,x,true; // true is for left
{2,null,true; // null is a constant
{3,y,true;
{4,x,false;
{-1,y,false;
Once all the terms are parsed then the equation is solved by summing all the terms for x, y and constants and moving everything to the left flipping the sign of any values on the right.
sumX = 2 + -4; //as 4x is on the right it becomes negative
sumY = 3 + 1;
const = 2;
Making the equation
-2x + 4y + 2 = 0
Then move the y out to the right and divide the left by its scalar.
-2x + 2 = 4y
(-2x + 2)/-4 = y
The result is a function that we can call from javascript will the value of x and get the value of y.
function(x){ return (-2 * x + 2) / 4; }
The Parser
The following function parses and returns a function for input equation for x. That function then use to plot the points in the demo below.
function parseEquation(input){
// Important that white spaces are removed first
input = input.replace(/\s+/g,""); // remove whitespaces
input = input.replace(/([\-\+])([xy])/g,"$11$2"); // convert -x -y or +x +y to -1x -1y or +1x +1y
// just to make the logic below a little simpler
var newTerm = () => {term = { val : null, scalar : 1, left : left, };} // create a new term
var pushTerm = () => {terms.push(term); term = null;} // push term and null current
// regExp [xy=] gets "x","y", or "="" or [\-\+]??[0-9\.]+ gets +- number with decimal
var reg =/[xy=]|[\-\+]??[0-9\.eE]+/g; // regExp to split the input string into parts
var parts = input.match(reg); // get all the parts of the equation
var terms = []; // an array of all terms parsed
var term = null; // Numbers as constants and variables with scalars are terms
var left = true; // which side of equation a term is
parts.forEach( p=> {
if (p === "x" || p === "y") {
if (term !== null && term.val !== null) { // is the variable defined
pushTerm(); // yes so push to the stack and null
}
if (term === null) { newTerm(); } // do we need a new term?
term.val = p;
} else if( p === "=") { // is it the equals sign
if (!left) { throw new SyntaxError("Unxpected `=` in equation."); }
if (term === null) { throw new SyntaxError("No left hand side of equation."); }// make sure that there is a left side
terms.push(term); // push the last left side term onto the stack
term = null;
left = false; // everything on the right from here on in
} else { // all that is left are numbers (we hope)
if (isNaN(p)){ throw new SyntaxError("Unknown value '"+p+"' in equation"); }//check that there is a number
if (term !== null && (p[0] === "+" || p[0] === "-")) { // check if number is a new term
pushTerm(); // yes so push to the stack and null
}
if (term === null) { newTerm(); } // do we need a new term?
term.scalar *= Number(p); // set the scalar to the new value
}
});
if (term !== null) { // there may or may not be a term left to push to the stack
pushTerm();
}
// now simplify the equation getting the scalar for left and right sides . x on left y on right
var scalarX = 0;
var scalarY = 0
var valC = 0; // any constants
terms.forEach(t => {
t.scalar *= !t.left ? -1 : 1; // everything on right is negative
if (t.val === "y") {
scalarY += -t.scalar; // reverse sign
} else if (t.val === "x") {
scalarX += t.scalar;
} else {
valC += t.scalar;
}
})
// now build the code string for the equation to solve for x and return y
var code = "return (" + scalarX + " * x + (" + valC + ")) / "+scalarY +";\n";
var equation = new Function("x",code); // create the function
return equation;
}
The following usage examples are all the same equation
var equation = parseEquation("x2+5y+x=230");
var y = equation(10); // get y for x = 10;
equation = parseEquation("x2+x=230-5y");
equation = parseEquation("x2+x-30=200-2y-3y");
equation = parseEquation("200- 2y-3y = x2+x-30");
equation = parseEquation("200-2y- 3y - x2-x+30=0");
equation = parseEquation("100.0 + 100-2y- 3y - x2-x+30=0");
equation = parseEquation("1e2 + 10E1-2y- 3y - x2-x+30=0");
Demo
I have added it to the code in the answer markE has already given. (hope you don't mind markE)
function plot(equation) {
var graph;
var xPadding = 30;
var yPadding = 30;
var data = {
values : [{
X : "1",
Y : 15
}, {
X : "2",
Y : 35
}, {
X : "3",
Y : 60
}, {
X : "4",
Y : 14
}, {
X : "5",
Y : 20
}, {
X : "6",
Y : -30
},
]
};
// Returns the max Y value in our data list
function getMaxY() {
var max = 0;
for (var i = 0; i < data.values.length; i++) {
if (data.values[i].Y > max) {
max = data.values[i].Y;
}
}
max += 10 - max % 10;
return max;
}
var scaleA = 1.4;
// Return the x pixel for a graph point
function getXPixel(val) {
return ((graph.width() / scaleA - xPadding) / data.values.length) * val + (xPadding * 1.5);
}
// Return the y pixel for a graph point
function getYPixel(val) {
return graph.height() / scaleA - (((graph.height() / scaleA - yPadding) / getMaxY()) * val) - yPadding;
}
graph = $('#graph');
var c = graph[0].getContext('2d');
c.clearRect(0,0,graph[0].width,graph[0].height);
c.lineWidth = 2;
c.strokeStyle = '#333';
c.font = 'italic 8pt sans-serif';
c.textAlign = "center";
// Draw the axises
c.beginPath();
c.moveTo(xPadding, 0);
c.lineTo(xPadding, graph.height() / scaleA - yPadding);
c.lineTo(graph.width(), graph.height() / scaleA - yPadding);
c.stroke();
// Draw the X value texts
for (var i = 0; i < data.values.length; i++) {
c.fillText(data.values[i].X, getXPixel(i), graph.height() / scaleA - yPadding + 20);
}
// Draw the Y value texts
c.textAlign = "right"
c.textBaseline = "middle";
for (var i = 0; i < getMaxY(); i += 10) {
c.fillText(i, xPadding - 10, getYPixel(i));
}
c.strokeStyle = '#f00';
// Draw the line graph
c.beginPath();
c.moveTo(getXPixel(0), getYPixel(equation(0)));
for (var i = 1; i < data.values.length; i++) {
c.lineTo(getXPixel(i), getYPixel(equation(i)));
}
c.stroke();
// Draw the dots
c.fillStyle = '#333';
for (var i = 0; i < data.values.length; i++) {
c.beginPath();
c.arc(getXPixel(i), getYPixel(equation(i)), 4, 0, Math.PI * 2, true);
c.fill();
}
}
var codeText = "";
function parseEquation(input){
// Important that white spaces are removed first
input = input.replace(/\s+/g,""); // remove whitespaces
input = input.replace(/([\-\+])([xy])/g,"$11$2"); // convert -x -y or +x +y to -1x -1y or +1x +1y
// just to make the logic below a little simpler
var newTerm = () => {term = { val : null, scalar : 1, left : left, };} // create a new term
var pushTerm = () => {terms.push(term); term = null;} // push term and null current
// regExp [xy=] gets "x","y", or "="" or [\-\+]??[0-9\.]+ gets +- number with decimal
var reg =/[xy=]|[\-\+]??[0-9\.eE]+/g; // regExp to split the input string into parts
var parts = input.match(reg); // get all the parts of the equation
var terms = []; // an array of all terms parsed
var term = null; // Numbers as constants and variables with scalars are terms
var left = true; // which side of equation a term is
parts.forEach(p=>{
if (p === "x" || p === "y") {
if (term !== null && term.val !== null) { // is the variable defined
pushTerm(); // yes so push to the stack and null
}
if (term === null) { newTerm(); } // do we need a new term?
term.val = p;
} else if( p === "="){ // is it the equals sign
if (!left) { throw new SyntaxError("Unxpected `=` in equation."); }
if (term === null) { throw new SyntaxError("No left hand side of equation."); }// make sure that there is a left side
terms.push(term); // push the last left side term onto the stack
term = null;
left = false; // everything on the right from here on in
} else { // all that is left are numbers (we hope)
if (isNaN(p)){ throw new SyntaxError("Unknown value '"+p+"' in equation"); }//check that there is a number
if (term !== null && (p[0] === "+" || p[0] === "-")){ // check if number is a new term
pushTerm(); // yes so push to the stack and null
}
if(term === null){ newTerm(); } // do we need a new term?
term.scalar *= Number(p); // set the scalar to the new value
}
});
if(term !== null){// there may or may not be a term left to push to the stack
pushTerm();
}
// now simplify the equation getting the scalar for left and right sides . x on left y on right
var scalarX = 0;
var scalarY = 0
var valC = 0; // any constants
terms.forEach(t => {
t.scalar *= !t.left ? -1 : 1; // everything on right is negative
if (t.val === "y") {
scalarY += -t.scalar; // reverse sign
} else if (t.val === "x") {
scalarX += t.scalar;
} else {
valC += t.scalar;
}
})
// now build the code string for the equation to solve for x and return y
var code = "return (" + scalarX + " * x + (" + valC + ")) / "+scalarY +";\n";
codeText = code;
var equation = new Function("x",code); // create the function
return equation;
}
function parseAndPlot(){
var input = eqInput.value;
try{
var equation = parseEquation(input);
plot(equation);
error.textContent ="Plot of "+input+ " as 'function(x){ "+codeText+"}'";
}catch(e){
error.textContent = "Error parsing equation. " + e.message;
}
}
var button = document.getElementById("plot");
var eqInput = document.getElementById("equation-text");
var error = document.getElementById("status");
button.addEventListener("click",parseAndPlot);
parseAndPlot();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<canvas id="graph" width="200" height="150"></canvas> <br>
Enter a linear equation : <input id="equation-text" value="x2 + 5y = 250" type="text"></input><input id="plot" value="plot" type=button></input><div id="status"></div>
I think I understand what you're asking...
Your existing code automatically puts your y-axis at the bottom of the canvas so negative y-values will be off-canvas.
Quick solution
The quickest solution is to divide graph.height()/2 so that your graph has it's y-axis near center-canvas. This leaves room for negative values.
Better solution
The better solution is to redesign your graphing system to allow for solutions in all axis directions.
Refactored code showing the quick solution:
I leave it to you to extend the y-axis labels in the negative direction (if desired)
var graph;
var xPadding = 30;
var yPadding = 30;
var data = { values:[
{ X: "1", Y: 15 },
{ X: "2", Y: 35 },
{ X: "3", Y: 60 },
{ X: "4", Y: 14 },
{ X: "5", Y: 20 },
{ X: "6", Y: -30 },
]};
// Returns the max Y value in our data list
function getMaxY() {
var max = 0;
for(var i = 0; i < data.values.length; i ++) {
if(data.values[i].Y > max) {
max = data.values[i].Y;
}
}
max += 10 - max % 10;
return max;
}
// Return the x pixel for a graph point
function getXPixel(val) {
return ((graph.width()/2 - xPadding) / data.values.length) * val + (xPadding * 1.5);
}
// Return the y pixel for a graph point
function getYPixel(val) {
return graph.height()/2 - (((graph.height()/2 - yPadding) / getMaxY()) * val) - yPadding;
}
graph = $('#graph');
var c = graph[0].getContext('2d');
c.lineWidth = 2;
c.strokeStyle = '#333';
c.font = 'italic 8pt sans-serif';
c.textAlign = "center";
// Draw the axises
c.beginPath();
c.moveTo(xPadding, 0);
c.lineTo(xPadding, graph.height()/2 - yPadding);
c.lineTo(graph.width(), graph.height()/2 - yPadding);
c.stroke();
// Draw the X value texts
for(var i = 0; i < data.values.length; i ++) {
c.fillText(data.values[i].X, getXPixel(i), graph.height()/2 - yPadding + 20);
}
// Draw the Y value texts
c.textAlign = "right"
c.textBaseline = "middle";
for(var i = 0; i < getMaxY(); i += 10) {
c.fillText(i, xPadding - 10, getYPixel(i));
}
c.strokeStyle = '#f00';
// Draw the line graph
c.beginPath();
c.moveTo(getXPixel(0), getYPixel(data.values[0].Y));
for(var i = 1; i < data.values.length; i ++) {
c.lineTo(getXPixel(i), getYPixel(data.values[i].Y));
}
c.stroke();
// Draw the dots
c.fillStyle = '#333';
for(var i = 0; i < data.values.length; i ++) {
c.beginPath();
c.arc(getXPixel(i), getYPixel(data.values[i].Y), 4, 0, Math.PI * 2, true);
c.fill();
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<canvas id="graph" width="200" height="300"></canvas>
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.