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?
I am trying to access JSON values. This is the JSON object:
{
"attrs": {
"width": 1728,
"height": 787,
"dragabble": true
},
"className": "Stage",
"children": [
{
"attrs": {},
"className": "Layer",
"children": [
{
"attrs": {
"stroke": "green",
"strokeWidth": "5",
"points": [
348,564.125
]
},
"className": "Line"
}
]
}
]
}
And I am trying to use these values, like points, here:
socket.on("canvas-data", function(data){
var interval = setInterval(function(){
if(isDrawing) return;
setIsDrawing(true);
clearInterval(interval);
var obj = JSON.parse(data);
setStageData(obj);
var layer = new Konva.Layer();
var lines = new Konva.Line(
{
stroke: stageData.stroke,
strokeWidth: stageData.strokeWidth,
points: stageData.points
})
layer.add(lines);
stageEl.current.add(layer);
}, 200)
})
data is the JSON string, I tried to parse data into obj, set my stageData to obj and then set the corresponding JSON attributes to the values like stroke, strokeWidth and points. This doesn't work however, they're undefined. How do I access them?
(I also tried skipping the step where I set my stageData to obj, and just use obj.stroke instead of stageData.stroke etc.)
You can just skip using setStageData() and use the parsed object directly if you wish, or just name the parsed object stageData by default.
In any case, when you have nested objects and values in an Object, you access them by using the correct index, in your case, it would look this way:
socket.on("canvas-data", function(data) {
var interval = setInterval(function() {
if (isDrawing) return;
setIsDrawing(true);
clearInterval(interval);
var stageData = JSON.parse(data);
var layer = new Konva.Layer();
var lines = new Konva.Line(
{
stroke: stageData.children[0].children[0].attrs.stroke,
strokeWidth: stageData.children[0].children[0].attrs.strokeWidth,
points: stageData.children[0].children[0].attrs.points
});
layer.add(lines);
stageEl.current.add(layer);
}, 200);
})
Doesn't look very nice, but it works. You can always use this app called JSON Path list, which shows you all the possible paths and their values in a JSON object.
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>");
I have an Array with this kind of values:
val = [ ['L-2-4-1','john','bla1'],
['L-1-1-26','bohn','bla2'],
['L-2-1','cohn','bla3'],
['L-1-1-05','rohn','bla4'],
['L-1-1','gohn','bla5']
['L-2-3-1','zohn','bla-finally'] ];
The number-sequence is always unique and "0" is never used.
What I'm trying to get would be something like this:
ser = [ [undefined],
[ [undefined],[ ['gohn'],['bla5'] ], [undefined], ... , [ ['bohn'], ['blah2'] ] ],
...
];
The purpose is to be able to access the data like this:
ser[2][4][1][0]; // Array('john','bla1')
ser[1][1][0]; // Array('gohn','bla5')
ser[1][1][26][0]; // Array('bohn','bla2')
and also to loop through all elements.. for instance:
for(var i = 0; i <= ser[1][1].length; i++){ //code }
The main problem I have is that I was not able to set the variables the same way I intend to read them. Because this does NOT work, since I need to declare all arrays separately as arrays (right?)
var ser[1][1][26][0] = ['john','bla1']; // Nop;
I don't know the maximum depth of the tree
Trying to build the arrays from "inside out" or from "right to left" -however it is best described- I always end up overwriting previously set array elements.
Maybe the whole idea is too complicated (or at least not ideal) for the purpose? What would you suggest? I have the feeling I´m trying to do the right thing but the wrong way... Something like organizing marbles on a glass surface. Everything keeps moving around...
Have you considered representing your data in JSON?
It allows for complex structures that are otherwise too confusing to keep in your head. It's like XML meets JavaScript arrays. Rather self-describing and easy to follow. You can read the lengths and sizes of objects easily and it's quite fast. You can use values instead of array positions and re-think the structure of your data.
http://json.org/example.html
Here is a record in JSON:
{
"id": "0001",
"type": "donut",
"name": "Cake",
"ppu": 0.55,
"batters":
{
"batter":
[
{ "id": "1001", "type": "Regular" },
{ "id": "1002", "type": "Chocolate" },
{ "id": "1003", "type": "Blueberry" },
{ "id": "1004", "type": "Devil's Food" }
]
},
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "id": "5005", "type": "Sugar" },
{ "id": "5007", "type": "Powdered Sugar" },
{ "id": "5006", "type": "Chocolate with Sprinkles" },
{ "id": "5003", "type": "Chocolate" },
{ "id": "5004", "type": "Maple" }
]
},
http://labs.adobe.com/technologies/spry/samples/data_region/JSONDataSetSample.html
short and sweet:
var i, j, t, final = [];
for (i = 0; i < val.length; i++) {
t = val[i][0].split('-');
for (j = 1; j < 5; j++) {
t[j] = parseInt(t[j], 10) || 0;
}
final[t[1]] = final[t[1]] || [];
final[t[1]][t[2]] = final[t[1]][t[2]] || [];
final[t[1]][t[2]][t[3]] = final[t[1]][t[2]][t[3]] || [];
final[t[1]][t[2]][t[3]][t[4]] = final[t[1]][t[2]][t[3]][t[4]] || [];
final[t[1]][t[2]][t[3]][t[4]].push(val[i].slice(1));
}
final now has the correct data as you specified...
however, you might want to consider using objects instead of arrays (change all [] to {}) as the random insertion points in arrays lead to series of empty (null) values, the only caveat would be that you'd have to use a for (var key in obj) style loop...
hope this helps -ck
IF YOU NEED DYNAMIC DEPTH
var i, j, t, o, depth = 4, final = [];
for (i = 0; i < val.length; i++) {
t = val[i][0].split('-');
o = final;
for (j = 1; j <= depth; j++) {
t[j] = parseInt(t[j], 10) || 0;
o[t[j]] = o[t[j]] || [];
o = o[t[j]];
}
o.push(val[i].slice(1));
}
now depth is the constant at which the data is stored missing or unparsable "keys" or "indices" depending on how you want to think of them, default to 0
enjoy -ck