Last active
August 29, 2015 13:55
-
-
Save widged/8703821 to your computer and use it in GitHub Desktop.
Vegetable Graph
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
== Plant Explorer in Neo4j | |
:neo4j-version: 2.0.0 | |
:author: Marielle Lange | |
:twitter: @widged | |
:tags: gardening | |
Note. A new extended version is available at link:http://widged.github.io/neoconsole/[github/neoconsole]. There, you will find the code rewritten in a more modular way and made to work within a gist as well as a within a standalone webpage. | |
The goal is to explore how grqphgist can be used to feed data to d3js visualisations. Vegetable data have been obtained through the scrapping of various websites. They have been normalized to bring to a common scale data on different measurement units or dates from different hemispheres. Then they have been reduced, to binify any continuous data. | |
To avoid to overtax the console, only one plant is loaded in this example ("anise"). | |
=== Embedded Javascript | |
Click any of the buttons and a call will be made to the Neo4j console through some javascript embedded within the gist. The json data returned by the script will cause the visualisation to refresh. | |
The visualisation is in the form of a density map. For instance, for soil ph, the values 6,7,8 are the ph values that the plant tolerates. The background color capture a custom color scale (mildly acidic in orange, neutral in yellow, alkaline in green). The bar under the number capture the relative frequency of that observation (2 observations for ph 6, 3 for ph 7 and 1 for ph 8). | |
Sowing months have been normalized to the Southern Hemisphere (as I live in NZ). | |
++++ | |
<a class="btn btn-small btn-success" data-toggle="tooltip" title="Soil PH" href="javascript:visualizeTrait('/soil/ph');">soil ph</i></a> | |
<a class="btn btn-small btn-success" data-toggle="tooltip" title="Plant Height" href="javascript:visualizeTrait('/physiology/size/height');">plant height</i></a> | |
<a class="btn btn-small btn-success" data-toggle="tooltip" title="Sowing Months" href="javascript:visualizeTrait('/propagate/cal');">Sowing Months</i></a> | |
<br/><br/> | |
<div id="densityMap">(a density map will appear here)</div> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="https://gist.github.com/widged/8703821/raw/ClassUtil.js""></script> | |
<script src="https://gist.github.com/widged/8703821/raw/DensityMap.js""></script> | |
<script> | |
var consolr; | |
function visualizeTrait(trait) { | |
var visuNode = d3.select('#densityMap'); | |
visuNode.html('Loading...') | |
if(consolr === undefined) { | |
CypherConsole({'url': 'http://neo4j-console-20.herokuapp.com/'}, function (conslr) { | |
consolr = conslr; | |
createGraph(whenGraph); | |
}); | |
} else { | |
whenGraph(); | |
} | |
function createGraph(asyncReturn) { | |
statements = [ | |
'CREATE (anise:plant {name: "anise"}), (anise)-[:kingdom {n: 1, trait: "/kingdom"}]->(plantae:kingdom {name: "plantae"}), (anise)-[:blight {n: 1, trait: "/control/disease/blight"}]->(sclerotinia_blight:disease {name: "sclerotinia blight"}), (anise)-[:blight {n: 2, trait: "/control/disease/blight"}]->(cercospora_blight:disease {name: "Cercospora blight"}), (anise)-[:blight {n: 1, trait: "/control/disease/blight"}]->(leaf_blight:disease {name: "leaf blight"}), (anise)-[:fungal {n: 1, trait: "/control/disease/fungal"}]->(pythium_blight:disease {name: "pythium blight"}), (anise)-[:fungal {n: 1, trait: "/control/disease/fungal"}]->(anthracnose:disease {name: "anthracnose"}), (anise)-[:fungal {n: 2, trait: "/control/disease/fungal"}]->(rust:disease {name: "rust"}), (anise)-[:fungal {n: 1, trait: "/control/disease/fungal"}]->(downy_mildew:disease {name: "downy mildew"}), (anise)-[:insect {n: 1, trait: "/control/pest/animal/insect"}]->(leafhopper:animal {name: "leafhopper"}), (anise)-[:insect {n: 1, trait: "/control/pest/animal/insect"}]->(caterpillar:animal {name: "caterpillar"}), (anise)-[:insect {n: 1, trait: "/control/pest/animal/insect"}]->(aphid:animal {name: "aphid"}), (anise)-[:insect {n: 1, trait: "/control/pest/animal/insect"}]->(grasshopper:animal {name: "grasshopper"}), (anise)-[:helps {n: 3, trait: "/site/companion/plant/helps"}]->(cabbage:plant {name: "cabbage"}), (anise)-[:helps {n: 3, trait: "/site/companion/plant/helps"}]->(grape:plant {name: "grape"}), (anise)-[:hinderedBy {n: 2, trait: "/site/companion/plant/hinderedBy"}]->(carrot:plant {name: "carrot"}), (anise)-[:hinderedBy {n: 2, trait: "/site/companion/plant/hinderedBy"}]->(radish:plant {name: "radish"}), (anise)-[:tag {d:"Apiaceae", n: 1, trait: "/group/en/family"}]->(family:tag {name: "family"}), (anise)-[:tag {d:"annual", n: 3, trait: "/physiology/lifecycle"}]->(lifecycle:tag {name: "lifecycle"}), (anise)-[:tag {d:"herb", n: 1, trait: "/group/en/plantae"}]->(plantae), (anise)-[:tag {d:"vegetable", n: 1, trait: "/group/en/plantae"}]->(plantae), (anise)-[:tag {d:"cool", n: 1, trait: "/growth/season"}]->(season:tag {name: "season"}), (anise)-[:tag {d:"warm", n: 1, trait: "/growth/season"}]->(season), (anise)-[:tag {d:"part sun", n: 1, trait: "/exposure/light"}]->(light:tag {name: "light"}), (anise)-[:tag {d:"full sun", n: 4, trait: "/exposure/light"}]->(light), (anise)-[:tag {d:"well drained", n: 3, trait: "/soil/drainage"}]->(drainage:tag {name: "drainage"}), (anise)-[:tag {d:"direct", n: 2, trait: "/propagate/method"}]->(method:tag {name: "method"}), (anise)-[:tag {d:"compost", n: 1, trait: "/fertilizer/need/product"}]->(product:tag {name: "product"}), (anise)-[:tag {d:"sidedress", n: 1, trait: "/fertilizer/need/method"}]->(method), (anise)-[:tag {d:"N", n: 1, trait: "/fertilizer/need/npk"}]->(npk:tag {name: "npk"}), (anise)-[:tag {d:"Apiaceae", n: 2, trait: "/group/latin/family"}]->(family), (anise)-[:tag {d:"food", n: 1, trait: "/use"}]->(use:tag {name: "use"}), (anise)-[:tag {d:"pimpinella", n: 1, trait: "/name/latin/genus"}]->(genus:tag {name: "genus"}), (anise)-[:tag {d:"anisum", n: 1, trait: "/name/latin/epithet"}]->(epithet:tag {name: "epithet"}), (anise)-[:tag {d:"broad", n: 1, trait: "/physiology/leaf/texture"}]->(texture:tag {name: "texture"}), (anise)-[:tag {d:"lobed", n: 1, trait: "/physiology/leaf/texture"}]->(texture), (anise)-[:tag {d:"feathery", n: 1, trait: "/physiology/leaf/texture"}]->(texture), (anise)-[:tag {d:"low", n: 1, trait: "/physiology/shape"}]->(shape:tag {name: "shape"}), (anise)-[:tag {d:"spreading", n: 1, trait: "/physiology/shape"}]->(shape), (anise)-[:tag {d:"bushy", n: 1, trait: "/physiology/shape"}]->(shape), (anise)-[:tag {d:"limegreen ", n: 1, trait: "/physiology/leaf/color"}]->(color:tag {name: "color"}), (anise)-[:tag {d:"yellow", n: 1, trait: "/physiology/flower/bloom/color"}]->(color), (anise)-[:tag {d:"white", n: 1, trait: "/physiology/flower/bloom/color"}]->(color), (anise)-[:tag {d:"730,#", n: 1, trait: "/use/preserve/seed/viability"}]->(viability:tag {name: "viability"}), (anise)-[:tag {d:"oct", n: 1, trait: "/propagate/cal"}]->(cal:tag {name: "cal"}), (anise)-[:tag {d:"nov", n: 1, trait: "/propagate/cal"}]->(cal), (anise)-[:measure {d:6, n: 2, trait: "/soil/ph"}]->(ph:measure {name: "ph"}), (anise)-[:measure {d:7, n: 3, trait: "/soil/ph"}]->(ph), (anise)-[:measure {d:8, n: 1, trait: "/soil/ph"}]->(ph), (anise)-[:measure {d:30, n: 3, trait: "/physiology/size/height"}]->(height:measure {name: "height"}), (anise)-[:measure {d:40, n: 3, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:50, n: 3, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:60, n: 3, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:70, n: 1, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:80, n: 1, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:90, n: 1, trait: "/physiology/size/height"}]->(height), (anise)-[:measure {d:30, n: 2, trait: "/propagate/space/plants"}]->(plants:measure {name: "plants"}), (anise)-[:measure {d:10, n: 1, trait: "/propagate/space/plants"}]->(plants), (anise)-[:measure {d:15, n: 2, trait: "/propagate/space/plants"}]->(plants), (anise)-[:measure {d:20, n: 2, trait: "/propagate/space/plants"}]->(plants), (anise)-[:measure {d:25, n: 1, trait: "/propagate/space/plants"}]->(plants), (anise)-[:measure {d:45, n: 2, trait: "/propagate/space/row"}]->(row:measure {name: "row"}), (anise)-[:measure {d:50, n: 2, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:55, n: 2, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:60, n: 2, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:20, n: 1, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:25, n: 1, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:30, n: 1, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:35, n: 1, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:40, n: 1, trait: "/propagate/space/row"}]->(row), (anise)-[:measure {d:20, n: 1, trait: "/germination/temperature"}]->(temperature:measure {name: "temperature"}), (anise)-[:measure {d:10, n: 1, trait: "/germination/temperature"}]->(temperature), (anise)-[:measure {d:20, n: 1, trait: "/growth/temperature"}]->(temperature), (anise)-[:measure {d:30, n: 1, trait: "/growth/temperature"}]->(temperature), (anise)-[:measure {d:0, n: 1, trait: "/use/preserve/store/temperature"}]->(temperature), (anise)-[:measure {d:5, n: 1, trait: "/germination/time/from/seed"}]->(seed:measure {name: "seed"}), (anise)-[:measure {d:10, n: 2, trait: "/germination/time/from/seed"}]->(seed), (anise)-[:measure {d:15, n: 2, trait: "/germination/time/from/seed"}]->(seed), (anise)-[:measure {d:20, n: 1, trait: "/germination/time/from/seed"}]->(seed), (anise)-[:measure {d:25, n: 1, trait: "/germination/time/from/seed"}]->(seed), (anise)-[:measure {d:30, n: 1, trait: "/germination/time/from/seed"}]->(seed), (anise)-[:measure {d:100, n: 2, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:120, n: 3, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:60, n: 2, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:125, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:130, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:135, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:140, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:145, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:150, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:50, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:55, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:65, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:70, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:105, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:110, n: 1, trait: "/maturity/time/from/seed"}]->(seed), (anise)-[:measure {d:115, n: 1, trait: "/maturity/time/from/seed"}]->(seed);' | |
] | |
consolr.query( statements, asyncReturn, cypherError); | |
} | |
function whenGraph() { | |
var type = "measure"; | |
if(trait && trait.indexOf('/cal') !== -1) { | |
type = "tag"; | |
} | |
statements = [ | |
'MATCH (m:'+type+')-[r2:'+type+' { trait: "'+ trait+'" }]-(p:plant { name: "anise" }) RETURN r2.d as d, r2.n as n' | |
] | |
consolr.query(statements, cypherSuccess, cypherError); | |
} | |
function asArray(pseudoArray) { | |
return Array.prototype.slice.call(pseudoArray, 0); | |
}; | |
function cypherSuccess(data, resultNo) { | |
console.log('success', data); | |
var json = asArray(data.json) | |
drawVisu(trait, json); | |
} | |
function cypherError(data, resultNo) { | |
console.log('error', data) | |
} | |
} | |
</script> | |
<script> | |
function drawVisu(trait, data) { | |
if(!data) { | |
document.getElementById('densityMap').innerHTML = 'N/A'; | |
} else if(trait && trait.indexOf('/cal') !== -1) { | |
monthDensities(data); | |
} else { | |
measureDensities(data); | |
} | |
// Month densities | |
function monthDensities(data) { | |
var months = "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec".split(","); | |
var seasons = "spr,spr,spr,smr,smr,smr,fll,fll,fll,wtr,wtr,wtr".split(","); | |
var colors = {spr: "#BF007F", smr: "#78B300", fll: "#805933", wtr: "#008FCC"}; | |
if(!data) {data = [{d: "jan", n: 10}, {d: "feb", n: 9}, {d: "mar", n: 2}]; } | |
var maxFreq = 0; | |
all = months.map(function(item, i) { | |
var season = seasons[i]; | |
return {n: 0, season: season, color: d3.rgb(colors[season]), d: item.substring(0, 1) }; | |
}) | |
var maxFreq = 0; | |
data.forEach(function(item) { | |
var mIdx = months.indexOf(item.d); | |
if(mIdx === -1) { return; } | |
all[mIdx].n += item.n; | |
maxFreq = Math.max(maxFreq, all[mIdx].n) | |
}) | |
var graph = DensityMap.instance().data(all).maxFreq(maxFreq); | |
document.getElementById('densityMap').innerHTML = graph.render(); | |
} | |
// Measure densities | |
function measureDensities(data) { | |
// data: [{d: 4, n: 1},{d: 5, n: 2},{d: 6, n: 4},{d: 7, n: 1},{d: 8, n: 0},{d: 9, n: 0}] | |
var config = {colors: "-10:#003366,30:#336600,40:#FF6600,50:#CC2222"}; | |
if(trait && trait.indexOf('/ph') !== -1 && trait.indexOf('/phy') === -1) { | |
config.colors = "0:#CC2222,5:#FF6600,6.0:#FFDD00,7.0:#336600"; | |
} | |
var maxFreq = 0; | |
data.forEach(function(item) { | |
maxFreq = Math.max(maxFreq, item.n) | |
}) | |
var graph = DensityMap.instance().data(data).config(config).maxFreq(maxFreq); | |
document.getElementById('densityMap').innerHTML = graph.render(); | |
} | |
} | |
</script> | |
++++ | |
=== Credits | |
* link:http://neo4j.org/[Neo4j] and link:http://gist.neo4j.org/[graphgist] | |
* link:http://d3js.org/[D3js] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.ClassUtil = {}; | |
(function(Class){ | |
// getSet public variables, the curry way. | |
Class.accessMaker = function(state, instance) { | |
return { | |
getSet: function(attr, aroundFns) { | |
return function(_) { | |
if(_ === undefined) { return state[attr]; } | |
if(_ !== state[attr]) { | |
(aroundFns || []).forEach(function(fn) { | |
fn(_, function(new_) { _ = new_; }); | |
}); | |
state[attr] = _; | |
} | |
return instance; | |
}; | |
} | |
}; | |
}; | |
})(ClassUtil) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.DensityMap = {}; | |
(function(Class){ | |
var rowPadding = 6, | |
cellSpacing = 1; | |
var render =function(data, maxFreq, config) { | |
if(!config) { config = {isVertical: false}; } | |
var isVertical = (config.isVertical === true || config.isHorizontal === false) ? true : false; | |
var rect = isVertical ? {w: 18, h: 14} : {w: 14, h: 16}; | |
var monthX = isVertical ? function(i) { return 0; } : function(i) { return i * (rect.w + cellSpacing); }; | |
var monthY = isVertical ? function(i) { return i * (rect.h + cellSpacing); } : function(i) { return 0; }; | |
var CELL_OPACITY = 0.6; | |
var colorFn = function() { return "#CCCCCC"; }; | |
if(config.colors) { | |
var colors = (config.colors || "").split(",").map(function(item) { | |
var p = item.split(":"); | |
return { d: parseFloat(p[0]), c: p[1] }; | |
}); | |
colorFn = function(d) { | |
var c = "#CCCCCC"; | |
colors.forEach(function(item) { if(d > item.d) { c = item.c; } }); | |
return c; | |
}; | |
} else if(config.colorNames) { | |
CELL_OPACITY = 1.0; | |
colorFn = function(d) { return d; }; | |
} | |
function setColorFn(colors) { | |
} | |
var probabilityScale = d3.scale.linear().domain([0, maxFreq]).range([0.0,1]).clamp(false); | |
// var colorFn = function(value,color) { return d3.interpolateRgb("#fff", color )(value / maxFreq); } // used alpha scale instead | |
var barGirth = 4; | |
var div = document.createElement('div'); | |
var easel = d3 .select(div) | |
.append("svg:svg") | |
.attr("width", monthX(12) || rect.w).attr("height", monthY(12) || rect.h ); | |
var monthRect = easel.selectAll("g") | |
.data(data) | |
.enter().append("svg:g") | |
.attr("width", rect.w) | |
.attr("height", function(d) { return rect.h; } ) | |
.attr("transform", function(d, i) { return "translate("+monthX(i)+"," + monthY(i)+")"; } ); | |
monthRect.append("svg:rect") | |
.attr("class", "month") | |
.attr("width", function(d, i) { return (isVertical ? rect.w - barGirth : rect.w) ; }) | |
.attr("height", function(d) { return (isVertical ? rect.h : rect.h - barGirth) ; }) | |
.attr("fill", function(d, i) { return colorFn(d.d); }) // colorFn(0.1, d.color) | |
.attr("fill-opacity", function(d, i) { return CELL_OPACITY; }); | |
monthRect.append("svg:rect") | |
.attr("class", "heat") | |
.attr("x", function(d, i) { return (isVertical ? rect.w - barGirth : 0) ; }) | |
.attr("y", function(d, i) { return (isVertical ? 0 : rect.h - barGirth) ; }) | |
.attr("width", isVertical ? barGirth : rect.w) | |
.attr("height", function(d) { return isVertical ? rect.h : barGirth; } ) | |
.attr("fill", function(d, i) { return (d || {}).color; }) // "#828387", d.color; however, difficult to differentiate between different colors | |
.attr("fill-opacity", function(d, i) { return probabilityScale((d || {}).n); }); | |
monthRect.append("svg:text") | |
.attr("x", function(d, i) { return (isVertical ? 7 : 7); }) | |
.attr("y", function(d, i) { return (isVertical ? 10 : 9); }) | |
.style("text-anchor", "middle") | |
.text(function(d, i) { return ((d || {}).d === undefined || config.colorNames ? "" : d.d).toString().toUpperCase(); }) // "" + i + "." + | |
.attr("font-size", "9"); | |
return div.innerHTML; | |
}; | |
Class.instance = function() { | |
var instance = {}, s = {}; | |
function warnChange() { dataChange = true; } | |
var access = ClassUtil.accessMaker(s, instance); | |
instance.data = access.getSet("data", [warnChange]); | |
instance.maxFreq = access.getSet("maxFreq", [warnChange]); | |
instance.config = access.getSet("config", [warnChange]); | |
instance.render = function(selector) { | |
return render(s.data, s.maxFreq, s.config); | |
}; | |
return instance; | |
}; | |
})(DensityMap); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.DocUtil = {}; | |
(function(Class){ | |
var FN = {}; | |
FN.asArray = function(pseudoArray) { | |
return Array.prototype.slice.call(pseudoArray, 0); | |
}; | |
Class.injectStyleSheets = function(sheets) { | |
sheets.forEach(Class.injectStyleSheet); | |
}; | |
Class.injectStyleSheet = function(url) { | |
if(Class.matchStyleSheet(url).length) { return; } | |
var link = document.createElement("link"); | |
link.setAttribute("rel", "stylesheet"); | |
link.setAttribute("type", "text/css"); | |
link.setAttribute("href", url); | |
document.getElementsByTagName("head")[0].appendChild(link); | |
}; | |
Class.matchStyleSheet = function(url) { | |
var list = []; | |
FN.asArray(document.getElementsByTagName('link')).forEach(function(node) { | |
if(node && node.getAttribute("href") && node.getAttribute("href").indexOf(url) !== -1) | |
list.push(node); | |
}); | |
return list; | |
}; | |
})(DocUtil) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment