Calculate the horizontal distance from each tree node - javascript

I'm creating a pedigree tree diagram for breeding rats and I'm having trouble calculating the horizontal distance I need for each tree node since the number of descendants vary or is dynamic.
I found a script that I'm using to draw the tree but it looks like it doesn't have any documentation with it.
I need help in this part
var node = new Node({
title: 'M: ' + pair.male + '<br />F: ' + pair.female,
stage: stage,
w: NODE_DIMENSIONS.w,
h: NODE_DIMENSIONS.h,
x: COORDINATES.x + (INCREMENTS.x * pair.column), // Formula should adjust based on descendants - HELP
y: COORDINATES.y + (INCREMENTS.y * pair.generation)
}).attach();
Here's a Fiddle if you would like to mess around with it and the complete code is provided below.
HTML
<div id="stage"></div>
CSS
.node h4 {
position: static;
left: auto;
bottom: auto;
width: 80px;
height: 80px;
text-align: center;
vertical-align: middle;
display: table-cell;
text-shadow: none;
color: #ffffff;
font-weight: bold;
}
Script
var stage = $('#stage');
var NODE_DIMENSIONS = { w: 80, h: 80 };
var SEGMENT_DIMENSIONS = { h: 5 };
var COORDINATES = { x: 50, y: 50 };
var INCREMENTS = { x: 200, y: 150 };
// Sample JSON array resulting from an AJAX call.
var mating = [{
"name": "one",
"male": 1234,
"female": 5643,
"male_lineage": null,
"female_parent": null,
"generation": 0,
"column": 0
}, {
"name": "two",
"male": 6737,
"female": 1627,
"male_lineage": ["four"],
"female_parent": null,
"generation": 0,
"column": 2
}, {
"name": "three",
"male": 9332,
"female": 6227,
"male_lineage": ["five", "six"],
"female_parent": null,
"generation": 0,
"column": 3
}, {
"name": "four",
"male": 1111,
"female": 6537,
"male_lineage": null,
"female_parent": "one",
"generation": 1,
"column": 1
}, {
"name": "five",
"male": 8853,
"female": 3189,
"male_lineage": null,
"female_parent": "two",
"generation": 1,
"column": 2
}, {
"name": "six",
"male": 8853,
"female": 3189,
"male_lineage": null,
"female_parent": "three",
"generation": 1,
"column": 3
}];
var m = new Map();
for (var i = 0; i < mating.length; i++) {
var pair = mating[i];
var node = new Node({
title: 'M: ' + pair.male + '<br />F: ' + pair.female,
stage: stage,
w: NODE_DIMENSIONS.w,
h: NODE_DIMENSIONS.h,
x: COORDINATES.x + (INCREMENTS.x * pair.column), // Formula should adjust based on descendants - HELP
y: COORDINATES.y + (INCREMENTS.y * pair.generation)
}).attach();
var element = {
"pair": pair,
"node": node
};
m.set(pair.name, element);
}
m.forEach(function(element, key, m) {
// We are going to create 2 segments
// First is for the male lineage
if (element.pair.male_lineage != null) {
for(var i = 0; i < element.pair.male_lineage.length; i++) {
new Segment({
h: SEGMENT_DIMENSIONS.h,
stage: stage,
origin: element.node,
destination: m.get(element.pair.male_lineage[i]).node
}).attach();
}
}
// Last is the female parent
if (element.pair.female_parent != null) {
new Segment({
h: SEGMENT_DIMENSIONS.h,
stage: stage,
origin: element.node,
destination: m.get(element.pair.female_parent).node
}).attach();
}
});

Without even attempting (pardon me ...) to dive into the whys-and-wherefores of “your” particular problem, I can categorically say that “tree-drawing is an inherently recursive problem.”
Furthermore, it is not a problem that maps directly to the HTML “DOM.” (You will have to “deal with the DOM” after, not before, you decide “what the tree needs to look like.”)
Generally speaking, tree-painting problems need to work, recursively, from the bottom up: the lowest non-leaf nodes distribute their leaves equally, then position themselves equidistant between the leftmost and the rightmost of their children, then report their width as being equal to the extent of their children’s width. (This recursive description of the problem then resolves itself, more-or-less satisfactorily, to encompass the entire tree.)
Once you have thus determined how the entire tree would like to appear, your next (but, altogether unrelated ...) problem is: how to manipulate the DOM-tree, which is “the thing that more-or-less drives the browser,” to actually produce the visual outcome that you want.
(“Uhh... except on Internet Explorer 8.”)

Related

How to access nested values of a JSON file when they are nested multiple times

I have a JSON file like the example listed below.
"SOLAR_SYSTEM": {
"PLANETS": {
"PLANET": [
{
"NAME": "Mercury",
"DISTANCE": "57.91km",
"RADIUS": "2340km",
"LENGTH_OF_YEAR": "0.24085",
"DAY": "88",
"MASS": "0.054",
"DENSITY": "6.03"
},
{
"NAME": "Earth",
"DISTANCE": "149.60",
"RADIUS": "6371",
"LENGTH_OF_YEAR": "1.000039",
"DAY": "24",
"MASS": "1.00",
"DENSITY": "5.517",
"SATELLITES": {
"SATELLITE": {
"NAME": "Moon",
"DISTANCE_FROM_PLANET": "384405",
"ORBIT": "27.322"
}
}
},
{
"NAME": "Mars",
"DISTANCE": "227.9",
"RADIUS": "3324",
"LENGTH_OF_YEAR": "1.88089",
"DAY": "24.5",
"MASS": "0.107",
"DENSITY": "4.16",
"SATELLITES": {
"SATELLITE": [
{
"NAME": "Phobos",
"DISTANCE_FROM_PLANET": "9380",
"ORBIT": "0.319"
},
{
"NAME": "Deimos",
"DISTANCE_FROM_PLANET": "23500",
"ORBIT": "1.262"
}
My question is how to access the data, specifically the SATELLITES --> SATELLITE --> name, distance_from_planet, and orbit.
The code that I've written below works for accessing the data of the non-nested data (a planet's name, distance, radius, etc). As seen below, I have tried using conditional statements to check if a planet has a satellite, but with no success.
$(document).ready(function(){
$.getJSON("./json/planets.json", function(data){
//Loop to retrieve and put data in for non nested json
for(i = 0; i < data.SOLAR_SYSTEM.PLANETS.PLANET.length; i++){
index = i + 1;
//All fields that aren't nested
$("#pName" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].NAME);
$("#pDistance" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].DISTANCE);
$("#pRadius" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].RADIUS);
$("#pLength" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].LENGTH_OF_YEAR);
$("#pDay" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].DAY);
$("#pMass" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].MASS);
$("#pDensity" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].DENSITY);
if(data.SOLAR_SYSTEM.PLANETS.PLANET[i].SATELLITES == null){
continue;
}else{
$("#pSatName_" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].SATELLITES.SATELLITE.NAME);
$("#pDistanceFP_" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].SATELLITES.SATELLITE.DISTANCE_FROM_PLANET);
$("#pOrbit_" + index).html(data.SOLAR_SYSTEM.PLANETS.PLANET[i].SATELLITES.SATELLITE.ORBIT);
//console.log(data.SOLAR_SYSTEM.PLANETS.PLANET[i].SATELLITES.SATELLITE.NAME);
}
}
First off, you can cut down a lot of typing with object destructuring.
let {PLANET} = data.SOLAR_SYSTEM.PLANETS
for(i = 0; i < PLANET.length; i++){
if(PLANET[i].SATTELITES)
let {SATTELITE} = PLANET[i].SATTELITES
else let SATTELITE = null
Then you don't have to type out the whole thing every time.
Like Rory said, data consistency would go a long way, but if you have no control over that you can check whether SATTELITE is an array or an object and access it appropriately.
if(SATTELITE && SATTELITE instanceof Array){
access = SATTELITE[num][key]
}
else if(SATTELITE && SATTELITE instanceof Object{
access = SATTELITE[key]
}

getConnectedNodes direction parameter

I have a small issue with the parameter direction of the function getConnectedNodes() based on the Vis.js documentation (search for "getConnectedNodes" in the link)
Any idea to get the direction of the edges using the parameter (i don't know how to)?
JSON Example
[
{ "x": 0, "y": 0, "id": "0", "connections": [ 2 ] // i think here should be a from?},
{ "x": 200, "y": 0, "id": "1", "connections": [ 3, 2 ] },
{ "x": 500, "y": 500, "id": "2", "connections": [ 0, 1 ] },
{ "x": 300, "y": -200, "id": "3", "connections": [ 1 ] }
]
Here part of the code
google.script.run.withSuccessHandler(([nodes, edges]) => new vis.Network(container, {nodes: nodes, edges: edges}, options)).sample();
let network;
function init() {
container = document.getElementById('mynetwork');
exportArea = document.getElementById('input_output');
network = google.script.run.withSuccessHandler(([nodes, edges]) => {network = new vis.Network(container, {nodes: nodes, edges: edges}, options);}).sample();
};
function addConnections(elem, index) {
elem.connections = network.getConnectedNodes(index); < I THINK THE PROBLEM IS HERE
}
function exportNetwork() {
var nodes = objectToArray(network.getPositions());
nodes.forEach(addConnections);
var exportValue = JSON.stringify(nodes, undefined, 2);
exportArea.innerHTML = exportValue;
}
function objectToArray(obj) {
return Object.keys(obj).map(function(key) {
obj[key].id = key;
return obj[key];
});
}
Before hand, thanks a lot!
index is the index of the array like 0, 1, 2,,,. The start index is 0. On the other hand, elem is the object like {x: ###, y: ###, id: ###}. From these situation, I thought that index of getConnectedNodes(index) might be elem.id. So how about the following modification?
From:
elem.connections = network.getConnectedNodes(index);
To:
elem.connections = network.getConnectedNodes(elem.id, "from");
From the document, if you want to retrieve "parent", you can retrieve it by adding from to the argument.
For a node id, returns an array with the id's of the connected nodes.
If optional parameter direction is set to string 'from', only parent nodes are returned.
If direction is set to 'to', only child nodes are returned.
Any other value or undefined returns both parent and child nodes.
When you want to retrieve "child", please add to to the argument instead of from.

Selectively flattening a nested JSON structure

So this is a problem that I have no idea where to even start so even just a pointer in the right direction would be great.
So I have data that looks like so:
data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
{
...
},
{
...
},
...
]
}
}
Now from the example, within 'agg' there are N 'agg1' results, within each 'agg1' result there is a 'keyWeWant'. Each 'agg1' result also has a list of 'some_nested_agg' results which each contain a 'keyWeWant2'. Each 'keyWeWant2' value is associated a single 'keyWeWant' value somewhere up in the hierarchy. Similarly each 'keyWeWant2' also contains a set of results for 'some_nested_agg2' (not a list but rather a map this time). Each of the set of results contains a 'keyWeWant3'.
Now I want to flatten this structure while still preserving the association between 'keyWeWant', 'keyWeWant2', and 'keyWeWant3' (I'm essentially de-normalizing) to get something like so:
What I want the function to look like:
[
{
"keyWeWant" : "*-20",
"keyWeWant2" : 20,
"keyWeWant3" : 2.857142857142857
},
{
"keyWeWant" : "*-20",
"keyWeWant2" : 25,
"keyWeWant3" : 6.375
},
{
...
},
{
...
}
]
This is an example where there is only depth 3 but there could be arbitrary depth with some nested values being lists and some being arrays/list.
What I would like to do is write a function to take in the keys I want and where to find them, and then go get the keys and denormalize.
Something that looks like:
function_name(data_map, {
"keyWeWant" : ['agg', 'agg1'],
"keyWeWant2" : ['agg', 'agg1', 'some_nested_agg'],
"keyWeWant" : ['agg', 'agg1', 'some_nested_agg', 'some_nested_agg2']
})
Any ideas? I'm familiar with Java, Clojure, Java-script, and Python and am just looking for a way to solve this that's relatively simple.
Here is a JavaScript (ES6) function you could use:
function flatten(data, keys) {
var key = keys[0];
if (key in data)
keys = keys.slice(1);
var res = keys.length && Object.keys(data)
.map( key => data[key] )
.filter( val => Object(val) === val )
.reduce( (res, val) => res.concat(flatten(val, keys)), []);
return !(key in data) ? res
: (res || [{}]).map ( obj => Object.assign(obj, { [key]: data[key] }) );
}
// Sample data
var data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
]
}
};
// Flatten it by array of keys
var res = flatten(data, ['keyWeWant', 'keyWeWant2', 'keyWeWant3']);
// Output result
console.log(res);
Alternative using paths
As noted in comments, the above code does not use path information; it just looks in all arrays. This could be an issue if the keys being looked for also occur in paths that should be ignored.
The following alternative will use path information, which should be passed as an array of sub-arrays, where each sub-array first lists the path keys, and as last element the value key to be retained:
function flatten(data, [path, ...paths]) {
return path && (
Array.isArray(data)
? data.reduce( (res, item) => res.concat(flatten(item, arguments[1])), [] )
: path[0] in data && (
path.length > 1
? flatten(data[path[0]], [path.slice(1), ...paths])
: (flatten(data, paths) || [{}]).map (
item => Object.assign(item, { [path[0]]: data[path[0]] })
)
)
);
}
// Sample data
var data = {
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}
]
},
]
}
};
// Flatten it by array of keys
var res = flatten(data, [
['agg', 'agg1', 'keyWeWant'],
['some_nested_agg', 'keyWeWant2'],
['some_nested_agg2', 'keyWeWant3']]);
// Output result
console.log(res);
There is probably a better way to solve this particular problem (using some ElasticSearch library or something), but here's a solution in Clojure using your requested input and output data formats.
I placed this test data in a file called data.json:
{
"agg": {
"agg1": [
{
"keyWeWant": "*-20.0",
"asdf": 0,
"asdf": 20,
"asdf": 14,
"some_nested_agg": [
{
"keyWeWant2": 20,
"to": 25,
"doc_count": 4,
"some_nested_agg2": {
"count": 7,
"min": 2,
"max": 5,
"keyWeWant3": 2.857142857142857,
"sum": 20
}
},
{
"keyWeWant2": 25,
"to": 30,
"doc_count": 10,
"some_nested_agg2": {
"count": 16,
"min": 2,
"max": 10,
"keyWeWant3": 6.375,
"sum": 102
}
}]
}]}
}
Then Cheshire JSON library parses the data to a Clojure data structure:
(use '[cheshire.core :as cheshire])
(def my-data (-> "data.json" slurp cheshire/parse-string))
Next the paths to get are defined as follows:
(def my-data-map
{"keyWeWant" ["agg", "agg1"],
"keyWeWant2" ["agg", "agg1", "some_nested_agg"],
"keyWeWant3" ["agg", "agg1", "some_nested_agg", "some_nested_agg2"]})
It is your data_map above without ":", single quotes changed to double quotes and the last "keyWeWant" changed to "keyWeWant3".
find-nested below has the semantics of Clojure's get-in, only then it works on maps with vectors, and returns all values instead of one.
When find-nested is given a search vector it finds all values in a nested map where some values can consist of a vector with a list of maps. Every map in the vector is checked.
(defn find-nested
"Finds all values in a coll consisting of maps and vectors.
All values are returned in a tree structure:
i.e, in your problem it returns (20 25) if you call it with
(find-nested ['agg', 'agg1', 'some_nested_agg', 'keyWeWant2']
my-data).
Returns nil if not found."
[ks c]
(let [k (first ks)]
(cond (nil? k) c
(map? c) (find-nested (rest ks) (get c k))
(vector? c) (if-let [e (-> c first (get k))]
(if (string? e) e ; do not map over chars in str
(map (partial find-nested (rest ks)) e))
(find-nested ks (into [] (rest c)))) ; create vec again
:else nil)))
find-nested finds the values for a search path:
(find-nested ["agg", "agg1", "some_nested_agg", "keyWeWant2"] my-data)
; => (20 25)
If all the paths towards the "keyWeWant's are mapped over my-data these are the slices of a tree:
(*-20.0
(20 25)
(2.857142857142857 6.375))
The structure you ask for (all end results with paths getting there) can be obtained from this tree in function-name like this:
(defn function-name
"Transforms data d by finding (nested keys) via data-map m in d and
flattening the structure."
[d m]
(let [tree (map #(find-nested (conj (second %) (first %)) d) m)
leaves (last tree)
leaf-indices (range (count leaves))
results (for [index leaf-indices]
(map (fn [slice]
(if (string? slice)
slice
(loop [node (nth slice index)]
(if node
node
(recur (nth slice (dec index)))))))
tree))
results-with-paths (mapv #(zipmap (keys m) %) results)
json (cheshire/encode results-with-paths)]
json))
results uses a loop to step back if a leaf-index is larger than that particular slice. I think it will work out for deeper nested structures as well -if a next slice is always double the size of a previous slice or the same size it should work out -, but I have not tested it.
Calling (function-name my-data my-data-map) leads to a JSON string in your requested format:
[{
"keyWeWant": "-20.0",
"keyWeWant2": 20,
"keyWeWant3": 2.857142857142857 }
{
"keyWeWant": "-20.0",
"keyWeWant2" 25,
"keyWeWant3" 6.375 }]
/edit
I see you were looking for a relatively simple solution, that this is not. :-) maybe there is one without having it available in a library. I would be glad to find out how it can be simplified.

Vector: Get closest vectors based on a single vector

Update:
Thanks to the user #Thomas I was able to fix my script.
You can find his comments in the comment section below.
To make it easier, here the most important quotes:
[...] why do your vectors contain strings instead of numbers?
I accidently kept on using strings in my vectors, thus breaking proper results.
Make sure not to use any strings when working with numbers.
In addition, make sure your results are proper sorted:
Sort by:
min(||house.min - client||, ||house.max - client||)
or
||(house.max + house.min)/2 - client||
The problem:
I have an array with 3 houses, each containing two X, Y, Z vectors. One of them is the min point, whereas the other is the max. Those two vectors represent the field/area the house is in.
In addition, I have one client which is represented by a single vector.
Now I want to get the closest houses a client is near to. Preferably based on the min and max vector of a house.
Values:
var houses = [
{
"max": { x: "-7839.9688", y: "-7469.0312", z: "215.537" },
"name": "House 1",
"min": { x: "-7112.0312", y: "-7932.9692", z: "72.0312" }
}, {
"max": { x: "-6380.0312", y: "-9586.1963", z: "372.1475" },
"name": "House 3",
"min": { x: "-6764.2441", y: "-9189.0312", z: "96.3953" },
}, {
"max": { x: "-7747.1074", y: "-8659.9023", z: "422.2812" },
"name": "House 2",
"min": { x: "-6827.3853", y: "-9668.9688", z: "72.0312" },
}
]
Please note: The array above does not represent the figure below. The figure is just for illustration purposes only. In this particular case,
client 1 is close to house 2. However. In a different use-case, he/she
might be close to house 3.
Illustration / Figure:
Approach 1:
var houses = [
{
"max": { x: "-7839.9688", y: "-7469.0312", z: "215.537" },
"name": "House 1",
"min": { x: "-7112.0312", y: "-7932.9692", z: "72.0312" }
},
{
"max": { x: "-6380.0312", y: "-9586.1963", z: "372.1475" },
"name": "House 3",
"min": { x: "-6764.2441", y: "-9189.0312", z: "96.3953" },
},
{
"max": { x: "-7747.1074", y: "-8659.9023", z: "422.2812" },
"name": "House 2",
"min": { x: "-6827.3853", y: "-9668.9688", z: "72.0312" },
}
]
var client = {}
client.x = '-7514.48'
client.y = '-9443.54'
client.z = '-183.969'
var distances = []
for (var i = 0; i < houses.length; i++) {
var house = houses[i]
var distance_between_house_min_max = Math.sqrt(((house.min.x - house.max.x)*(house.min.x - house.max.x)) + ((house.min.y - house.max.y)*(house.min.y - house.max.y)) + ((house.min.z - house.max.z)*(house.min.z - house.max.z)));
// Here I would like to take house.max and house.min together in order to get the clients distance to a the house
var distance_of_client_to_house = Math.sqrt(((client.x - house.max.x)*(client.x - house.max.x)) + ((client.y - house.max.y)*(client.y - house.max.y)) + ((client.z - house.max.z)*(client.z - house.max.z)));
console.log("Distance to " + house.name + " is: " + distance_of_client_to_house)
distances.push({ house: house, distance: distance_of_client_to_house })
}
// Order distances by lowest distance
distances = distances.sort(function (a, b) {
return a.distance > b.distance
});
console.log("Distances:")
console.log(distances)
Approach 2:
Since Stackoverflow do not support node JS modules, I did not include this approach in the first place. That's why I headed for approach 1 with strings instead of 'real' vector objects.
Basically, in this approach we use the vec3 node JS module which offers vector manipulation.
var vector = require("vec3").Vec3
var houses = [
{
"max": new vector(-7839.9688, -7469.0312, 215.537),
"name": "House 1",
"min": new vector(-7112.0312, -7932.9692, 72.0312)
},
{
"max": new vector(-6380.0312, -9586.1963, 372.1475),
"name": "House 3",
"min": new vector(-6764.2441, -9189.0312, 96.3953),
},
{
"max": new vector(-7747.1074, -8659.9023, 422.2812),
"name": "House 2",
"min": new vector(-6827.3853, -9668.9688, 72.0312),
}
]
var client = new vector(-7514.48, -9443.54, -183.969)
var distances = []
for (var i = 0; i < houses.length; i++) {
var house = houses[i]
var distance_between_house_min_max = house.min.distanceTo(house.max);
// Here I would like to take house.max and house.min together in order to get the clients distance to a the house
var distance_of_client_to_house = client.distanceTo(house.max);
console.log("Distance to " + house.name + " is: " + distance_of_client_to_house)
distances.push({ house: house, distance: distance_of_client_to_house })
}
// Order distances by lowest distance
distances = distances.sort(function (a, b) {
return a.distance > b.distance
});
console.log("Distances:")
console.log(distances)
Demo: https://tonicdev.com/stevemuster/57c3a52fee25751400967ddd
Question: How can I correctly guess the houses a client is near to? I am open for all your suggestions/hints. Thank you.

Sorting a multidimensional array by multiple properties

I am trying to sort an array by multiple properties, but the problem is that my array is multidimensional.
Currently I have built this:
// Private function to get the value of the property
var _getPropertyValue = function (object, notation) {
// Get all the properties
var properties = notation.split('.');
// If we only have one property
if (properties.length === 1) {
// Return our value
return object[properties];
}
// Loop through our properties
for (var property in object) {
// Make sure we are a property
if (object.hasOwnProperty(property)) {
// If we our property name is the same as our first property
if (property === properties[0]) {
// Remove the first item from our properties
properties.splice(0, 1);
// Create our new dot notation
var dotNotation = properties.join('.');
// Find the value of the new dot notation
return _getPropertyValue(object[property], dotNotation);
}
}
}
};
// Create a service
var service = {
// Sorts our products
sort: function (products, notation) {
notation = notation || 'details.title';
// Call sort
products.sort(function (a, b) {
// Get our values
var aValue = _getPropertyValue(a, notation),
bValue = _getPropertyValue(b, notation);
console.log(bValue);
// If our attribute name is not the same as the second attribute
if (aValue <= bValue) {
// Return -1
return -1;
}
// Otherwise return 1
return 1;
});
}
};
// Return our service
return service;
And this is one item from the array (products)
{
"id": 1,
"gtin": "8714574627946|4549292038446",
"productId": "0592C022",
"make": "Canon",
"model": "750D + EF-S 18-55mm",
"expert": false,
"sponsored": false,
"attributes": {
"id": 1,
"compatibleMemory": "SD, SDHC, SDXC\"",
"whiteBalance": "ATW, Cloudy, Custom modes, Daylight, Flash, Fluorescent L, Shade, Tungsten\"",
"sceneModes": "Food, Landscape, Sports\"",
"shootingModes": "",
"photoEffects": "",
"cameraPlayback": "Movie, Single image, Slide show\"",
"tripod": false,
"directPrinting": false,
"colour": "Black",
"picture": {
"id": 1,
"megapixel": "24.2 MP",
"type": "SLR Camera Kit",
"sensorType": "CMOS",
"maxResolution": "6000 x 4000 pixels",
"resolutions": "3984x2656, 2976x1984, 1920x1280, 720x480, 5328x4000, 3552x2664, 2656x1992, 1696x1280, 640x480, 6000x3368, 3984x2240, 2976x1680, 1920x1080, 720x480, 4000x4000, 2656x2656, 1984x1984, 1280x1280, 480x480\"",
"stablizer": true,
"location": "Lens",
"supportedAspectRatios": "2.9 cm",
"totalMegapixels": "24.7 MP",
"formats": "JPG"
},
"video": {
"id": 1,
"maxResolution": "1920 x 1080 pixels",
"resolutions": "640 x 480, 1280 x 720, 1920 x 1080 pixels\"",
"captureResolution": "",
"frameRate": "",
"fullHD": true,
"supportedFormats": null
},
"audio": {
"id": 1,
"supportedFormats": ""
},
"battery": {
"id": 1,
"powerSource": "Battery",
"technology": "Lithium-Ion (Li-Ion)",
"life": "",
"type": "LP-E17"
},
"dimensions": {
"id": 1,
"width": "",
"depth": "7.78 cm",
"height": "10.1 cm",
"weight": "",
"weightIncludingBattery": "555 g"
},
"display": {
"id": 1,
"type": "LCD",
"diagonal": "7.62 cm (3\"\")\"",
"resolution": "1040000 pixels"
},
"exposure": {
"id": 1,
"isoSensitivity": "100, 6400, 12800, Auto\"",
"mode": "Auto, Manual\"",
"correction": "�5EV (1/2; 1/3 EV step)",
"metering": "Centre-weighted, Evaluative (Multi-pattern), Partial, Spot\"",
"minimum": 100,
"maxiumum": 12800
},
"flash": {
"id": 1,
"modes": "Hi-speed sync, Red-eye reduction\"",
"exposureLock": true,
"rangeWide": "",
"rangeTelephoto": "",
"rechargeTime": "",
"speed": "1/200"
},
"focusing": {
"id": 1,
"focus": "TTL-CT-SIR",
"adjustment": "",
"autoFocusModes": "",
"closestDistance": "0.25 m",
"normalRange": "",
"macroRangeTelephoto": "",
"macroRangeWide": "",
"autoModeTelephoto": "",
"autoModeWide": ""
},
"interface": {
"id": 1,
"pictBridge": true,
"usbVersion": "2.0",
"usbType": "",
"hdmi": true,
"hdmiType": "Mini"
},
"lens": {
"id": 1,
"focalLength": "18 - 55 mm",
"minimumFocalLength": "2.9 cm",
"maximumFocalLength": "8.8 cm",
"minimumAperture": "3.5",
"maximumAperture": "38",
"lensStructure": "13/11",
"zoom": {
"id": 1,
"optical": "",
"digital": "",
"extraSmart": "",
"combined": ""
}
},
"network": {
"id": 1,
"wiFi": false,
"wiFiStandards": "",
"nfc": false
},
"shutter": {
"id": 1,
"fastestSpeed": "1/4000 s",
"slowestSpeed": "30 s"
}
},
"details": {
"id": 1,
"title": "Canon EOS 750D + EF-S 18-55mm",
"description": "\"<b>Take your pictures to the next level with EOS 750D</b>\\n- Effortlessly take your pictures to the next level with the latest DSLR technology and Scene Intelligent Auto mode.\\n- Effortlessly capture stunning detail in any situation\\n- Record cinematic movies as easily as you shoot stills\\n- Easily connect and share your images with the world\\n\\n<b>Take your pictures to the next level with EOS 750D</b>\\n<b>Range of shooting modes</b>\\nEffortlessly capture stunning images using the latest DSLR technology with Basic and Creative modes, which allow you to take as much or as little control as you like.\\n\\n<b>Moveable screen for creative framing</b>\\nExplore creative shooting angles and enjoy simple and intuitive access to controls using the 3.0\"\" (7.7cm) Vari Angle LCD touch screen\\n\\n<b>Intelligent Viewfinder</b>\\nEOS 750D features an Intelligent Viewfinder which gives a much enhanced shooting experience. As you look through the viewfinder you can more easily see the focus point and any active AF areas, also the shooting information is clearly displayed.\\n\\n<b>Effortlessly capture stunning detail in any situation</b>\\nCapture vivid, detailed, high-resolution images with better dynamic range, lower noise and excellent control over depth of field thanks to a 24.2 Megapixel APS-C sensor.\\n\\n<b>19 all cross-type AF points for accurate subject tracking</b>\\nKeep track of fast moving action thanks to a fast and accurate autofocus system comprising 19 cross-type AF points.\\n\\n<b>Fast processor for action</b>\\nA powerful DIGIC 6 processor delivers full resolution shooting at 5 fps � so you�ll never miss that decisive moment.\\n\\n<b>Great low light shots</b>\\nTake memorable low light pictures without using flash thanks to a large ISO sensitivity range of ISO 100-12800 (extendable to ISO 25600)\\n\\n<b>Record cinematic Full HD movies as easily as you shoot stills</b>\\nShoot superbly detailed Full HD movies with a cinematic feel thanks to DSLR control over depth of field. Record your movies in MP4 format for quicker online sharing and easier transfer to other devices.\\n\\n<b>Smoother results</b>\\nEasily shoot cinematic Full HD movies with Hybrid CMOS AF III to track movement and focus smoothly between subjects.\\n\\n<b>Empower your creativity with easy shooting modes</b>\\nLet the camera do the work for you and capture creative photos with ease using a range of Scene Modes\\n\\n<b>Creative movie modes</b>\\nExpand the range of shooting possibilities in movies with features like Miniature Effect in movie.\"",
"shortDescription": "\"22.3 x 14.9mm CMOS, 24.2 megapixels, 3:2, DIGIC 6, LCD, ISO 12800, Full HD Movie, USB, HDMI mini, SD/SDHC/SDXC, Black\"",
"summary": "\"Canon 750D + EF-S 18-55mm, EOS. Megapixel: 24.2 MP, Camera type: SLR Camera Kit, Sensor type: CMOS. Focal length range (f-f): 18 - 55 mm, Minimum focal length (35mm film equiv): 2.9 cm, Maximum focal length (35mm film equiv): 8.8 cm. Focus: TTL-CT-SIR, Closest focusing distance: 0.25 m. ISO sensitivity: 100, 6400, 12800, Auto, Light exposure modes: Auto, Manual, Light exposure control: Program AE. Fastest camera shutter speed: 1/4000 s, Slowest camera shutter speed: 30 s, Camera shutter type: Electronic\"",
"shortSummary": "\"Canon EOS 750D + EF-S 18-55mm, ATW, Cloudy, Custom modes, Daylight, Flash, Fluorescent L, Shade, Tungsten, Food, Landscape, Sports, Movie, Single image, Slide show, Battery, SLR Camera Kit, TTL-CT-SIR\""
},
"category": null,
"preview": {
"id": 1,
"highRes": "http://images.icecat.biz/img/norm/high/26171112-1991.jpg",
"lowRes": "http://images.icecat.biz/img/norm/low/26171112-1991.jpg",
"manual": ""
}
}
This works for 1 property. Does anyone know how I can efficiently rehash this to work with multiple properties?
I have tried to do this:
// Create a service
var service = {
// Sorts our products
sort: function (products, notations) {
// Call sort
products.sort(function (a, b) {
// For each notation
for (var i = 0; i < notations.length; i++) {
// Get our notation
var notation = notations[i];
// Get our values
var aValue = _getPropertyValue(a, notation),
bValue = _getPropertyValue(b, notation);
console.log(bValue);
// If our attribute name is not the same as the second attribute
if (aValue <= bValue) {
// Return -1
return -1;
}
// Otherwise return 1
return 1;
}
});
}
};
and invoked it like this:
handler.sort(self.products, ['attributes.dimensions.weightIncludingBattery', 'attributes.network.wiFi']);
but this only seems to sort by the first property and not the second.
With the link that #Nina Scholz posted I managed to create a set of functions that seem to work fast. The set of functions look like this:
// Private function to get the value of the property
var _getPropertyValue = function (object, notation) {
// Get all the properties
var properties = notation.split('.');
// If we only have one property
if (properties.length === 1) {
// Return our value
return object[properties];
}
// Loop through our properties
for (var property in object) {
// Make sure we are a property
if (object.hasOwnProperty(property)) {
// If we our property name is the same as our first property
if (property === properties[0]) {
// Remove the first item from our properties
properties.splice(0, 1);
// Create our new dot notation
var dotNotation = properties.join('.');
// Find the value of the new dot notation
return _getPropertyValue(object[property], dotNotation);
}
}
}
};
// Get our fields
var _getFields = function (notations) {
// Create our array
var fields = [];
// For each notation
angular.forEach(notations, function (notation) {
// Get our field
var names = notation.split('.'),
len = names.length,
name = names[len - 1];
// Push our name into our array
fields.push({ name: name, notation: notation });
});
// Return our fields
return fields;
};
// Create a mapped array
var _createMapped = function (array, notations) {
// Get our fields
var fields = _getFields(notations);
// Create our mapped array
var mapped = array.map(function (a, i) {
// Create our object
var obj = {
index: i
};
// For each of our fields
angular.forEach(fields, function (field) {
// Map our field
obj[field.name] = _getPropertyValue(a, field.notation);
});
// Return our object
return obj;
});
// Return our mapped array
return mapped;
};
// Create a service
var service = {
// Sorts our products
sort: function (products, notations) {
// Get our fields
var mapped = _createMapped(products, notations);
// Sort our mapped array
mapped.sort(function (a, b) {
// Loop through our properties
for (var i = 0; i < notations.length; i++) {
// Get our value (skip the first)
var o1 = a[i + 1];
var o2 = b[i + 1];
// Compare the values
if (o1 < o2) return -1;
if (o1 > o2) return 1;
}
// Default return
return 0;
});
// Get our result
var result = mapped.map(function (item) {
return products[item.index];
});
// Return our result
return result;
}
};
// Return our service
return service;
Basically you need something like that:
For the access to a property's value an iteration through the object
function getValue(string, object) {
return string.split('.').reduce(function (r, a) {
return r[a];
}, object);
}
And for the sort mechanism the iteration over the wanted sort parameters. Actually I assume, that all values are strings.
// handler.sort
function sort(array, order) {
array.sort(function (a, b) {
var r = 0;
order.some(function (s) {
r = getValue(s, a).localeCompare(getValue(s, b));
return r;
});
return r;
});
}
The drawback of this is a very slow sorting, because of the lookup mechanism of a specific value.
A faster way would be sorting with map, where the map contains only the wanted values from the getValue
Sorting over multiple properties can be done as in the following example
var data = [
{
a : 10,
b : 24
},
{
a : 11,
b : 20
},
{
a : 12,
b : 21
},
{
a : 12,
b : 10
},
{
a : 10,
b : 12
},
{
a : 15,
b : 7
},
{
a : 10,
b : 18
}
]
var sortData = (arr, prop1, prop2) => arr.sort((p,c) => p[prop1] < c[prop1] ? -1: p[prop1] == c[prop1] ? p[prop2] <= c[prop2] ? -1 : 1: 1);
sorted = sortData(data,"a","b");
document.write("<pre>" + JSON.stringify(sorted,null,2) + "</pre>");

Categories