From 96af291b33c7f847c6e8fe8deb55f219530cab42 Mon Sep 17 00:00:00 2001
From: cv5ch <176032962+cv5ch@users.noreply.github.com>
Date: Fri, 11 Jul 2025 15:43:39 +0200
Subject: [PATCH 1/2] Refactor ECharts to dedicated components
---
src/app/agents/agent.module.ts | 4 +-
.../edit-agent/edit-agent.component.html | 14 +-
.../agents/edit-agent/edit-agent.component.ts | 216 +-----------
src/app/core/_pipes/hashrate-pipe.ts | 35 ++
src/app/home/home.component.html | 14 +-
src/app/home/home.component.ts | 310 +++++++-----------
src/app/home/home.module.ts | 4 +-
.../agent-stat-graph.component.spec.ts | 23 ++
.../agent-stat-graph.component.ts | 192 +++++++++++
.../graphs/echarts/echarts.component.html | 1 -
.../graphs/echarts/echarts.component.ts | 29 --
.../shared/graphs/echarts/echarts.config.ts | 34 ++
.../heatmap-chart.component.spec.ts | 23 ++
.../heatmap-chart/heatmap-chart.component.ts | 110 +++++++
.../task-speed-graph.component.spec.ts | 22 ++
.../task-speed-graph.component.ts | 242 ++++++++++++++
src/app/shared/pipes.module.ts | 51 +--
.../edit-tasks/edit-tasks.component.html | 3 +-
.../tasks/edit-tasks/edit-tasks.component.ts | 211 ------------
src/app/tasks/tasks.module.ts | 4 +-
src/main.ts | 27 +-
21 files changed, 866 insertions(+), 703 deletions(-)
create mode 100644 src/app/core/_pipes/hashrate-pipe.ts
create mode 100644 src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.spec.ts
create mode 100644 src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
delete mode 100644 src/app/shared/graphs/echarts/echarts.component.html
delete mode 100644 src/app/shared/graphs/echarts/echarts.component.ts
create mode 100644 src/app/shared/graphs/echarts/echarts.config.ts
create mode 100644 src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.spec.ts
create mode 100644 src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.ts
create mode 100644 src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.spec.ts
create mode 100644 src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
diff --git a/src/app/agents/agent.module.ts b/src/app/agents/agent.module.ts
index c91ba50ca..40a0ad8f1 100644
--- a/src/app/agents/agent.module.ts
+++ b/src/app/agents/agent.module.ts
@@ -8,6 +8,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { CoreComponentsModule } from '@components/core-components.module';
+import { AgentStatGraphComponent } from '@src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component';
import { AgentStatusModalComponent } from '@src/app/agents/agent-status/agent-status-modal/agent-status-modal.component';
import { AgentStatusComponent } from '@src/app/agents/agent-status/agent-status.component';
@@ -42,7 +43,8 @@ import { PipesModule } from '@src/app/shared/pipes.module';
RouterModule,
FormsModule,
PipesModule,
- NgbModule
+ NgbModule,
+ AgentStatGraphComponent
]
})
export class AgentsModule {}
diff --git a/src/app/agents/edit-agent/edit-agent.component.html b/src/app/agents/edit-agent/edit-agent.component.html
index f40b3e1c7..e18e4b3ea 100644
--- a/src/app/agents/edit-agent/edit-agent.component.html
+++ b/src/app/agents/edit-agent/edit-agent.component.html
@@ -76,27 +76,31 @@
-
-
+
+
+
+
+
+
diff --git a/src/app/agents/edit-agent/edit-agent.component.ts b/src/app/agents/edit-agent/edit-agent.component.ts
index a17cf1a05..adf914640 100644
--- a/src/app/agents/edit-agent/edit-agent.component.ts
+++ b/src/app/agents/edit-agent/edit-agent.component.ts
@@ -1,21 +1,3 @@
-import { LineChart } from 'echarts/charts';
-import {
- GridComponent,
- GridComponentOption,
- LegendComponent,
- MarkLineComponent,
- MarkLineComponentOption,
- MarkPointComponent,
- TitleComponent,
- TitleComponentOption,
- ToolboxComponent,
- ToolboxComponentOption,
- TooltipComponent,
- TooltipComponentOption
-} from 'echarts/components';
-import * as echarts from 'echarts/core';
-import { UniversalTransition } from 'echarts/features';
-import { CanvasRenderer } from 'echarts/renderers';
import { firstValueFrom } from 'rxjs';
import { Component, OnDestroy, OnInit } from '@angular/core';
@@ -23,7 +5,6 @@ import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { JAgentAssignment } from '@models/agent-assignment.model';
-import { JAgentStat } from '@models/agent-stats.model';
import { JAgent } from '@models/agent.model';
import { JChunk } from '@models/chunk.model';
import { FilterType } from '@models/request-params.model';
@@ -89,6 +70,7 @@ export class EditAgentComponent implements OnInit, OnDestroy {
getchunks: JChunk[];
currentAssignment: JAgentAssignment;
+ public ASC = ASC;
constructor(
private unsubscribeService: UnsubscribeService,
@@ -125,9 +107,7 @@ export class EditAgentComponent implements OnInit, OnDestroy {
this.loadSelectUsers();
this.assignChunksInit(this.editedAgentIndex);
this.initForm();
- this.drawGraphs(this.showagent.agentStats);
}
-
/**
* Lifecycle hook called before the component is destroyed.
* Unsubscribes from all subscriptions to prevent memory leaks.
@@ -190,7 +170,6 @@ export class EditAgentComponent implements OnInit, OnDestroy {
this.serializer.deserialize(responseBody),
DEFAULT_FIELD_MAPPING
);
- console.log(this.selectUsers);
});
this.unsubscribeService.add(loadUsersSubscription$);
}
@@ -351,197 +330,4 @@ export class EditAgentComponent implements OnInit, OnDestroy {
.map((device) => `${deviceCountMap[device]} x ${device}`)
.join('
');
}
-
- // //
- // GRAPHS SECTION
- // //
- /**
- * Draw all graphs for GPU temperature and utilisation and CPU utilisation
- * @param agentStatList List of agentStats objects
- */
- drawGraphs(agentStatList: JAgentStat[]) {
- this.drawGraph(
- agentStatList.filter((agentStat) => agentStat.statType == ASC.GPU_TEMP),
- ASC.GPU_TEMP,
- 'tempgraph'
- ); // filter Device Temperature
- this.drawGraph(
- agentStatList.filter((agentStat) => agentStat.statType == ASC.GPU_UTIL),
- ASC.GPU_UTIL,
- 'devicegraph'
- ); // filter Device Utilisation
- this.drawGraph(
- agentStatList.filter((agentStat) => agentStat.statType == ASC.CPU_UTIL),
- ASC.CPU_UTIL,
- 'cpugraph'
- ); // filter CPU Utilisation
- }
-
- /**
- * Draw single Graph from AgentStats
- * @param agentStatList List of AgentStats objects
- * @param status Number to determine device and displayed stats (GPU_TEMP: 1, GPU_UTIL: 2, CPU_UTIL: 3)
- * @param name Name of Graph
- */
- drawGraph(agentStatList: JAgentStat[], status: number, name: string) {
- echarts.use([
- TitleComponent,
- ToolboxComponent,
- TooltipComponent,
- GridComponent,
- LegendComponent,
- MarkLineComponent,
- MarkPointComponent,
- LineChart,
- CanvasRenderer,
- UniversalTransition
- ]);
-
- type EChartsOption = echarts.ComposeOption<
- | TitleComponentOption
- | ToolboxComponentOption
- | TooltipComponentOption
- | GridComponentOption
- | MarkLineComponentOption
- >;
-
- let templabel = '';
-
- if (ASC.GPU_TEMP === status) {
- if (this.getTemp2() > 100) {
- templabel = '°F';
- } else {
- templabel = '°C';
- }
- }
- if (ASC.GPU_UTIL === status) {
- templabel = '%';
- }
- if (ASC.CPU_UTIL === status) {
- templabel = '%';
- }
-
- const arr = [];
- const max = [];
- const devlabels = [];
- const result = agentStatList;
-
- for (let i = 0; i < result.length; i++) {
- const val = result[i].value;
- for (let i = 0; i < val.length; i++) {
- const iso = this.transDate(result[i].time);
- arr.push({ time: iso, value: val[i], device: i });
- max.push(result[i].time);
- devlabels.push('Device ' + i + '');
- }
- }
-
- const grouped = [];
- arr.forEach(function (a) {
- grouped[a.device] = grouped[a.device] || [];
- grouped[a.device].push({ time: a.time, value: a.value });
- });
-
- const labels = [...new Set(devlabels)];
-
- const startdate = Math.max(...max);
- const xAxis = this.generateIntervalsOf(1, +startdate - 500, +startdate);
-
- const chartDom = document.getElementById(name);
- const myChart = echarts.init(chartDom);
- let option: EChartsOption;
-
- const seriesData = [];
- for (let i = 0; i < grouped.length; i++) {
- seriesData.push({
- name: 'Device ' + i + '',
- type: 'line',
- data: grouped[i],
- markLine: {
- data: [{ type: 'average', name: 'Avg' }],
- symbol: ['none', 'none']
- }
- });
- }
-
- const self = this;
- option = {
- tooltip: {
- position: 'top'
- },
- legend: {
- data: labels
- },
- toolbox: {
- show: true,
- feature: {
- dataZoom: {
- yAxisIndex: 'none'
- },
- dataView: { readOnly: false },
- restore: {},
- saveAsImage: {
- name: 'Device Temperature'
- }
- }
- },
- useUTC: true,
- xAxis: {
- data: xAxis.map(function (item: any[] | any) {
- return self.transDate(item);
- })
- },
- yAxis: {
- type: 'value',
- axisLabel: {
- formatter: '{value} ' + templabel + ''
- }
- },
- series: seriesData
- };
- option && myChart.setOption(option);
- }
-
- getTemp1() {
- // Temperature Config Setting
- return this.uiService.getUIsettings('agentTempThreshold1').value;
- }
-
- getTemp2() {
- // Temperature 2 Config Setting
- return this.uiService.getUIsettings('agentTempThreshold2').value;
- }
-
- transDate(dt: number) {
- const date = new Date(dt * 1000);
- return (
- date.getUTCDate() +
- '-' +
- this.leading_zeros(date.getUTCMonth() + 1) +
- '-' +
- date.getUTCFullYear() +
- ',' +
- this.leading_zeros(date.getUTCHours()) +
- ':' +
- this.leading_zeros(date.getUTCMinutes()) +
- ':' +
- this.leading_zeros(date.getUTCSeconds())
- );
- }
-
- leading_zeros(dt) {
- return (dt < 10 ? '0' : '') + dt;
- }
-
- generateIntervalsOf(interval: number, start: number, end: number) {
- const result = [];
- let current = start;
-
- while (current < end) {
- result.push(current);
- current += interval;
- }
-
- return result;
- }
}
diff --git a/src/app/core/_pipes/hashrate-pipe.ts b/src/app/core/_pipes/hashrate-pipe.ts
new file mode 100644
index 000000000..bef717ebd
--- /dev/null
+++ b/src/app/core/_pipes/hashrate-pipe.ts
@@ -0,0 +1,35 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+/**
+ * Transform hash rate into human-readable format (e.g. H/s, kH/s, MH/s, GH/s)
+ * @param hashrate - The hash rate number (e.g. 1000000)
+ * @param decimals - Optional number of decimal places (default: 2)
+ * Usage:
+ * value | hashRate
+ * Example:
+ * {{ 1000000 | hashRate }}
+ * @returns 1 MH/s
+ */
+@Pipe({
+ name: 'hashRate',
+ standalone: false
+})
+export class HashRatePipe implements PipeTransform {
+ private readonly units = ['H/s', 'kH/s', 'MH/s', 'GH/s', 'TH/s', 'PH/s'];
+
+ transform(value: number, decimals: number = 2, asObject: boolean = false): string | { value: number; unit: string } {
+ if (!value || value <= 0) {
+ return asObject ? { value: 0, unit: 'H/s' } : '0 H/s';
+ }
+
+ let i = 0;
+ while (value >= 1000 && i < this.units.length - 1) {
+ value /= 1000;
+ i++;
+ }
+
+ const rounded = +value.toFixed(decimals);
+
+ return asObject ? { value: rounded, unit: this.units[i] } : `${rounded} ${this.units[i]}`;
+ }
+}
diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html
index c1659293a..c21f780c3 100644
--- a/src/app/home/home.component.html
+++ b/src/app/home/home.component.html
@@ -105,19 +105,19 @@ Cracks
}"
>
-
+
-
+
+
No permission to view chart data
-
Last updated: {{ lastUpdated }}
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts
index b5b17b167..ccf2049d6 100644
--- a/src/app/home/home.component.ts
+++ b/src/app/home/home.component.ts
@@ -1,11 +1,7 @@
-import { HeatmapChart } from 'echarts/charts';
-import { CalendarComponent, TitleComponent, TooltipComponent, VisualMapComponent } from 'echarts/components';
-import * as echarts from 'echarts/core';
-import { CanvasRenderer } from 'echarts/renderers';
import { Subscription } from 'rxjs';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
-import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
import { TaskType } from '@models/task.model';
@@ -33,7 +29,10 @@ import { formatDate, formatUnixTimestamp, unixTimestampInPast } from '@src/app/s
standalone: false
})
@PageTitle(['Dashboard'])
-export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
+export class HomeComponent implements OnInit, OnDestroy {
+ /**
+ * Utility class for UI settings retrieval and updates.
+ */
util: UISettingsUtilityClass;
/** Flags for responsive design */
@@ -42,8 +41,11 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
screenM = false;
screenL = false;
screenXL = false;
+
+ /** Whether dark mode is enabled */
isDarkMode = false;
- /** Counters for dashboard statistics */
+
+ /** Dashboard statistics counters */
activeAgents = 0;
totalAgents = 0;
totalTasks = 0;
@@ -51,19 +53,35 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
totalCracks = 0;
completedSupertasks = 0;
totalSupertasks = 0;
+
+ /** Timestamp string for last update */
lastUpdated: string;
- /** User permissions */
+ /** User permission flags */
canReadAgents = false;
canReadTasks = false;
canReadCracks = false;
- /** DarkMode */
- protected uiSettings: UISettingsUtilityClass;
+ /**
+ * Heatmap chart data as array of tuples: [date string, count].
+ * Example: [['2025-07-01', 5], ['2025-07-02', 7], ...]
+ */
+ heatmapData: [string, number][] = [];
+
+ private uiSettings: UISettingsUtilityClass;
private subscriptions: Subscription[] = [];
private pageReloadTimeout: NodeJS.Timeout;
- private crackedChart: echarts.ECharts;
+ /**
+ * HomeComponent constructor.
+ * Sets up breakpoint observers for responsive layout and reads initial theme mode.
+ *
+ * @param gs GlobalService for API calls.
+ * @param service LocalStorageService to manage UI config.
+ * @param alertService AlertService to show messages.
+ * @param breakpointObserver BreakpointObserver to detect screen sizes.
+ * @param permissionService PermissionService to check user permissions.
+ */
constructor(
private gs: GlobalService,
private service: LocalStorageService,
@@ -73,94 +91,87 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
) {
this.uiSettings = new UISettingsUtilityClass(this.service);
this.isDarkMode = this.uiSettings.getSetting('theme') === 'dark';
- // Observe screen breakpoints for responsive design
+
+ // Observe screen size breakpoints for responsive behavior
this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small, Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge])
.subscribe((result) => {
const breakpoints = result.breakpoints;
- this.screenXS = false;
- this.screenS = false;
- this.screenM = false;
- this.screenL = false;
- this.screenXL = false;
-
- if (breakpoints[Breakpoints.XSmall]) {
- this.screenXS = true;
- } else if (breakpoints[Breakpoints.Small]) {
- this.screenS = true;
- } else if (breakpoints[Breakpoints.Medium]) {
- this.screenM = true;
- } else if (breakpoints[Breakpoints.Large]) {
- this.screenL = true;
- } else if (breakpoints[Breakpoints.XLarge]) {
- this.screenXL = true;
- }
+ this.screenXS = breakpoints[Breakpoints.XSmall] || false;
+ this.screenS = breakpoints[Breakpoints.Small] || false;
+ this.screenM = breakpoints[Breakpoints.Medium] || false;
+ this.screenL = breakpoints[Breakpoints.Large] || false;
+ this.screenXL = breakpoints[Breakpoints.XLarge] || false;
});
}
/**
- * Get the autorefresh interval setting.
+ * Returns the current refresh interval from UI settings.
*/
get refreshInterval(): number {
return this.util.getSetting('refreshInterval');
}
/**
- * Check if autorefresh of the page is enabled.
+ * Returns whether auto-refresh page reload is enabled in UI settings.
*/
get refreshPage(): boolean {
return this.util.getSetting('refreshPage');
}
+ /**
+ * Angular lifecycle hook: initializes permissions, UI utilities,
+ * loads initial data, and sets up auto-refresh if enabled.
+ */
ngOnInit(): void {
this.canReadAgents = this.permissionService.hasPermissionSync(Perm.Agent.READ);
this.canReadTasks = this.permissionService.hasPermissionSync(Perm.Task.READ);
this.canReadCracks = this.permissionService.hasPermissionSync(Perm.Hash.READ);
this.util = new UISettingsUtilityClass(this.service);
+
this.initData();
this.onAutorefresh();
}
- ngAfterViewInit() {
- if (this.canReadCracks) {
- this.initChart();
- }
- }
-
+ /**
+ * Angular lifecycle hook: unsubscribes all subscriptions and clears any active timeout.
+ */
ngOnDestroy(): void {
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
+ if (this.pageReloadTimeout) {
+ clearTimeout(this.pageReloadTimeout);
+ }
}
/**
- * Automatically refresh the page based on the configured interval.
+ * Initiates a page reload at intervals defined by refreshInterval setting,
+ * if the refreshPage setting is enabled.
*/
- onAutorefresh() {
- const timeout = this.refreshInterval;
- if (this.refreshPage) {
- this.pageReloadTimeout = setTimeout(() => {
- this.initData();
- this.onAutorefresh();
- }, timeout * 1000);
+ onAutorefresh(): void {
+ if (!this.refreshPage) {
+ return;
}
+ const timeoutMs = this.refreshInterval * 1000;
+ this.pageReloadTimeout = setTimeout(() => {
+ this.initData();
+ this.onAutorefresh();
+ }, timeoutMs);
}
/**
- * Toggle the autoreload setting and refresh the data.
+ * Enables or disables the auto-reload functionality and displays
+ * a corresponding success message.
*
- * @param flag - New autoreload flag.
+ * @param flag True to enable autoreload, false to pause it.
*/
- setAutoreload(flag: boolean) {
+ setAutoreload(flag: boolean): void {
const updatedSettings = this.util.updateSettings({ refreshPage: flag });
- let message = '';
-
if (updatedSettings) {
- if (flag) {
- message = 'Autoreload is enabled';
- } else {
- message = 'Autoreload is paused';
+ const message = flag ? 'Autoreload is enabled' : 'Autoreload is paused';
+ if (!flag && this.pageReloadTimeout) {
clearTimeout(this.pageReloadTimeout);
}
this.alertService.showSuccessMessage(message);
@@ -170,153 +181,44 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
}
/**
- * Initialize dashboard data conditionally on user's permission.
+ * Fetches all dashboard data depending on user permissions.
+ * Retrieves agents, tasks, supertasks, cracks, and heatmap data.
*/
initData(): void {
if (this.canReadAgents) {
this.getAgents();
}
-
if (this.canReadTasks) {
this.getTasks();
this.getSuperTasks();
}
-
if (this.canReadCracks) {
this.getCracks();
}
}
/**
- * Count the occurrences of items in an array.
+ * Counts occurrences of each string in the given array.
*
- * @param arr - The array to count occurrences in.
- * @returns An object with the occurrences of each item.
+ * @param arr Array of strings (e.g., dates)
+ * @returns Object with keys as strings and values as their counts
*/
countOccurrences(arr: string[]): { [key: string]: number } {
- return arr.reduce((counts, date) => {
- counts[date] = (counts[date] || 0) + 1;
+ return arr.reduce((counts, item) => {
+ counts[item] = (counts[item] || 0) + 1;
return counts;
}, {});
}
/**
- * Initialize the heatmap chart.
- */
- initChart(): void {
- echarts.use([
- TitleComponent,
- CalendarComponent,
- TooltipComponent,
- VisualMapComponent,
- HeatmapChart,
- CanvasRenderer
- ]);
- const isDarkTheme = this.isDarkMode ? 'dark' : '';
- const chartDom = document.getElementById('pcard');
- this.crackedChart = echarts.init(chartDom, isDarkTheme);
- }
-
- /**
- * Update the heatmap chart.
- */
- updateChart(): void {
- const params = new RequestParamBuilder()
- .addFilter({
- field: 'isCracked',
- operator: FilterType.EQUAL,
- value: 1
- })
- .create();
- this.subscriptions.push(
- this.gs.getAll(SERV.HASHES, params).subscribe((response: ResponseWrapper) => {
- const hashes = new JsonAPISerializer().deserialize({
- data: response.data,
- included: response.included
- });
- const currentDate = new Date();
- const currentYear = currentDate.getFullYear();
-
- // Extract and format cracked dates
- const formattedDates: string[] = hashes.map((hash) => formatUnixTimestamp(hash.timeCracked, 'yyyy-MM-dd'));
-
- // Count occurrences of each date
- const dateCounts: { [key: string]: number } = this.countOccurrences(formattedDates);
-
- // Convert date counts to the required format
- const countsExtended = Object.keys(dateCounts).map((date) => [date, dateCounts[date]]);
-
- // DarkMode
- const backgroundColor = this.isDarkMode ? '#212121' : '';
-
- const option = {
- darkMode: true,
- title: {},
- tooltip: {
- position: 'top',
- formatter: function (p) {
- const format = echarts.time.format(p.data[0], '{dd}-{MM}-{yyyy}', false);
- return format + ': ' + p.data[1];
- }
- },
- backgroundColor: backgroundColor,
- visualMap: {
- min: 0,
- max: Math.ceil(hashes.length / 10) * 10,
- type: 'piecewise',
- orient: 'horizontal',
- left: 'center',
- top: 65
- },
- calendar: {
- top: 120,
- left: 30,
- right: 30,
- cellSize: ['auto', 13],
- range: currentYear,
- splitLine: this.isDarkMode
- ? {
- lineStyle: {
- color: '#FFFFFF'
- }
- }
- : {},
- itemStyle: {
- borderWidth: 0.5
- },
- yearLabel: { show: false }
- },
- series: {
- type: 'heatmap',
- coordinateSystem: 'calendar',
- data: countsExtended,
- label: {
- show: true,
- formatter: function (params) {
- return currentDate.getDate() === params.data[0] ? 'X' : '';
- }
- }
- }
- };
-
- this.crackedChart.setOption(option);
- this.lastUpdated = formatDate(new Date(), this.util.getSetting('timefmt'));
- })
- );
- }
-
- /**
- * Get the list of active agents.
+ * Fetches the number of active and total agents.
*/
private getAgents(): void {
const params = new RequestParamBuilder()
- .addFilter({
- field: 'isActive',
- operator: FilterType.EQUAL,
- value: true
- })
+ .addFilter({ field: 'isActive', operator: FilterType.EQUAL, value: true })
.addIncludeTotal(true)
.create();
+
this.subscriptions.push(
this.gs.getAll(SERV.AGENTS_COUNT, params).subscribe((response: ResponseWrapper) => {
this.totalAgents = response.meta.total_count;
@@ -326,18 +228,15 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
}
/**
- * Get the list of tasks.
+ * Fetches total and completed tasks count.
*/
private getTasks(): void {
const paramsTotalTasks = new RequestParamBuilder()
.addInclude('tasks')
- .addFilter({
- field: 'taskType',
- operator: FilterType.EQUAL,
- value: 0
- })
+ .addFilter({ field: 'taskType', operator: FilterType.EQUAL, value: 0 })
.addFilter({ field: 'isArchived', operator: FilterType.EQUAL, value: 0 })
.create();
+
this.subscriptions.push(
this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsTotalTasks).subscribe((response: ResponseWrapper) => {
this.totalTasks = response.meta.count;
@@ -346,13 +245,10 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
const paramsCompletedTasks = new RequestParamBuilder()
.addInclude('tasks')
- .addFilter({
- field: 'taskType',
- operator: FilterType.EQUAL,
- value: 0
- })
+ .addFilter({ field: 'taskType', operator: FilterType.EQUAL, value: 0 })
.addFilter({ field: 'keyspace', operator: FilterType.GREATER, value: 0 })
.create();
+
this.subscriptions.push(
this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsCompletedTasks).subscribe((response: ResponseWrapper) => {
this.completedTasks = response.meta.count;
@@ -361,20 +257,20 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
}
/**
- * Get the list of supertasks.
+ * Fetches total and completed supertasks count.
*/
private getSuperTasks(): void {
- const paramsTotalTasks = new RequestParamBuilder()
+ const paramsTotalSupertasks = new RequestParamBuilder()
.addFilter({ field: 'taskType', operator: FilterType.EQUAL, value: TaskType.SUPERTASK })
.create();
this.subscriptions.push(
- this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsTotalTasks).subscribe((response: ResponseWrapper) => {
+ this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsTotalSupertasks).subscribe((response: ResponseWrapper) => {
this.totalSupertasks = response.meta.count;
})
);
- const paramsCompletedTasks = new RequestParamBuilder()
+ const paramsCompletedSupertasks = new RequestParamBuilder()
.addFilter({ field: 'keyspace', operator: FilterType.EQUAL, value: 'keyspaceProgress' })
.addFilter({ field: 'keyspace', operator: FilterType.GREATER, value: 0 })
.addFilter({ field: 'taskType', operator: FilterType.EQUAL, value: TaskType.SUPERTASK })
@@ -382,29 +278,51 @@ export class HomeComponent implements OnInit, OnDestroy, AfterViewInit {
.create();
this.subscriptions.push(
- this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsCompletedTasks).subscribe((response: ResponseWrapper) => {
+ this.gs.getAll(SERV.TASKS_WRAPPER_COUNT, paramsCompletedSupertasks).subscribe((response: ResponseWrapper) => {
this.completedSupertasks = response.meta.count;
})
);
}
/**
- * Get the list of cracked hashes from the last seven days.
+ * Fetches cracks count for the past 7 days and updates the heatmap data.
*/
private getCracks(): void {
const timestampInPast = unixTimestampInPast(7);
const params = new RequestParamBuilder()
- .addFilter({
- field: 'timeCracked',
- operator: FilterType.GREATER,
- value: timestampInPast
- })
+ .addFilter({ field: 'timeCracked', operator: FilterType.GREATER, value: timestampInPast })
.create();
this.subscriptions.push(
this.gs.getAll(SERV.HASHES_COUNT, params).subscribe((response: ResponseWrapper) => {
this.totalCracks = response.meta.count;
- this.updateChart();
+ this.updateHeatmapData();
+ })
+ );
+ }
+
+ /**
+ * Loads cracked hashes and prepares heatmap data as date/count pairs.
+ * Updates the lastUpdated timestamp.
+ */
+ private updateHeatmapData(): void {
+ const params = new RequestParamBuilder()
+ .addFilter({ field: 'isCracked', operator: FilterType.EQUAL, value: 1 })
+ .create();
+
+ this.subscriptions.push(
+ this.gs.getAll(SERV.HASHES, params).subscribe((response: ResponseWrapper) => {
+ const hashes = new JsonAPISerializer().deserialize({
+ data: response.data,
+ included: response.included
+ });
+
+ const formattedDates: string[] = hashes.map((hash) => formatUnixTimestamp(hash.timeCracked, 'yyyy-MM-dd'));
+
+ const dateCounts = this.countOccurrences(formattedDates);
+ this.heatmapData = Object.entries(dateCounts).map(([date, count]) => [date, count]);
+
+ this.lastUpdated = formatDate(new Date(), this.util.getSetting('timefmt'));
})
);
}
diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts
index aba246bd7..a7e531ad6 100644
--- a/src/app/home/home.module.ts
+++ b/src/app/home/home.module.ts
@@ -17,6 +17,7 @@ import { ComponentsModule } from '@src/app/shared/components.module';
import { HomeComponent } from '@src/app/home/home.component';
import { HomeRoutingModule } from '@src/app/home/home-routing.module';
import { PipesModule } from '@src/app/shared/pipes.module';
+import { HeatmapChartComponent } from '@src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component';
@NgModule({
declarations: [HomeComponent],
@@ -35,7 +36,8 @@ import { PipesModule } from '@src/app/shared/pipes.module';
MatCardModule,
MatButtonModule,
MatTooltipModule,
- NgbModule
+ NgbModule,
+ HeatmapChartComponent
]
})
export class HomeModule {}
diff --git a/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.spec.ts b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.spec.ts
new file mode 100644
index 000000000..21cfd68ea
--- /dev/null
+++ b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AgentStatGraphComponent } from './agent-stat-graph.component';
+
+describe('AgentStatGraphComponent', () => {
+ let component: AgentStatGraphComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AgentStatGraphComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AgentStatGraphComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
new file mode 100644
index 000000000..d90e44067
--- /dev/null
+++ b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
@@ -0,0 +1,192 @@
+import { EChartsCoreOption } from 'echarts/core';
+import { type EChartsType, init } from 'echarts/core';
+
+import { Component, ElementRef, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild } from '@angular/core';
+
+import { JAgentStat } from '@models/agent-stats.model';
+
+import { ASC } from '@src/app/core/_constants/agentsc.config';
+
+/**
+ * Component that renders agent statistics in an ECharts line graph.
+ * Displays one line per device for a specific stat type (e.g. GPU temperature, utilization).
+ */
+@Component({
+ selector: 'app-agent-stat-graph',
+ template: ``
+})
+export class AgentStatGraphComponent implements OnChanges, OnDestroy {
+ /**
+ * List of agent statistics to be rendered in the graph.
+ * These are filtered by `statType` before rendering.
+ */
+ @Input() agentStats: JAgentStat[] = [];
+
+ /**
+ * The statistic type to render. Should match one of the values in the ASC enum
+ * (e.g. ASC.GPU_TEMP, ASC.GPU_UTIL, ASC.CPU_UTIL).
+ */
+ @Input() statType: number;
+
+ /**
+ * Reference to the DOM element where the chart will be rendered.
+ */
+ @ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
+
+ /**
+ * Holds the ECharts instance for rendering and updating the graph.
+ */
+ private chartInstance: EChartsType | null = null;
+
+ /**
+ * Lifecycle hook called when any data-bound property changes.
+ * Re-renders the chart when `agentStats` or `statType` change.
+ *
+ * @param changes The changed input properties.
+ */
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['agentStats'] || changes['statType']) {
+ this.renderChart();
+ }
+ }
+
+ /**
+ * Renders the chart using the current agentStats and statType.
+ * Initializes the chart instance if not already created.
+ */
+ private renderChart() {
+ if (!this.agentStats || !this.chartContainer) return;
+
+ if (!this.chartInstance) {
+ this.chartInstance = init(this.chartContainer.nativeElement);
+ }
+
+ const option = this.buildOption(this.agentStats, this.statType);
+ this.chartInstance.setOption(option);
+ }
+
+ /**
+ * Builds the ECharts configuration object for the given statistics and type.
+ * Each device is rendered as a separate line with a legend and average mark line.
+ *
+ * @param agentStats - Full list of agent stats.
+ * @param statType - Type of statistic to filter and render (e.g. GPU_TEMP).
+ * @returns ECharts option object used to configure the chart.
+ */
+ private buildOption(agentStats: JAgentStat[], statType: number): EChartsCoreOption {
+ const filteredStats = agentStats.filter((s) => s.statType === statType);
+
+ let titleText = '';
+ let yAxisLabel = '';
+
+ switch (statType) {
+ case ASC.GPU_TEMP:
+ titleText = 'GPU Temperature';
+ yAxisLabel = '°C';
+ break;
+ case ASC.GPU_UTIL:
+ titleText = 'GPU Utilization';
+ yAxisLabel = '%';
+ break;
+ case ASC.CPU_UTIL:
+ titleText = 'CPU Utilization';
+ yAxisLabel = '%';
+ break;
+ }
+
+ const flattened = [];
+ const timestamps: number[] = [];
+
+ for (const stat of filteredStats) {
+ timestamps.push(stat.time);
+ for (let i = 0; i < stat.value.length; i++) {
+ flattened.push({
+ time: new Date(stat.time * 1000).toISOString(),
+ value: stat.value[i],
+ device: i
+ });
+ }
+ }
+
+ const minTime = Math.min(...timestamps);
+ const maxTime = Math.max(...timestamps);
+
+ const deviceCount = Math.max(...flattened.map((f) => f.device)) + 1;
+
+ const seriesData = [];
+ const legends: string[] = [];
+
+ for (let device = 0; device < deviceCount; device++) {
+ const deviceData = flattened
+ .filter((f) => f.device === device)
+ .map((f) => ({
+ name: `Device ${device}`,
+ value: [f.time, f.value]
+ }));
+
+ seriesData.push({
+ name: `Device ${device}`,
+ type: 'line',
+ data: deviceData,
+ markLine: {
+ data: [{ type: 'average', name: 'Avg' }],
+ symbol: ['none', 'none']
+ }
+ });
+
+ legends.push(`Device ${device}`);
+ }
+
+ return {
+ title: {
+ text: titleText
+ },
+ tooltip: {
+ trigger: 'axis'
+ },
+ legend: {
+ data: legends
+ },
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: 'none'
+ },
+ dataView: { readOnly: true },
+ restore: {},
+ saveAsImage: { name: titleText }
+ }
+ },
+ xAxis: {
+ type: 'time',
+ min: minTime * 1000,
+ max: maxTime * 1000,
+ axisLabel: {
+ formatter: (value: number) =>
+ new Date(value).toLocaleString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ })
+ }
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ formatter: `{value} ${yAxisLabel}`
+ }
+ },
+ series: seriesData
+ };
+ }
+
+ /**
+ * Lifecycle hook called when the component is destroyed.
+ * Disposes the chart instance to prevent memory leaks.
+ */
+ ngOnDestroy() {
+ if (this.chartInstance) {
+ this.chartInstance.dispose();
+ }
+ }
+}
diff --git a/src/app/shared/graphs/echarts/echarts.component.html b/src/app/shared/graphs/echarts/echarts.component.html
deleted file mode 100644
index 8b1378917..000000000
--- a/src/app/shared/graphs/echarts/echarts.component.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/app/shared/graphs/echarts/echarts.component.ts b/src/app/shared/graphs/echarts/echarts.component.ts
deleted file mode 100644
index 6d0379186..000000000
--- a/src/app/shared/graphs/echarts/echarts.component.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {
- Component,
- AfterViewInit,
- DoCheck,
- ElementRef,
- EventEmitter,
- Input,
- NgZone,
- OnChanges,
- OnDestroy,
- OnInit,
- Output,
- SimpleChanges
-} from '@angular/core'
-import {EChartsOption, ECharts} from 'echarts'
-import {fromEvent, Observable, Subscription} from 'rxjs'
-import {debounceTime, switchMap} from 'rxjs/operators'
-import {init} from 'echarts/lib/echarts'
-
-@Component({
- selector: 'app-echarts',
- templateUrl: './echarts.component.html'
-})
-export class EchartsComponent {
-
- @Input() options: EChartsOption
-
-
-}
diff --git a/src/app/shared/graphs/echarts/echarts.config.ts b/src/app/shared/graphs/echarts/echarts.config.ts
new file mode 100644
index 000000000..b705a1d29
--- /dev/null
+++ b/src/app/shared/graphs/echarts/echarts.config.ts
@@ -0,0 +1,34 @@
+// echarts-config.ts
+import { HeatmapChart, LineChart } from 'echarts/charts';
+import {
+ CalendarComponent,
+ GridComponent,
+ LegendComponent,
+ MarkLineComponent,
+ MarkPointComponent,
+ TitleComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ VisualMapComponent
+} from 'echarts/components';
+import { use } from 'echarts/core';
+import { UniversalTransition } from 'echarts/features';
+import { CanvasRenderer } from 'echarts/renderers';
+
+export function registerEChartsModules() {
+ use([
+ TitleComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ GridComponent,
+ LegendComponent,
+ MarkLineComponent,
+ MarkPointComponent,
+ VisualMapComponent,
+ CalendarComponent,
+ LineChart,
+ HeatmapChart,
+ CanvasRenderer,
+ UniversalTransition
+ ]);
+}
diff --git a/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.spec.ts b/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.spec.ts
new file mode 100644
index 000000000..f65224c33
--- /dev/null
+++ b/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HeatmapChartComponent } from './heatmap-chart.component';
+
+describe('HeatmapChartComponent', () => {
+ let component: HeatmapChartComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [HeatmapChartComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HeatmapChartComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.ts b/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.ts
new file mode 100644
index 000000000..a5c3886ff
--- /dev/null
+++ b/src/app/shared/graphs/echarts/heatmap-chart/heatmap-chart.component.ts
@@ -0,0 +1,110 @@
+// heatmap-chart.component.ts
+
+import { EChartsType, init, time } from 'echarts/core';
+
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ Input,
+ OnChanges,
+ OnDestroy,
+ SimpleChanges,
+ ViewChild
+} from '@angular/core';
+
+@Component({
+ selector: 'app-heatmap-chart',
+ template: ``
+})
+export class HeatmapChartComponent implements AfterViewInit, OnChanges, OnDestroy {
+ @Input() data: [string, number][] = [];
+ @Input() isDarkMode = false;
+
+ @ViewChild('chartContainer', { static: false }) chartContainer!: ElementRef;
+
+ private chart: EChartsType | undefined;
+
+ ngAfterViewInit() {
+ if (this.chartContainer?.nativeElement) {
+ this.initChart();
+ if (this.data.length) {
+ this.setOption();
+ }
+ } else {
+ console.warn('Chart container not available in ngAfterViewInit');
+ }
+ }
+
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (this.chart && (changes['data'] || changes['isDarkMode'])) {
+ this.setOption();
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.chart) {
+ this.chart.dispose();
+ }
+ }
+
+ private initChart() {
+ if (!this.chartContainer || !this.chartContainer.nativeElement) {
+ console.error('Chart container DOM element is missing');
+ return;
+ }
+ this.chart = init(this.chartContainer.nativeElement, this.isDarkMode ? 'dark' : undefined);
+ }
+
+ private setOption() {
+ if (!this.chart) return;
+
+ const currentYear = new Date().getFullYear();
+
+ const option = {
+ darkMode: this.isDarkMode,
+ tooltip: {
+ position: 'top',
+ formatter: (p: any) => {
+ const formattedDate = time.format(p.data[0], '{dd}-{MM}-{yyyy}', false);
+ return `${formattedDate}: ${p.data[1]}`;
+ }
+ },
+ visualMap: {
+ min: 0,
+ max: Math.max(...this.data.map((d) => d[1]), 10),
+ type: 'piecewise',
+ orient: 'horizontal',
+ left: 'center',
+ top: 65
+ },
+ calendar: {
+ top: 120,
+ left: 30,
+ right: 30,
+ cellSize: ['auto', 13],
+ range: currentYear,
+ splitLine: this.isDarkMode ? { lineStyle: { color: '#FFFFFF' } } : {},
+ itemStyle: { borderWidth: 0.5 },
+ yearLabel: { show: false }
+ },
+ series: [
+ {
+ type: 'heatmap',
+ coordinateSystem: 'calendar',
+ data: this.data,
+ label: {
+ show: true,
+ formatter: (params: any) => {
+ const todayStr = new Date().toISOString().slice(0, 10);
+ return params.data[0] === todayStr ? 'X' : '';
+ }
+ }
+ }
+ ]
+ };
+
+ this.chart.setOption(option);
+ }
+}
diff --git a/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.spec.ts b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.spec.ts
new file mode 100644
index 000000000..01b4df162
--- /dev/null
+++ b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TaskSpeedGraphComponent } from '@src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component';
+
+describe('TaskSpeedGraphComponent', () => {
+ let component: TaskSpeedGraphComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TaskSpeedGraphComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TaskSpeedGraphComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
new file mode 100644
index 000000000..d51bf09cb
--- /dev/null
+++ b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
@@ -0,0 +1,242 @@
+import { LineChart, LineSeriesOption } from 'echarts/charts';
+import {
+ DataZoomComponent,
+ DataZoomComponentOption,
+ GridComponent,
+ GridComponentOption,
+ MarkLineComponent,
+ MarkLineComponentOption,
+ TitleComponent,
+ TitleComponentOption,
+ ToolboxComponent,
+ ToolboxComponentOption,
+ TooltipComponent,
+ TooltipComponentOption
+} from 'echarts/components';
+import { ComposeOption, EChartsType, init } from 'echarts/core';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+
+import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
+
+import { HashRatePipe } from '@src/app/core/_pipes/hashrate-pipe';
+
+type EChartsOption = ComposeOption<
+ | TitleComponentOption
+ | ToolboxComponentOption
+ | TooltipComponentOption
+ | GridComponentOption
+ | DataZoomComponentOption
+ | MarkLineComponentOption
+ | LineSeriesOption
+>;
+
+use([
+ LineChart,
+ CanvasRenderer,
+ TitleComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ GridComponent,
+ DataZoomComponent,
+ MarkLineComponent
+]);
+
+@Component({
+ selector: 'app-task-speed-graph',
+ template: ``,
+ providers: [HashRatePipe]
+})
+export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
+ @Input() speeds: any[] = [];
+
+ @ViewChild('chart', { static: true }) chartRef!: ElementRef;
+
+ private chart: EChartsType;
+
+ constructor(private hashratePipe: HashRatePipe) {}
+
+ ngAfterViewInit() {
+ if (this.speeds?.length) {
+ this.drawChart();
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['speeds'] && !changes['speeds'].firstChange) {
+ this.drawChart();
+ }
+ }
+
+ private drawChart() {
+ if (!this.chart) {
+ this.chart = init(this.chartRef.nativeElement);
+ }
+
+ const data = this.speeds;
+ const arr = [];
+ const max = [];
+ const result = [];
+
+ data.reduce((res, value) => {
+ if (!res[value.time]) {
+ res[value.time] = { time: value.time, speed: 0 };
+ result.push(res[value.time]);
+ }
+ res[value.time].speed += value.speed;
+ return res;
+ }, {});
+
+ for (let i = 0; i < result.length; i++) {
+ const iso = this.transDate(result[i]['time']);
+ const { value: speed, unit } = this.hashratePipe.transform(result[i]['speed'], 2, true) as {
+ value: number;
+ unit: string;
+ };
+
+ arr.push({
+ name: iso,
+ value: [iso, speed],
+ unit: unit
+ });
+
+ max.push(result[i]['time']);
+ }
+
+ const displayUnit = arr.length ? arr[arr.length - 1].unit : 'H/s';
+ const startdate = max[0];
+ const enddate = max[max.length - 1];
+ const datelabel = this.transDate(enddate);
+ const speedsOnly = arr.map((item) => item.value[1]);
+ const maxSpeed = Math.max(...speedsOnly);
+ const minSpeed = Math.min(...speedsOnly);
+ const maxIndex = speedsOnly.indexOf(maxSpeed);
+ const minIndex = speedsOnly.indexOf(minSpeed);
+
+ const xAxis = this.generateIntervalsOf(1, +startdate, +enddate);
+ const xAxisData = xAxis.map((ts) => this.transDate(ts));
+
+ const option: EChartsOption = {
+ title: {
+ subtext: 'Last record: ' + datelabel
+ },
+ tooltip: {
+ position: 'top',
+ formatter: (params) => {
+ if (params.componentType === 'markPoint') {
+ return `${params.name}: ${params.data.value}`;
+ }
+
+ const data = params.data;
+ if (data && Array.isArray(data.value)) {
+ return `${params.name}: ${data.value[1]} ${data.unit ?? ''}`;
+ }
+
+ return '';
+ }
+ },
+ grid: {
+ left: '5%',
+ right: '4%'
+ },
+ xAxis: {
+ type: 'category',
+ data: xAxisData
+ },
+ yAxis: {
+ type: 'value',
+ name: displayUnit,
+ position: 'left',
+ alignTicks: true
+ },
+ useUTC: true,
+ toolbox: {
+ show: true,
+ right: 20,
+ top: 10,
+ itemGap: 10,
+ feature: {
+ dataZoom: {
+ yAxisIndex: 'none', // disables zoom on y-axis
+ title: {
+ zoom: 'Zoom in',
+ back: 'Zoom reset'
+ }
+ },
+ restore: {},
+ saveAsImage: { name: 'Task Speed' }
+ }
+ },
+ dataZoom: [
+ {
+ type: 'slider',
+ xAxisIndex: 0,
+ start: 0,
+ end: 100
+ },
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ start: 0,
+ end: 100
+ }
+ ],
+ series: {
+ name: '',
+ type: 'line',
+ data: arr,
+ connectNulls: true,
+ markPoint: {
+ data: [
+ {
+ name: 'Max',
+ coord: arr[maxIndex].value,
+ value: `${arr[maxIndex].value[1]} ${arr[maxIndex].unit}`
+ },
+ {
+ name: 'Min',
+ coord: arr[minIndex].value,
+ value: `${arr[minIndex].value[1]} ${arr[minIndex].unit}`
+ }
+ ]
+ },
+ markLine: {
+ lineStyle: { color: '#333' }
+ }
+ }
+ };
+
+ this.chart.setOption(option);
+ }
+
+ private leadingZeros(dt: number): string {
+ return dt < 10 ? '0' + dt : '' + dt;
+ }
+
+ private transDate(dt: number): string {
+ const date = new Date(dt * 1000);
+ return (
+ date.getUTCDate() +
+ '-' +
+ this.leadingZeros(date.getUTCMonth() + 1) +
+ '-' +
+ date.getUTCFullYear() +
+ ',' +
+ this.leadingZeros(date.getUTCHours()) +
+ ':' +
+ this.leadingZeros(date.getUTCMinutes()) +
+ ':' +
+ this.leadingZeros(date.getUTCSeconds())
+ );
+ }
+
+ private generateIntervalsOf(interval: number, start: number, end: number): number[] {
+ const result = [];
+ let current = start;
+ while (current < end) {
+ result.push(current);
+ current += interval;
+ }
+ return result;
+ }
+}
diff --git a/src/app/shared/pipes.module.ts b/src/app/shared/pipes.module.ts
index ef55e0ac4..94ec15176 100644
--- a/src/app/shared/pipes.module.ts
+++ b/src/app/shared/pipes.module.ts
@@ -1,30 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { HealthCheckStatusPipe } from '../core/_pipes/healthcheck-status.pipe';
-import { TaskDispatchedPipe } from '../core/_pipes/task-dispatched.pipe';
-import { TaskTimeSpentPipe } from '../core/_pipes/task-timespent.pipe';
-import { ReplaceStringPipe } from '../core/_pipes/replace-string.pipe';
-import { ShortenStringPipe } from '../core/_pipes/shorten-string.pipe';
-import { SecondsToTimePipe } from '../core/_pipes/secondsto-time.pipe';
-import { AgentSColorPipe } from '../core/_pipes/agentstat-color.pipe';
-import { WarningColorPipe } from '../core/_pipes/warning-color.pipe';
-import { TaskSearchedPipe } from '../core/_pipes/task-searched.pipe';
-import { HashesFilterPipe } from '../core/_pipes/hashes-filter.pipe';
-import { KeyspaceCalcPipe } from '../core/_pipes/keyspace-calc.pipe';
-import { StaticArrayPipe } from '../core/_pipes/static-array.pipe';
-import { MaximizePipe } from '../core/_pipes/maximize-object.pipe';
-import { TaskCrackedPipe } from '../core/_pipes/task-cracked.pipe';
-import { ArraySortPipe } from '../core/_pipes/orderby-item.pipe';
-import { AveragePipe } from '../core/_pipes/average-object.pipe';
-import { FilterItemPipe } from '../core/_pipes/filter-item.pipe';
-import { SearchPipe } from '../core/_pipes/filter-search.pipe';
-import { FileSizePipe } from '../core/_pipes/file-size.pipe';
-import { FileTypePipe } from '../core/_pipes/file-type.pipe';
-import { GroupByPipe } from '../core/_pipes/groupby.pipe';
-import { SumPipe } from '../core/_pipes/sum-object.pipe';
-import { SplitPipe } from '../core/_pipes/split.pipe';
-import { uiDatePipe } from '../core/_pipes/date.pipe';
+import { AgentSColorPipe } from '@src/app/core/_pipes/agentstat-color.pipe';
+import { AveragePipe } from '@src/app/core/_pipes/average-object.pipe';
+import { uiDatePipe } from '@src/app/core/_pipes/date.pipe';
+import { FileSizePipe } from '@src/app/core/_pipes/file-size.pipe';
+import { FileTypePipe } from '@src/app/core/_pipes/file-type.pipe';
+import { FilterItemPipe } from '@src/app/core/_pipes/filter-item.pipe';
+import { SearchPipe } from '@src/app/core/_pipes/filter-search.pipe';
+import { GroupByPipe } from '@src/app/core/_pipes/groupby.pipe';
+import { HashesFilterPipe } from '@src/app/core/_pipes/hashes-filter.pipe';
+import { HashRatePipe } from '@src/app/core/_pipes/hashrate-pipe';
+import { HealthCheckStatusPipe } from '@src/app/core/_pipes/healthcheck-status.pipe';
+import { KeyspaceCalcPipe } from '@src/app/core/_pipes/keyspace-calc.pipe';
+import { MaximizePipe } from '@src/app/core/_pipes/maximize-object.pipe';
+import { ArraySortPipe } from '@src/app/core/_pipes/orderby-item.pipe';
+import { ReplaceStringPipe } from '@src/app/core/_pipes/replace-string.pipe';
+import { SecondsToTimePipe } from '@src/app/core/_pipes/secondsto-time.pipe';
+import { ShortenStringPipe } from '@src/app/core/_pipes/shorten-string.pipe';
+import { SplitPipe } from '@src/app/core/_pipes/split.pipe';
+import { StaticArrayPipe } from '@src/app/core/_pipes/static-array.pipe';
+import { SumPipe } from '@src/app/core/_pipes/sum-object.pipe';
+import { TaskCrackedPipe } from '@src/app/core/_pipes/task-cracked.pipe';
+import { TaskDispatchedPipe } from '@src/app/core/_pipes/task-dispatched.pipe';
+import { TaskSearchedPipe } from '@src/app/core/_pipes/task-searched.pipe';
+import { TaskTimeSpentPipe } from '@src/app/core/_pipes/task-timespent.pipe';
+import { WarningColorPipe } from '@src/app/core/_pipes/warning-color.pipe';
@NgModule({
declarations: [
@@ -45,6 +46,7 @@ import { uiDatePipe } from '../core/_pipes/date.pipe';
ArraySortPipe,
FileSizePipe,
FileTypePipe,
+ HashRatePipe,
MaximizePipe,
AveragePipe,
GroupByPipe,
@@ -71,6 +73,7 @@ import { uiDatePipe } from '../core/_pipes/date.pipe';
FilterItemPipe,
ArraySortPipe,
FileSizePipe,
+ HashRatePipe,
FileTypePipe,
MaximizePipe,
AveragePipe,
diff --git a/src/app/tasks/edit-tasks/edit-tasks.component.html b/src/app/tasks/edit-tasks/edit-tasks.component.html
index 242ab4a85..6e803471b 100644
--- a/src/app/tasks/edit-tasks/edit-tasks.component.html
+++ b/src/app/tasks/edit-tasks/edit-tasks.component.html
@@ -157,8 +157,9 @@
-
+
+
diff --git a/src/app/tasks/edit-tasks/edit-tasks.component.ts b/src/app/tasks/edit-tasks/edit-tasks.component.ts
index 446e48113..2fb023d5a 100644
--- a/src/app/tasks/edit-tasks/edit-tasks.component.ts
+++ b/src/app/tasks/edit-tasks/edit-tasks.component.ts
@@ -1,23 +1,3 @@
-import { LineChart, LineSeriesOption } from 'echarts/charts';
-import {
- DataZoomComponent,
- DataZoomComponentOption,
- GridComponent,
- GridComponentOption,
- MarkLineComponent,
- MarkLineComponentOption,
- TitleComponent,
- TitleComponentOption,
- ToolboxComponent,
- ToolboxComponentOption,
- TooltipComponent,
- TooltipComponentOption,
- VisualMapComponent,
- VisualMapComponentOption
-} from 'echarts/components';
-import * as echarts from 'echarts/core';
-import { UniversalTransition } from 'echarts/features';
-import { CanvasRenderer } from 'echarts/renderers';
import { Subscription, finalize } from 'rxjs';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
@@ -199,8 +179,6 @@ export class EditTasksComponent implements OnInit, OnDestroy {
this.color = task.color;
this.crackerinfo = task.crackerBinary;
this.taskWrapperId = task.taskWrapperId;
- // Graph Speed
- this.initTaskSpeed(task.speeds);
// Assigned Agents init
this.assingAgentInit();
// Hashlist Description and Type
@@ -373,11 +351,6 @@ export class EditTasksComponent implements OnInit, OnDestroy {
});
}
- toggleIsAll() {
- this.chunkview = this.chunkview === 1 ? 0 : 1;
- this.chunkTable.reload();
- }
-
onChunkViewChange(event: MatButtonToggleChange): void {
this.chunkview = event.value;
}
@@ -399,188 +372,4 @@ export class EditTasksComponent implements OnInit, OnDestroy {
}
});
}
-
- /**
- * Task Speed Grap
- *
- **/
- initTaskSpeed(obj: object) {
- echarts.use([
- TitleComponent,
- ToolboxComponent,
- TooltipComponent,
- GridComponent,
- VisualMapComponent,
- DataZoomComponent,
- MarkLineComponent,
- LineChart,
- CanvasRenderer,
- UniversalTransition
- ]);
-
- type EChartsOption = echarts.ComposeOption<
- | TitleComponentOption
- | ToolboxComponentOption
- | TooltipComponentOption
- | GridComponentOption
- | VisualMapComponentOption
- | DataZoomComponentOption
- | MarkLineComponentOption
- | LineSeriesOption
- >;
-
- const data: any = obj;
- const arr = [];
- const max = [];
- const result = [];
-
- data.reduce(function (res, value) {
- if (!res[value.time]) {
- res[value.time] = { time: value.time, speed: 0 };
- result.push(res[value.time]);
- }
- res[value.time].speed += value.speed;
- return res;
- }, {});
-
- for (let i = 0; i < result.length; i++) {
- const iso = this.transDate(result[i]['time']);
-
- arr.push([
- iso,
- this.fs.transform(result[i]['speed'], false, 1000).match(/\d+(\.\d+)?/)[0],
- this.fs.transform(result[i]['speed'], false, 1000).slice(-2)
- ]);
- max.push(result[i]['time']);
- }
-
- const startdate = max.slice(0)[0];
- const enddate = max.slice(-1)[0];
- const datelabel = this.transDate(enddate);
- const xAxis = this.generateIntervalsOf(1, +startdate, +enddate);
-
- const newDiv = document.createElement('div');
- newDiv.id = 'tspeed';
- newDiv.style.height = '310px';
-
- // Replace the old chart container with the new one,
- // otherwise the same chart container is always used when the URL is changed and never recreated
- const chartDomDelete = document.getElementById('tspeed');
- chartDomDelete.replaceWith(newDiv);
- const chartDom = document.getElementById('tspeed');
- const myChart = echarts.init(chartDom);
-
- const self = this;
-
- const option: EChartsOption = {
- title: {
- subtext: 'Last record: ' + datelabel
- },
- tooltip: {
- position: 'top',
- formatter: function (p) {
- return p.data[0] + ': ' + p.data[1] + ' ' + p.data[2] + ' H/s';
- }
- },
- grid: {
- left: '5%',
- right: '4%'
- },
- xAxis: {
- data: xAxis.map(function (item: any[] | any) {
- return self.transDate(item);
- })
- },
- yAxis: {
- type: 'value',
- name: 'H/s',
- position: 'left',
- alignTicks: true
- },
- useUTC: true,
- toolbox: {
- itemGap: 10,
- show: true,
- left: '85%',
- feature: {
- dataZoom: {
- yAxisIndex: 'none'
- },
- restore: {},
- saveAsImage: {
- name: 'Task Speed'
- }
- }
- },
- dataZoom: [
- {
- type: 'slider',
- show: true,
- start: 94,
- end: 100,
- handleSize: 8
- },
- {
- type: 'inside',
- start: 70,
- end: 100
- }
- ],
- series: {
- name: '',
- type: 'line',
- data: arr,
- connectNulls: true,
- markPoint: {
- data: [
- { type: 'max', name: 'Max' },
- { type: 'min', name: 'Min' }
- ]
- },
- markLine: {
- lineStyle: {
- color: '#333'
- }
- }
- }
- };
- if (data.length > 0) {
- option && myChart.setOption(option);
- }
- }
-
- leading_zeros(dt) {
- return (dt < 10 ? '0' : '') + dt;
- }
-
- transDate(dt) {
- const date: any = new Date(dt * 1000);
- // American Format
- // return date.getUTCFullYear()+'-'+this.leading_zeros((date.getUTCMonth() + 1))+'-'+date.getUTCDate()+','+this.leading_zeros(date.getUTCHours())+':'+this.leading_zeros(date.getUTCMinutes())+':'+this.leading_zeros(date.getUTCSeconds());
- return (
- date.getUTCDate() +
- '-' +
- this.leading_zeros(date.getUTCMonth() + 1) +
- '-' +
- date.getUTCFullYear() +
- ',' +
- this.leading_zeros(date.getUTCHours()) +
- ':' +
- this.leading_zeros(date.getUTCMinutes()) +
- ':' +
- this.leading_zeros(date.getUTCSeconds())
- );
- }
-
- generateIntervalsOf(interval, start, end) {
- const result = [];
- let current = start;
-
- while (current < end) {
- result.push(current);
- current += interval;
- }
-
- return result;
- }
}
diff --git a/src/app/tasks/tasks.module.ts b/src/app/tasks/tasks.module.ts
index e72d1036a..f8f2f7a66 100644
--- a/src/app/tasks/tasks.module.ts
+++ b/src/app/tasks/tasks.module.ts
@@ -27,6 +27,7 @@ import { TasksRoutingModule } from './tasks-routing.module';
import { WrbulkComponent } from './import-supertasks/wrbulk/wrbulk.component';
import { CoreFormsModule } from '../shared/forms.module';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
+import { TaskSpeedGraphComponent } from '@src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component';
@NgModule({
@@ -61,7 +62,8 @@ import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-
FormsModule,
NgbModule,
MatButtonToggle,
- MatButtonToggleGroup
+ MatButtonToggleGroup,
+ TaskSpeedGraphComponent
],
exports: [ModalPretasksComponent, ModalSubtasksComponent]
})
diff --git a/src/main.ts b/src/main.ts
index 309a8bd89..acc9e7dc3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,20 +1,25 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-import { AppModule } from './app/app.module';
-import { environment } from './environments/environment';
+import { AppModule } from '@src/app/app.module';
+import { registerEChartsModules } from '@src/app/shared/graphs/echarts/echarts.config';
+import { environment } from '@src/environments/environment';
if (environment.production) {
enableProdMode();
}
-platformBrowserDynamic().bootstrapModule(AppModule).then
-(ref => {
- // Ensure Angular destroys itself on hot reloads.
- if (window['ngRef']) {
- window['ngRef'].destroy();
- }
- window['ngRef'] = ref;
+registerEChartsModules();
- // Otherwise, log the boot error
-}).catch(err => console.error(err));
+platformBrowserDynamic()
+ .bootstrapModule(AppModule)
+ .then((ref) => {
+ // Ensure Angular destroys itself on hot reloads.
+ if (window['ngRef']) {
+ window['ngRef'].destroy();
+ }
+ window['ngRef'] = ref;
+
+ // Otherwise, log the boot error
+ })
+ .catch((err) => console.error(err));
From 0457ac96e131346cbe489be7c8cf777b43c00504 Mon Sep 17 00:00:00 2001
From: cv5ch <176032962+cv5ch@users.noreply.github.com>
Date: Fri, 11 Jul 2025 16:00:57 +0200
Subject: [PATCH 2/2] Some fixes
---
.../agent-stat-graph.component.ts | 104 ++++++-----------
.../shared/graphs/echarts/echarts.config.ts | 5 +-
.../task-speed-graph.component.ts | 107 +++++++++++-------
src/app/shared/graphs/graphs.module.ts | 19 ++--
4 files changed, 110 insertions(+), 125 deletions(-)
diff --git a/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
index d90e44067..4825937f6 100644
--- a/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
+++ b/src/app/shared/graphs/echarts/agent-stat-graph/agent-stat-graph.component.ts
@@ -8,8 +8,8 @@ import { JAgentStat } from '@models/agent-stats.model';
import { ASC } from '@src/app/core/_constants/agentsc.config';
/**
- * Component that renders agent statistics in an ECharts line graph.
- * Displays one line per device for a specific stat type (e.g. GPU temperature, utilization).
+ * Displays agent stats (GPU temp/util or CPU util) in a continuous ECharts line graph.
+ * Each device’s data is shown as a separate line over time.
*/
@Component({
selector: 'app-agent-stat-graph',
@@ -17,45 +17,37 @@ import { ASC } from '@src/app/core/_constants/agentsc.config';
})
export class AgentStatGraphComponent implements OnChanges, OnDestroy {
/**
- * List of agent statistics to be rendered in the graph.
- * These are filtered by `statType` before rendering.
+ * List of agent statistics to visualize.
*/
@Input() agentStats: JAgentStat[] = [];
/**
- * The statistic type to render. Should match one of the values in the ASC enum
- * (e.g. ASC.GPU_TEMP, ASC.GPU_UTIL, ASC.CPU_UTIL).
+ * The stat type to filter and render (e.g., ASC.GPU_TEMP, ASC.CPU_UTIL).
*/
@Input() statType: number;
/**
- * Reference to the DOM element where the chart will be rendered.
+ * DOM element used as the chart container.
*/
@ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
- /**
- * Holds the ECharts instance for rendering and updating the graph.
- */
private chartInstance: EChartsType | null = null;
- /**
- * Lifecycle hook called when any data-bound property changes.
- * Re-renders the chart when `agentStats` or `statType` change.
- *
- * @param changes The changed input properties.
- */
- ngOnChanges(changes: SimpleChanges) {
+ ngOnChanges(changes: SimpleChanges): void {
if (changes['agentStats'] || changes['statType']) {
this.renderChart();
}
}
+ ngOnDestroy(): void {
+ this.chartInstance?.dispose();
+ }
+
/**
- * Renders the chart using the current agentStats and statType.
- * Initializes the chart instance if not already created.
+ * Initializes or updates the ECharts chart.
*/
- private renderChart() {
- if (!this.agentStats || !this.chartContainer) return;
+ private renderChart(): void {
+ if (!this.agentStats?.length || !this.chartContainer) return;
if (!this.chartInstance) {
this.chartInstance = init(this.chartContainer.nativeElement);
@@ -66,33 +58,18 @@ export class AgentStatGraphComponent implements OnChanges, OnDestroy {
}
/**
- * Builds the ECharts configuration object for the given statistics and type.
- * Each device is rendered as a separate line with a legend and average mark line.
- *
- * @param agentStats - Full list of agent stats.
- * @param statType - Type of statistic to filter and render (e.g. GPU_TEMP).
- * @returns ECharts option object used to configure the chart.
+ * Builds chart options using filtered agent statistics.
*/
private buildOption(agentStats: JAgentStat[], statType: number): EChartsCoreOption {
const filteredStats = agentStats.filter((s) => s.statType === statType);
- let titleText = '';
- let yAxisLabel = '';
-
- switch (statType) {
- case ASC.GPU_TEMP:
- titleText = 'GPU Temperature';
- yAxisLabel = '°C';
- break;
- case ASC.GPU_UTIL:
- titleText = 'GPU Utilization';
- yAxisLabel = '%';
- break;
- case ASC.CPU_UTIL:
- titleText = 'CPU Utilization';
- yAxisLabel = '%';
- break;
- }
+ // Configure chart title based on stat type
+ const titleText =
+ {
+ [ASC.GPU_TEMP]: 'GPU Temperature',
+ [ASC.GPU_UTIL]: 'GPU Utilization',
+ [ASC.CPU_UTIL]: 'CPU Utilization'
+ }[statType] ?? 'Agent Stat';
const flattened = [];
const timestamps: number[] = [];
@@ -110,7 +87,6 @@ export class AgentStatGraphComponent implements OnChanges, OnDestroy {
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
-
const deviceCount = Math.max(...flattened.map((f) => f.device)) + 1;
const seriesData = [];
@@ -128,6 +104,9 @@ export class AgentStatGraphComponent implements OnChanges, OnDestroy {
name: `Device ${device}`,
type: 'line',
data: deviceData,
+ showSymbol: false,
+ smooth: false,
+ connectNulls: true,
markLine: {
data: [{ type: 'average', name: 'Avg' }],
symbol: ['none', 'none']
@@ -138,20 +117,12 @@ export class AgentStatGraphComponent implements OnChanges, OnDestroy {
}
return {
- title: {
- text: titleText
- },
- tooltip: {
- trigger: 'axis'
- },
- legend: {
- data: legends
- },
+ title: { text: titleText },
+ tooltip: { trigger: 'axis' },
+ legend: { data: legends },
toolbox: {
feature: {
- dataZoom: {
- yAxisIndex: 'none'
- },
+ dataZoom: { yAxisIndex: 'none' },
dataView: { readOnly: true },
restore: {},
saveAsImage: { name: titleText }
@@ -171,22 +142,13 @@ export class AgentStatGraphComponent implements OnChanges, OnDestroy {
}
},
yAxis: {
- type: 'value',
- axisLabel: {
- formatter: `{value} ${yAxisLabel}`
- }
+ type: 'value' // Label format removed (you requested no extra unit)
},
+ dataZoom: [
+ { type: 'slider', xAxisIndex: 0 },
+ { type: 'inside', xAxisIndex: 0 }
+ ],
series: seriesData
};
}
-
- /**
- * Lifecycle hook called when the component is destroyed.
- * Disposes the chart instance to prevent memory leaks.
- */
- ngOnDestroy() {
- if (this.chartInstance) {
- this.chartInstance.dispose();
- }
- }
}
diff --git a/src/app/shared/graphs/echarts/echarts.config.ts b/src/app/shared/graphs/echarts/echarts.config.ts
index b705a1d29..e3e21b57e 100644
--- a/src/app/shared/graphs/echarts/echarts.config.ts
+++ b/src/app/shared/graphs/echarts/echarts.config.ts
@@ -1,7 +1,7 @@
// echarts-config.ts
import { HeatmapChart, LineChart } from 'echarts/charts';
import {
- CalendarComponent,
+ CalendarComponent, DataZoomComponent,
GridComponent,
LegendComponent,
MarkLineComponent,
@@ -29,6 +29,7 @@ export function registerEChartsModules() {
LineChart,
HeatmapChart,
CanvasRenderer,
- UniversalTransition
+ UniversalTransition,
+ DataZoomComponent
]);
}
diff --git a/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
index d51bf09cb..f9b03fdf8 100644
--- a/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
+++ b/src/app/shared/graphs/echarts/task-speed-graph/task-speed-graph.component.ts
@@ -17,10 +17,19 @@ import { ComposeOption, EChartsType, init } from 'echarts/core';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
-import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ Input,
+ OnChanges,
+ SimpleChanges,
+ ViewChild
+} from '@angular/core';
import { HashRatePipe } from '@src/app/core/_pipes/hashrate-pipe';
+// Compose ECharts option type
type EChartsOption = ComposeOption<
| TitleComponentOption
| ToolboxComponentOption
@@ -56,29 +65,39 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
constructor(private hashratePipe: HashRatePipe) {}
- ngAfterViewInit() {
+ /**
+ * Initializes the chart after view is ready.
+ */
+ ngAfterViewInit(): void {
if (this.speeds?.length) {
this.drawChart();
}
}
- ngOnChanges(changes: SimpleChanges) {
+ /**
+ * Redraw chart on input changes.
+ */
+ ngOnChanges(changes: SimpleChanges): void {
if (changes['speeds'] && !changes['speeds'].firstChange) {
this.drawChart();
}
}
- private drawChart() {
+ /**
+ * Renders the line chart from speed data.
+ */
+ private drawChart(): void {
if (!this.chart) {
this.chart = init(this.chartRef.nativeElement);
}
- const data = this.speeds;
- const arr = [];
- const max = [];
- const result = [];
+ if (!this.speeds || !this.speeds.length) {
+ this.chart.clear();
+ return;
+ }
- data.reduce((res, value) => {
+ const result = [];
+ const reducer = this.speeds.reduce((res, value) => {
if (!res[value.time]) {
res[value.time] = { time: value.time, speed: 0 };
result.push(res[value.time]);
@@ -87,32 +106,43 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
return res;
}, {});
- for (let i = 0; i < result.length; i++) {
- const iso = this.transDate(result[i]['time']);
- const { value: speed, unit } = this.hashratePipe.transform(result[i]['speed'], 2, true) as {
+ const arr = [];
+ const timestamps = [];
+
+ for (const item of result) {
+ const iso = this.transDate(item.time);
+ const transformed = this.hashratePipe.transform(item.speed, 2, true) as {
value: number;
unit: string;
};
+ if (!transformed) continue;
+
arr.push({
name: iso,
- value: [iso, speed],
- unit: unit
+ value: [iso, transformed.value],
+ unit: transformed.unit
});
- max.push(result[i]['time']);
+ timestamps.push(item.time);
+ }
+
+ if (!arr.length) {
+ this.chart.clear();
+ return;
}
- const displayUnit = arr.length ? arr[arr.length - 1].unit : 'H/s';
- const startdate = max[0];
- const enddate = max[max.length - 1];
- const datelabel = this.transDate(enddate);
const speedsOnly = arr.map((item) => item.value[1]);
const maxSpeed = Math.max(...speedsOnly);
const minSpeed = Math.min(...speedsOnly);
const maxIndex = speedsOnly.indexOf(maxSpeed);
const minIndex = speedsOnly.indexOf(minSpeed);
+ const displayUnit = arr[maxIndex]?.unit || 'H/s';
+ const startdate = timestamps[0];
+ const enddate = timestamps[timestamps.length - 1];
+ const datelabel = this.transDate(enddate);
+
const xAxis = this.generateIntervalsOf(1, +startdate, +enddate);
const xAxisData = xAxis.map((ts) => this.transDate(ts));
@@ -126,12 +156,10 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
if (params.componentType === 'markPoint') {
return `${params.name}: ${params.data.value}`;
}
-
const data = params.data;
- if (data && Array.isArray(data.value)) {
+ if (data?.value?.[1]) {
return `${params.name}: ${data.value[1]} ${data.unit ?? ''}`;
}
-
return '';
}
},
@@ -157,7 +185,7 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
itemGap: 10,
feature: {
dataZoom: {
- yAxisIndex: 'none', // disables zoom on y-axis
+ yAxisIndex: 'none',
title: {
zoom: 'Zoom in',
back: 'Zoom reset'
@@ -168,18 +196,8 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
}
},
dataZoom: [
- {
- type: 'slider',
- xAxisIndex: 0,
- start: 0,
- end: 100
- },
- {
- type: 'inside',
- xAxisIndex: 0,
- start: 0,
- end: 100
- }
+ { type: 'slider', xAxisIndex: 0, start: 0, end: 100 },
+ { type: 'inside', xAxisIndex: 0, start: 0, end: 100 }
],
series: {
name: '',
@@ -190,13 +208,13 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
data: [
{
name: 'Max',
- coord: arr[maxIndex].value,
- value: `${arr[maxIndex].value[1]} ${arr[maxIndex].unit}`
+ coord: arr[maxIndex]?.value ?? [],
+ value: `${arr[maxIndex]?.value?.[1]} ${arr[maxIndex]?.unit}`
},
{
name: 'Min',
- coord: arr[minIndex].value,
- value: `${arr[minIndex].value[1]} ${arr[minIndex].unit}`
+ coord: arr[minIndex]?.value ?? [],
+ value: `${arr[minIndex]?.value?.[1]} ${arr[minIndex]?.unit}`
}
]
},
@@ -209,10 +227,16 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
this.chart.setOption(option);
}
+ /**
+ * Returns a date string with leading zeros for formatting.
+ */
private leadingZeros(dt: number): string {
- return dt < 10 ? '0' + dt : '' + dt;
+ return dt < 10 ? '0' + dt : dt.toString();
}
+ /**
+ * Converts a UNIX timestamp to formatted date string (UTC).
+ */
private transDate(dt: number): string {
const date = new Date(dt * 1000);
return (
@@ -230,6 +254,9 @@ export class TaskSpeedGraphComponent implements AfterViewInit, OnChanges {
);
}
+ /**
+ * Generates an array of timestamps from `start` to `end` with a fixed `interval`.
+ */
private generateIntervalsOf(interval: number, start: number, end: number): number[] {
const result = [];
let current = start;
diff --git a/src/app/shared/graphs/graphs.module.ts b/src/app/shared/graphs/graphs.module.ts
index c8f299451..cdafbbaf4 100644
--- a/src/app/shared/graphs/graphs.module.ts
+++ b/src/app/shared/graphs/graphs.module.ts
@@ -1,16 +1,11 @@
-import { TaskVisualComponent } from "./task-visual/task-visual.component";
-import { CommonModule } from "@angular/common";
-import { NgModule } from "@angular/core";
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { TaskVisualComponent } from '@src/app/shared/graphs/task-visual/task-visual.component';
@NgModule({
- declarations:[
- TaskVisualComponent,
- ],
- imports:[
- CommonModule
- ],
- exports:[
- TaskVisualComponent
- ]
+ declarations: [TaskVisualComponent],
+ imports: [CommonModule],
+ exports: [TaskVisualComponent]
})
export class GraphsModule {}