From 4c0ce59b116378aff8e67e22b846c2c3bb2ccdea Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Fri, 16 Jun 2023 17:25:06 +0300 Subject: [PATCH 01/59] mock --- cvat/apps/analytics_report/apps.py | 12 ++ cvat/apps/analytics_report/serializers.py | 3 + cvat/apps/analytics_report/signals.py | 3 + cvat/apps/analytics_report/urls.py | 13 +++ cvat/apps/analytics_report/views.py | 135 ++++++++++++++++++++++ cvat/settings/base.py | 1 + cvat/urls.py | 3 + 7 files changed, 170 insertions(+) create mode 100644 cvat/apps/analytics_report/apps.py create mode 100644 cvat/apps/analytics_report/serializers.py create mode 100644 cvat/apps/analytics_report/signals.py create mode 100644 cvat/apps/analytics_report/urls.py create mode 100644 cvat/apps/analytics_report/views.py diff --git a/cvat/apps/analytics_report/apps.py b/cvat/apps/analytics_report/apps.py new file mode 100644 index 000000000000..1f09bc92daeb --- /dev/null +++ b/cvat/apps/analytics_report/apps.py @@ -0,0 +1,12 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + name = 'cvat.apps.analytics_report' + + def ready(self): + from . import signals # pylint: disable=unused-import diff --git a/cvat/apps/analytics_report/serializers.py b/cvat/apps/analytics_report/serializers.py new file mode 100644 index 000000000000..14ae051b6ea7 --- /dev/null +++ b/cvat/apps/analytics_report/serializers.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT diff --git a/cvat/apps/analytics_report/signals.py b/cvat/apps/analytics_report/signals.py new file mode 100644 index 000000000000..14ae051b6ea7 --- /dev/null +++ b/cvat/apps/analytics_report/signals.py @@ -0,0 +1,3 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT diff --git a/cvat/apps/analytics_report/urls.py b/cvat/apps/analytics_report/urls.py new file mode 100644 index 000000000000..173523c4f290 --- /dev/null +++ b/cvat/apps/analytics_report/urls.py @@ -0,0 +1,13 @@ + +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from rest_framework import routers + +from . import views + +router = routers.DefaultRouter(trailing_slash=False) +router.register('analytics/report', views.AnalyticsReportViewSet, basename='analytics_report') + +urlpatterns = router.urls diff --git a/cvat/apps/analytics_report/views.py b/cvat/apps/analytics_report/views.py new file mode 100644 index 000000000000..0478c21f5c04 --- /dev/null +++ b/cvat/apps/analytics_report/views.py @@ -0,0 +1,135 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import json + +from django.conf import settings +from rest_framework import status, viewsets +from rest_framework.response import Response +from drf_spectacular.utils import OpenApiResponse, OpenApiParameter, extend_schema +from drf_spectacular.types import OpenApiTypes +from rest_framework.renderers import JSONRenderer + + +from cvat.apps.iam.permissions import EventsPermission +from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS +from cvat.apps.events.serializers import ClientEventsSerializer +from cvat.apps.engine.log import vlogger + +class AnalyticsReportViewSet(viewsets.ViewSet): + serializer_class = None + + def list(self, request): + moc_data = { + "last_updated": "2023-06-06T00:00:00Z", + "type": "job", + "id": 123, + "statistics": { + "objects": { + "title": "Some Title", + "description": "Detailed description", + "granularity": "day", + "dafault_view": "histogram", + "dataseries": { + "created" : [ + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-05T00:00:00Z" + }, + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-06T00:00:00Z" + } + ], + "updated": [ + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-05T00:00:00Z" + }, + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-06T00:00:00Z" + } + ], + "deleted": [ + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-05T00:00:00Z" + }, + { + "value": { + "tracked_shapes": 123, + "shapes": 234, + "tags": 345 + }, + "datetime": "2023-06-06T00:00:00Z" + } + ] + } + }, + "working_time": { + "title": "Some Title", + "description": "Detailed description", + "granularity": "day", + "dafault_view": "histogram", + "dataseries": { + "object_count": [ + { + "value": 123, + "datetime": "2023-06-05T00:00:00Z" + }, + { + "value": 123, + "datetime": "2023-06-06T00:00:00Z" + } + ], + "working_time": [ + { + "value": 123, + "datetime": "2023-06-05T00:00:00Z" + }, + { + "value": 123, + "datetime": "2023-06-06T00:00:00Z" + } + ] + } + }, + "annotation_time": { + "title": "Some Title", + "description": "Detailed description", + "dafault_view": "numeric", + "dataseries": { + "total_annotating_time": [ + { + "value": 123, + "datetime": "2023-06-06T00:00:00Z" + } + ] + } + } + } + } + + return Response(data=moc_data, status=status.HTTP_200_OK) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 25f6bfebe601..51ede73c75be 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -142,6 +142,7 @@ def add_ssh_keys(): 'cvat.apps.health', 'cvat.apps.events', 'cvat.apps.quality_control', + 'cvat.apps.analytics_report', ] SITE_ID = 1 diff --git a/cvat/urls.py b/cvat/urls.py index df95a399c3f8..9f34b83dfe04 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -54,3 +54,6 @@ if apps.is_installed('health_check'): urlpatterns.append(path('api/server/health/', include('health_check.urls'))) + +if apps.is_installed('cvat.apps.analytics_report'): + urlpatterns.append(path('api/', include('cvat.apps.analytics_report.urls'))) From e60723c243298815243457b0a51067b9a68f6f51 Mon Sep 17 00:00:00 2001 From: klakhov Date: Thu, 22 Jun 2023 10:12:10 +0300 Subject: [PATCH 02/59] updated mock data --- cvat/apps/analytics_report/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cvat/apps/analytics_report/views.py b/cvat/apps/analytics_report/views.py index 0478c21f5c04..194348957924 100644 --- a/cvat/apps/analytics_report/views.py +++ b/cvat/apps/analytics_report/views.py @@ -22,7 +22,7 @@ class AnalyticsReportViewSet(viewsets.ViewSet): def list(self, request): moc_data = { - "last_updated": "2023-06-06T00:00:00Z", + "created_date": "2023-06-06T00:00:00Z", "type": "job", "id": 123, "statistics": { @@ -30,7 +30,7 @@ def list(self, request): "title": "Some Title", "description": "Detailed description", "granularity": "day", - "dafault_view": "histogram", + "default_view": "histogram", "dataseries": { "created" : [ { @@ -92,7 +92,7 @@ def list(self, request): "title": "Some Title", "description": "Detailed description", "granularity": "day", - "dafault_view": "histogram", + "default_view": "histogram", "dataseries": { "object_count": [ { @@ -119,7 +119,7 @@ def list(self, request): "annotation_time": { "title": "Some Title", "description": "Detailed description", - "dafault_view": "numeric", + "default_view": "numeric", "dataseries": { "total_annotating_time": [ { From 57a17f1914364f83ed5da214c4805510c460ea47 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 11:22:24 +0300 Subject: [PATCH 03/59] added cvat-core part --- cvat-core/src/analytics-report.ts | 84 +++++++++++++++++++++++++++++ cvat-core/src/api-implementation.ts | 24 +++++++++ cvat-core/src/api.ts | 8 +++ cvat-core/src/server-proxy.ts | 25 +++++++++ 4 files changed, 141 insertions(+) create mode 100644 cvat-core/src/analytics-report.ts diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts new file mode 100644 index 000000000000..861899d8a275 --- /dev/null +++ b/cvat-core/src/analytics-report.ts @@ -0,0 +1,84 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +export interface SerializedDataEntry { + datetime?: string; + value?: number | Record +} + +export interface SerializedAnalyticsEntry { + title?: string; + description?: string; + granularity?: string; + default_view?: string; + dataseries?: Record; +} + +export interface SerializedAnalyticsReport { + id?: number; + type?: string; + created_date?: string; + statistics?: Record +} + +export enum AnalyticsReportType { + JOB = 'job', + TASK = 'task', + PROJECT = 'project', +} + +export enum AnalyticsEntryView { + HISTOGRAM = 'histogram', + NUMERIC = 'numeric', +} + +export class AnalyticsEntry { + #title: string; + #description: string; + #granularity: string; + #defaultView: AnalyticsEntryView; + #dataseries: Record; + + constructor(initialData: SerializedAnalyticsEntry) { + this.#title = initialData.title; + this.#description = initialData.description; + this.#granularity = initialData.granularity; + this.#defaultView = initialData.default_view as AnalyticsEntryView; + this.#dataseries = initialData.dataseries; + } +} + +export default class AnalyticsReport { + #id: number; + #type: AnalyticsReportType; + #createdDate: string; + #statistics: Record; + + constructor(initialData: SerializedAnalyticsReport) { + this.#id = initialData.id; + this.#type = initialData.type as AnalyticsReportType; + this.#createdDate = initialData.created_date; + + this.#statistics = {}; + for (const [key, analyticsEntry] of Object.entries(initialData.statistics)) { + this.#statistics[key] = new AnalyticsEntry(analyticsEntry); + } + } + + get id(): number { + return this.#id; + } + + get type(): AnalyticsReportType { + return this.#type; + } + + get createdDate(): string { + return this.#createdDate; + } + + get statistics(): Record { + return this.#statistics; + } +} diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 421c7ed5287a..c7d6e4063472 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -25,6 +25,7 @@ import Project from './project'; import CloudStorage from './cloud-storage'; import Organization from './organization'; import Webhook from './webhook'; +import AnalyticsReport from './analytics-report'; export default function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; @@ -357,5 +358,28 @@ export default function implementAPI(cvat) { return webhooks; }; + cvat.analytics.common.reports.implementation = async (filter) => { + let updatedParams: Record = {}; + + if ('taskID' in filter) { + updatedParams = { + task_id: filter.reportId, + }; + } + if ('jobID' in filter) { + updatedParams = { + job_id: filter.reportId, + }; + } + if ('projectID' in filter) { + updatedParams = { + project_id: filter.reportId, + }; + } + + const reportData = await serverProxy.analytics.common.reports(updatedParams); + return new AnalyticsReport(reportData); + }; + return cvat; } diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 786598d48c5b..640bc9fbf282 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -270,6 +270,14 @@ function build() { return result; }, }, + analytics: { + common: { + async reports(filter = {}) { + const result = await PluginRegistry.apiWrapper(cvat.analytics.common.reports, filter); + return result; + }, + }, + }, classes: { User, Project: implementProject(Project), diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index ec692db17aad..a7237ed8e099 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -13,6 +13,7 @@ import { SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, SerializedRegister, JobsFilter, SerializedJob, } from 'server-response-types'; +import { SerializedAnalyticsReport } from './analytics-report'; import { Storage } from './storage'; import { StorageLocation, WebhookSourceType } from './enums'; import { isEmail, isResourceURL } from './common'; @@ -2167,6 +2168,24 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise } } +async function getAnalyticsReports(filter): Promise { + const params = enableOrganization(); + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/analytics/report`, { + params: { + ...params, + ...filter, + }, + }); + + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + export default Object.freeze({ server: Object.freeze({ setAuthData, @@ -2310,4 +2329,10 @@ export default Object.freeze({ ping: pingWebhook, events: receiveWebhookEvents, }), + + analytics: Object.freeze({ + common: Object.freeze({ + reports: getAnalyticsReports, + }), + }), }); From f3e96498e0238250b16ce50adc43a474582cce30 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 13:19:31 +0300 Subject: [PATCH 04/59] added raw project analytics --- cvat-core/src/analytics-report.ts | 27 +++- cvat-core/src/api.ts | 1 + cvat-core/src/server-proxy.ts | 22 ++- cvat-ui/package.json | 2 + .../analytics-overview/analytics-overview.tsx | 52 +++++++ .../analytics-overview/histogram-view.tsx | 68 ++++++++ .../components/analytics-overview/styles.scss | 0 cvat-ui/src/components/cvat-app.tsx | 2 + .../project-analytics-page.tsx | 145 ++++++++++++++++++ .../project-analytics-page/styles.scss | 0 cvat-ui/src/cvat-core-wrapper.ts | 4 + cvat-ui/webpack.config.js | 2 +- yarn.lock | 44 ++++-- 13 files changed, 353 insertions(+), 16 deletions(-) create mode 100644 cvat-ui/src/components/analytics-overview/analytics-overview.tsx create mode 100644 cvat-ui/src/components/analytics-overview/histogram-view.tsx create mode 100644 cvat-ui/src/components/analytics-overview/styles.scss create mode 100644 cvat-ui/src/components/project-analytics-page/project-analytics-page.tsx create mode 100644 cvat-ui/src/components/project-analytics-page/styles.scss diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts index 861899d8a275..0e26117485ee 100644 --- a/cvat-core/src/analytics-report.ts +++ b/cvat-core/src/analytics-report.ts @@ -28,7 +28,7 @@ export enum AnalyticsReportType { PROJECT = 'project', } -export enum AnalyticsEntryView { +export enum AnalyticsEntryViewType { HISTOGRAM = 'histogram', NUMERIC = 'numeric', } @@ -37,16 +37,37 @@ export class AnalyticsEntry { #title: string; #description: string; #granularity: string; - #defaultView: AnalyticsEntryView; + #defaultView: AnalyticsEntryViewType; #dataseries: Record; constructor(initialData: SerializedAnalyticsEntry) { this.#title = initialData.title; this.#description = initialData.description; this.#granularity = initialData.granularity; - this.#defaultView = initialData.default_view as AnalyticsEntryView; + this.#defaultView = initialData.default_view as AnalyticsEntryViewType; this.#dataseries = initialData.dataseries; } + + get title(): string { + return this.#title; + } + + get description(): string { + return this.#description; + } + + // Probably need to create enum for this + get granularity(): string { + return this.#granularity; + } + + get defaultView(): AnalyticsEntryViewType { + return this.#defaultView; + } + + get dataseries(): Record { + return this.#dataseries; + } } export default class AnalyticsReport { diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index bb118d9016b7..172cc09bca0b 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -283,6 +283,7 @@ function build() { async reports(filter = {}) { const result = await PluginRegistry.apiWrapper(cvat.analytics.common.reports, filter); return result; + }, }, quality: { async reports(filter: any) { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index ea745bf1e37c..29eeab2b4971 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -13,9 +13,9 @@ import { SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, } from 'server-response-types'; -import { SerializedAnalyticsReport } from './analytics-report'; import { SerializedQualityReportData } from 'quality-report'; import { SerializedQualitySettingsData } from 'quality-settings'; +import { SerializedAnalyticsReport } from './analytics-report'; import { Storage } from './storage'; import { StorageLocation, WebhookSourceType } from './enums'; import { isEmail, isResourceURL } from './common'; @@ -2318,6 +2318,22 @@ async function getQualityReports(filter): Promise } } +async function getAnalyticsReports(filter): Promise { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/analytics/report`, { + params: { + ...filter, + }, + }); + + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + export default Object.freeze({ server: Object.freeze({ setAuthData, @@ -2463,13 +2479,13 @@ export default Object.freeze({ ping: pingWebhook, events: receiveWebhookEvents, }), - + guides: Object.freeze({ get: getGuide, create: createGuide, update: updateGuide, }), - + assets: Object.freeze({ create: createAsset, }), diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f96a51c6dc7a..93a1bc6eafbb 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -37,6 +37,7 @@ "@types/resize-observer-browser": "^0.1.6", "@uiw/react-md-editor": "^3.22.0", "antd": "~4.18.9", + "chart.js": "^4.3.0", "copy-to-clipboard": "^3.3.1", "cvat-canvas": "link:./../cvat-canvas", "cvat-canvas3d": "link:./../cvat-canvas3d", @@ -53,6 +54,7 @@ "prop-types": "^15.7.2", "react": "^16.14.0", "react-awesome-query-builder": "^4.5.1", + "react-chartjs-2": "^5.2.0", "react-color": "^2.19.3", "react-cookie": "^4.0.3", "react-dom": "^16.14.0", diff --git a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx new file mode 100644 index 000000000000..7726be8bf489 --- /dev/null +++ b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx @@ -0,0 +1,52 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { useHistory, useParams } from 'react-router'; +import { Row, Col } from 'antd/lib/grid'; +import Tabs from 'antd/lib/tabs'; +import Text from 'antd/lib/typography/Text'; +import Title from 'antd/lib/typography/Title'; +import Spin from 'antd/lib/spin'; +import Button from 'antd/lib/button'; +import notification from 'antd/lib/notification'; +import { LeftOutlined } from '@ant-design/icons/lib/icons'; +import { useIsMounted } from 'utils/hooks'; +import { Project } from 'reducers'; +import { AnalyticsReport, AnalyticsEntry, AnalyticsEntryViewType } from 'cvat-core-wrapper'; + +interface Props { + report: AnalyticsReport | null; +} + +function AnalyticsOverview(props: Props): JSX.Element | null { + const { report } = props; + + // TODO make it more expressive + if (!report) return null; + + const views = Object.entries(report.statistics).map(([name, entry]) => { + switch (entry.defaultView) { + case AnalyticsEntryViewType.NUMERIC: { + return
numeric
; + } + case AnalyticsEntryViewType.HISTOGRAM: { + return
histogram
; + } + default: { + throw Error(`View type ${entry.defaultView} is not supported`); + } + } + }); + + return ( +
+ {views} +
+ ); +} + +export default React.memo(AnalyticsOverview); diff --git a/cvat-ui/src/components/analytics-overview/histogram-view.tsx b/cvat-ui/src/components/analytics-overview/histogram-view.tsx new file mode 100644 index 000000000000..ad388adbb06e --- /dev/null +++ b/cvat-ui/src/components/analytics-overview/histogram-view.tsx @@ -0,0 +1,68 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +); + +interface Props { + report: AnalyticsReport | null; +} +export const options = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Chart.js Bar Chart', + }, + }, +}; + +const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; + +export const data = { + labels, + datasets: [ + { + label: 'Dataset 1', + data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), + backgroundColor: 'rgba(255, 99, 132, 0.5)', + }, + { + label: 'Dataset 2', + data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), + backgroundColor: 'rgba(53, 162, 235, 0.5)', + }, + ], +}; + +function HistogramView(props: Props): JSX.Element | null { + const { report } = props; + + return ; +} + +export default React.memo(HistogramView); diff --git a/cvat-ui/src/components/analytics-overview/styles.scss b/cvat-ui/src/components/analytics-overview/styles.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index d7821f404190..c4b424f60459 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -78,6 +78,7 @@ import CreateModelPage from './create-model-page/create-model-page'; import CreateJobPage from './create-job-page/create-job-page'; import TaskAnalyticsPage from './task-analytics-page/task-analytics-page'; import OrganizationWatcher from './watchers/organization-watcher'; +import ProjectAnalyticsPage from './project-analytics-page/project-analytics-page'; interface CVATAppProps { loadFormats: () => void; @@ -463,6 +464,7 @@ class CVATApplication extends React.PureComponent + diff --git a/cvat-ui/src/components/project-analytics-page/project-analytics-page.tsx b/cvat-ui/src/components/project-analytics-page/project-analytics-page.tsx new file mode 100644 index 000000000000..fee966364b59 --- /dev/null +++ b/cvat-ui/src/components/project-analytics-page/project-analytics-page.tsx @@ -0,0 +1,145 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useEffect, useState } from 'react'; +import { useHistory, useParams } from 'react-router'; +import { Row, Col } from 'antd/lib/grid'; +import Tabs from 'antd/lib/tabs'; +import Text from 'antd/lib/typography/Text'; +import Title from 'antd/lib/typography/Title'; +import Spin from 'antd/lib/spin'; +import Button from 'antd/lib/button'; +import notification from 'antd/lib/notification'; +import { LeftOutlined } from '@ant-design/icons/lib/icons'; +import { useIsMounted } from 'utils/hooks'; +import { Project } from 'reducers'; +import { AnalyticsReport, getCore } from 'cvat-core-wrapper'; +import AnalyticsOverview from 'components/analytics-overview/analytics-overview'; + +const core = getCore(); + +function ProjectAnalyticsPage(): JSX.Element { + const [fetchingProject, setFetchingProject] = useState(true); + const [projectInstance, setProjectInstance] = useState(null); + const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); + const isMounted = useIsMounted(); + + const history = useHistory(); + + const id = +useParams<{ id: string }>().id; + + const receieveProject = (): void => { + if (Number.isInteger(id)) { + core.projects.get({ id }) + .then(([project]: Project[]) => { + if (isMounted() && project) { + setProjectInstance(project); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested project from the server', + description: error.toString(), + }); + } + }).finally(() => { + if (isMounted()) { + setFetchingProject(false); + } + }); + } else { + notification.error({ + message: 'Could not receive the requested project from the server', + description: `Requested task id "${id}" is not valid`, + }); + setFetchingProject(false); + } + }; + + // TODO make it via get Analytics report action after Honeypot merge + const receieveReport = (): void => { + if (Number.isInteger(id)) { + core.analytics.common.reports({ projectID: id }) + .then((report: AnalyticsReport) => { + if (isMounted() && report) { + setAnalyticsReportInstance(report); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested report from the server', + description: error.toString(), + }); + } + }); + } + }; + console.log(analyticsReportInstance); + + useEffect((): void => { + receieveProject(); + receieveReport(); + }, []); + + return ( +
+ { + fetchingProject ? ( +
+ +
+ ) : ( + + + + + + + + {projectInstance.name} + + + {`#${projectInstance.id}`} + + + + + Overview + + )} + key='Overview' + > + + + + + + ) + } +
+ ); +} + +export default React.memo(ProjectAnalyticsPage); diff --git a/cvat-ui/src/components/project-analytics-page/styles.scss b/cvat-ui/src/components/project-analytics-page/styles.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index 5b7c19bc14c3..888c4413e549 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -28,6 +28,7 @@ import Comment from 'cvat-core/src/comment'; import User from 'cvat-core/src/user'; import Organization from 'cvat-core/src/organization'; import AnnotationGuide from 'cvat-core/src/guide'; +import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-core/src/analytics-report'; import { Dumper } from 'cvat-core/src/annotation-formats'; import { APIWrapperEnterOptions } from 'cvat-core/src/plugins'; @@ -76,6 +77,9 @@ export { AnnotationConflict, ConflictSeverity, FramesMetaData, + AnalyticsReport, + AnalyticsEntry, + AnalyticsEntryViewType, }; export type { diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index 46f37c843e41..8ebf3f669d06 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -14,7 +14,7 @@ const CopyPlugin = require('copy-webpack-plugin'); module.exports = (env) => { const defaultAppConfig = path.join(__dirname, 'src/config.tsx'); - const defaultPlugins = ['plugins/sam_plugin']; + const defaultPlugins = []; const appConfigFile = process.env.UI_APP_CONFIG ? process.env.UI_APP_CONFIG : defaultAppConfig; const pluginsList = process.env.CLIENT_PLUGINS ? [...defaultPlugins, ...process.env.CLIENT_PLUGINS.split(':')] .map((s) => s.trim()).filter((s) => !!s) : defaultPlugins diff --git a/yarn.lock b/yarn.lock index cbfff391e155..816c31a431ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1395,6 +1395,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -1839,7 +1844,7 @@ resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043" integrity sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w== -"@types/prettier@2.4.1", "@types/prettier@^2.1.5": +"@types/prettier@^2.1.5": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== @@ -1877,12 +1882,12 @@ "@types/react" "*" "@types/reactcss" "*" -"@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5": - version "18.2.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.6.tgz#ad621fa71a8db29af7c31b41b2ea3d8a6f4144d1" - integrity sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A== +"@types/react-dom@^16.9.14": + version "16.9.19" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.19.tgz#6a139c26b02dec533a7fa131f084561babb10a8f" + integrity sha512-xC8D280Bf6p0zguJ8g62jcEOKZiUbx9sIe6O3tT/lKfR87A7A6g65q13z6D5QUMIa/6yFPkNhqjF5z/VVZEYqQ== dependencies: - "@types/react" "*" + "@types/react" "^16" "@types/react-grid-layout@^1.3.2": version "1.3.2" @@ -1891,7 +1896,7 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.24": +"@types/react-redux@^7.1.18": version "7.1.25" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== @@ -1901,7 +1906,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@^5.1.9", "@types/react-router-dom@^5.3.3": +"@types/react-router-dom@^5.1.9": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== @@ -1925,7 +1930,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.14.15", "@types/react@^17.0.30": +"@types/react@*": version "17.0.62" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.62.tgz#2efe8ddf8533500ec44b1334dd1a97caa2f860e3" integrity sha512-eANCyz9DG8p/Vdhr0ZKST8JV12PhH2ACCDYlFw6DIO+D+ca+uP4jtEDEpVqXZrh/uZdXQGwk7whJa3ah5DtyLw== @@ -1934,6 +1939,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^16", "@types/react@^16.14.15": + version "16.14.43" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.43.tgz#bc6e7a0e99826809591d38ddf1193955de32c446" + integrity sha512-7zdjv7jvoLLQg1tTvpQsm+hyNUMT2mPlNV1+d0I8fbGhkJl82spopMyBlu4wb1dviZAxpGdk5eHu/muacknnfw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.6" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc" @@ -3366,6 +3380,13 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== +chart.js@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1" + integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g== + dependencies: + "@kurkle/color" "^0.3.0" + check-links@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/check-links/-/check-links-1.1.8.tgz#842184178c85d9c2ab119175bcc2672681bc88a4" @@ -10028,6 +10049,11 @@ react-awesome-query-builder@^4.5.1: redux "^4.1.0" sqlstring "^2.3.2" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-color@^2.19.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" From a9883ce4c4a4f20c5db843ff4432c62f6a589064 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 15:27:10 +0300 Subject: [PATCH 05/59] added support for histograms --- cvat-core/src/analytics-report.ts | 6 +- .../analytics-overview/analytics-overview.tsx | 53 ++++++++++++----- .../analytics-overview/histogram-view.tsx | 58 +++++++++---------- 3 files changed, 69 insertions(+), 48 deletions(-) diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts index 0e26117485ee..4bc695dc439a 100644 --- a/cvat-core/src/analytics-report.ts +++ b/cvat-core/src/analytics-report.ts @@ -12,7 +12,7 @@ export interface SerializedAnalyticsEntry { description?: string; granularity?: string; default_view?: string; - dataseries?: Record; + dataseries?: Record; } export interface SerializedAnalyticsReport { @@ -38,7 +38,7 @@ export class AnalyticsEntry { #description: string; #granularity: string; #defaultView: AnalyticsEntryViewType; - #dataseries: Record; + #dataseries: Record; constructor(initialData: SerializedAnalyticsEntry) { this.#title = initialData.title; @@ -65,7 +65,7 @@ export class AnalyticsEntry { return this.#defaultView; } - get dataseries(): Record { + get dataseries(): Record { return this.#dataseries; } } diff --git a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx index 7726be8bf489..34b6c61a9241 100644 --- a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx +++ b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx @@ -4,19 +4,10 @@ import './styles.scss'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory, useParams } from 'react-router'; -import { Row, Col } from 'antd/lib/grid'; -import Tabs from 'antd/lib/tabs'; -import Text from 'antd/lib/typography/Text'; -import Title from 'antd/lib/typography/Title'; -import Spin from 'antd/lib/spin'; -import Button from 'antd/lib/button'; -import notification from 'antd/lib/notification'; -import { LeftOutlined } from '@ant-design/icons/lib/icons'; -import { useIsMounted } from 'utils/hooks'; -import { Project } from 'reducers'; -import { AnalyticsReport, AnalyticsEntry, AnalyticsEntryViewType } from 'cvat-core-wrapper'; +import React from 'react'; +import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; +import moment from 'moment'; +import HistogramView from './histogram-view'; interface Props { report: AnalyticsReport | null; @@ -34,7 +25,41 @@ function AnalyticsOverview(props: Props): JSX.Element | null { return
numeric
; } case AnalyticsEntryViewType.HISTOGRAM: { - return
histogram
; + const firstDataset = Object.keys(entry.dataseries)[0]; + const dateLabels = entry.dataseries[firstDataset].map((dataEntry) => ( + moment.utc(dataEntry.datetime).local().format('YYYY-MM-DD HH:mm:ss') + )); + + const datasets = Object.entries(entry.dataseries).map(([key, series]) => { + let label = key.split('_').join(' '); + label = label.charAt(0).toUpperCase() + label.slice(1); + + const data = series.map((s) => { + if (Number.isFinite(s.value)) { + return s.value; + } + + if (typeof s.value === 'object') { + return Object.keys(s.value).reduce((acc, key) => acc + s.value[key], 0); + } + }); + + return { + label, + data, + // Just random now + // eslint-disable-next-line no-bitwise + backgroundColor: `#${(Math.random() * 0xFFFFFF << 0).toString(16)}`, + }; + }); + return ( + + ); } default: { throw Error(`View type ${entry.defaultView} is not supported`); diff --git a/cvat-ui/src/components/analytics-overview/histogram-view.tsx b/cvat-ui/src/components/analytics-overview/histogram-view.tsx index ad388adbb06e..64624821be51 100644 --- a/cvat-ui/src/components/analytics-overview/histogram-view.tsx +++ b/cvat-ui/src/components/analytics-overview/histogram-view.tsx @@ -25,42 +25,38 @@ ChartJS.register( Legend, ); +export interface HistogramDataset { + label: string; + data: number[]; + backgroundColor: string; +} + interface Props { - report: AnalyticsReport | null; + labels: string[]; + datasets: HistogramDataset[]; + title: string; } -export const options = { - responsive: true, - plugins: { - legend: { - position: 'top' as const, - }, - title: { - display: true, - text: 'Chart.js Bar Chart', - }, - }, -}; -const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; +function HistogramView(props: Props): JSX.Element | null { + const { datasets, labels, title } = props; -export const data = { - labels, - datasets: [ - { - label: 'Dataset 1', - data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), - backgroundColor: 'rgba(255, 99, 132, 0.5)', - }, - { - label: 'Dataset 2', - data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), - backgroundColor: 'rgba(53, 162, 235, 0.5)', - }, - ], -}; + const data = { + labels, + datasets, + }; -function HistogramView(props: Props): JSX.Element | null { - const { report } = props; + const options = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: title, + }, + }, + }; return ; } From 7a3f1ce91f658be8a082c572a555d9bb300f6d73 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 15:52:07 +0300 Subject: [PATCH 06/59] added numeric value, improved colors --- .../analytics-card.tsx | 2 - .../analytics-overview/analytics-overview.tsx | 37 ++++++++++++++----- .../quality/gt-conflicts.tsx | 2 +- .../task-analytics-page/quality/issues.tsx | 2 +- .../quality/mean-quality.tsx | 2 +- 5 files changed, 31 insertions(+), 14 deletions(-) rename cvat-ui/src/components/{task-analytics-page/quality => analytics-overview}/analytics-card.tsx (98%) diff --git a/cvat-ui/src/components/task-analytics-page/quality/analytics-card.tsx b/cvat-ui/src/components/analytics-overview/analytics-card.tsx similarity index 98% rename from cvat-ui/src/components/task-analytics-page/quality/analytics-card.tsx rename to cvat-ui/src/components/analytics-overview/analytics-card.tsx index 109acd285e28..540cc063137a 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/analytics-card.tsx +++ b/cvat-ui/src/components/analytics-overview/analytics-card.tsx @@ -2,8 +2,6 @@ // // SPDX-License-Identifier: MIT -import '../styles.scss'; - import React from 'react'; import Text from 'antd/lib/typography/Text'; import { Col, Row } from 'antd/lib/grid'; diff --git a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx index 34b6c61a9241..485d9d3c345c 100644 --- a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx +++ b/cvat-ui/src/components/analytics-overview/analytics-overview.tsx @@ -5,14 +5,24 @@ import './styles.scss'; import React from 'react'; -import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; import moment from 'moment'; +import Text from 'antd/lib/typography/Text'; +import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; import HistogramView from './histogram-view'; +import AnalyticsCard from './analytics-card'; interface Props { report: AnalyticsReport | null; } +const colors = [ + 'rgba(255, 99, 132, 0.5)', + 'rgba(53, 162, 235, 0.5)', + 'rgba(170, 83, 85, 0.5)', + 'rgba(44, 70, 94, 0.5)', + 'rgba(28, 66, 98, 0.5)', +]; + function AnalyticsOverview(props: Props): JSX.Element | null { const { report } = props; @@ -22,7 +32,14 @@ function AnalyticsOverview(props: Props): JSX.Element | null { const views = Object.entries(report.statistics).map(([name, entry]) => { switch (entry.defaultView) { case AnalyticsEntryViewType.NUMERIC: { - return
numeric
; + return ( + {entry.description}} + /> + ); } case AnalyticsEntryViewType.HISTOGRAM: { const firstDataset = Object.keys(entry.dataseries)[0]; @@ -30,26 +47,28 @@ function AnalyticsOverview(props: Props): JSX.Element | null { moment.utc(dataEntry.datetime).local().format('YYYY-MM-DD HH:mm:ss') )); + let colorIndex = -1; const datasets = Object.entries(entry.dataseries).map(([key, series]) => { let label = key.split('_').join(' '); label = label.charAt(0).toUpperCase() + label.slice(1); - const data = series.map((s) => { - if (Number.isFinite(s.value)) { - return s.value; + const data: number[] = series.map((s) => { + if (Number.isInteger(s.value)) { + return s.value as number; } if (typeof s.value === 'object') { - return Object.keys(s.value).reduce((acc, key) => acc + s.value[key], 0); + return Object.keys(s.value).reduce((acc, _key) => acc + s.value[_key], 0); } + + return 0; }); + colorIndex += 1; return { label, data, - // Just random now - // eslint-disable-next-line no-bitwise - backgroundColor: `#${(Math.random() * 0xFFFFFF << 0).toString(16)}`, + backgroundColor: colors[colorIndex], }; }); return ( diff --git a/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx b/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx index 14cca1e884f4..204607d050e5 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx +++ b/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx @@ -10,7 +10,7 @@ import { QualityReport, QualitySummary, Task } from 'cvat-core-wrapper'; import { useSelector } from 'react-redux'; import { CombinedState } from 'reducers'; import { Col, Row } from 'antd/lib/grid'; -import AnalyticsCard from './analytics-card'; +import AnalyticsCard from '../../analytics-overview/analytics-card'; import { percent, clampValue } from './common'; interface Props { diff --git a/cvat-ui/src/components/task-analytics-page/quality/issues.tsx b/cvat-ui/src/components/task-analytics-page/quality/issues.tsx index 19048b33bb99..9521e08f8472 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/issues.tsx +++ b/cvat-ui/src/components/task-analytics-page/quality/issues.tsx @@ -9,7 +9,7 @@ import Text from 'antd/lib/typography/Text'; import notification from 'antd/lib/notification'; import { Task } from 'cvat-core-wrapper'; import { useIsMounted } from 'utils/hooks'; -import AnalyticsCard from './analytics-card'; +import AnalyticsCard from '../../analytics-overview/analytics-card'; import { percent, clampValue } from './common'; interface Props { diff --git a/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx b/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx index 95847d274e70..d4d15735d37e 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx +++ b/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx @@ -13,7 +13,7 @@ import { CombinedState } from 'reducers'; import Button from 'antd/lib/button'; import { DownloadOutlined, MoreOutlined } from '@ant-design/icons'; import { analyticsActions } from 'actions/analytics-actions'; -import AnalyticsCard from './analytics-card'; +import AnalyticsCard from '../../analytics-overview/analytics-card'; import { toRepresentation } from './common'; interface Props { From eda75e8de27cf67bc3e4dec7f28dc58683c44366 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 16:53:38 +0300 Subject: [PATCH 07/59] unified analytics component --- .../components/analytics-overview/styles.scss | 0 .../analytics-card.tsx | 0 .../analytics-overview.tsx | 1 + .../analytics-page/analytics-page.tsx | 351 ++++++++++++++++++ .../histogram-view.tsx | 0 .../quality/common.ts | 0 .../quality/empty-job.tsx | 0 .../quality/gt-conflicts.tsx | 2 +- .../quality/issues.tsx | 2 +- .../quality/job-list.tsx | 0 .../quality/mean-quality.tsx | 2 +- .../quality/quality-settings-modal.tsx | 0 .../quality/task-quality-component.tsx | 0 .../styles.scss | 2 +- cvat-ui/src/components/cvat-app.tsx | 8 +- .../project-analytics-page.tsx | 145 -------- .../project-analytics-page/styles.scss | 0 .../task-analytics-page.tsx | 143 ------- 18 files changed, 360 insertions(+), 296 deletions(-) delete mode 100644 cvat-ui/src/components/analytics-overview/styles.scss rename cvat-ui/src/components/{analytics-overview => analytics-page}/analytics-card.tsx (100%) rename cvat-ui/src/components/{analytics-overview => analytics-page}/analytics-overview.tsx (98%) create mode 100644 cvat-ui/src/components/analytics-page/analytics-page.tsx rename cvat-ui/src/components/{analytics-overview => analytics-page}/histogram-view.tsx (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/common.ts (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/empty-job.tsx (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/gt-conflicts.tsx (98%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/issues.tsx (96%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/job-list.tsx (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/mean-quality.tsx (98%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/quality-settings-modal.tsx (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/quality/task-quality-component.tsx (100%) rename cvat-ui/src/components/{task-analytics-page => analytics-page}/styles.scss (99%) delete mode 100644 cvat-ui/src/components/project-analytics-page/project-analytics-page.tsx delete mode 100644 cvat-ui/src/components/project-analytics-page/styles.scss delete mode 100644 cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx diff --git a/cvat-ui/src/components/analytics-overview/styles.scss b/cvat-ui/src/components/analytics-overview/styles.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/cvat-ui/src/components/analytics-overview/analytics-card.tsx b/cvat-ui/src/components/analytics-page/analytics-card.tsx similarity index 100% rename from cvat-ui/src/components/analytics-overview/analytics-card.tsx rename to cvat-ui/src/components/analytics-page/analytics-card.tsx diff --git a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx b/cvat-ui/src/components/analytics-page/analytics-overview.tsx similarity index 98% rename from cvat-ui/src/components/analytics-overview/analytics-overview.tsx rename to cvat-ui/src/components/analytics-page/analytics-overview.tsx index 485d9d3c345c..dd37a85f78fb 100644 --- a/cvat-ui/src/components/analytics-overview/analytics-overview.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-overview.tsx @@ -38,6 +38,7 @@ function AnalyticsOverview(props: Props): JSX.Element | null { value={entry.dataseries[Object.keys(entry.dataseries)[0]][0].value as number} size={12} bottomElement={{entry.description}} + key={name} /> ); } diff --git a/cvat-ui/src/components/analytics-page/analytics-page.tsx b/cvat-ui/src/components/analytics-page/analytics-page.tsx new file mode 100644 index 000000000000..ac05c92e9f75 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/analytics-page.tsx @@ -0,0 +1,351 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useCallback, useEffect, useState } from 'react'; +import { useHistory, useLocation, useParams } from 'react-router'; +import { Row, Col } from 'antd/lib/grid'; +import Tabs from 'antd/lib/tabs'; +import Text from 'antd/lib/typography/Text'; +import Title from 'antd/lib/typography/Title'; +import Spin from 'antd/lib/spin'; +import Button from 'antd/lib/button'; +import notification from 'antd/lib/notification'; +import { LeftOutlined } from '@ant-design/icons/lib/icons'; +import { useIsMounted } from 'utils/hooks'; +import { Project, Task } from 'reducers'; +import { AnalyticsReport, Job, getCore } from 'cvat-core-wrapper'; +import AnalyticsOverview from './analytics-overview'; +import TaskQualityComponent from './quality/task-quality-component'; + +const core = getCore(); + +function AnalyticsPage(): JSX.Element { + const location = useLocation(); + let instanceType = ''; + if (location.pathname.includes('projects')) { + instanceType = 'project'; + } else if (location.pathname.includes('jobs')) { + instanceType = 'job'; + } else { + instanceType = 'task'; + } + + const [fetching, setFetching] = useState(true); + const [instance, setInstance] = useState(null); + const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); + const isMounted = useIsMounted(); + + const history = useHistory(); + + let instanceID: number | null = null; + let reportRequestID: number | null = null; + switch (instanceType) { + case 'project': + case 'task': + { + instanceID = +useParams<{ id: string }>().id; + reportRequestID = +useParams<{ id: string }>().id; + break; + } + case 'job': { + instanceID = +useParams<{ tid: string }>().tid; + reportRequestID = +useParams<{ jid: string }>().jid; + break; + } + default: { + throw Error(`Unsupported instance type ${instanceType}`); + } + } + + const receieveInstance = (): void => { + let instanceRequest = null; + switch (instanceType) { + case 'project': { + instanceRequest = core.projects.get({ id: instanceID }); + break; + } + case 'task': + case 'job': + { + instanceRequest = core.tasks.get({ id: instanceID }); + break; + } + default: { + throw Error(`Unsupported instance type ${instanceType}`); + } + } + + if (Number.isInteger(instanceID)) { + instanceRequest + .then(([_instance]: Task[] | Project[]) => { + if (isMounted() && _instance) { + setInstance(_instance); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested instance from the server', + description: error.toString(), + }); + } + }).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); + } else { + notification.error({ + message: 'Could not receive the requested task from the server', + description: `Requested "${instanceID}" is not valid`, + }); + setFetching(false); + } + }; + + const receieveReport = (): void => { + if (Number.isInteger(instanceID) && Number.isInteger(reportRequestID)) { + let reportRequest = null; + switch (instanceType) { + case 'project': { + reportRequest = core.analytics.common.reports({ projectID: reportRequestID }); + break; + } + case 'task': { + reportRequest = core.analytics.common.reports({ taskID: reportRequestID }); + break; + } + case 'job': { + reportRequest = core.analytics.common.reports({ jobID: reportRequestID }); + break; + } + default: { + throw Error(`Unsupported instance type ${instanceType}`); + } + } + + reportRequest + .then((report: AnalyticsReport) => { + if (isMounted() && report) { + setAnalyticsReportInstance(report); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested report from the server', + description: error.toString(), + }); + } + }); + } + }; + + useEffect((): void => { + receieveInstance(); + receieveReport(); + }, []); + + const onJobUpdate = useCallback((job: Job): void => { + setFetching(true); + job.save().then(() => { + if (isMounted()) { + receieveInstance(); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not update the job', + description: error.toString(), + }); + } + }).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); + }, [notification]); + + let backNavigation: JSX.Element | null = null; + let title: JSX.Element | null = null; + let tabs: JSX.Element | null = null; + if (instance) { + switch (instanceType) { + case 'project': { + backNavigation = ( + + + + ); + title = ( + + + {instance.name} + + + {`#${instance.id}`} + + + ); + tabs = ( + + + Overview + + )} + key='Overview' + > + + + + ); + break; + } + case 'task': { + backNavigation = ( + + + + ); + title = ( + + + {instance.name} + + + {`#${instance.id}`} + + + ); + tabs = ( + + + Overview + + )} + key='overview' + > + + + + Quality + + )} + key='quality' + > + + + + ); + break; + } + case 'job': + { + backNavigation = ( + + + + ); + title = ( + + + Job + {' '} + {`#${instance.id}`} + {' '} + of + {' '} + {instance.name} + + + ); + tabs = ( + + + Overview + + )} + key='overview' + > + + + + ); + break; + } + default: { + throw Error(`Unsupported instance type ${instanceType}`); + } + } + } + + return ( +
+ { + fetching ? ( +
+ +
+ ) : ( + + {backNavigation} + + {title} + {tabs} + + + ) + } +
+ ); +} + +export default React.memo(AnalyticsPage); diff --git a/cvat-ui/src/components/analytics-overview/histogram-view.tsx b/cvat-ui/src/components/analytics-page/histogram-view.tsx similarity index 100% rename from cvat-ui/src/components/analytics-overview/histogram-view.tsx rename to cvat-ui/src/components/analytics-page/histogram-view.tsx diff --git a/cvat-ui/src/components/task-analytics-page/quality/common.ts b/cvat-ui/src/components/analytics-page/quality/common.ts similarity index 100% rename from cvat-ui/src/components/task-analytics-page/quality/common.ts rename to cvat-ui/src/components/analytics-page/quality/common.ts diff --git a/cvat-ui/src/components/task-analytics-page/quality/empty-job.tsx b/cvat-ui/src/components/analytics-page/quality/empty-job.tsx similarity index 100% rename from cvat-ui/src/components/task-analytics-page/quality/empty-job.tsx rename to cvat-ui/src/components/analytics-page/quality/empty-job.tsx diff --git a/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx b/cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx similarity index 98% rename from cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx rename to cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx index 204607d050e5..64831929b3d0 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/gt-conflicts.tsx +++ b/cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx @@ -10,7 +10,7 @@ import { QualityReport, QualitySummary, Task } from 'cvat-core-wrapper'; import { useSelector } from 'react-redux'; import { CombinedState } from 'reducers'; import { Col, Row } from 'antd/lib/grid'; -import AnalyticsCard from '../../analytics-overview/analytics-card'; +import AnalyticsCard from '../../analytics-page/analytics-card'; import { percent, clampValue } from './common'; interface Props { diff --git a/cvat-ui/src/components/task-analytics-page/quality/issues.tsx b/cvat-ui/src/components/analytics-page/quality/issues.tsx similarity index 96% rename from cvat-ui/src/components/task-analytics-page/quality/issues.tsx rename to cvat-ui/src/components/analytics-page/quality/issues.tsx index 9521e08f8472..099d24edc4df 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/issues.tsx +++ b/cvat-ui/src/components/analytics-page/quality/issues.tsx @@ -9,7 +9,7 @@ import Text from 'antd/lib/typography/Text'; import notification from 'antd/lib/notification'; import { Task } from 'cvat-core-wrapper'; import { useIsMounted } from 'utils/hooks'; -import AnalyticsCard from '../../analytics-overview/analytics-card'; +import AnalyticsCard from '../../analytics-page/analytics-card'; import { percent, clampValue } from './common'; interface Props { diff --git a/cvat-ui/src/components/task-analytics-page/quality/job-list.tsx b/cvat-ui/src/components/analytics-page/quality/job-list.tsx similarity index 100% rename from cvat-ui/src/components/task-analytics-page/quality/job-list.tsx rename to cvat-ui/src/components/analytics-page/quality/job-list.tsx diff --git a/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx b/cvat-ui/src/components/analytics-page/quality/mean-quality.tsx similarity index 98% rename from cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx rename to cvat-ui/src/components/analytics-page/quality/mean-quality.tsx index d4d15735d37e..76ecc1d8b7f4 100644 --- a/cvat-ui/src/components/task-analytics-page/quality/mean-quality.tsx +++ b/cvat-ui/src/components/analytics-page/quality/mean-quality.tsx @@ -13,7 +13,7 @@ import { CombinedState } from 'reducers'; import Button from 'antd/lib/button'; import { DownloadOutlined, MoreOutlined } from '@ant-design/icons'; import { analyticsActions } from 'actions/analytics-actions'; -import AnalyticsCard from '../../analytics-overview/analytics-card'; +import AnalyticsCard from '../../analytics-page/analytics-card'; import { toRepresentation } from './common'; interface Props { diff --git a/cvat-ui/src/components/task-analytics-page/quality/quality-settings-modal.tsx b/cvat-ui/src/components/analytics-page/quality/quality-settings-modal.tsx similarity index 100% rename from cvat-ui/src/components/task-analytics-page/quality/quality-settings-modal.tsx rename to cvat-ui/src/components/analytics-page/quality/quality-settings-modal.tsx diff --git a/cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx b/cvat-ui/src/components/analytics-page/quality/task-quality-component.tsx similarity index 100% rename from cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx rename to cvat-ui/src/components/analytics-page/quality/task-quality-component.tsx diff --git a/cvat-ui/src/components/task-analytics-page/styles.scss b/cvat-ui/src/components/analytics-page/styles.scss similarity index 99% rename from cvat-ui/src/components/task-analytics-page/styles.scss rename to cvat-ui/src/components/analytics-page/styles.scss index bf29074cd2b9..69d6e3dc3173 100644 --- a/cvat-ui/src/components/task-analytics-page/styles.scss +++ b/cvat-ui/src/components/analytics-page/styles.scss @@ -97,7 +97,7 @@ right: $grid-unit-size * 4; } -.cvat-task-analytics-page { +.cvat-analytics-page { height: 100%; } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index c4b424f60459..60d2152d79ff 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -76,9 +76,8 @@ import EmailVerificationSentPage from './email-confirmation-pages/email-verifica import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation'; import CreateModelPage from './create-model-page/create-model-page'; import CreateJobPage from './create-job-page/create-job-page'; -import TaskAnalyticsPage from './task-analytics-page/task-analytics-page'; import OrganizationWatcher from './watchers/organization-watcher'; -import ProjectAnalyticsPage from './project-analytics-page/project-analytics-page'; +import AnalyticsPage from './analytics-page/analytics-page'; interface CVATAppProps { loadFormats: () => void; @@ -464,14 +463,15 @@ class CVATApplication extends React.PureComponent - + - + + (null); - const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); - const isMounted = useIsMounted(); - - const history = useHistory(); - - const id = +useParams<{ id: string }>().id; - - const receieveProject = (): void => { - if (Number.isInteger(id)) { - core.projects.get({ id }) - .then(([project]: Project[]) => { - if (isMounted() && project) { - setProjectInstance(project); - } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not receive the requested project from the server', - description: error.toString(), - }); - } - }).finally(() => { - if (isMounted()) { - setFetchingProject(false); - } - }); - } else { - notification.error({ - message: 'Could not receive the requested project from the server', - description: `Requested task id "${id}" is not valid`, - }); - setFetchingProject(false); - } - }; - - // TODO make it via get Analytics report action after Honeypot merge - const receieveReport = (): void => { - if (Number.isInteger(id)) { - core.analytics.common.reports({ projectID: id }) - .then((report: AnalyticsReport) => { - if (isMounted() && report) { - setAnalyticsReportInstance(report); - } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not receive the requested report from the server', - description: error.toString(), - }); - } - }); - } - }; - console.log(analyticsReportInstance); - - useEffect((): void => { - receieveProject(); - receieveReport(); - }, []); - - return ( -
- { - fetchingProject ? ( -
- -
- ) : ( - - - - - - - - {projectInstance.name} - - - {`#${projectInstance.id}`} - - - - - Overview - - )} - key='Overview' - > - - - - - - ) - } -
- ); -} - -export default React.memo(ProjectAnalyticsPage); diff --git a/cvat-ui/src/components/project-analytics-page/styles.scss b/cvat-ui/src/components/project-analytics-page/styles.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx b/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx deleted file mode 100644 index f0c3f593cc6a..000000000000 --- a/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import './styles.scss'; - -import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory, useParams } from 'react-router'; -import { Row, Col } from 'antd/lib/grid'; -import Tabs from 'antd/lib/tabs'; -import Text from 'antd/lib/typography/Text'; -import Title from 'antd/lib/typography/Title'; -import Spin from 'antd/lib/spin'; -import Button from 'antd/lib/button'; -import notification from 'antd/lib/notification'; -import { LeftOutlined } from '@ant-design/icons/lib/icons'; -import { useIsMounted } from 'utils/hooks'; -import { Task } from 'reducers'; -import { Job, getCore } from 'cvat-core-wrapper'; -import TaskQualityComponent from './quality/task-quality-component'; - -const core = getCore(); - -function TaskAnalyticsPage(): JSX.Element { - const [fetchingTask, setFetchingTask] = useState(true); - const [taskInstance, setTaskInstance] = useState(null); - const isMounted = useIsMounted(); - - const history = useHistory(); - - const id = +useParams<{ id: string }>().id; - - const receieveTask = (): void => { - if (Number.isInteger(id)) { - core.tasks.get({ id }) - .then(([task]: Task[]) => { - if (isMounted() && task) { - setTaskInstance(task); - } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not receive the requested task from the server', - description: error.toString(), - }); - } - }).finally(() => { - if (isMounted()) { - setFetchingTask(false); - } - }); - } else { - notification.error({ - message: 'Could not receive the requested task from the server', - description: `Requested task id "${id}" is not valid`, - }); - setFetchingTask(false); - } - }; - - const onJobUpdate = useCallback((job: Job): void => { - setFetchingTask(true); - job.save().then(() => { - if (isMounted()) { - receieveTask(); - } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not update the job', - description: error.toString(), - }); - } - }).finally(() => { - if (isMounted()) { - setFetchingTask(false); - } - }); - }, [notification]); - - useEffect((): void => { - receieveTask(); - }, []); - - return ( -
- { - fetchingTask ? ( -
- -
- ) : ( - - - - - - - - {taskInstance.name} - - - {`#${taskInstance.id}`} - - - - - Quality - - )} - key='quality' - > - - - - - - ) - } -
- ); -} - -export default React.memo(TaskAnalyticsPage); From 19cf3214f5de7341cea5dc881955863ef096cefe Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 16:57:44 +0300 Subject: [PATCH 08/59] added analytics links to all menus --- .../components/job-item/job-actions-menu.tsx | 3 +++ .../components/projects-page/actions-menu.tsx | 23 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cvat-ui/src/components/job-item/job-actions-menu.tsx b/cvat-ui/src/components/job-item/job-actions-menu.tsx index 9a751a126412..e7911dd05541 100644 --- a/cvat-ui/src/components/job-item/job-actions-menu.tsx +++ b/cvat-ui/src/components/job-item/job-actions-menu.tsx @@ -57,6 +57,8 @@ function JobActionsMenu(props: Props): JSX.Element { dispatch(importActions.openImportDatasetModal(job)); } else if (action.key === 'export_job') { dispatch(exportActions.openExportDatasetModal(job)); + } else if (action.key === 'view_analytics') { + history.push(`/tasks/${job.taskId}/jobs/${job.id}/analytics`); } else if (action.key === 'renew_job') { job.state = core.enums.JobState.NEW; job.stage = JobStage.ANNOTATION; @@ -81,6 +83,7 @@ function JobActionsMenu(props: Props): JSX.Element { Go to the bug tracker Import annotations Export annotations + View analytics {[JobStage.ANNOTATION, JobStage.VALIDATION].includes(job.stage) ? Finish the job : null} {job.stage === JobStage.ACCEPTANCE ? diff --git a/cvat-ui/src/components/projects-page/actions-menu.tsx b/cvat-ui/src/components/projects-page/actions-menu.tsx index 7110a467b839..111bae2ae8be 100644 --- a/cvat-ui/src/components/projects-page/actions-menu.tsx +++ b/cvat-ui/src/components/projects-page/actions-menu.tsx @@ -69,6 +69,25 @@ function ProjectActionsMenuComponent(props: Props): JSX.Element { ), 20]); + menuItems.push([( + + { + e.preventDefault(); + history.push({ + pathname: `/projects/${projectInstance.id}/analytics`, + }); + return false; + }} + > + View analytics + + + ), 30]); + menuItems.push([( - ), 30]); + ), 40]); menuItems.push([( @@ -93,7 +112,7 @@ function ProjectActionsMenuComponent(props: Props): JSX.Element { Delete - ), 40]); + ), 50]); menuItems.push( ...plugins.map(({ component: Component, weight }, index) => { From 7d37b9cb17807b97b1752d232c3deff8d4e965e8 Mon Sep 17 00:00:00 2001 From: klakhov Date: Mon, 26 Jun 2023 17:36:12 +0300 Subject: [PATCH 09/59] added time interval select --- cvat-core/src/api-implementation.ts | 5 +- .../analytics-page/analytics-overview.tsx | 43 +++++++++++- .../analytics-page/analytics-page.tsx | 68 ++++++++++++++++--- .../analytics-page/quality/gt-conflicts.tsx | 2 +- .../analytics-page/quality/issues.tsx | 2 +- .../analytics-page/quality/mean-quality.tsx | 2 +- .../{ => views}/analytics-card.tsx | 0 .../{ => views}/histogram-view.tsx | 2 - 8 files changed, 106 insertions(+), 18 deletions(-) rename cvat-ui/src/components/analytics-page/{ => views}/analytics-card.tsx (100%) rename cvat-ui/src/components/analytics-page/{ => views}/histogram-view.tsx (97%) diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 928886708e21..231ec91c31ad 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -404,23 +404,26 @@ export default function implementAPI(cvat) { const result = await serverProxy.frames.getMeta(type, id); return new FramesMetaData({ ...result }); }; - + cvat.analytics.common.reports.implementation = async (filter) => { let updatedParams: Record = {}; if ('taskID' in filter) { updatedParams = { task_id: filter.reportId, + ...filter, }; } if ('jobID' in filter) { updatedParams = { job_id: filter.reportId, + ...filter, }; } if ('projectID' in filter) { updatedParams = { project_id: filter.reportId, + ...filter, }; } diff --git a/cvat-ui/src/components/analytics-page/analytics-overview.tsx b/cvat-ui/src/components/analytics-page/analytics-overview.tsx index dd37a85f78fb..67dacc753141 100644 --- a/cvat-ui/src/components/analytics-page/analytics-overview.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-overview.tsx @@ -7,12 +7,22 @@ import './styles.scss'; import React from 'react'; import moment from 'moment'; import Text from 'antd/lib/typography/Text'; +import Select from 'antd/lib/select'; import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; -import HistogramView from './histogram-view'; -import AnalyticsCard from './analytics-card'; +import { Col, Row } from 'antd/lib/grid'; +import HistogramView from './views/histogram-view'; +import AnalyticsCard from './views/analytics-card'; + +export enum DateIntervals { + LAST_WEEK = 'Last 7 days', + LAST_MONTH = 'Last 30 days', + LAST_QUARTER = 'Last 90 days', + LAST_YEAR = 'Last 365 days', +} interface Props { report: AnalyticsReport | null; + onTimePeriodChange: (val: DateIntervals) => void; } const colors = [ @@ -24,7 +34,7 @@ const colors = [ ]; function AnalyticsOverview(props: Props): JSX.Element | null { - const { report } = props; + const { report, onTimePeriodChange } = props; // TODO make it more expressive if (!report) return null; @@ -89,6 +99,33 @@ function AnalyticsOverview(props: Props): JSX.Element | null { return (
+ + + Date: Wed, 19 Jul 2023 14:17:11 +0300 Subject: [PATCH 49/59] Fixed hash for shapes and tags --- cvat-core/src/annotations-collection.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 8755867c4e79..cd382b0b4b85 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -800,6 +800,7 @@ export default class Collection { frame: state.frame, label_id: state.label.id, group: 0, + source: state.source, }); } else { checkObjectType('state occluded', state.occluded, 'boolean', null); @@ -825,6 +826,7 @@ export default class Collection { frame: state.frame, group: 0, label_id: state.label.id, + outside: state.outside || false, occluded: state.occluded || false, points: state.shapeType === 'mask' ? (() => { const { width, height } = this.frameMeta[state.frame]; From f52a5046a2d9e0781d6b9b8fac85ee34a0ee9912 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 19 Jul 2023 14:19:05 +0300 Subject: [PATCH 50/59] Updated version of cvat-core --- cvat-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-core/package.json b/cvat-core/package.json index b35b9122a9f0..5aca1d1ab763 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "9.2.0", + "version": "9.2.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { From 4c4401d341ced36674594bc123e4be53bc50c832 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 19 Jul 2023 14:34:17 +0300 Subject: [PATCH 51/59] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 604f34deb964..f83b5d856715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - \[SDK\] Ability to create attributes with blank default values () - \[SDK\] SDK should not change input data in models () +- Export hash for shapes and tags in a corner case () - 3D job can not be opened in validation mode () - Memory leak related to unclosed av container () From 9023f9ac100e9bfadc812d8ecf0ce9b5649dee4e Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 19 Jul 2023 14:43:04 +0300 Subject: [PATCH 52/59] title --- .../analytics-page/analytics-page.tsx | 38 +++++++++---------- .../src/components/analytics-page/styles.scss | 18 +++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/cvat-ui/src/components/analytics-page/analytics-page.tsx b/cvat-ui/src/components/analytics-page/analytics-page.tsx index 596b9f26f335..bb0a54d98888 100644 --- a/cvat-ui/src/components/analytics-page/analytics-page.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-page.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useLocation, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; import { Row, Col } from 'antd/lib/grid'; import Tabs from 'antd/lib/tabs'; import Text from 'antd/lib/typography/Text'; @@ -53,7 +54,7 @@ function AnalyticsPage(): JSX.Element { } const [fetching, setFetching] = useState(true); - const [instance, setInstance] = useState(null); + const [instance, setInstance] = useState(null); const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); const isMounted = useIsMounted(); @@ -71,7 +72,7 @@ function AnalyticsPage(): JSX.Element { break; } case 'job': { - instanceID = +useParams<{ tid: string }>().tid; + instanceID = +useParams<{ jid: string }>().jid; reportRequestID = +useParams<{ jid: string }>().jid; break; } @@ -87,10 +88,13 @@ function AnalyticsPage(): JSX.Element { instanceRequest = core.projects.get({ id: instanceID }); break; } - case 'task': + case 'task': { + instanceRequest = core.tasks.get({ id: instanceID }); + break; + } case 'job': { - instanceRequest = core.tasks.get({ id: instanceID }); + instanceRequest = core.jobs.get({ jobID: instanceID }); break; } default: { @@ -100,7 +104,7 @@ function AnalyticsPage(): JSX.Element { if (Number.isInteger(instanceID)) { instanceRequest - .then(([_instance]: Task[] | Project[]) => { + .then(([_instance]: Task[] | Project[] | Job[]) => { if (isMounted() && _instance) { setInstance(_instance); } @@ -222,11 +226,10 @@ function AnalyticsPage(): JSX.Element { title = ( - {instance.name} + Analytics for + {' '} + <Link to={`/projects/${instance.id}`}>{`Project #${instance.id}`}</Link> - - {`#${instance.id}`} - ); tabs = ( @@ -257,11 +260,10 @@ function AnalyticsPage(): JSX.Element { title = ( - {instance.name} + Analytics for + {' '} + <Link to={`/tasks/${instance.id}`}>{`Task #${instance.id}`}</Link> - - {`#${instance.id}`} - ); tabs = ( @@ -301,15 +303,11 @@ function AnalyticsPage(): JSX.Element { ); title = ( - + - Job - {' '} - {`#${instance.id}`} - {' '} - of + Analytics for {' '} - {instance.name} + <Link to={`/tasks/${instance.taskId}/jobs/${instance.id}`}>{`Job #${instance.id}`}</Link> ); diff --git a/cvat-ui/src/components/analytics-page/styles.scss b/cvat-ui/src/components/analytics-page/styles.scss index b25cc6e25f32..0825b0c8ad63 100644 --- a/cvat-ui/src/components/analytics-page/styles.scss +++ b/cvat-ui/src/components/analytics-page/styles.scss @@ -108,6 +108,15 @@ padding-bottom: $grid-unit-size * 2; } +.cvat-project-analytics-title { + margin-bottom: $grid-unit-size; + + h4 { + display: inline; + margin-right: $grid-unit-size; + } +} + .cvat-task-analytics-title { margin-bottom: $grid-unit-size; @@ -117,6 +126,15 @@ } } +.cvat-job-analytics-title { + margin-bottom: $grid-unit-size; + + h4 { + display: inline; + margin-right: $grid-unit-size; + } +} + .cvat-task-quality-reports-hint { margin-bottom: $grid-unit-size * 3; } From 341698ad34fe08096bdedb1a8ddba37e5183a877 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 19 Jul 2023 15:21:09 +0300 Subject: [PATCH 53/59] Fixed export hash for skeletons --- cvat-core/src/annotations-collection.ts | 2 +- cvat-core/src/annotations-objects.ts | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index cd382b0b4b85..ac3e9d127d2a 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -891,7 +891,7 @@ export default class Collection { frame: state.frame, type: element.shapeType, points: [...element.points], - zOrder: state.zOrder, + z_order: state.zOrder, outside: element.outside || false, occluded: element.occluded || false, rotation: element.rotation || 0, diff --git a/cvat-core/src/annotations-objects.ts b/cvat-core/src/annotations-objects.ts index 8b8f3e702448..99212d38bc90 100644 --- a/cvat-core/src/annotations-objects.ts +++ b/cvat-core/src/annotations-objects.ts @@ -1956,6 +1956,7 @@ export class SkeletonShape extends Shape { group: this.group, z_order: this.zOrder, rotation: 0, + elements: undefined, })); const result: RawShapeData = { @@ -2891,7 +2892,24 @@ export class SkeletonTrack extends Track { // Method is used to export data to the server public toJSON(): RawTrackData { const result: RawTrackData = Track.prototype.toJSON.call(this); - result.elements = this.elements.map((el) => el.toJSON()); + result.elements = this.elements.map((el) => ({ + ...el.toJSON(), + elements: undefined, + source: this.source, + group: this.group, + })); + result.elements.forEach((element) => { + element.shapes.forEach((shape) => { + shape.rotation = 0; + const { frame } = shape; + const skeletonShape = result.shapes + .find((_skeletonShape) => _skeletonShape.frame === frame); + if (skeletonShape) { + shape.z_order = skeletonShape.z_order; + } + }); + }); + return result; } From e9ab3101ef7f4540ad8caacbc7e1fabb0d7f7a19 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 19 Jul 2023 19:08:22 +0300 Subject: [PATCH 54/59] fix --- cvat/apps/dataset_manager/task.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index e9dfeb5da771..ce047a438fd7 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -452,7 +452,11 @@ def _delete(self, data=None): deleted_shapes += labeledshape_set.delete()[0] deleted_shapes += labeledtrack_set.delete()[0] - deleted_data = data + deleted_data = { + "tags": data["tags"], + "shapes": data["shapes"], + "tracks": data["tracks"], + } if deleted_shapes: self._set_updated_date() From dbb962a32fe32b023e2732cf7838b41b74e544ec Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 19 Jul 2023 21:31:51 +0300 Subject: [PATCH 55/59] linter --- cvat/apps/analytics_report/report/get.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cvat/apps/analytics_report/report/get.py b/cvat/apps/analytics_report/report/get.py index 3681cc1ff429..82622e5921a1 100644 --- a/cvat/apps/analytics_report/report/get.py +++ b/cvat/apps/analytics_report/report/get.py @@ -38,8 +38,9 @@ def _convert_datetime_to_date(statistics): del df["datetime"] return statistics + def _clamp_working_time(statistics): - affected_metrics = ("annotation_speed") + affected_metrics = "annotation_speed" for metric in statistics: if metric["name"] not in affected_metrics: continue @@ -50,6 +51,7 @@ def _clamp_working_time(statistics): return statistics + def _get_object_report(obj_model, pk, start_date, end_date): try: db_obj = obj_model.objects.get(pk=pk) From 215798be167ddb847ba6b164dba16934abf43119 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 20 Jul 2023 13:23:49 +0300 Subject: [PATCH 56/59] v9.3.0 --- cvat-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-core/package.json b/cvat-core/package.json index 5aca1d1ab763..d8970e107443 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "9.2.1", + "version": "9.3.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { From 458b446af95371b92e0402318a15ddb5b082f3a3 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 20 Jul 2023 13:24:05 +0300 Subject: [PATCH 57/59] v1.54.0 --- cvat-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/package.json b/cvat-ui/package.json index b2e061979b09..0274e31bb68e 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.53.4", + "version": "1.54.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { From 43538b24d27975094d9438ff32faa18e64e7f05c Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 20 Jul 2023 13:25:57 +0300 Subject: [PATCH 58/59] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ec9d655517..842f85099508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - \[SDK\] `cvat_sdk.datasets`, a framework-agnostic equivalent of `cvat_sdk.pytorch` () +- Analytics for Jobs, Tasks and Projects () ### Changed - TDB From 9202238cff915a48deec5039e45e93ee32e6ea92 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Thu, 20 Jul 2023 17:50:03 +0300 Subject: [PATCH 59/59] apply comments --- cvat/apps/analytics_report/report/create.py | 2 +- .../derived_metrics/annotation_speed.py | 10 -------- .../report/derived_metrics/objects.py | 19 ++++++--------- .../report/primary_metrics/objects.py | 8 +++---- cvat/apps/analytics_report/serializers.py | 24 +++++++++++++++---- cvat/schema.yml | 21 +++++++++++++++- 6 files changed, 51 insertions(+), 33 deletions(-) diff --git a/cvat/apps/analytics_report/report/create.py b/cvat/apps/analytics_report/report/create.py index a7db325e4497..3a8da9dca55a 100644 --- a/cvat/apps/analytics_report/report/create.py +++ b/cvat/apps/analytics_report/report/create.py @@ -522,7 +522,7 @@ def _compute_report_for_project( ), ] - statistics = [self._get_empty_statistics_entry(dm) for dm in derived_metrics] + statistics = [self._get_statistics_entry(dm) for dm in derived_metrics] db_report.statistics = statistics return db_report, task_reports, job_reports diff --git a/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py b/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py index 84aead890c1f..5835c3774e0f 100644 --- a/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py +++ b/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py @@ -6,7 +6,6 @@ from dateutil import parser -from cvat.apps.analytics_report.models import BinaryOperatorType, TransformOperationType from cvat.apps.analytics_report.report.primary_metrics import JobAnnotationSpeed from .base import DerivedMetricBase @@ -15,15 +14,6 @@ class TaskAnnotationSpeed(DerivedMetricBase, JobAnnotationSpeed): _description = "Metric shows the annotation speed in objects per hour for the Task." _query = None - _transformations = [ - { - "name": "annotation_speed", - "type": TransformOperationType.BINARY, - "left": "object_count", - "operator": BinaryOperatorType.DIVISION, - "right": "working_time", - }, - ] def calculate(self): combined_statistics = {} diff --git a/cvat/apps/analytics_report/report/derived_metrics/objects.py b/cvat/apps/analytics_report/report/derived_metrics/objects.py index 3679c4015a40..07b62d4c58b5 100644 --- a/cvat/apps/analytics_report/report/derived_metrics/objects.py +++ b/cvat/apps/analytics_report/report/derived_metrics/objects.py @@ -26,15 +26,14 @@ def calculate(self): entry = combined_statistics.setdefault( parser.parse(c_entry["datetime"]).date(), { - "created": {"tags": 0, "shapes": 0, "tracks": 0}, - "updated": {"tags": 0, "shapes": 0, "tracks": 0}, - "deleted": {"tags": 0, "shapes": 0, "tracks": 0}, + "created": 0, + "updated": 0, + "deleted": 0, }, ) - for t in ("tags", "shapes", "tracks"): - entry["created"][t] += c_entry["value"][t] - entry["updated"][t] += u_entry["value"][t] - entry["deleted"][t] += d_entry["value"][t] + entry["created"] += c_entry["value"] + entry["updated"] += u_entry["value"] + entry["deleted"] += d_entry["value"] combined_data_series = { "created": [], @@ -50,11 +49,7 @@ def calculate(self): for action in ("created", "updated", "deleted"): combined_data_series[action].append( { - "value": { - "tracks": combined_statistics[key][action]["tracks"], - "shapes": combined_statistics[key][action]["shapes"], - "tags": combined_statistics[key][action]["tags"], - }, + "value": combined_statistics[key][action], "datetime": timestamp_str, } ) diff --git a/cvat/apps/analytics_report/report/primary_metrics/objects.py b/cvat/apps/analytics_report/report/primary_metrics/objects.py index dca8fab0593a..d7d369ca146e 100644 --- a/cvat/apps/analytics_report/report/primary_metrics/objects.py +++ b/cvat/apps/analytics_report/report/primary_metrics/objects.py @@ -41,11 +41,9 @@ def calculate(self): for date in sorted(dates): objects_statistics[action].append( { - "value": { - "tracks": statistics[action]["tracks"].get(date, 0), - "shapes": statistics[action]["shapes"].get(date, 0), - "tags": statistics[action]["tags"].get(date, 0), - }, + "value": sum( + statistics[action][t].get(date, 0) for t in ["tracks", "shapes", "tags"] + ), "datetime": date.isoformat() + "Z", } ) diff --git a/cvat/apps/analytics_report/serializers.py b/cvat/apps/analytics_report/serializers.py index 066b7464aabb..309119dbd405 100644 --- a/cvat/apps/analytics_report/serializers.py +++ b/cvat/apps/analytics_report/serializers.py @@ -14,14 +14,30 @@ class BinaryOperationSerializer(serializers.Serializer): - left = serializers.CharField(required=False, allow_null=True) + left = serializers.CharField( + required=False, + allow_null=True, + help_text="The name of the data series used as the left (first) operand of the binary operation.", + ) operator = serializers.ChoiceField(choices=BinaryOperatorType.choices()) - right = serializers.CharField(required=False, allow_null=True) + right = serializers.CharField( + required=False, + allow_null=True, + help_text="The name of the data series used as the right (second) operand of the binary operation.", + ) class TransformationSerializer(serializers.Serializer): name = serializers.CharField() - binary = BinaryOperationSerializer(required=False, allow_null=True) + binary = BinaryOperationSerializer( + required=False, + allow_null=True, + ) + + +class DataFrameSerializer(serializers.Serializer): + value = serializers.FloatField() + date = serializers.DateField() class MetricSerializer(serializers.Serializer): @@ -32,7 +48,7 @@ class MetricSerializer(serializers.Serializer): choices=GranularityChoice.choices(), required=False, allow_null=True ) default_view = serializers.ChoiceField(choices=ViewChoice.choices()) - data_series = serializers.DictField() + data_series = serializers.DictField(child=DataFrameSerializer(many=True)) transformations = serializers.ListField(child=TransformationSerializer()) diff --git a/cvat/schema.yml b/cvat/schema.yml index b263bf8ca8e8..8daba12a02b0 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -6226,11 +6226,15 @@ components: left: type: string nullable: true + description: The name of the data series used as the left (first) operand + of the binary operation. operator: $ref: '#/components/schemas/OperatorEnum' right: type: string nullable: true + description: The name of the data series used as the right (second) operand + of the binary operation. required: - operator ChunkType: @@ -6440,6 +6444,18 @@ components: * `KEY_FILE_PATH` - KEY_FILE_PATH * `ANONYMOUS_ACCESS` - ANONYMOUS_ACCESS * `CONNECTION_STRING` - CONNECTION_STRING + DataFrame: + type: object + properties: + value: + type: number + format: double + date: + type: string + format: date + required: + - date + - value DataMetaRead: type: object properties: @@ -7776,7 +7792,10 @@ components: $ref: '#/components/schemas/DefaultViewEnum' data_series: type: object - additionalProperties: {} + additionalProperties: + type: array + items: + $ref: '#/components/schemas/DataFrame' transformations: type: array items: