Skip to content

Instantly share code, notes, and snippets.

@widged
Last active August 29, 2015 13:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save widged/8703821 to your computer and use it in GitHub Desktop.
Save widged/8703821 to your computer and use it in GitHub Desktop.
Vegetable Graph
== 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]
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)
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);
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