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 {}