graph visualization

This commit is contained in:
Trevi Awater
2022-01-11 00:07:00 +01:00
parent 78b3557635
commit bf2022e614
14 changed files with 392 additions and 11 deletions

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pancakeView="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
x:Class="Xamarin.Neo4j.Controls.QueryBuilder">
<pancakeView:PancakeView BackgroundColor="White" CornerRadius="2" Padding="4" Margin="16">
<pancakeView:PancakeView CornerRadius="2">
<pancakeView:PancakeView.Border>
<pancakeView:Border Thickness="2" Color="#d8e5f1" />
</pancakeView:PancakeView.Border>
<StackLayout Orientation="Horizontal">
<Editor x:Name="editor" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
<Button Clicked="InvokeExecuteQuery" Text="{x:Static fonts:FontAwesomeSolid.Play}" FontFamily="{StaticResource FontAwesomeSolid}" VerticalOptions="CenterAndExpand" HorizontalOptions="End" FontSize="20" Margin="0, 0, 5, 0" />
</StackLayout>
</pancakeView:PancakeView>
</pancakeView:PancakeView>
</ContentView>

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Xamarin.Neo4j.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class QueryBuilder : ContentView
{
public event EventHandler ExecuteQuery;
public QueryBuilder()
{
InitializeComponent();
editor.SetBinding(Editor.TextProperty, new Binding(nameof(Query), source: this));
}
#region Bindable Properties
public string Query
{
get => (string)GetValue(QueryProperty);
set => SetValue(QueryProperty, value);
}
public static BindableProperty QueryProperty = BindableProperty.Create("Query", typeof(string),
typeof(QueryBuilder), string.Empty, BindingMode.TwoWay);
#endregion
private void InvokeExecuteQuery (object sender, EventArgs e)
{
ExecuteQuery?.Invoke(sender, e);
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
x:Class="Xamarin.Neo4j.Controls.QueryResultCell">
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Query}" />
<Button Clicked="OpenNeovis" Text="{x:Static fonts:FontAwesomeSolid.ProjectDiagram}" FontFamily="{StaticResource FontAwesomeSolid}" VerticalOptions="CenterAndExpand" FontSize="20" Margin="0, 0, 5, 0" />
</StackLayout>
</ViewCell>

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.Pages;
namespace Xamarin.Neo4j.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class QueryResultCell : ViewCell
{
public QueryResult QueryResult => (QueryResult) BindingContext;
public QueryResultCell()
{
InitializeComponent();
}
private async void OpenNeovis(object sender, EventArgs e)
{
await App.Current.MainPage.Navigation.PushAsync(new VisualizationPage(QueryResult));
}
}
}

View File

@@ -0,0 +1,30 @@
//
// QueryResult.cs
//
// Trevi Awater
// 13-11-2021
//
// © Xamarin.Neo4j
//
using System.Collections;
using System.Collections.Generic;
using Neo4j.Driver;
namespace Xamarin.Neo4j.Models
{
public class QueryResult
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public string Query { get; set; }
public Neo4jConnectionString ConnectionString { get; set; }
public IEnumerable<INode> Nodes { get; set; }
public IEnumerable<IRelationship> Relationships { get; set; }
}
}

View File

@@ -3,6 +3,7 @@
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
x:Class="Xamarin.Neo4j.Pages.SessionPage">
<NavigationPage.TitleView>
<StackLayout Spacing="0">
@@ -25,8 +26,15 @@
<ContentPage.Content>
<StackLayout>
<Editor Text="{Binding Query}" AutoSize="TextChanges" />
<Button Text="Run Query" Command="{Binding Commands[RunQuery]}" />
<controls:QueryBuilder ExecuteQuery="OnExecuteQuery" Query="{Binding Query}" />
<ListView ItemsSource="{Binding QueryResults}">
<ListView.ItemTemplate>
<DataTemplate>
<controls:QueryResultCell />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@@ -14,6 +14,8 @@ namespace Xamarin.Neo4j.Pages
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SessionPage : ContentPage
{
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
public SessionPage(Neo4jConnectionString connectionString)
{
InitializeComponent();
@@ -25,5 +27,10 @@ namespace Xamarin.Neo4j.Pages
{
databasePicker.Focus();
}
private void OnExecuteQuery(object sender, EventArgs e)
{
ViewModel.ExecuteQuery();
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Graph"
x:Class="Xamarin.Neo4j.Pages.VisualizationPage">
<ContentPage.Content>
<WebView Source="{Binding Source}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />
</ContentPage.Content>
</ContentPage>

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.ViewModels;
namespace Xamarin.Neo4j.Pages
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class VisualizationPage : ContentPage
{
public VisualizationPage(QueryResult queryResult)
{
InitializeComponent();
BindingContext = new VisualizationViewModel(Navigation, queryResult);
}
}
}

View File

@@ -71,14 +71,54 @@ namespace Xamarin.Neo4j.Services
return databases;
}
public async Task ExecuteQuery(string query, string database)
public async Task<QueryResult> ExecuteQuery(string query, Neo4jConnectionString connectionString)
{
var session = GraphClient.Driver.AsyncSession(d => d.WithDatabase(database));
try
{
var session = GraphClient.Driver.AsyncSession(d => d.WithDatabase(connectionString.Database));
var cursor = await session.RunAsync(query);
var nodes = new List<INode>();
var relationships = new List<IRelationship>();
while (await cursor.FetchAsync())
{
foreach (var record in cursor.Current.Values)
{
var value = record.Value;
switch (value)
{
case INode _:
nodes.Add(value.As<INode>());
break;
case IRelationship _:
relationships.Add(value.As<IRelationship>());
break;
}
}
}
return new QueryResult()
{
Success = true,
Query = query,
ConnectionString = connectionString,
Nodes = nodes,
Relationships = relationships
};
}
catch (ClientException e)
{
return new QueryResult()
{
Success = false,
Query = query,
ConnectionString = connectionString,
ErrorMessage = e.Message
};
}
}
}

View File

@@ -8,13 +8,17 @@
//
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Acr.UserDialogs;
using Xamarin.Forms;
using Xamarin.Neo4j.Annotations;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.Pages;
using Xamarin.Neo4j.Services;
namespace Xamarin.Neo4j.ViewModels
@@ -29,20 +33,36 @@ namespace Xamarin.Neo4j.ViewModels
private List<Database> _availableDatabases;
private ObservableCollection<QueryResult> _queryResults;
private Neo4jConnectionString _connectionString;
private string _query;
public SessionViewModel(INavigation navigation, Neo4jConnectionString connectionString) : base(navigation)
{
_connectionString = connectionString;
_neo4jService = DependencyService.Resolve<Neo4jService>();
Commands.Add("RunQuery", new Command(async () =>
{
_neo4jService.ExecuteQuery(Query, CurrentDatabase.Name);
}));
QueryResults = new ObservableCollection<QueryResult>();
InitializeConnection(connectionString);
}
public async void ExecuteQuery()
{
_connectionString.Database = CurrentDatabase.Name;
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
if (result.Success)
QueryResults.Add(result);
else
UserDialogs.Instance.Alert(result.ErrorMessage);
}
private async void InitializeConnection(Neo4jConnectionString connectionString)
{
await _neo4jService.EstablishConnection(connectionString);
@@ -88,6 +108,18 @@ namespace Xamarin.Neo4j.ViewModels
}
}
public ObservableCollection<QueryResult> QueryResults
{
get => _queryResults;
set
{
_queryResults = value;
OnPropertyChanged();
}
}
public string Query
{
get => _query;

View File

@@ -0,0 +1,78 @@
//
// VisualizationViewModel.cs
//
// Trevi Awater
// 10-01-2022
//
// © Xamarin.Neo4j
//
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Xamarin.Neo4j.Annotations;
using Xamarin.Neo4j.Models;
namespace Xamarin.Neo4j.ViewModels
{
public class VisualizationViewModel : ViewModelBase, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private HtmlWebViewSource _source;
public VisualizationViewModel(INavigation navigation, QueryResult queryResult) : base(navigation)
{
ParseNeovisHtml(queryResult);
}
private void ParseNeovisHtml(QueryResult queryResult)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "Xamarin.Neo4j.Visualization.neovis.html";
using (var stream = assembly.GetManifestResourceStream(resourceName))
using (var reader = new StreamReader(stream))
{
var result = reader.ReadToEnd();
result = result.Replace("{{host}}", queryResult.ConnectionString.Host);
result = result.Replace("{{port}}", queryResult.ConnectionString.Port.ToString());
result = result.Replace("{{database}}", queryResult.ConnectionString.Database);
result = result.Replace("{{username}}", queryResult.ConnectionString.Username);
result = result.Replace("{{password}}", queryResult.ConnectionString.Password);
result = result.Replace("{{encryption}}", queryResult.ConnectionString.Encrypted ? "ENCRYPTION_ON" : "ENCRYPTION_OFF");
result = result.Replace("{{query}}", queryResult.Query);
Source = new HtmlWebViewSource
{
Html = result
};
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#region Bindable Properties
public HtmlWebViewSource Source
{
get => _source;
set
{
_source = value;
OnPropertyChanged();
}
}
#endregion
}
}

View File

@@ -0,0 +1,41 @@
<!doctype html>
<html>
<head>
<title>Neovis.js Simple Example</title>
<style type="text/css">
html, body {
font: 16pt arial;
}
#viz {
width: 100vw;
height: 100vh;
border: 1px solid lightgray;
font: 22pt arial;
}
</style>
<script src="https://cdn.neo4jlabs.com/neovis.js/v1.5.0/neovis.js"></script>
</head>
<body onload="draw()">
<div id="viz"></div>
</body>
<script type="text/javascript">
var viz;
function draw() {
var config = {
container_id: "viz",
server_url: "bolt://{{host}}:{{port}}",
server_user: "{{username}}",
server_database: "{{database}}",
server_password: "{{password}}",
encrypted: "{{encryption}}",
initial_cypher: "{{query}}"
};
viz = new NeoVis.default(config);
viz.render();
}
</script>
</html>

View File

@@ -14,9 +14,15 @@
<PackageReference Include="Neo4jClient" Version="4.1.18" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2196" />
<PackageReference Include="Xamarin.Forms.PancakeView" Version="2.3.0.759" />
</ItemGroup>
<ItemGroup>
<Folder Include="Converters" />
</ItemGroup>
<ItemGroup>
<None Remove="Visualization\neovis.html" />
<EmbeddedResource Include="Visualization\neovis.html" />
</ItemGroup>
</Project>