Skip to content

Commit efd63b4

Browse files
committed
More features for Map visualization
1 parent a6dc79c commit efd63b4

File tree

5 files changed

+274
-40
lines changed

5 files changed

+274
-40
lines changed

public/style.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ code {
119119
.tab a .input-field input {
120120
width: 200px;
121121
margin-left: -20px;
122+
line-height: 10px;
123+
border-bottom: none !important;
124+
box-shadow: none !important;
125+
-webkit-box-shadow: none !important;
122126
}
123127
.tab {
124128
width: 250px;
@@ -138,6 +142,12 @@ code {
138142
.btn-add-tab i {
139143
margin-top: -13px;
140144
}
145+
.leaflet-rel-popup {
146+
margin-bottom: 18px !important;
147+
}
148+
path.leaflet-interactive{
149+
stroke-width: 3.5px;
150+
}
141151
.btn-remove-tab {
142152

143153
width: 30px;

src/card/NeoCard.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ export class NeoCard extends React.Component {
447447
// Set the query used to one used for autocompletion
448448
this.state.query = `MATCH (n:\`${label}\`)
449449
WHERE toLower(toString(n.\`${property}\`)) CONTAINS toLower($input)
450-
RETURN n.\`${property}\` as value LIMIT 4`;
450+
RETURN DISTINCT n.\`${property}\` as value LIMIT 4`;
451451
this.state.content =
452452
<NeoPropertySelectReport
453453
connection={this.props.connection}

src/card/report/NeoMapReport.js

Lines changed: 162 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,17 @@ class NeoMapReport extends NeoReport {
2323
this.state.width = this.props.clientWidth - 50; //-90 + props.width * 105 - xShift * 0.5;
2424
this.state.height = -145 + this.props.height * 100;
2525

26-
this.state.nodesAndPositions = []
26+
this.state.nodesAndPositions = {}
27+
this.state.relationshipsAndPositions = {}
2728
if (this.state.data) {
28-
this.state.data.forEach(record => {
29-
Object.values(record).forEach(v => {
30-
if (v.identity && v.properties && v.properties.latitude && v.properties.longitude ) {
31-
let lat = parseFloat(v.properties.latitude);
32-
let long = parseFloat(v.properties.longitude);
33-
if (!isNaN(lat) && !isNaN(long)){
34-
this.state.nodesAndPositions.push({pos: [lat, long], node: v})
35-
}
36-
}else if (v.identity && v.properties && v.properties.lat && v.properties.long ) {
37-
let lat = parseFloat(v.properties.lat);
38-
let long = parseFloat(v.properties.long);
39-
if (!isNaN(lat) && !isNaN(long)){
40-
this.state.nodesAndPositions.push({pos: [lat, long], node: v})
41-
}
42-
}else if (v.identity && v.properties) {
43-
Object.values(v.properties).forEach(p => {
44-
// We found a property that holds a Neo4j point object
45-
if (p.srid && p.x && p.y) {
46-
if (!isNaN(p.x) && !isNaN(p.y)) {
47-
this.state.nodesAndPositions.push({pos: [p.y, p.x], node: v})
48-
}
49-
}
50-
})
51-
}
52-
})
53-
54-
})
29+
this.extractNodesFromAllRecords();
30+
this.extractRelationshipsFromAllRecords();
5531
}
5632

5733
// This is where math happens - we try to come up with the optimal zoom to fit all rendered nodes...
58-
let longitudePositions = this.state.nodesAndPositions.map(i => i.pos[0] + 180);
59-
this.state.centerLongitude = longitudePositions.reduce((a, b) => a + b, 0) / this.state.nodesAndPositions.length;
34+
let nodesAndPositionsValues = Object.values(this.state.nodesAndPositions);
35+
let longitudePositions = nodesAndPositionsValues.map(i => i.pos[0] + 180);
36+
this.state.centerLongitude = longitudePositions.reduce((a, b) => a + b, 0) / nodesAndPositionsValues.length;
6037
let maxLong = Math.max.apply(null, longitudePositions);
6138
if ((maxLong === this.state.centerLongitude)) {
6239
maxLong += 0.000000001;
@@ -67,8 +44,8 @@ class NeoMapReport extends NeoReport {
6744
let longZoomFit = Math.ceil(Math.log2(1.0 / longProjectedHeight));
6845

6946

70-
let latitudePositions = this.state.nodesAndPositions.map(i => i.pos[1] + 90);
71-
this.state.centerLatitude = latitudePositions.reduce((a, b) => a + b, 0) / this.state.nodesAndPositions.length;
47+
let latitudePositions = nodesAndPositionsValues.map(i => i.pos[1] + 90);
48+
this.state.centerLatitude = latitudePositions.reduce((a, b) => a + b, 0) / nodesAndPositionsValues.length;
7249
let maxLat = Math.max.apply(null, latitudePositions);
7350
if ((maxLat === this.state.centerLatitude)) {
7451
maxLat += 0.000000001;
@@ -83,20 +60,147 @@ class NeoMapReport extends NeoReport {
8360

8461
}
8562

63+
extractRelationshipsFromAllRecords() {
64+
this.state.data.forEach(record => {
65+
Object.values(record).forEach(recordField => {
66+
// single relationship
67+
if (recordField && recordField["type"] && recordField["start"] && recordField["end"] && recordField["identity"] && recordField["properties"]) {
68+
let start = recordField["start"]
69+
let end = recordField["end"]
70+
if (start && end && this.state.nodesAndPositions[start.low] && this.state.nodesAndPositions[end.low]) {
71+
this.state.relationshipsAndPositions[recordField["identity"]] =
72+
{
73+
start: this.state.nodesAndPositions[start.low].pos,
74+
end: this.state.nodesAndPositions[end.low].pos,
75+
rel: recordField
76+
}
77+
}
78+
} else if (recordField["start"] && recordField["end"] && recordField["segments"] && recordField["length"]) {
79+
// paths
80+
recordField["segments"].forEach(s => {
81+
let segment = s["relationship"];
82+
if (segment && segment["type"] && segment["start"] && segment["end"] && segment["identity"] && segment["properties"]) {
83+
let start = segment["start"]
84+
let end = segment["end"]
85+
if (start && end && this.state.nodesAndPositions[start.low] && this.state.nodesAndPositions[end.low]) {
86+
this.state.relationshipsAndPositions[segment["identity"]] =
87+
{
88+
start: this.state.nodesAndPositions[start.low].pos,
89+
end: this.state.nodesAndPositions[end.low].pos,
90+
rel: segment
91+
}
92+
}
93+
}
94+
})
95+
}
96+
// TODO - collections of relationships
97+
})
98+
})
99+
}
100+
101+
extractNodesFromAllRecords() {
102+
let nodeLabels = {}
103+
this.state.data.forEach(record => {
104+
Object.values(record).forEach(recordField => {
105+
if (recordField && Array.isArray(recordField)) {
106+
// Arrays (of nodes)
107+
recordField.forEach(element => {
108+
let nodeIdandPos = this.extractGeocoordsFromNode(element, nodeLabels);
109+
if (nodeIdandPos) {
110+
this.state.nodesAndPositions[nodeIdandPos[0]] = nodeIdandPos[1];
111+
}
112+
})
113+
} else if (recordField["start"] && recordField["end"] && recordField["segments"] && recordField["length"]) {
114+
// Paths
115+
let nodeIdandPos = this.extractGeocoordsFromNode(recordField["start"], nodeLabels);
116+
if (nodeIdandPos) {
117+
this.state.nodesAndPositions[nodeIdandPos[0]] = nodeIdandPos[1];
118+
}
119+
let nodeIdandPos2 = this.extractGeocoordsFromNode(recordField["end"], nodeLabels);
120+
if (nodeIdandPos2) {
121+
this.state.nodesAndPositions[nodeIdandPos2[0]] = nodeIdandPos2[1];
122+
}
123+
} else {
124+
// Single nodes
125+
let nodeIdandPos = this.extractGeocoordsFromNode(recordField, nodeLabels);
126+
if (nodeIdandPos) {
127+
this.state.nodesAndPositions[nodeIdandPos[0]] = nodeIdandPos[1];
128+
}
129+
}
130+
})
131+
});
132+
133+
this.state.nodeLabels = Object.keys(nodeLabels)
134+
}
135+
136+
extractGeocoordsFromNode(recordField, nodeLabels) {
137+
/**
138+
* Extracts node geo-coordinates from a record.
139+
*/
140+
if (recordField && recordField.identity && recordField.properties && recordField.properties.latitude && recordField.properties.longitude) {
141+
let lat = parseFloat(recordField.properties.latitude);
142+
let long = parseFloat(recordField.properties.longitude);
143+
if (!isNaN(lat) && !isNaN(long)) {
144+
recordField.labels.forEach(l => nodeLabels[l] = null);
145+
return [recordField.identity.low, {pos: [lat, long], node: recordField}];
146+
}
147+
} else if (recordField && recordField.identity && recordField.properties && recordField.properties.lat && recordField.properties.long) {
148+
let lat = parseFloat(recordField.properties.lat);
149+
let long = parseFloat(recordField.properties.long);
150+
if (!isNaN(lat) && !isNaN(long)) {
151+
recordField.labels.forEach(l => nodeLabels[l] = null);
152+
return [recordField.identity.low, {pos: [lat, long], node: recordField}];
153+
}
154+
} else if (recordField && recordField.identity && recordField.properties) {
155+
let result = null;
156+
Object.values(recordField.properties).forEach(p => {
157+
// We found a property that holds a Neo4j point object
158+
159+
if (p.srid != null && p.x != null && p.y != null) {
160+
if (!isNaN(p.x) && !isNaN(p.y)) {
161+
recordField.labels.forEach(l => nodeLabels[l] = null);
162+
// TODO - this only returns the first point object it finds on a node...
163+
result = [recordField.identity.low, {pos: [p.y, p.x], node: recordField}];
164+
}
165+
}
166+
})
167+
return result;
168+
}
169+
return null;
170+
}
171+
172+
/**
173+
*
174+
*/
175+
neoPropertyToString(property) {
176+
if (property.srid) {
177+
return "(lat:" + property.y + ", long:" + property.x + ")";
178+
}
179+
return property;
180+
}
86181

87182
/**
88183
* Creates the leaflet visualization to render in the report.
89184
*/
90185
createMapVisualization() {
91186
let colors = ["#588c7e", "#f2e394", "#f2ae72", "#d96459", "#5b9aa0", "#d6d4e0", "#b8a9c9", "#622569", "#ddd5af", "#d9ad7c", "#a2836e", "#674d3c", "grey"]
92-
let markers = (this.state.nodesAndPositions) ?
93-
this.state.nodesAndPositions.map(i =>
187+
let nodesAndPositionsValues = Object.values(this.state.nodesAndPositions);
188+
let relationshipsAndPositionsValues = Object.values(this.state.relationshipsAndPositions);
189+
let markers = nodesAndPositionsValues ?
190+
nodesAndPositionsValues.map(i =>
94191
<Marker position={i.pos}
95-
icon={<div style={{color: colors[0]}}><Icon className="close">place</Icon></div>}>
96-
<Popup><h6>{i.node.labels.map(b => b + " ")}</h6><code>{Object.keys(i.node.properties).map(key =>
97-
<pre>{key + ": " + i.node.properties[key] + "\n"}</pre>)}</code></Popup>
192+
icon={<div
193+
style={{color: colors[this.state.nodeLabels.indexOf(i.node.labels[i.node.labels.length - 1]) % colors.length]}}>
194+
<Icon className="close">place</Icon></div>}>
195+
{this.createPopupFromNodeProperties(i)}
98196
</Marker>) : <div></div>
99-
let lines = <div></div>// [<Polyline key={0} positions={[this.state.pos1, this.state.pos2]} color={colors[0]}/>];
197+
let lines = (relationshipsAndPositionsValues) ?
198+
relationshipsAndPositionsValues.map(i =>
199+
<Polyline width="5" key={0} positions={[i.start, i.end]} color={"grey"}>
200+
{this.createPopupFromRelProperties(i)}
201+
</Polyline>
202+
) : <div></div>
203+
100204

101205
return <MapContainer key={0} style={{"width": this.state.width + "px", "height": this.state.height + "px"}}
102206
center={
@@ -117,6 +221,26 @@ class NeoMapReport extends NeoReport {
117221
</MapContainer>;
118222
}
119223

224+
createPopupFromRelProperties(i) {
225+
return <Popup className={"leaflet-rel-popup"}>
226+
<h6><b>{i.rel.type}</b></h6>
227+
<code>{(Object.keys(i.rel.properties).length > 0) ?
228+
Object.keys(i.rel.properties).map(key =>
229+
<pre>{key + ": " + this.neoPropertyToString(i.rel.properties[key]) + "\n"}</pre>)
230+
: "(no properties)"}
231+
</code>
232+
</Popup>;
233+
}
234+
235+
createPopupFromNodeProperties(i) {
236+
return <Popup>
237+
<h6><b>{(i.node.labels.length > 0) ? i.node.labels.map(b => b + " ") : "(No labels)"}</b></h6>
238+
<code>{Object.keys(i.node.properties).map(key =>
239+
<pre>{key + ": " + this.neoPropertyToString(i.node.properties[key]) + "\n"}</pre>)}
240+
</code>
241+
</Popup>;
242+
}
243+
120244
render() {
121245
let rendered = super.render();
122246
if (rendered) {

src/card/report/NeoMarkdownReport.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class NeoMarkdownReport extends NeoReport {
1818
markdown = this.props.data.replace(/\n\n/g, "\n\n &nbsp; \n\n").replace(/\n \n/g, "\n\n &nbsp; \n\n");
1919
}
2020
// TODO - add better markdown support (bullets, nicer code view, etc.
21-
let result = <ReactMarkdown plugins={[gfm]} children={markdown} />
21+
let result =<div><base target="_blank"/> <ReactMarkdown plugins={[gfm]} children={markdown} /></div>
2222
return (result);
2323
}
2424
}

src/data/beer_dashboard.json

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{
2+
"title": "Beer & Breweries Dashboard 🍻",
3+
"version": "1.1",
4+
"editable": true,
5+
"pagenumber": 1,
6+
"pages": [
7+
{
8+
"title": "main page",
9+
"reports": [
10+
{
11+
"title": "Hi there 👋",
12+
"width": 12,
13+
"height": 4,
14+
"type": "text",
15+
"query": "This is an example dashboard that uses the [Beers and Breweries Graphgist](https://neo4j.com/graphgist/beer-amp-breweries-graphgist/).\n\n\n**Check out the second tab of this dashboard (\"Beers by Country\") for some example reports!**\n",
16+
"page": 1,
17+
"properties": [],
18+
"parameters": "",
19+
"refresh": 0
20+
},
21+
{}
22+
]
23+
},
24+
{
25+
"title": "beers by COUNTRY",
26+
"reports": [
27+
{
28+
"title": "Select a country",
29+
"width": 4,
30+
"height": 4,
31+
"type": "select",
32+
"query": "MATCH (n:`Brewery`) \nWHERE toLower(toString(n.`country`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`country` as value LIMIT 4",
33+
"page": 1,
34+
"properties": [
35+
"Brewery",
36+
"country"
37+
],
38+
"parameters": "",
39+
"refresh": 0
40+
},
41+
{
42+
"title": "Beers",
43+
"width": 8,
44+
"height": 4,
45+
"type": "table",
46+
"query": "MATCH p=(bb:Beer)--(b:Brewery)\nWHERE b.country = $neodash_brewery_country\nRETURN b.name as Brewery, p as Relationship, collect(bb.name) as Beer",
47+
"page": 1,
48+
"properties": [],
49+
"parameters": "",
50+
"refresh": 0
51+
},
52+
{
53+
"title": "Breweries",
54+
"width": 8,
55+
"height": 4,
56+
"type": "map",
57+
"query": "MATCH p=(b:Brewery)--(x)\nWHERE b.country = $neodash_brewery_country\nRETURN p",
58+
"page": 1,
59+
"properties": [],
60+
"parameters": "",
61+
"refresh": 0
62+
},
63+
{
64+
"title": "Graph View",
65+
"width": 4,
66+
"height": 4,
67+
"type": "graph",
68+
"query": "MATCH p=(b:Brewery)--(x)\nWHERE b.country = $neodash_brewery_country\nRETURN p",
69+
"page": 8,
70+
"properties": [
71+
"name",
72+
"name",
73+
"country",
74+
"name"
75+
],
76+
"parameters": "",
77+
"refresh": 0
78+
},
79+
{
80+
"title": "Beer Strengths",
81+
"width": 4,
82+
"height": 4,
83+
"type": "bar",
84+
"query": "MATCH (b:Brewery)--(bb:Beer)\nWHERE b.country = $neodash_brewery_country\nRETURN bb.name as Beer, bb.abv as Strength\nORDER By Strength ASC SKIP 1 LIMIT 10 ",
85+
"page": 204,
86+
"properties": [],
87+
"parameters": "{\"color\":\"#f2e394\"}",
88+
"refresh": 0
89+
},
90+
{}
91+
]
92+
},
93+
{
94+
"title": "BEER CATEGORIES",
95+
"reports": [
96+
{}
97+
]
98+
}
99+
]
100+
}

0 commit comments

Comments
 (0)