Skip to content

Commit

Permalink
feat: trace list for waveform viewer (#60)
Browse files Browse the repository at this point in the history
* WIP add trace list to viewer

* use fluentui
* improve project structure and rollup config

* feat: trace list for waveform viewer
  • Loading branch information
kahojyun authored Sep 3, 2023
1 parent 631fe1d commit 0508d18
Show file tree
Hide file tree
Showing 28 changed files with 1,947 additions and 841 deletions.
8 changes: 6 additions & 2 deletions src/Qynit.PulseGen.Server/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/* eslint-env node */
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["**/wwwroot/**/*", "Pages/**/*.js"],
ignorePatterns: ["wwwroot/**/*"],
};
3 changes: 1 addition & 2 deletions src/Qynit.PulseGen.Server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
## Ignore bundled JS files
**/*.razor.js
**/*.razor.js.map
wwwroot/dist/
53 changes: 41 additions & 12 deletions src/Qynit.PulseGen.Server/App.razor
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@inject IJSRuntime JS

<FluentDesignSystemProvider BaseLayerLuminance="@BaseLayerLuminance">
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</FluentDesignSystemProvider>

@code
{
private bool _isDarkMode = false;
private float BaseLayerLuminance => _isDarkMode ? 0.23f : 1f;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await using var module = await JS.ImportComponentModule<App>();
var objRef = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("init", objRef);
_isDarkMode = await module.InvokeAsync<bool>("isSystemDarkMode");
StateHasChanged();
}
}

[JSInvokable]
public async ValueTask OnDarkModeChanged(bool isDarkMode)
{
_isDarkMode = isDarkMode;
await InvokeAsync(StateHasChanged);
}
}
13 changes: 13 additions & 0 deletions src/Qynit.PulseGen.Server/App.razor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DotNet } from "@microsoft/dotnet-js-interop";

const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");

export function isSystemDarkMode() {
return darkModePreference.matches;
}

export function init(objRef: DotNet.DotNetObject) {
darkModePreference.addEventListener("change", (e) => {
objRef.invokeMethodAsync("OnDarkModeChanged", e.matches);
});
}
9 changes: 0 additions & 9 deletions src/Qynit.PulseGen.Server/DisposableExtensions.cs

This file was deleted.

13 changes: 13 additions & 0 deletions src/Qynit.PulseGen.Server/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace Qynit.PulseGen.Server;

internal static class Extensions
{
public static async ValueTask<IJSObjectReference> ImportComponentModule<T>(this IJSRuntime js) where T : ComponentBase
{
var path = JsLocation.GetPath<T>();
return await js.InvokeAsync<IJSObjectReference>("import", path);
}
}
25 changes: 25 additions & 0 deletions src/Qynit.PulseGen.Server/JsLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Components;

namespace Qynit.PulseGen.Server;

internal class JsLocation
{
public const string Root = "./dist";

public static string GetPath<T>() where T : ComponentBase
{
return PathCache<T>.Path;
}

private class PathCache<T>
{
public static readonly string Path = GetPath();
private static string GetPath()
{
var type = typeof(T);
var rootNamespace = typeof(JsLocation).Namespace!;
var path = type.FullName!.Replace(rootNamespace, "").Replace(".", "/").TrimStart('/');
return $"{Root}/{path}.js";
}
}
}
5 changes: 0 additions & 5 deletions src/Qynit.PulseGen.Server/MainLayout.razor

This file was deleted.

7 changes: 7 additions & 0 deletions src/Qynit.PulseGen.Server/Pages/Hello.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@page "/Hello"

<h3>Hello</h3>

@code {

}
154 changes: 55 additions & 99 deletions src/Qynit.PulseGen.Server/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@using Qynit.PulseGen.Server.Hubs
@using Qynit.PulseGen.Server.Models;
@using Qynit.PulseGen.Server.Services
@using Qynit.PulseGen.Server.Shared
@using System.IO.Pipelines;
@using System.Buffers;
@using System.Collections.Concurrent;
Expand All @@ -15,140 +16,95 @@
@implements IPlotClient

<div class="box">
<div @ref="_chart" class="row content" />
<div class="aside">
<FluentDataGrid RowsData="@FilteredTraces">
<PropertyColumn Property="@(p => p.Name)" Sortable="true">
<ColumnOptions>
<FluentSearch @bind-Value="_nameFilter" />
</ColumnOptions>
</PropertyColumn>
<TemplateColumn>
<HeaderCellItemTemplate>
<TriCheckbox @bind-Value="AnyVisible" Indeterminate="IsIndeterminate">Show</TriCheckbox>
</HeaderCellItemTemplate>
<ChildContent>
<FluentCheckbox @bind-Value="@context.Visible" />
</ChildContent>
</TemplateColumn>
</FluentDataGrid>
</div>
<div class="plot">
<WaveformViewer Names="VisibleTraceNames" PlotService="PlotService"/>
</div>
</div>

@code
{
private HubConnection? _hubConnection;
private ElementReference _chart;
private IJSObjectReference? _module;
private DotNetObjectReference<Index>? _objRef;
private Task? _renderTask;
private CancellationTokenSource? _renderCts;
private ConcurrentDictionary<string, bool> _channelNewValue = new();
private ConcurrentDictionary<string, bool> _channelVisible = new();
private Channel<string> _renderQueue = Channel.CreateUnbounded<string>();
private string _nameFilter = string.Empty;

protected override async Task OnInitializedAsync()
private class Trace
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri(PlotHub.Uri))
.Build();

_hubConnection.On<IEnumerable<string>>(nameof(ReceiveNames), ReceiveNames);

await _hubConnection.StartAsync();
public string Name { get; set; } = string.Empty;
public bool Visible { get; set; }
public bool NeedUpdate { get; set; }
}

protected override async Task OnAfterRenderAsync(bool firstRender)
private List<Trace> Traces { get; set; } = new();
private IEnumerable<string>? VisibleTraceNames => Traces.Where(p => p.Visible).Select(p => p.Name).ToList();
IQueryable<Trace> FilteredTraces => Traces.AsQueryable().Where(p => p.Name.Contains(_nameFilter));
private bool AnyVisible
{
if (firstRender)
get => FilteredTraces.Any(p => p.Visible);
set
{
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./Pages/Index.razor.js");
_objRef = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("init", _chart, _objRef);
_renderCts = new();
_renderTask = RenderInBackground(_renderCts.Token);
foreach (var p in FilteredTraces)
{
p.Visible = value;
}
}
}
private bool IsIndeterminate => FilteredTraces.Any(p => p.Visible) && FilteredTraces.Any(p => !p.Visible);

[JSInvokable]
public async ValueTask VisibilityChanged(string name, bool visible)
protected override void OnInitialized()
{
var flag = false;
_channelVisible.AddOrUpdate(name, visible, (k, v) =>
{
flag = visible && !v;
return visible;
});
if (flag)
{
await EnqueueRender(name);
}
var names = PlotService.GetNames();
Traces = names.Select(x => new Trace { Name = x, Visible = true, NeedUpdate = true }).ToList();
}

public async Task ReceiveNames(IEnumerable<string> names)
protected override async Task OnInitializedAsync()
{
foreach (var name in names)
{
_channelNewValue[name] = true;
await EnqueueRender(name);
}
}
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri(PlotHub.Uri))
.Build();

private async ValueTask EnqueueRender(string name)
{
await _renderQueue.Writer.WriteAsync(name);
_hubConnection.On<IEnumerable<string>>(nameof(ReceiveNames), ReceiveNames);

await _hubConnection.StartAsync();
}

private async Task RenderInBackground(CancellationToken token)
public async Task ReceiveNames(IEnumerable<string> names)
{
while (!token.IsCancellationRequested)
var tracesLookUp = Traces.ToDictionary(x => x.Name);
foreach (var name in names)
{
var name = await _renderQueue.Reader.ReadAsync(token);
if (ChannelNeedUpdate(name) && ChannelShouldRender(name))
if (tracesLookUp.TryGetValue(name, out var trace))
{
await RenderWaveform(name, token);
_channelNewValue[name] = false;
trace.NeedUpdate = true;
}
}
}

private bool ChannelNeedUpdate(string name)
{
return _channelNewValue.TryGetValue(name, out var needUpdate) && needUpdate;
}

private bool ChannelShouldRender(string name)
{
var hasValue = _channelVisible.TryGetValue(name, out var visible);
return !hasValue || visible;
}

private async ValueTask RenderWaveform(string name, CancellationToken token)
{
if (PlotService.TryGetPlot(name, out var arc))
{
using (arc)
else
{
var pipe = new Pipe();
var writer = pipe.Writer;
writer.Write(MemoryMarshal.AsBytes(arc.Target.DataI));
writer.Write(MemoryMarshal.AsBytes(arc.Target.DataQ));
await writer.CompleteAsync();
using var streamRef = new DotNetStreamReference(pipe.Reader.AsStream());
var dataType = DataType.Float32;
var isReal = arc.Target.IsReal;
await _module!.InvokeVoidAsync("renderWaveform", token, name, dataType, isReal, streamRef);
Traces.Add(new Trace { Name = name, Visible = true, NeedUpdate = true });
}
}
await InvokeAsync(StateHasChanged);
}

public async ValueTask DisposeAsync()
{
_renderCts?.Cancel();
if (_renderTask is not null)
{
try
{
await _renderTask;
}
catch (OperationCanceledException) { }
}
_renderCts?.Dispose();
_objRef?.Dispose();
if (_hubConnection is not null)
{
await _hubConnection.DisposeAsync();
}
if (_module is not null)
{
try
{
await _module.DisposeAsync();
}
catch (JSDisconnectedException) { }
}
}
}
6 changes: 3 additions & 3 deletions src/Qynit.PulseGen.Server/Pages/Index.razor.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.box {
display: flex;
flex-flow: column;
flex-flow: row;
height: 100%;
}

.box .row.header {
.aside {
flex: 0 1 auto;
}

.box .row.content {
.plot {
flex: 1 1 auto;
}
Loading

0 comments on commit 0508d18

Please sign in to comment.