How to get grouped boxplots with vertical subplots - javascript

I'm trying to create a chart like the one in this image using Plotly.js:
It's a grouped boxplot (by sites, currently only one) with two y axes.
I've managed to create two versions, both of which don't work:
Create 5 traces (1 for each box) so that you can define the correct y axis for each box. This then puts them all next to each other, because they're different traces.
Create 3 traces to represent A, B and C. But then (afaik) I have to pick one y axis for each, which means that I can't have the same trace on two y axes.
Here's the code from approach 1 (https://codepen.io/wacmemphis/pen/gJQJeO?editors=0010)
var data =[
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"B",
"type":"box",
"boxpoints":false,
"y":[
"1.54",
"1.54",
"1.60",
"1.41",
"1.65",
"1.47"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
}
];
var layout = {
yaxis: {
domain: [0, 0.5],
title: 'axis 1',
},
yaxis2: {
domain: [0.5, 1],
title: 'axis2',
},
boxmode: 'group'
};
Plotly.newPlot('myDiv', data, layout);
Does anyone have any ideas?

Disclaimer
First of all I would like to emphasize that this is rather a workaraound, because Plotly currently does not support to distribute a single data source to multiple axis without interpreting them as new trace-instances (although it would be great to just set an array of target axis like { yaxis: [ "y", "y2" ] }).
However, Plotly is very deterministic in the way it handles ordering and grouping of traces, which can be taken to our advantage.
The following workaround approaches the problem in the following way:
Use two charts with one xaxis/yaxis instead of two axes
Use a single source of data for each trace (A, B, C)
Add traces to each (or both) of the plots dynamically, based on external decision
Use one of the following tactics to insert ghost objects and thus keep traces of both plots on the same x-axis positions:
a) use opacity
b) use a minimal width
c) use a threshold
1. Use two charts instead of two axes
Let's assume we can use two charts with the same layout:
<head>
<!-- Plotly.js -->
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<!-- render the upper axis 2 chart -->
<div id="myDiv_upper"></div>
<!-- render the lower axis 1 chart -->
<div id="myDiv_lower"></div>
<script>
/* JAVASCRIPT CODE GOES HERE */
</script>
</body>
With the accompanying js code to create two inital empty charts with the given layouts:
const myDiv = document.getElementById("myDiv_lower");
const myDiv2 = document.getElementById("myDiv_upper");
const layout = {
yaxis: {
domain: [0, 0.5],
title: "axis 1",
constrain: "range"
},
margin: {
t: 0,
b: 0,
pad: 0
},
showlegend: false,
boxmode: "group"
};
const layout2 = {
yaxis: {
domain: [ 0.5, 1 ],
title: "axis 2",
},
xaxis: {
domain: [ 0, 1 ]
},
margin: {
t: 0,
b: 0,
pad: 0
},
boxmode: "group"
};
Plotly.newPlot(myDiv, [], layout);
Plotly.newPlot(myDiv2, [], layout2);
The resulting empty plot will look like this, if no further data is added:
2. Use a single source of data for each trace (A, B, C)
We can then split the data into three main source-objects:
const A = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "A",
legendgroup: "A",
type: "box",
boxpoints: false,
y: ["3.81", "3.74", "3.62", "3.50", "3.50", "3.54"]
};
const B = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "B",
legendgroup: "B",
type: "box",
boxpoints: false,
y: ["1.54", "1.54", "1.60", "1.41", "1.65", "1.47"]
};
const C = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "C",
legendgroup: "C",
type: "box",
boxpoints: false,
y: ["3.31", "3.81", "3.74", "3.63", "3.76", "3.68"]
}
3. Add traces to each (or both) of the plots dynamically, based on external decision
First of all we create a helper add, that updates the charts, based on new incoming data and one that creates our ghost object helper, named placeholder:
const placeholder = src => {
const copy = Object.assign({}, src)
// use one of the strategies here to make this a ghost object
return copy
}
const add = ({ src, y1, y2 }) => {
let src2
if (y1 && y2) {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
} else if (y1 && !y2) {
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src2])
} else if (!y1 && y2) {
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src2])
Plotly.addTraces(myDiv2, [src])
} else {
throw new Error('require either y1 or y2 to be true to add data')
}
}
Based on the given images the decisions to add the data to the axis would result in the following calls:
add({ src: A, y1: true, y2: true })
add({ src: B, y1: true, y2: false })
add({ src: C, y1: true, y2: true })
This would create the following (yet not satisfiable) result:
Now we have at least resolved the grouping and color. The next step is to look for possible ways of making B a ghost object, that requires spacing in the upper chart but won't display the data.
4. Use one of the following tactics to insert ghost objects and thus keep traces of both plots on the same x-axis positions
Before we look into the different options, let's see what happens, if we remove the data or null the data.
remove the data
Removing the data would mean, that the placeholder has no x/y values:
const placeholder = src => {
const copy = Object.assign({}, src)
delete copy.x
delete copy.y
return copy
}
The result would still not satisfy the requirements:
null the data
Nulling the data has the nice effect, that the data is added to the legend (which has basically the same effect as visible: 'legendonly':
const placeholder = src => {
const copy = Object.assign({}, src)
copy.x = [null]
copy.y = [null]
return copy
}
The result would still not satisfy the requirements, allthough at least the legend grouping is now correct:
a) use opacity
One option to create a ghost object is to set it's opacity to zero:
const placeholder = src => {
const copy = Object.assign({}, src)
copy.opacity = 0
copy.hoverinfo = "none" // use "name" to show "B"
return copy
}
The result has the advantage, that it pleaces the objects in the right positions. A big disadvantage is, that the legend's opactiy for B is bound to the object's opacity and this shows only the label B but not the colored box.
Another disadvantage is that the data of B still affects the yaxis scaling:
b) use a minimal width
Using a minimal amount greater zero causes the trace to nearly disappear, while a small line remains.
const placeholder = src => {
const copy = Object.assign({}, src)
copy.width = 0.000000001
copy.hoverinfo = "none" // or use "name"
return copy
}
This example keeps the grouping, positioning and legend correct but the scaling is still affected and the remaining line can be misinterpreted (which can be very problematic IMO):
c) use a threshold
Now this is the only solution that satisfies all the requirements with a great caveit: it requires a range to be set on the yaxis:
const layout2 = {
yaxis: {
domain: [ 0.5, 1 ],
title: "axis 2",
range: [3.4, 4] // this is hardcoded for now
},
xaxis: {
domain: [ 0, 1 ]
},
margin: {
t: 0,
b: 0,
pad: 0
},
boxmode: "group"
}
// ...
// with ranges we can safely add
// data to both charts, because they
// get ghosted, based on their fit
// within / outside the range
const add = ({ src }) => {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
}
add({ src: A })
add({ src: B })
add({ src: C })
The result will then look like the following:
Now the only question remains, how to determin the range after the new data has been added? Fortunately Plotly provides a function to update the layout, named Plotly.relayout.
For this example we may choose a simple anchor, like the mean. Of course any other method to determine the range is possible.
const add = ({ src }) => {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
return src.y
}
// add the data and generate a sum of all values
const avalues = add({ src: A })
const bvalues = add({ src: B })
const cvalues = add({ src: C })
const allValues = [].concat(avalues, bvalues, cvalues)
// some reusable helpers to determine our range
const highest = arr => Math.max.apply( Math, arr )
const mean = arr => arr.reduce((a, b) => Number(a) + Number(b), 0) / arr.length
const upperRange = highest(allValues) // 3.81
const meanRange = mean(allValues) // 2.9361111111111113
// our new values to update the upper layour
const updatedLayout = {
yaxis: {
range: [meanRange, upperRange]
}
}
Plotly.relayout(myDiv2, updatedLayout)
The resulting graph looks mostly like the desired result:
You can use this link to play around and improve it at your wish: https://codepen.io/anon/pen/agzKBV?editors=1010
Summary
This example is still to be considered a workaround and is not tested beyond the given data. There is also room for improvement regarding the reusability and code efficiency and it is all written down in a sequencial manner to make this code understandable as possible.
Please also keep in mind, that displaying the same data on two different axis can be misleading to be interpreted as two different sets of data.
Any suggestions for improvement are allowed, code is free to use.

Related

How to sort dc.graph.js node colors based on a specific attribute in the dataset?

I want to visualize a dataset using dc.graph.js, and I want to have the nodes to show different colors based on different values of a specific attribute in the data file. Here is a sample json dataset that I would like to visualize:
"nodes": [
{
"id": 0,
"name": "beans",
"fiber content":"high"
},
{
"id": 1,
"name": "meat",
"fiber content":"low"
},
{
"id": 2,
"name": "apple",
"fiber content":"high"
},
{
"id": 3,
"name": "walnut",
"fiber content":"medium"
},
{
"id": 4,
"name": "egg",
"fiber content":"low"
};
I want all the food items with high fiber content to have the same color on the graph, and those with medium fiber content to show another color, and the ones with low fiber content to have a third color. In other words, I hope to basically have the nodes grouped into three groups: high, medium and low fiber content, then assign colors respectively to each group. I read through the dc.graph.js API and found that nodeKey is a unique identifier of the node's attributes, so I looked at the demo js and found this function which does the data file import and define the attribute terms:
function on_load(filename, error, data) {
var graph_data = dc_graph.munge_graph(data),
nodes = graph_data.nodes,
edges = graph_data.edges,
sourceattr = graph_data.sourceattr,
targetattr = graph_data.targetattr,
nodekeyattr = graph_data.nodekeyattr;
var edge_key = function(d) {
return d[sourceattr] + '-' + d[targetattr] + (d.par ? ':' + d.par : '');
};
var edge_flat = dc_graph.flat_group.make(edges, edge_key),
node_flat = dc_graph.flat_group.make(nodes, function(d) { return d[nodekeyattr]; }),
cluster_flat = dc_graph.flat_group.make(data.clusters || [], function(d) { return d.key; });
I'm not sure how it recognizes the attributes from the imported dataset, so I wonder if I need to point out and link to the "fiber content" attribute somewhere in my code to tell it what my color groups are based on. And for the color settings, the brushing-filtering demo actually has colors on the graph working but the graphs are auto-generated randomly without a dataset, so there isn't any grouping around attributes involved. I tried to add this following code snippet about colors (which I mostly followed from the brushing-filtering.js) to the same on_load function above, but it didn't work:
var colorDimension = node_flat.crossfilter.dimension(function(n) {
return n.color;
}),
colorGroup = colorDimension.group(),
dashDimension = edge_flat.crossfilter.dimension(function(e) {
return e.dash;
}),
dashGroup = dashDimension.group();
var colors = ['#1b9e77', '#d95f02', '#7570b3'];
var dasheses = [
{name: 'solid', ray: null},
{name: 'dash', ray: [5,5]},
{name: 'dot', ray: [1,5]},
{name: 'dot-dash', ray: [15,10,5,10]}
];
Diagram
.autoZoom('once-noanim')
.altKeyZoom(true)
.nodeFixed(function(n) { return n.value.fixed; })
.nodeLabelFill(function(n) {
var rgb = d3.rgb(Diagram.nodeFillScale()(Diagram.nodeFill()(n))),
// https://www.w3.org/TR/AERT#color-contrast
brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
return brightness > 127 ? 'black' : 'ghostwhite';
})
.nodeFill(function(kv) {
return kv.value.color;
})
.nodeFillScale(d3.scale.ordinal().domain([0,1,2]).range(colors))
Any tips on how I might accomplish this?

how to limit the number of function each?

I have the following object:
"data": [
{
"label": "dataName",
"sections": [
{
"label": "label sections 1",
"fields": [
{
"id": 1,
"name": "field 1",
"value": "value field 1"
},
{
"id": 2,
"name": "field 2",
"value": "value field 2"
}
]
},
{
"label": "label sections 2",
"fields": [
{
"id": 5,
"name": "field 3",
"value": "value field 3"
}
]
}
]
I would like to create a new array by retrieving data from each field.
like this :
array [
{id: field.id, name: field.name, value: field.value }
{id: field.id, name: field.name, value: field.value }
]
I thought I would use each function like this :
_.each(data, function (elt) {
_.each(elt.ections, function (elt) {
....
})
});
but using the each function I should multiply the functions each.
Is there a solution to get the same result without using several functions each?
If you have a solution ?
Cordially
Use the reduce method:
var reduceSections = data.reduce((a,b) => (a.concat(b.sections)),[]);
var reduceFields = reduceSections.reduce((a,b) => (a.concat(b.fields)),[]);
var result = reduceFields;
console.log(result);
For more information, see
MDN JavaScript Reference - Array.prototype.reduce
MDN JavaScript Reference - Array.prototype.concat
The DEMO
var data = [{
"label": "dataName",
"sections": [{
"label": "label sections 1",
"fields": [{
"id": 1,
"name": "field 1",
"value": "value field 1"
},{
"id": 2,
"name": "field 2",
"value": "value field 2"
}]
},{
"label": "label sections 2",
"fields": [{
"id": 5,
"name": "field 3",
"value": "value field 3"
}]
}]
}];
var reduceSections = data.reduce((a,b) => (a.concat(b.sections)),[]);
var reduceFields = reduceSections.reduce((a,b) => (a.concat(b.fields)),[]);
var result = reduceFields;
console.log(result);
Only downside is that mutating the original data object will mutate the result in the array. (no shallow cloning)
That may or may not be a downside depending on the application.
If you want to clone the objects:
var clone = result.map(obj => Object.assign({},obj));
For more information, see
MDN JavaScript Reference - Object.assign
MDN JavaScript Reference - Array.prototype.map
As you are making use of lodash already, you have access to _.flatMap, _.map and _.clone.
Unfortunately, with your data structure, iterating over the arrays in your data is required, but with depending on what you are trying to achieve, there are alternatives to _.each.
Assuming you want to join all of cloned entries in fields, that are nested in each entry of the array sections, that are nested in each entry of the array data, you can use the following code:
function cloneFields(elt) { return _.map(elt.fields, _.clone) }
var allClonedFields = _.flatMap(data, elt => {
return _.flatMap(elt.sections, cloneFields);
});
The function cloneFields() is initialized outside of the loop for performance so that it isn't created on every iteration.
This code will pull out each entry in data, then from that entry pull out each entry in the sections key, then return the clone of each entry in the fields key and then join them into one large array giving the following result:
[ { id: 1, name: 'field 1', value: 'value field 1' },
{ id: 2, name: 'field 2', value: 'value field 2' },
{ id: 5, name: 'field 3', value: 'value field 3' } ]
If you don't know exactly how "deep" is your object i recommand you using recursive function. Here is what i suggest :
function recursivlyCreateObject(data) {
let result;
if(Array.isArray(data)) {
result = [];
data.forEach(function(element) {
result.push(recursivlyCreateObject(element));
});
} else {
result = {};
Object.keys(data).forEach(function(key) {
result[key] = data[key];
});
}
return result;
}
You can test it here
EDIT : note that this won't do much more than a simple console.log over the data but can help you about iterating an object recursivly
If i understand correctly, you're trying to get the fields property from each element in the array. To do this, take a look at array.map().
using array.map, you could do something like this:
let array = data.map(x => x.fields);
or:
let array = data.map(x => (
{id: x.fields.id,name: x.fields.name, value: x.fields.value }
));
https://www.w3schools.com/jsref/jsref_map.asp

Partial Lenses: Group array of objects by property, use prop value as key

I have an array of objects like this:
[
{ name: "Group 1", value: "Foo" },
{ name: "Group 2", value: "Bar" },
{ name: "Group 1", value: "Baz" }
]
I'd like to use Partial Lenses library to transform these groups to keys of an object with corresponding group's items, like this:
{
"Group 1": [
{ name: "Group 1", value: "Foo" },
{ name: "Group 1", value: "Baz" }
],
"Group 2": [
{ name: "Group 2", value: "Bar" }
]
}
My current approach is like this, assuming I have the source data in a variable called data:
const grouped = L.collect([L.groupBy('name'), L.entries], data)
const setKey = [L.elems, 0]
const getName = [L.elems, 1, 0, 'name']
const correctPairs = L.disperse(setKey, L.collectTotal(getName, grouped), grouped)
L.get(L.inverse(L.keyed), correctPairs)
I don't like that I need to use the grouped and correctPairs variables to hold data, as I probably should be able to do the transformation directly in the composition. Could you help me to compose the same functionality in a more meaningful way?
Here's a Partial Lenses Playground with the above code.
I assume the goal is to actually create an isomorphism through which one can
view such an array as an object of arrays and also perform updates. Like a
bidirectional version of e.g. Ramda's
R.groupBy function.
Indeed, one approach would be to just use Ramda's
R.groupBy to implement a new primitive
isomorphism using L.iso.
Something like this:
const objectBy = keyL => L.iso(
R.cond([[R.is(Array), R.groupBy(L.get(keyL))]]),
R.cond([[R.is(Object), L.collect([L.values, L.elems])]])
)
The conditionals are needed to allow for the possibility that the data is not of
the expected type and to map the result to undefined in case it isn't.
Here is a playground with the above Ramda based
objectBy
implementation.
Using only the current version of Partial Lenses, one way to compose a similar
objectBy combinator would be as follows:
const objectBy = keyL => [
L.groupBy(keyL),
L.array(L.unzipWith1(L.iso(x => [L.get(keyL, x), x], L.get(1)))),
L.inverse(L.keyed)
]
Perhaps the interesting part in the above is the middle part that converts an
array of arrays into an array of key-array pairs (or the other way around).
L.unzipWith1
checks that all the keys within a group match, and if they don't, that group
will be mapped to undefined and filtered out by
L.array. If desired,
it is possible to get stricter behaviour by using
L.arrays.
Here is a playground with the above composed
objectBy
implementation.
You don't need any library, use a generic function that returns a reducer, that way you can use to group any collection with any key. In the example below I used this to group by name, but also by value.
const groupBy = key => (result,current) => {
let item = Object.assign({},current);
// optional
// delete item[key];
if (typeof result[current[key]] == 'undefined'){
result[current[key]] = [item];
}else{
result[current[key]].push(item);
}
return result;
};
const data = [{ name: "Group 1", value: "Foo" },{ name: "Group 2", value: "Bar" },{ name: "Group 1", value: "Baz" }];
const grouped = data.reduce(groupBy('name'),{});
console.log(grouped);
const groupedByValue = data.reduce(groupBy('value'),{});
console.log(groupedByValue);
You can use Array.reduce
let arr = [{ name: "Group 1", value: "Foo" },{ name: "Group 2", value: "Bar" },{ name: "Group 1", value: "Baz" }];
let obj = arr.reduce((a,c) => Object.assign(a, {[c.name]: (a[c.name] || []).concat(c)}), {});
console.log(obj);

JavaScript: Organizing a Matrix By Best Fit

How would one organize a dynamic matrix for best fit? So, let say you are attempting to always display the best fit for a display, and need to organize all cells so that there are no gaps between each item. Each item can either have a size from 1 - 12, and the max width of each row is 12. Using the example dataset, how can will dynamic sort and generate a new array that best fits the display?
let matrixExample = [{
size: 10,
type: 'card'
}, {
size: 4,
type: 'card'
}, {
size: 2,
type: 'card'
}, {
size: 11,
type: 'card'
}, {
size: 6,
type: 'card'
}];
let endingResult = [
[{
size: 10,
type: 'card'
}, {
size: 2,
type: 'card'
}],
[{
size: 4,
type: 'card'
}, {
size: 6,
type: 'card'
}],
[{
size: 11,
type: 'card'
}]
];
The user purpose of this?
When generating dynamic data to a UI, and the UI needs to optimize for component space.
This appears to be an example of the bin packing problem.
This isn't a particularly easy problem to solve and more precise fits are likely to be more complicated.
Below is a greedy algorithm that should solve your problem with a rough estimate. It's possible to get better matches but, as you do, you make things more complicated and computationally expensive.
This solution happens to be recursive and somewhat functional, but that's only my preference; it's probably possible to make a neater and less expensive algorithm if you're not interested in making the code functional or recursive.
const matrixExample = [{
size: 10,
type: 'card'
}, {
size: 4,
type: 'card'
}, {
size: 2,
type: 'card'
}, {
size: 11,
type: 'card'
}, {
size: 6,
type: 'card'
}];
const sumCardList = cardList => cardList.reduce((prev, curr) => prev + curr.size, 0);
const packNextToBin = (cards, bins, max) => {
if (cards.length === 0) {
// there are no more cards to pack, use bins as is
return bins;
}
// get the next card to pack into the bins
const cardToPack = cards[0];
// get the indices of bins which can still be filled
const availableBinIndices = bins
.map((bin, i) => ({sum: sumCardList(bin), index: i}))
.filter(binData => binData.sum + cardToPack.size < max)
.map(binData => binData.index);
// if there are no more bins which can fit this card, makea new bin
if (availableBinIndices.length === 0) {
const updatedBins = [
...bins,
[
cardToPack
]
];
return packNextToBin(
cards.slice(1),
updatedBins,
max
);
}
// get the first available bin which can accept the card
const binToPack = availableBinIndices[0];
// get a version of the matched bin with the new card added
const binWithInsertion = [
...bins[binToPack],
cardToPack,
];
// get the bins with the updated bin updated
const updatedBins = bins
.map((bin, i) => i === binToPack ?
binWithInsertion :
bin
);
// pack the next card into the bins
return packNextToBin(
cards.slice(1),
updatedBins,
max
);
}
const results = packNextToBin(matrixExample, [[]], 12)
console.dir(results)

Jquery and json multiple arrays

I have a JSON file with a lot of category names in it. Each category can have subcategories or none. So what I try to do is create an array of category names for each level. When A category hasn't any subcategories then it should show the array from previous level.
2 Examples of outcome:
// Subcategory A has subcategories so it should show them
Example 1: Kitchen -> Subcategory A -> Subsubcategory A1
Subcategory B Subsubcategory A2
Subsubcategory A3 -> etcetc...
Subsubcategory A4
// Subcategory B hasn't any subcategories so it should show the categories from the previous level
Example 2: Kitchen -> Subcategory A
Subcategory B -> Subcategory A
Subcategory B
How do you do that? I'm looking for directions/examples...
My JSON looks like this (I cut out all unessecary stuff). Categories can go to 6 levels deep!!
shop": {
categories": {
"1944163": {
id": "1944163",
"title": "Kitchen",
subs": {
"1944122": {
id": "1944122",
"title": "Subcategory A",
subs": {
"1944182": {
id": "1944182",
"title": "Subsubcategory A1",
"1944152": {
id": "1944152",
"title": "Subsubcategory A2",
// etc etc
"1944123": {
id": "1944123",
"title": "Subcategory B",
// etc..
When it comes to the Jquery I don't come further then this:
$.getJSON('url?format=json', function(data){
$.each(data.shop.categories, function(i, category) {
var currentCatId = $('.sidebar-left .sidebar-box ul').find('li.active').data('catid');
if(category.id == currentCatId) {
if(category.subs) {
$.each(category.subs, function(i, category) {
$('.categories-breadcrumbs ul').append('<li>'+category.title+'</li>');
});
}
}
}
});
});
Here's how I might approach it, and it involves a drastic change to the structure of your data. It does, however, greatly simplify it.
In short you have a flat array of objects that contain id, parentId, and label properties. parentId is the important property as it links the objects without having to deep-nest the information.
var cats = [
{ id: 1, parentId: 0, label: 'Kitchen' },
{ id: 2, parentId: 0, label: 'Man Cave' },
{ id: 3, parentId: 1, label: 'Cooker' },
{ id: 4, parentId: 3, label: 'Hob' },
{ id: 5, parentId: 2, label: 'Moose head' },
{ id: 6, parentId: 2, label: 'Cognac' }
];
You can then use the following function to grab the objects for the correct level based on the id:
function getData(arr, id) {
return arr.filter(function (el) {
return el.parentId === id;
});
}
I've mocked a demo to show you how simple it is to use this data

Categories