Merge pull request #508 from mempool/simon/bisq-new-dashboard

New Bisq Markets Dashboard Design
This commit is contained in:
wiz 2021-05-11 13:40:32 +09:00 committed by GitHub
commit f137f45cef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 461 additions and 29 deletions

View File

@ -8,11 +8,9 @@
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<div class="chart-container">
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
<br><br>

View File

@ -0,0 +1,124 @@
<div class="container-xl">
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
<div class="big-fiat">
<span class="green-color" *ngIf="bisqMarketPrice; else loading">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
<div class="big-fiat">
<span class="green-color" *ngIf="usdPrice$ | async as usdPrice; else loading">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">US Dollar - BTC/USD</h5>
<div class="chart-container">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
<br>
<div class="text-center"><a href="" [routerLink]="['/bisq/market/btc_usd']" i18n="dashboard.view-all">View all &raquo;</a></div>
</ng-container>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
<div class="chart-container">
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
<br>
<div class="text-center"><a href="" [routerLink]="['/bisq/markets']" i18n="dashboard.view-all">View all &raquo;</a></div>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">
<ng-template [ngIf]="stateService.env.OFFICIAL_BISQ_MARKETS" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h5>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/bisq/markets']" i18n="dashboard.view-all">View all &raquo;</a></div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/bisq/markets']" i18n="dashboard.view-all">View all &raquo;</a></div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loading>
<div class="skeleton-loader shorter"></div>
</ng-template>

View File

@ -0,0 +1,84 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingGraphs {
position: relative;
top: 45%;
z-index: 100;
}
.table-container {
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}
.chart-container {
height: 350px;
}
.big-fiat {
color: #3bcc49;
font-size: 26px;
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
}
.info-block {
float: left;
width: 350px;
line-height: 25px;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
}
.bg-warning {
background-color: #b58800 !important;
}
.skeleton-loader {
max-width: 100%;
&.shorter {
max-width: 150px;
}
}
.more-padding {
padding: 1.25rem 2rem 1.25rem 2rem;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}

View File

@ -0,0 +1,192 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-main-bisq-dashboard',
templateUrl: './bisq-main-dashboard.component.html',
styleUrls: ['./bisq-main-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMainDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
hlocData$: Observable<any>;
usdPrice$: Observable<number>;
isLoadingGraph = true;
bisqMarketPrice = 0;
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle(`Markets`);
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
map((conversions) => conversions.USD)
);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers.slice(0, 10);
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (!this.stateService.env.OFFICIAL_BISQ_MARKETS) {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
})).slice(0, 10);
})
);
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
.pipe(
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = 86400;
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@ -2,7 +2,7 @@
<table class="table table-borderless table-striped">
<thead>
<th i18n>Date</th>
<th i18n>Price</th>
<th *ngIf="view === 'all'" i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
<th>
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
@ -14,7 +14,7 @@
<td>
{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td>
<td *ngIf="view === 'all'">
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
@ -39,7 +39,7 @@
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
@ -7,7 +7,16 @@ import { Observable } from 'rxjs';
styleUrls: ['./bisq-trades.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTradesComponent {
export class BisqTradesComponent implements OnChanges {
@Input() trades$: Observable<any>;
@Input() market: any;
@Input() view: 'all' | 'small' = 'all';
loadingColumns = [1, 2, 3, 4];
ngOnChanges() {
if (this.view === 'small') {
this.loadingColumns = [1, 2, 3];
}
}
}

View File

@ -11,6 +11,7 @@ import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
@ -42,6 +43,7 @@ import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
BisqDashboardComponent,
BisqMarketComponent,
BisqTradesComponent,
BisqMainDashboardComponent,
],
imports: [
BisqRoutingModule,

View File

@ -10,10 +10,15 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { ApiDocsComponent } from '../components/api-docs/api-docs.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
const routes: Routes = [
{
{
path: '',
component: BisqMainDashboardComponent,
},
{
path: 'markets',
component: BisqDashboardComponent,
},
{

View File

@ -1,5 +1,5 @@
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts-area',
@ -7,10 +7,11 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDes
styleUrls: ['./lightweight-charts-area.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsAreaComponent implements OnChanges, OnDestroy {
export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() lineData: any;
@Input() precision: number;
@Input() height = 500;
areaSeries: any;
volumeSeries: any;
@ -18,12 +19,14 @@ export class LightweightChartsAreaComponent implements OnChanges, OnDestroy {
lineSeries: any;
container: any;
width = 1110;
height = 500;
width: number;
constructor(
private element: ElementRef,
) {
) { }
ngOnInit() {
this.width = this.element.nativeElement.parentElement.offsetWidth;
this.container = document.createElement('div');
const chartholder = this.element.nativeElement.appendChild(this.container);
@ -112,16 +115,22 @@ export class LightweightChartsAreaComponent implements OnChanges, OnDestroy {
toolTip.style.left = left + 'px';
toolTip.style.top = top + 'px';
});
this.updateData();
}
businessDayToString(businessDay) {
return businessDay.year + '-' + businessDay.month + '-' + businessDay.day;
}
ngOnChanges() {
if (!this.data) {
ngOnChanges(changes: SimpleChanges) {
if (!changes.value || changes.value.isFirstChange()){
return;
}
this.updateData();
}
updateData() {
this.areaSeries.setData(this.data);
this.lineSeries.setData(this.lineData);
}

View File

@ -1,5 +1,5 @@
import { createChart, CrosshairMode } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts',
@ -7,10 +7,11 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDes
styleUrls: ['./lightweight-charts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsComponent implements OnChanges, OnDestroy {
export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() volumeData: any;
@Input() precision: number;
@Input() height = 500;
lineSeries: any;
volumeSeries: any;
@ -18,10 +19,12 @@ export class LightweightChartsComponent implements OnChanges, OnDestroy {
constructor(
private element: ElementRef,
) {
) { }
ngOnInit() {
this.chart = createChart(this.element.nativeElement, {
width: 1110,
height: 500,
width: this.element.nativeElement.parentElement.offsetWidth,
height: this.height,
layout: {
backgroundColor: '#000000',
textColor: '#d1d4dc',
@ -52,12 +55,22 @@ export class LightweightChartsComponent implements OnChanges, OnDestroy {
bottom: 0,
},
});
this.updateData();
}
ngOnChanges() {
if (!this.data) {
ngOnChanges(changes: SimpleChanges) {
if (!changes.value || changes.value.isFirstChange()){
return;
}
this.updateData();
}
ngOnDestroy() {
this.chart.remove();
}
updateData() {
this.lineSeries.setData(this.data);
this.volumeSeries.setData(this.volumeData);
@ -70,8 +83,4 @@ export class LightweightChartsComponent implements OnChanges, OnDestroy {
});
}
ngOnDestroy() {
this.chart.remove();
}
}