Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: update dashboard climate data #331

Merged
merged 8 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/picsa-apps/dashboard/src/app/app.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
:host {
// force page padding which normally only applies to tailwind breakpoints
--page-padding: 16px;
}

mat-sidenav {
width: 256px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export const ApiMapping = (
},
});
if (error) throw error;
console.log('summary data', data);
// HACK - API issue returning huge data for some stations
if (data.data.length > 1000) {
console.error({ country_code, station_id, station_name, total_rows: data.data.length });
throw new Error(`[rainfallSummary] Too many rows | ${station_name} ${data.data.length}`);
}
// TODO - gen types and handle mapping
const entry: IClimateProductInsert = {
data: data as any,
Expand Down Expand Up @@ -90,48 +94,49 @@ export const ApiMapping = (
},
//
forecasts: async (country_code: IAPICountryCode) => {
const { data, error } = await api
.getObservableClient(`forecasts/${country_code}`)
.GET(`/v1/forecasts/{country_code}`, { params: { path: { country_code } } });
if (error) throw error;
const forecasts = data.map((d): IForecastInsert => {
const { date, filename, format, type } = d;
// TODO - handle format
return { date_modified: date, filename, country_code, type, id: filename.split('/').pop() as string };
});
const { error: dbError, data: dbData } = await db
.table('climate_forecasts')
.upsert<IForecastInsert>(forecasts)
.select<'*', IForecastRow>('*');
if (dbError) throw dbError;
return dbData || [];
// const { data, error } = await api
// .getObservableClient(`forecasts/${country_code}`)
// .GET(`/v1/forecasts/{country_code}`, { params: { path: { country_code } } });
// if (error) throw error;
// const forecasts = data.map((d): IForecastInsert => {
// const { date, filename, format, type } = d;
// // TODO - handle format
// return { date_modified: date, filename, country_code, type, id: filename.split('/').pop() as string };
// });
// const { error: dbError, data: dbData } = await db
// .table('climate_forecasts')
// .upsert<IForecastInsert>(forecasts)
// .select<'*', IForecastRow>('*');
// if (dbError) throw dbError;
// return dbData || [];
return [];
},
forecast_file: async (row: IForecastRow) => {
const { country_code, filename } = row;
const { data, error } = await api.getObservableClient(`forecasts/${filename}`).GET(`/v1/forecasts/{file_name}`, {
params: { path: { file_name: filename } },
parseAs: 'blob',
});
if (error) throw error;
// setup metadata
const fileBlob = data as Blob;
const bucketId = country_code as string;
const folderPath = 'climate/forecasts';
// upload to storage
await storage.putFile({ bucketId, fileBlob, filename, folderPath });
// TODO - handle error if filename already exists
const storageEntry = await storage.getFileAlt({ bucketId, filename, folderPath });
if (storageEntry) {
const { error: dbError } = await db
.table('climate_forecasts')
.upsert<IForecastInsert>({ ...row, storage_file: storageEntry.id })
.select('*');
if (dbError) {
throw dbError;
}
return;
}
throw new Error('Storage file not found');
// const { country_code, filename } = row;
// const { data, error } = await api.getObservableClient(`forecasts/${filename}`).GET(`/v1/forecasts/{file_name}`, {
// params: { path: { file_name: filename } },
// parseAs: 'blob',
// });
// if (error) throw error;
// // setup metadata
// const fileBlob = data as Blob;
// const bucketId = country_code as string;
// const folderPath = 'climate/forecasts';
// // upload to storage
// await storage.putFile({ bucketId, fileBlob, filename, folderPath });
// // TODO - handle error if filename already exists
// const storageEntry = await storage.getFileAlt({ bucketId, filename, folderPath });
// if (storageEntry) {
// const { error: dbError } = await db
// .table('climate_forecasts')
// .upsert<IForecastInsert>({ ...row, storage_file: storageEntry.id })
// .select('*');
// if (dbError) {
// throw dbError;
// }
// return;
// }
// throw new Error('Storage file not found');
},
};
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JsonPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, effect } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
Expand All @@ -13,7 +13,7 @@ import { ChartConfiguration } from 'c3';

import { ClimateService } from '../../../../climate.service';
import { DashboardClimateApiStatusComponent, IApiStatusOptions } from '../../../../components/api-status/api-status';
import { APITypes, IClimateProductRow } from '../../../../types';
import { APITypes, IClimateProductRow, IStationRow } from '../../../../types';

type AnnualRainfallSummariesdata = APITypes.components['schemas']['AnnualRainfallSummariesdata'];

Expand All @@ -39,13 +39,20 @@ interface IRainfallSummary {
styleUrl: './rainfall-summary.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RainfallSummaryComponent implements AfterViewInit {
export class RainfallSummaryComponent {
public summaryMetadata: IRainfallSummary['metadata'] = {};
public summaryData: IRainfallSummary['data'] = [];
public apiClientId: string;
public chartDefintions: IChartMeta[] = [];
public activeChartConfig: Partial<ChartConfiguration>;
constructor(private service: ClimateService, private cdr: ChangeDetectorRef, private supabase: SupabaseService) {}
constructor(private service: ClimateService, private cdr: ChangeDetectorRef, private supabase: SupabaseService) {
effect(() => {
const activeStation = this.service.activeStation();
if (activeStation) {
this.loadActiveStation(activeStation);
}
});
}

public tableOptions: IDataTableOptions = {
paginatorSizes: [25, 50],
Expand All @@ -56,17 +63,20 @@ export class RainfallSummaryComponent implements AfterViewInit {
showStatusCode: true,
};

private get activeStation() {
return this.service.activeStation();
}

private get db() {
return this.supabase.db.table('climate_products');
}

async ngAfterViewInit() {
const { activeStation } = this.service;
private async loadActiveStation(station: IStationRow) {
// Load data stored in supabase db if available. Otherwise load from api
// TODO - nicer if could include db lookups as part of mapping doc
const { data, error } = await this.db
.select<'*', IClimateProductRow>('*')
.eq('station_id', activeStation().id)
.eq('station_id', station.id)
.eq('type', 'rainfallSummary')
.single();
if (data) {
Expand All @@ -79,9 +89,9 @@ export class RainfallSummaryComponent implements AfterViewInit {
}

public async refreshData() {
if (this.service.activeStation) {
this.apiClientId = `rainfallSummary_${this.service.activeStation().id}`;
const data = await this.service.loadFromAPI.rainfallSummaries(this.service.activeStation());
if (this.activeStation) {
this.apiClientId = `rainfallSummary_${this.activeStation.id}`;
const data = await this.service.loadFromAPI.rainfallSummaries(this.activeStation);
const summary = data?.[0];
if (summary) {
this.loadData(summary.data as any);
Expand All @@ -98,11 +108,12 @@ export class RainfallSummaryComponent implements AfterViewInit {

private loadData(summary: IRainfallSummary) {
console.log('load data', summary);
this.tableOptions.exportFilename = `${this.service.activeStation().station_name}_rainfallSummary.csv`;
this.tableOptions.exportFilename = `${this.activeStation.id}.csv`;
const { data, metadata } = summary;
this.summaryData = this.convertAPIDataToLegacyFormat(data);
// this.summaryData = data;
this.summaryMetadata = metadata;
const { country_code } = this.service.activeStation();
const { country_code } = this.activeStation;
const definitions = CLIMATE_CHART_DEFINTIONS[country_code] || CLIMATE_CHART_DEFINTIONS.default;
this.chartDefintions = Object.values(definitions);
}
Expand All @@ -114,10 +125,12 @@ export class RainfallSummaryComponent implements AfterViewInit {
// HACK - use either end_rains or end_season depending on which has data populated
// TODO - push for single value to be populated at api level
End: el.end_rains_doy || el.end_season_doy,
Extreme_events: null as any,
// HACK - extreme events not currently supported
// Extreme_events: null as any,
Length: el.season_length,
// HACK - replace 0mm with null value
Rainfall: el.annual_rain || undefined,
// HACK - newer api returning seasonal_rain instead of annual_rain
Rainfall: el.seasonal_rain || undefined,
Start: el.start_rains_doy,
}));
return data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="page-content">
@if(station){
@if(station(); as station){
<h2>{{ station.station_name }}</h2>
<table>
<table style="max-width: 600px">
<tr>
@for(key of stationSummary.keys; track $index){
@for(key of stationSummary().keys; track $index){
<th>{{ key }}</th>
}
</tr>
@for(value of stationSummary.values; track $index){
@for(value of stationSummary().values; track $index){
<td>{{ value }}</td>
}
</table>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Component, computed } from '@angular/core';
import { MatProgressBarModule } from '@angular/material/progress-bar';

import { ClimateService } from '../../climate.service';
Expand All @@ -12,22 +12,17 @@ import { RainfallSummaryComponent } from './components/rainfall-summary/rainfall
templateUrl: './station-details.component.html',
styleUrls: ['./station-details.component.scss'],
})
export class StationDetailsPageComponent implements OnInit {
public get station() {
return this.service.activeStation();
}
export class StationDetailsPageComponent {
public station = this.service.activeStation;

public get stationSummary() {
const entries = Object.entries(this.station || {}).filter(([key]) => !['id', 'country_code'].includes(key));
stationSummary = computed(() => {
const station = this.service.activeStation();
const entries = Object.entries(station || {}).filter(([key]) => !['id', 'country_code'].includes(key));
return {
keys: entries.map(([key]) => key),
values: entries.map(([, value]) => value),
};
}
});

constructor(private service: ClimateService) {}

async ngOnInit() {
await this.service.ready();
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Climate Data</h2>
<dashboard-climate-api-status clientId="station" [options]="apiStatusOptions"></dashboard-climate-api-status>
</div>
<h3>Stations ({{ service.apiCountryCode }})</h3>
<div style="display: flex; gap: 1rem">
<div style="flex: 1">
<table mat-table class="stations-table" [dataSource]="service.stations()" style="width: 200px">
<!-- index -->
<ng-container matColumnDef="station_id">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let station; let i = index">{{ i + 1 }}</td>
</ng-container>
<!-- station name -->
<ng-container matColumnDef="station_name">
<th mat-header-cell *matHeaderCellDef>station_name</th>
<td mat-cell *matCellDef="let station">{{ station.station_name }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr
mat-row
class="station-row"
[routerLink]="row.station_id"
*matRowDef="let row; columns: displayedColumns"
></tr>
</table>
<dashboard-climate-api-status clientId="station" [options]="apiStatusOptions"></dashboard-climate-api-status>
<div style="display: flex; gap: 1rem; flex-wrap: wrap">
<div style="flex: 1; min-width: 400px; max-width: 600px">
<picsa-data-table [options]="tableOptions" [data]="tableData()"> </picsa-data-table>
</div>
<div style="flex: 1; min-width: 400px; height: calc(100vh - 96px); position: sticky; top: 0; right: 0">
<picsa-map
style="width: 100%; height: 100%"
[markers]="mapMarkers()"
(onMarkerClick)="handleMarkerClick($event)"
></picsa-map>
</div>
<picsa-map
style="height: 500px; width: 500px"
[markers]="mapMarkers()"
(onMarkerClick)="handleMarkerClick($event)"
></picsa-map>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ tr.station-row {
th {
font-weight: bold;
}

dashboard-climate-api-status {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.95);
border: 2px solid black;
z-index: 2;
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import { CommonModule } from '@angular/common';
import { Component, computed, OnInit } from '@angular/core';
import { Component, computed } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IDataTableOptions, PicsaDataTableComponent } from '@picsa/shared/features';
import { IMapMarker, PicsaMapComponent } from '@picsa/shared/features/map/map';

import { ClimateService } from '../../climate.service';
import { DashboardClimateApiStatusComponent, IApiStatusOptions } from '../../components/api-status/api-status';
import { IStationRow } from '../../types';

const displayColumns: (keyof IStationRow)[] = ['district', 'station_name'];

@Component({
selector: 'dashboard-climate-station-page',
standalone: true,
imports: [
CommonModule,
DashboardClimateApiStatusComponent,
MatTableModule,
RouterModule,
PicsaDataTableComponent,
PicsaMapComponent,
MatProgressSpinnerModule,
],
templateUrl: './station.component.html',
styleUrls: ['./station.component.scss'],
})
export class ClimateStationPageComponent implements OnInit {
public displayedColumns: (keyof IStationRow)[] = ['station_id', 'station_name'];
export class ClimateStationPageComponent {
public tableOptions: IDataTableOptions = {
displayColumns: ['map', ...displayColumns],
sort: { id: 'district', start: 'desc' },
handleRowClick: (station: IStationRow) => this.goToStation(station),
};
public tableData = computed(() => {
const stations = this.service.stations();
return stations.map((station, index) => {
return { ...station, map: index + 1 };
});
});

public mapMarkers = computed<IMapMarker[]>(() => {
const stations = this.service.stations();
Expand All @@ -38,13 +50,13 @@ export class ClimateStationPageComponent implements OnInit {

constructor(public service: ClimateService, private router: Router, private route: ActivatedRoute) {}

async ngOnInit() {
await this.service.ready();
}

public handleMarkerClick(marker: IMapMarker) {
const { _index } = marker;
const station = this.service.stations()[_index];
this.goToStation(station);
}

private goToStation(station: IStationRow) {
this.router.navigate(['./', station.station_id], { relativeTo: this.route });
}

Expand Down
Loading
Loading