For example I have 3 points: (y:0, x:0), (y:100, x:10), (y:50, x:100). So I need to get 10 points among this polyline the distance between those is equal. I know how to get points between 2 ones, but I defenitely don't know how to receive among several points.
For receiving distance between 2 points I use this:
function getDistance(y1,x1,y2,x2){
return Math.sqrt(Math.pow(y2-y1, 2) + Math.pow(x2-x1, 2))
}
For computing single points I use it:
function interpolate(a, b, frac){
return {
y: a.y+(b.y-a.y)*frac,
x: a.x+(b.x-a.x)*frac
};
}
Can anyone help me?
This is working fine (for the example I'm using 3 points on the same line, but it should work for every combination)
function getDistance({y: y1, x:x1}, {y:y2, x:x2}){
return Math.sqrt(Math.pow(y2-y1, 2) + Math.pow(x2-x1, 2))
}
function interpolate(a, b, frac){
return {
x: a.x+(b.x-a.x)*frac,
y: a.y+(b.y-a.y)*frac,
};
}
//generate N point in a line
function generateOnLineEveryDistance(a, b, n, distance){
let res = [];
for(let i = 0; i < n ; i++){
// add a point interpolating on a line after (i + 1) * distance from the beginning point (i+1 to avoid the starting point 0,0)
res.push(interpolate(a, b, (i + 1) * distance))
}
return res;
}
function generate(points, n){
// calculate total distance to find out how distant we have to place the points
let totalDistance = 0;
for(let i = 1; i < points.length; i++){
totalDistance += getDistance(points[i - 1], points[i])
}
// distance to place the points
const pointDistance = totalDistance / (n - 1);
// the first point is always included
let res = [points[0]];
// now, we will consider a segment as point[i - 1] & point[i], and we will consider only the piece where we can fit point:
// eg. in a segment long 10 ([x:0, y:0], [x:0, y:10]), and a pointDistance of 4, we will consider only [x:0, y:0], [x:0, y:8]
// and the remainder is 2... this remainder will be used in the next segment, "appending it at the beginning"
// let's say the second segment is [x:0, y:10], [x:0, y:20], with the remainder it will be [x:0, y:8], [x:0, y:20]
let remainder = 0;
for(let i = 1; i < points.length ; i++){
// add the remainder if exists at the beginning of the current segment (point[i-1], point[i])
// source https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
if(remainder > 0){
let a = points[i];
let b = points[i - 1];
let lengthAB = Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
points[i - 1].x = b.x + (b.x - a.x) / lengthAB * remainder;
points[i - 1].y = b.y + (b.y - a.y) / lengthAB * remainder;
}
// points we need to generate
let nPoints = Math.floor(getDistance(points[i - 1], points[i]) / pointDistance)
// remainder to add to the next iteration
remainder = getDistance(points[i - 1], points[i]) - nPoints * pointDistance;
// add to the result the points
res = [
...res, // previous result
...generateOnLineEveryDistance( // new points
points[i - 1], // first point to consider
points[i], // second point to consider
nPoints, // number of points to generate
pointDistance / getDistance(points[i - 1], points[i]) // the ratio we want to use to generate them
)
]
}
// small fix in case of .333333 decimals that will "miss" the last point because it's .9999 and not 1.00000
if(res.length < n){
res = [...res, points[points.length - 1]]
}
return res;
}
const points = [
{
x: 0,
y: 0
} , {
x: 0,
y: 10
} , {
x: 0,
y: 20
}
]
console.log(generate(points, 4))
however, I can see this library already doing it, I've not checked it out, but maybe is worth checking it out because the code I'm providing is pretty untested and unreadable
UPDATE:
I've tested it against the examples they are providing and it's giving back the same result, so GG
So for example I have an array with 3 waypoints:
[ [ 526, 1573, 24 ], [ 2224, 809, -1546 ], [ 6869, 96, -3074 ] ]
I also know I want to rest for lets say n times between between arriving at the first and last waypoint. So in the end I want an array of n points.
How do I go about finding those n resting-points in JS?
Thanks in advance!
EDIT: Note this is not a single object! Imagine each axis being one person. They have to stop the same amount of time and at the same time but they do not have to be at the same place obviously.
You want to use linear interpolation.
A quick example:
const POINTS = [ [ 526, 1573, 24 ], [ 2224, 809, -1546 ], [ 6869, 96, -3074 ] ];
const N = 10;
function getDistance(point1, point2) {
// speed in 3d space is mutated according only to the X distance,
// to keep speed constant in X dimension
return Math.abs(point1[0] - point2[0]);
}
function go(points, n) {
const pointDistances = points.slice(1).map((point, index) => getDistance(points[index], point));
const fullDistance = pointDistances.reduce((sum, distance) => sum + distance, 0);
const distancePerSection = fullDistance / n;
return points.slice(1)
.reduce((last, point, index) => {
const thisDistance = pointDistances[index];
const numRestPoints = Math.max(0, Math.floor(thisDistance / distancePerSection) - 1);
if (!numRestPoints) {
return last.concat([point]);
}
const thisYVector = point[1] - points[index][1];
const thisZVector = point[2] - points[index][2];
return last.concat(new Array(numRestPoints).fill(0)
.reduce((section, item, restIndex) => {
return section.concat([[
points[index][0] + (restIndex + 1) * distancePerSection,
points[index][1] + (restIndex + 1) * thisYVector * distancePerSection / thisDistance,
points[index][2] + (restIndex + 1) * thisZVector * distancePerSection / thisDistance
]]);
}, [])
.concat([point])
);
}, points.slice(0, 1));
}
function test() {
const result = go(POINTS, N);
if (result.length !== N) {
throw new Error('Must be N length');
}
if (!result[0].every((value, index) => value === POINTS[0][index])) {
throw new Error('Doesn\'t start at the first point');
}
if (!result[N - 1].every((value, index) => value === POINTS[POINTS.length - 1][index])) {
throw new Error('Doesn\'t end at the last point');
}
if (!POINTS.slice(1, N - 1).every(point =>
result.some(resultPoint => resultPoint.every((value, index) => value === point[index]))
)) {
throw new Error('Doesn\'t go through every provided point');
}
console.log(result.slice(1).map((point, index) => getDistance(point, result[index])));
console.log('The result passed the tests!');
console.log(JSON.stringify(result, null, 2));
}
test();
I'm basically going through the list of points, and determining if there should exist any rest points between them, inserting them if so.
Please comment if you want further clarification!
I also solved this problem now with linear interpolation:
My solution:
var waypoints = [[526,1573,24],[2224,809,-1546],[6869,96,-3074]];
var pauses = 20;
generateWaypopints();
function generateWaypopints(){
var newWaypoints = [];
var progressAtMainPoints = 1 / (waypoints.length - 1)
var pausesBetweenWaypoints = pauses * progressAtMainPoints;
var progressAtPauses = 1 / pausesBetweenWaypoints;
newWaypoints.push(waypoints[0]);
var sector = 0;
var pausesInSector = 0;
for(var i = 0; i < pauses; i++){
var progress = progressAtPauses * (pausesInSector + 1)
var x = Math.round(waypoints[sector][0] + (waypoints[sector + 1][0] - waypoints[sector][0]) * progress);
var y = Math.round(waypoints[sector][1] + (waypoints[sector + 1][1] - waypoints[sector][1]) * progress);
var z = Math.round(waypoints[sector][2] + (waypoints[sector + 1][2] - waypoints[sector][2]) * progress);
if(progress >= 1){
sector++;
pausesInSector = 0;
}else
pausesInSector++;
newWaypoints.push([x,y,z]);
}
console.log(newWaypoints);
return newWaypoints;
}
I'm having problems with my jQuery homework (I'm a student and a JS beginner).
Basically the assignment is to create a Connect four game with jQuery using the MV(C) (we don't use the controller) schema.
The playing field is a 2D array and looks like this.
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
and the players play by pressing the columns (f.ex. Player 1 presses 3)
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , x , - , - , - , -
and the Player 2 presses 4
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , - , - , - , - , -
- , - , x , o , - , - , -
and so on.
When the game is over the winning four letter should change to uppercase.
I'm stuck and don't really know how to go on so I created a GitHub repository with all the code.
https://github.com/VeronicaLeeds/connectfour
Basically it initializes the playing field and you can press the numbers but then the play() method won't work.
It would be great if anyone could tell me why it doesn't change the value of the array from - to x/o ?
This is the code:
checkAvailableRow(column) {
for (var i = 5 ; i >= 0 ; i--) {
console.log(currentPlayer);
if (playFieldArray[column][i] === "-") {
return i;
if(currentPlayer == "Player 1"){
playFieldArray[column][i] = "x";
} else {
playFieldArray[column][i] = "o";
}
}
}
return -1;
}
And why the checkIfWon() function in the Model.js isn't working.
Would be eternally grateful for any help.
return i;
A return causes the logical process to return out of the function it is in. As such, none of the logic after it will ever happen. Comment this statement and see if anything changes for you (it should).
Or move it to after the if/else that changes the array value.
Here is an example of how to get available index of the y axes when providing a grid and x value:
//function to get available index for y when given grid and x
// it will return false if no y is available
const getY = (grid,index)=>{
const getIndex = (arr,index) => {
if(arr[index]==="-"){
return index;
}
if(index<0){
return false;
}
return getIndex(arr,index-1);
}
return getIndex(grid[index],grid[index].length-1);
}
//Run some tests:
const rotate = grid =>//just to make the grid look normal in console.log
grid[0].map((i,y)=>grid.map((i,x)=>[y,x]))
.map((row)=>row.map(([y,x])=>grid[x][y]));
const format = grid => rotate(grid).map(row=>row.join(" ")).join("\n");
//function to mutate grid setting items for testing
const setItem = (x,grid,value) => y => grid[x][y]=value;
//7 by 6 grid containing -
const grid = Array.from(new Array(7),()=>Array.from(new Array(6),()=>"-"));
//add some values to grid
[0,1,2,3,4,5].forEach(x=>[0,1,2,3,4,5].slice(x).forEach(setItem(x,grid,"O")));
console.log("grid is:");
console.log(format(grid));
//see the values for y when asking for
console.log("Testing....");
[0,1,2,3,4,5,6].forEach(
x=>console.log(`y is ${getY(grid,x)} when x is:${x}`)
);
The following is a function to check if a player wins given a grid and a position.
const isWin = (grid,position) => {
const [x,y] = position;
const player = grid[x][y];
if(player==="-"){//current location in grid is empty, return false
return false;
}
const countConnectedAllDirections = (directions,position,player) => {
const connectCount = (direction,position,player,count=1)=>{
const [x,y]=[position[0]+count*direction[0],position[1]+count*direction[1]];
if((grid[x] && grid[x][y])!==player){
return count;
}
return connectCount(direction,position,player,count+1);
};
return directions.map(
([back,forward])=>[connectCount(back,position,player),forward]
).map(
([countBack,forward])=>[countBack,connectCount(forward,position,player)]
).map(
([countBack,countForward])=>countBack+countForward-1
).reduce(
(highest,item)=>(item>highest)?item:highest,0
);
}
const directions = [[0,-1],[-1,-1],[-1,0],[-1,1]].map(([x,y])=>[[x,y],[x*-1,y*-1]]);
return countConnectedAllDirections(directions,position,player)>=4;
};
//Run some tests:
const rotate = grid =>//just to make the grid look normal in console.log
grid[0].map((i,y)=>grid.map((i,x)=>[y,x]))
.map((row)=>row.map(([y,x])=>grid[x][y]));
const format = grid => rotate(grid).map(row=>row.join(" ")).join("\n");
const test = (message,pass) =>
(pass)
? console.log(`Passed: ${message}`)
: console.error(`Failed: ${message}`)
//function to mutate grid setting items for testing
const setItem = (x,y,value) => grid[x][y]=value;
//7 by 6 grid containing -
const createGrid = () => Array.from(new Array(7),()=>Array.from(new Array(6),()=>"-"));
var grid = createGrid();
test("Should return false when position is empty",isWin(grid,[0,5])===false);
const posReducer = (all,pos)=>all&&isWin(grid,pos);
[[0,5],[0,4],[0,3]].forEach(([x,y])=>setItem(x,y,"X"));
console.log(format(grid));
test(
"Should return false when position is not empty but not connect 4",
[[0,5],[0,4],[0,3]].reduce((all,pos)=>all||isWin(grid,pos),false)===false
);
[[0,5],[0,4],[0,3],[0,2]].forEach(([x,y])=>setItem(x,y,"X"));
console.log(format(grid));
test(
"Should return true when position is not empty and connect 4 vertical",
[[0,5],[0,4],[0,3],[0,2]].reduce(posReducer,true)
);
grid = createGrid();//reset grid
[[1,5],[2,5],[3,5],[4,5]].forEach(([x,y])=>setItem(x,y,"X"));
console.log(format(grid));
test(
"Should return true when position is not empty and connect 4 horizontal",
[[1,5],[2,5],[3,5],[4,5]].reduce(posReducer,true)
);
grid = createGrid();//reset grid
[[1,5],[2,4],[3,3],[4,2]].forEach(([x,y])=>setItem(x,y,"X"));
console.log(format(grid));
test(
"Should return true when position is not empty and connect 4 diagonal bottom left to top right",
[[1,5],[2,4],[3,3],[4,2]].reduce(posReducer,true)
);
grid = createGrid();//reset grid
[[1,2],[2,3],[3,4],[4,5]].forEach(([x,y])=>setItem(x,y,"X"));
console.log(format(grid));
test(
"Should return true when position is not empty and connect 4 diagonal top left to bottom right",
[[1,2],[2,3],[3,4],[4,5]].reduce(posReducer,true)
);
The following is the complete game:
//view part
const [init,paint] = (()=>{
const getStyle = gridValue =>
(gridValue==="-")
? ""
: `style="background-color:${(gridValue==="RED")?"red":"green"}"`;
const createTD = ([x,y,gridValue]) =>
`<td ${getStyle(gridValue)} data-role="turn" data-position="${x},${y}"></td>`;
const createTR = gridRow =>
`<tr>${gridRow.reduce((result,value)=>result+createTD(value),"")}</tr>`;
const createTable = grid =>
`<table>${
grid.reduce((all,row)=>all+createTR(row),"")
}</table>`
;
const createMessage = message => `<div><h1>${message}</h1></div>`;
const createResetButton = () => `<button data-role="reset">Reset</button>`;
const createUndoRedo = () => `
<div>
<button data-role="undo_redo" data-direction="-1">Undo</button>
<button data-role="undo_redo" data-direction="1">Redo</button>
</div>
`;
const paint = state =>
state.container.innerHTML =
createTable(state.grid) +
createMessage(state.message) +
createResetButton(state) +
createUndoRedo();
const init = state => state.container.addEventListener(
"click",
e=>{
const role = e.target.getAttribute("data-role");
if(role==="turn"){
const xy = e.target.getAttribute("data-position");
handleEvent(role,xy.split(",").map(Number));
}
if(role==="reset"){
handleEvent(role,{});
}
if(role==="undo_redo"){
const direction = e.target.getAttribute("data-direction");
handleEvent(role,Number(direction));
}
}
);
return [init,paint];
})();
const handleEvent = (()=>{
const createState = ([x,y])=>({
grid:Array.from(new Array(x),()=>Array.from(new Array(y),()=>"-")),
container:document.querySelector("#content"),
currentPlayer:"RED",
winner:false,
message:`It's RED's turn`
});
var state = createState([7,6]);
const isWin = (grid,position) => {
const [x,y] = position;
const player = grid[x][y];
if(player==="-"){//current location in grid is empty, return false
return false;
}
const countConnectedAllDirections = (directions,position,player) => {
const connectCount = (direction,position,player,count=1)=>{
const [x,y]=[position[0]+count*direction[0],position[1]+count*direction[1]];
if((grid[x] && grid[x][y])!==player){
return count;
}
return connectCount(direction,position,player,count+1);
};
return directions.map(
([back,forward])=>[connectCount(back,position,player),forward]
).map(
([countBack,forward])=>[countBack,connectCount(forward,position,player)]
).map(
([countBack,countForward])=>countBack+countForward-1
).reduce(
(highest,item)=>(item>highest)?item:highest,0
);
}
const directions = [[0,-1],[-1,-1],[-1,0],[-1,1]].map(([x,y])=>[[x,y],[x*-1,y*-1]]);
return countConnectedAllDirections(directions,position,player)>=4;
};
const getAvailableIndex = (grid,x)=>{
const getIndex = (arr,y) => {
if(arr[y]==="-"){
return y;
}
if(y<0){
return false;
}
return getIndex(arr,y-1);
}
return getIndex(grid[x],grid[x].length-1);
}
const play = (player,grid,[posX,posY])=>{
return grid.map((arr,x)=>arr.map(
(val,y)=>(x===posX && y===posY) ? player : val
));
}
const rotateGrid = grid =>
grid[0].map((i,y)=>grid.map((i,x)=>[y,x]))
.map((row)=>row.map(([y,x])=>[x,y,grid[x][y]]));
const paintRotate = state => paint({...state,grid:rotateGrid(state.grid)});
const canMove = grid => grid.reduce(
(result,ignore,index)=>result||(getAvailableIndex(grid,index)!==false),false
)
const handleEvent = ((states,statesIndex) => (eventType,data) =>{
const newState={...states[statesIndex]};
if(eventType==="turn"){
if(newState.winner){
return;
}
if(!canMove(newState.grid)){
newState.message=`It's a draw, press reset to play again.`;
paintRotate(newState);
return;
}
const availableY = getAvailableIndex(newState.grid,data[0]);
if(availableY===false){
newState.message=`Cannot play this move, still ${newState.currentPlayer}'s turn.`
if(states[statesIndex].message!==newState.message){
statesIndex=states.length;
states.push(newState);
paintRotate(newState);
}
return;
}
statesIndex=states.length;
const newGrid = play(newState.currentPlayer,newState.grid,[data[0],availableY]);
newState.grid=newGrid;
newState.winner=isWin(newState.grid,[data[0],availableY]);
if(newState.winner){
newState.message=`Winner is:${newState.currentPlayer}`;
states.push(newState)
paintRotate(newState);
return;
}
if(!canMove(newState.grid)){
newState.message=`It's a draw, press reset to play again.`;
states.push(newState);
paintRotate(newState);
return;
}
newState.currentPlayer=(newState.currentPlayer==="RED")?"GREEN":"RED";
newState.message=`It's ${newState.currentPlayer}'s turn`;
states.push(newState);
paintRotate(newState);
}
if(eventType==="reset"){
state = createState([newState.grid.length,newState.grid[0].length]);
statesIndex=states.length;
states.push(state);
paintRotate(state);
}
if(eventType==="undo_redo"){
const newIndex = statesIndex+data;
if(newIndex<0){
paintRotate({...states[statesIndex],message:"Cannot undo. "+states[statesIndex].message});
return;
}
if(newIndex>=states.length){
paintRotate({...states[statesIndex],message:"Cannot redo. "+states[statesIndex].message});
return;
}
statesIndex=newIndex;
paintRotate(states[statesIndex]);
}
})([state],0);
init(state);
paintRotate(state);
return handleEvent;
})();
td {
width:30px;
height:30px;
border:1px solid gray;
cursor:pointer;
}
<div id="content"></div>
I just started with dc.js and was looking at the NASDAQ example on the main site: https://dc-js.github.io/dc.js/
I created a Fiddle with some sample dummy data and just the two relevant charts for this question.
Similar to the NASDAQ example, I want to have a bubble chart with the Y-Axis being the % Change in value over a timespan controlled by a brush in a different chart. The code for the NASDAQ example does the following:
var yearlyPerformanceGroup = yearlyDimension.group().reduce(
/* callback for when data is added to the current filter results */
function (p, v) {
++p.count;
p.absGain += v.close - v.open;
p.fluctuation += Math.abs(v.close - v.open);
p.sumIndex += (v.open + v.close) / 2;
p.avgIndex = p.sumIndex / p.count;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* callback for when data is removed from the current filter results */
function (p, v) {
--p.count;
p.absGain -= v.close - v.open;
p.fluctuation -= Math.abs(v.close - v.open);
p.sumIndex -= (v.open + v.close) / 2;
p.avgIndex = p.count ? p.sumIndex / p.count : 0;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* initialize p */
function () {
return {
count: 0,
absGain: 0,
fluctuation: 0,
fluctuationPercentage: 0,
sumIndex: 0,
avgIndex: 0,
percentageGain: 0
};
}
);
which I currently interpret as summing(close-open) across all data and dividing by the average of the average daily index. But this is not a percent change formula I am familiar with. (e.g. (new-old)/old x 100)
While it seems to work for the NASDAQ example, my data would be more like the following:
country_id,received_week,product_type,my_quantity,my_revenue,country_other_quantity
3,2017-04-02,1,1,361,93881
1,2017-04-02,4,45,140,93881
2,2017-04-02,4,2,30,93881
3,2017-04-02,3,1,462,93881
2,2017-04-02,3,48,497,93881
etc.. over many months and product_types.
Let's say I was interested in computing the percent change for a particular Country. How do I get the start and end quantities for a given country so I can compute change as end-start/start * 100?
I was thinking of something such as the following (assuming I set up the proper dimensions and everything)
var country_dim = ndx.dimension(function (d) { return d['country_id']; })
var received_day_dim = ndx.dimension(function (d) { return d['received_day']; })
var date_min = received_day_dim.bottom(1)[0]['received_day']
var date_max = received_day_dim.top(1)[0]['received_day']
Then in my custom reduce function currently in the vein of the example (wrong):
var statsByCountry = country_dim.group().reduce(
function (p, v) {
++p.count;
p.units += +v["my_units"];
p.example_rate = +v['my_units']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate += p.opp_buy_rate;
p.avgRate = p.opp_buy_rate/p.count;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars += +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
// p.min_date
//dateDimension.top(Infinity)[dateDimension.top(Infinity).length - 1]['distance'] - dateDimension.top(Infinity)[0]['distance']
return p;
},
function (p, v) {
--p.count;
if (v.region_id > 2) {
p.test -= 100;
}
p.units -= +v["quantity_unpacked"];
p.opp_buy_rate = +v['quantity_unpacked']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate -= p.opp_buy_rate;
p.avgRate = p.count ? p.opp_buy_rate/p.count : 0;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars -= +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
return p;
},
function () {
return {quantity_unpacked: 0,
count: 0,
units: 0,
opp_buy_rate: 0,
sumRate: 0,
avgRate: 0,
percentageGain: 0,
dollars: 0,
test: 0
};//, dollars: 0}
}
);
and my chart:
country_bubble
.width(990)
.height(250)
.margins({top:10, right: 50, bottom: 30, left:80})
.dimension(country_dim)
.group(statsByCountry)
.keyAccessor(function (p) {
return p.value.units;
})
.valueAccessor(function (p) { //y alue
return p.value.percentageGain;
})
.radiusValueAccessor(function (p) { //radius
return p.value.dollars/10000000;
})
.maxBubbleRelativeSize(0.05)
.elasticX(true)
.elasticY(true)
.elasticRadius(true)
.x(d3.scale.linear())
.y(d3.scale.linear())
// .x(d3.scale.linear().domain([0, 1.2*bubble_xmax]))
// .y(d3.scale.linear().domain([0, 10000000]))
.r(d3.scale.linear().domain([0, 10]))
.yAxisPadding('25%')
.xAxisPadding('15%')
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.on('renderlet', function(chart, filter){
chart.svg().select(".chart-body").attr("clip-path",null);
});
Originally thought of having something similar to the following in statsbycountry:
if (v.received_day == date_min) {
p.start_value += v.my_quantity;
}
if (v.received_day == date_max) {
p.end_value += v.my_quantity;
}
This seems a bit clumsy? But if I do this, I don't think this will continually update as other filters change (say time or product)? Ethan suggested I use fake groups, but I'm a bit lost.
With the working fiddle, we can demonstrate one way to do this. I don't really think this is the best way to go about it, but it is the Crossfilter way.
First you need to maintain an ordered array of all data in a group as part of the group using your custom reduce function:
var statsByCountry = country_dim.group().reduce(
function(p, v) {
++p.count;
p.units += +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars += +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 0, v);
return p;
},
function(p, v) {
--p.count;
p.units -= +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars -= +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 1)
return p;
},
function() {
return {
data: [],
count: 0,
units: 0,
country_rate: 0,
dollars: 0,
percent_change: 0
}; //, dollars: 0}
}
);
Above, I've updated your reduce function to maintain this ordered array (ordered by received_week) under the .data property. It uses Crossfilter's bisect function to maintain order efficiently.
Then in your valueAccessor you want to actually calculate your change in value based on this data:
.valueAccessor(function(p) { //y alue
// Calculate change in units/day from first day to last day.
var firstDay = p.value.data[p.value.data.length-1].received_week.toString();
var lastDay = p.value.data[0].received_week.toString();
var firstDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === firstDay ? d.my_quantity : 0 })
var lastDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === lastDay ? d.my_quantity : 0 })
return lastDayUnits - firstDayUnits;
})
You do this in the value accessor because it only runs once per filter change, whereas the reduce functions run once per record added or removed, which can be thousands of times per filter.
If you want to calculate % change, you can do this here as well, but the key question for % calculations is always "% of what?" and the answer to that question wasn't clear to me from your question.
It's worth noting that with this approach your group structure is going to get really big as you are storing your entire data set in the groups. If you are having performance problems while filtering, I would still recommend moving away from this approach and towards one based on a fake group.
Working updated fiddle: https://jsfiddle.net/vysbxd1h/1/
I've just implemented a topological sort algorithm on my isometric game using this guide: https://mazebert.com/2013/04/18/isometric-depth-sorting/
The issue
Here's a little example (this is just a drawing to illustrate my problem because as we say, a picture is worth a thousand words), what I'm expecting is in left and the result of the topological sorting algorithm is in right
So in the right image, the problem is that the box is drawn BEFORE the character and I'm expecting it to be drawn AFTER like in the left image.
Code of the topological sorting algorithm (Typescript)
private TopologicalSort2() {
// https://mazebert.com/2013/04/18/isometric-depth-sorting/
for(var i = 0; i < this.Stage.children.length; i++) {
var a = this.Stage.children[i];
var behindIndex = 0;
for(var j = 0; j < this.Stage.children.length; j++) {
if(i == j) {
continue;
}
var b = this.Stage.children[j];
if(!a.isoSpritesBehind) {
a.isoSpritesBehind = [];
}
if(!b.isoSpritesBehind) {
b.isoSpritesBehind = [];
}
if(b.posX < a.posX + a.sizeX && b.posY < a.posY + a.sizeY && b.posZ < a.posZ + a.sizeZ) {
a.isoSpritesBehind[behindIndex++] = b;
}
}
a.isoVisitedFlag = 0;
}
var _sortDepth = 0;
for(var i = 0; i < this.Stage.children.length; ++i) {
visitNode(this.Stage.children[i]);
}
function visitNode(n: PIXI.DisplayObject) {
if(n.isoVisitedFlag == 0) {
n.isoVisitedFlag = 1;
if(!n.isoSpritesBehind) {
return;
}
for(var i = 0; i < n.isoSpritesBehind.length; i++) {
if(n.isoSpritesBehind[i] == null) {
break;
} else {
visitNode(n.isoSpritesBehind[i]);
n.isoSpritesBehind[i] = null;
}
}
n.isoDepth = _sortDepth++;
}
}
this.Stage.children.sort((a, b) => {
if(a.isoDepth - b.isoDepth != 0) {
return a.isoDepth - b.isoDepth;
}
return 0;
});
}
Informations
Player:
posX: [the x coordinate of the player]
posY: [the y coordinate of the player]
posZ: 0
sizeX: 1
sizeY: 1
sizeZ: 1
Box:
posX: [the x coordinate of the box]
posY: [the y coordinate of the box]
posZ: 0
sizeX: 3
sizeY: 1
sizeZ: 1
X and Y axis
Do you have any idea of the source of this problem? and maybe how to solve it?
The way to determine whether one object is before the other requires a bit more linear algebra.
First of all, I would suggest to translate the coordinates from the "world" coordinates to the "view" 2D coordinates, i.e. to the rows and columns of the display.
Note also that the original Z coordinate does not influence the sort order (imagine that an object would be lifted up along the Z axis: we can find a sort order where this move would not have any impact). So the above-mentioned translation could assume all points are at Z=0.
Let's take this set-up, but depicted from "above", so when looking along the Z axis down to the game floor:
In the picture there are 7 objects, numbered from 0 to 6. The line of view in the game would be from the bottom-left of this picture. The coordinate system in which I would suggest to translate some points is depicted with the red row/col axis.
The white diagonals in each object link the two points that would be translated and used in the algorithm. The assumption is that when one object is in front of another, their diagonal lines will not intersect. If they would, it would mean that objects are overlapping each other in the game world, which would mean they are like gasses, not solids :) I will assume this is not the case.
One object A could be in front of another object B when in the new coordinate system, the left-most column coordinate of B falls between the two column coordinates of A (or vice versa). There might not really be such an overlap when their Z coordinates differ enough, but we can ignore that, because when there is no overlap we can do no harm in specifying a certain order anyway.
Now, when the coordinates indicate an overlap, the coordinates of diagonals (of A and B) must be compared with some linear algebra formula, which will determine which one is in front of the other.
Here is your adapted function that does that:
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
I add a snippet where I completed the class with a minimum of code, enough to make it run and output the final order in terms of object index numbers (as they were originally inserted):
class Game {
constructor() {
this.Stage = { children: [] };
}
addObject(posX, posY, posZ, sizeX, sizeY, sizeZ) {
this.Stage.children.push({posX, posY, posZ, sizeX, sizeY, sizeZ,
id: this.Stage.children.length}); // add a unique id
}
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
toString() { // Just print the ids of the children
return JSON.stringify(this.Stage.children.map( x => x.id ));
}
}
const game = new Game();
game.addObject( 2, 2, 0, 1, 1, 1 );
game.addObject( 1, 3, 0, 3, 1, 1 );
game.addObject( 6, 1, 0, 1, 3, 1 );
game.addObject( 9, 3, 0, 1, 1, 1 );
game.addObject( 5, 3, 0, 1, 3, 1 );
game.addObject( 7, 2, 0, 1, 1, 1 );
game.addObject( 8, 2, 0, 3, 1, 1 );
game.topologicalSort();
console.log(game + '');
The objects in the snippet are the same as in the picture with the same numbers. The output order is [0,1,4,2,5,6,3] which is the valid sequence for drawing the objects.