Recently I've added a nice way to visualize shared relationships using force-directed graphs to a web app that I'm porting(Java/Spring->C#/.Net).
The main reason why I wanted to visualize these shared relationships, is that it very quickly put a lot of information into perspective (and they're pretty and I like graphs, is that so bad?)
I used to use Neo4J but I've decided to implement the graph relationship myself using Entity Framework Core and for the most part, it's quite good. EF.Core isn't quite that same as EF.net(Fat version) but its damn near close.
What you're seeing below are various investment groups which investments are placed into and how they and other investments share those groups. I've also coded in a weighting system. So in effect, the bigger the dot/group, the more investments are in that group. I've got the same idea throughout my web app. This is what it looks like in a static screenshot but in reality, these graphs are 'alive' and have gravity you can swirl them around and they respond!

To do this, a little bit of javascript makes anything seems possible(Its Typescript actually). Here is the code that you can add to a component in Angular. The key part of this is the render () function which brings this all together :
import { Component, Input, NgModule, OnInit, AfterViewInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { ApiService } from './../../apiservice.service';
import { GraphData } from '../../Models/GraphData';
import { EntityTypes } from '../../Utilities';
import * as d3 from 'd3';
interface Datum {
name: string;
value: number;
}
@Component({
selector: 'app-graph',
templateUrl: './graph.component.html',
styleUrls: ['./graph.component.css'],
encapsulation: ViewEncapsulation.None
})
export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
EntityTypes = EntityTypes;
@Input() InvestmentId: number;
@Input() EntityType: EntityTypes;
name: string;
svg;
color;
simulation;
link;
node;
circles;
labels;
data: GraphData;
constructor(protected apiService: ApiService) { }
ngOnInit() { }
ngAfterViewInit() {
this.apiService
.GetInvestmentGraphData(this.EntityType, this.InvestmentId)
.subscribe( (graphData) => this.render(graphData),
error => console.log('Error occured getting graph data:' + error));
}
ticked() {
this.link
.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
this.node.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
render(graph) {
const SvgTagName = '#' + EntityTypes[this.EntityType];
this.svg = d3.select(SvgTagName);
const width = +this.svg.attr('width');
const height = +this.svg.attr('height');
this.color = d3.scaleOrdinal(d3.schemeCategory20);
this.simulation = d3.forceSimulation()
.force('link', d3.forceLink().distance(90))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
this.link = this.svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(graph.links)
.enter().append('line')
.attr('stroke-width', function(d) { return Math.sqrt(d.value); });
this.node = this.svg.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(graph.nodes)
.enter()
.append('g');
this.circles = this.node
.append('circle')
.attr('r', function(d) { return Math.sqrt(d.value) * 5; })
.attr('fill', (d) => this.color(d.value))
.call(d3.drag()
.on('start', (d) => this.dragstarted(d))
.on('drag', (d) => this.dragged(d))
.on('end', (d) => this.dragended(d)));
this.labels = this.node.append('text')
.text(function (d) { return d.name; })
.attr('x', 6)
.attr('y', 3);
this.node.append('title').text(function(d){ return d.value; });
this.simulation
.nodes(graph.nodes)
.on('tick', () => this.ticked());
this.simulation.force('link')
.links(graph.links);
this.simulation.alpha(0.8).restart();
}
dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
dragended(d) {
if (!d3.event.active) { this.simulation.alphaTarget(0); }
d.fx = null;
d.fy = null;
}
dragstarted(d) {
if (!d3.event.active) { this.simulation.alphaTarget(0.3).restart(); }
d.fx = d.x;
d.fy = d.y;
}
ngOnDestroy() { }
}
This uses D3.js V4 to represent the node data into these lovely looking graphs. Look it up if you like it :-)
Apart from this, I've also implemented a new search component and well its kind of great, so I thought I’d mention it.
It's the kind that as-you-type it filters down your selection type thingy, something that's usually only really possible through the use of a JavaScript trickery and black magic(which for the most part is what development is). This, for a long time, is been an envy of mine and honestly, using Angular 4 with its new "Pipes" functionality makes this so easy it is disturbing.
Ok, so let me show you real quick what it looks like (see the new empty search bar at the top - before):
And as you type it filters out the collection(after), so this is showing only those investments that have 'tech' in their names.
Pretty cool, eh?
Anyway, how this basically works is that it uses is a simple search criterion and either match against your search term(includes it) or doesn't(filters it out)
First, you pass in your objects through the filter like this:
<tr *ngFor="let investment of Investments | filter : searchText">
<td>
<strong><a *ngIf="investment" [routerLink]="['/InvestmentDetails', investment.id]">{{ investment.name }}</a></strong>
</td>
<td>{{investment.description}}</td>
<td>{{investment.symbol}}</td>
<td>{{investment.value}}</td>
<td><a (click)="delete(investment.id)" href="javascript:void(0)">Delete</a></td>
</tr>
And then basically my investment objects are filtered down by the filter and the filter is defined with that criterion or predicate i was talking about back there that takes some objects out or in of the collection on the fly. Let me show you:
import { Pipe, PipeTransform } from '@angular/core';
import { Investment } from './Models/Investment';
@Pipe({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
transform(items: Investment[], searchText: string): any[] {
if (!items) { return []; }
if (!searchText) { return items; }
searchText = searchText.toLowerCase();
// So notice here that I'm choosing what needs to be included in the collection and this is dynamically evaluated against what I type in. Awesome.
return items.filter( it => it.name.toLowerCase().includes(searchText));
}
}
And the result is quite a lovely experience in my opinion.
There is a video I made also:
Peace.