From 6b9b276691237db3a1a849cccbd059fd3c082730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=C2=AA=20Fern=C3=A1ndez?= Date: Thu, 11 Apr 2024 01:04:02 +0200 Subject: [PATCH 1/5] Both community and project views are now able to show the same tabs. Now it is possible to show a Markdown description in community entries, and to show the events, datasets and tools in project entries. --- pages/benchmarking/_community/index.vue | 21 +++++- pages/projects/_project/index.vue | 97 ++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/pages/benchmarking/_community/index.vue b/pages/benchmarking/_community/index.vue index f2e6662e..9c69293b 100644 --- a/pages/benchmarking/_community/index.vue +++ b/pages/benchmarking/_community/index.vue @@ -43,12 +43,17 @@ Tools + + mdi-information-outline + Summary + + + + + + + + + + @@ -89,6 +106,7 @@ From 614efa0c911a118f08aed4a9c1eeef5e48ff36fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=C2=AA=20Fern=C3=A1ndez?= Date: Thu, 11 Apr 2024 02:58:33 +0200 Subject: [PATCH 2/5] Added new challenge participant view, to show raw numeric results table --- .../ChallengeParticipantMetricsTable.vue | 63 +++++ .../CommunityClassificationTable.vue | 3 + nuxt.config.js | 13 + package.json | 2 +- .../{_id.spec.js => _id/index.spec.js} | 0 .../_community/{_id.vue => _id/index.vue} | 27 +- .../_community/_id/participants.spec.js | 142 ++++++++++ .../_community/_id/participants.vue | 214 ++++++++++++++ store/challenge.js | 263 +++++++++++++++++- 9 files changed, 704 insertions(+), 23 deletions(-) create mode 100644 components/Challenges/ChallengeParticipantMetricsTable.vue rename pages/benchmarking/_community/{_id.spec.js => _id/index.spec.js} (100%) rename pages/benchmarking/_community/{_id.vue => _id/index.vue} (84%) create mode 100644 pages/benchmarking/_community/_id/participants.spec.js create mode 100644 pages/benchmarking/_community/_id/participants.vue diff --git a/components/Challenges/ChallengeParticipantMetricsTable.vue b/components/Challenges/ChallengeParticipantMetricsTable.vue new file mode 100644 index 00000000..31d92e8d --- /dev/null +++ b/components/Challenges/ChallengeParticipantMetricsTable.vue @@ -0,0 +1,63 @@ + + + diff --git a/components/Communities/CommunityClassificationTable.vue b/components/Communities/CommunityClassificationTable.vue index 2a0cca9d..4129ebcd 100644 --- a/components/Communities/CommunityClassificationTable.vue +++ b/components/Communities/CommunityClassificationTable.vue @@ -53,6 +53,9 @@ {{ item.acronym }} + ( + participants ) diff --git a/nuxt.config.js b/nuxt.config.js index 6a6ebeae..bafb29c0 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -203,6 +203,19 @@ export default { path: '/scientific/:community/:id', component: resolve(__dirname, 'pages/benchmarking/_community/_id'), }); + routes.push({ + name: 'scientific-community-challenge-participants', + path: '/scientific/:community/:id/participants', + component: resolve( + __dirname, + 'pages/benchmarking/_community/_id/participants' + ), + }); + // routes.push({ + // name: 'scientific-community-challenge-participants', + // path: '/benchmarking/:community/:id/participants', + // component: resolve(__dirname, 'pages/benchmarking/_community/_id/participants'), + // }); }, }, }; diff --git a/package.json b/package.json index ac95b0b3..2a04a848 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openEBench-nuxt", - "version": "1.0.3", + "version": "1.0.4", "private": true, "author": "Dominik Brüchner , José Mª Fernández ", "scripts": { diff --git a/pages/benchmarking/_community/_id.spec.js b/pages/benchmarking/_community/_id/index.spec.js similarity index 100% rename from pages/benchmarking/_community/_id.spec.js rename to pages/benchmarking/_community/_id/index.spec.js diff --git a/pages/benchmarking/_community/_id.vue b/pages/benchmarking/_community/_id/index.vue similarity index 84% rename from pages/benchmarking/_community/_id.vue rename to pages/benchmarking/_community/_id/index.vue index c3fb01d5..7657b3df 100644 --- a/pages/benchmarking/_community/_id.vue +++ b/pages/benchmarking/_community/_id/index.vue @@ -7,18 +7,18 @@ />

- {{ challenge.acronym }} + {{ challenge.challenge_label }} ({{ challenge._id }})

{{ challenge.name }}

In this 2D plot two metrics from the challenge - {{ challenge.acronym }} are represented in the X and Y axis, showing the - results from the participating tools in this challenge. The gray line - represents the pareto frontier, which runs over the participants tools, - showing the best efficiency, while the arrow in the plot represents the - optimal corner. + {{ challenge.challenge_label }} are represented in the X and Y axis, + showing the results from the participating tools in this challenge. The + gray line represents the pareto frontier, which runs over the + participants tools, showing the best efficiency, while the arrow in the + plot represents the optimal corner.

The menu button above the diagram can be used to switch between the @@ -85,7 +85,7 @@ import ChartBarplotVisualizerWrapper from '~/components/Widgets/ChartBarplotVisu import ChartScatterVisualizerWrapper from '~/components/Widgets/ChartScatterVisualizerWrapper'; export default { - name: 'CommunityParticipantPage', + name: 'CommunityChallengePlotsPage', components: { ChartBarplotVisualizerWrapper, ChartScatterVisualizerWrapper }, data() { return { @@ -117,12 +117,13 @@ export default { to: '/benchmarking', }, { - text: this.community.acronym - ? this.community.acronym - : this.$route.params.community + ' Events', + text: + (this.community.acronym + ? this.community.acronym + : this.$route.params.community) + ' Events', disabled: false, exact: true, - to: 'events', + to: './events', }, { text: this.currentEvent @@ -130,10 +131,10 @@ export default { : this.$route.params.community + ' Results', disabled: false, exact: true, - to: './', + to: './?event=' + this.$route.params.id, }, { - text: this.challenge ? this.challenge.acronym : '', + text: this.challenge ? this.challenge.challenge_label : '', disabled: true, to: this.$route.params.id, }, diff --git a/pages/benchmarking/_community/_id/participants.spec.js b/pages/benchmarking/_community/_id/participants.spec.js new file mode 100644 index 00000000..d485a610 --- /dev/null +++ b/pages/benchmarking/_community/_id/participants.spec.js @@ -0,0 +1,142 @@ +import { mount } from '@vue/test-utils'; +import Challenge from './_id.vue'; +import MockCommunity from '~/test/unit/mockData/Community'; +import MockEvent from '~/test/unit/mockData/Event'; +import MockEvents from '~/test/unit/mockData/Events'; +import MockChallenge from '~/test/unit/mockData/Challenge'; +/* +import ChartBarplotVisualizerWrapper from '~/components/Widgets/ChartBarplotVisualizerWrapper'; +import ChartScatterVisualizerWrapper from '~/components/Widgets/ChartScatterVisualizerWrapper'; +*/ + +const factory = (mockStore) => { + return mount(Challenge, { + ...createComponentMocks({ store: mockStore }), + mocks: { + $route: { params: { community: 'TESTCOMMUNITY', id: 'TESTID' } }, + $config: { OEB_LEGACY_ANGULAR_URI: 'https://jest-openebench.bsc.es/' }, + }, + }); +}; + +describe('Community Participant', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const mockStore = { + community: { + getters: { + events: () => { + return MockEvents; + }, + currentEvent: () => { + return MockEvent; + }, + participants: () => { + return []; + }, + tools: () => { + return []; + }, + community: () => { + return MockCommunity; + }, + getEventById: () => { + return () => MockEvent; + }, + }, + actions: { + setCurrentEvent: jest.fn(), + getCommunity: jest.fn(), + }, + mutations: { + setCommunity: jest.fn(), + setEvents: jest.fn(), + setCurrentEvent: jest.fn(), + setParticipants: jest.fn(), + setTools: jest.fn(), + setLoading: jest.fn(), + }, + state: () => { + return { + loading: { + events: false, + tools: false, + participants: false, + community: false, + }, + community: MockCommunity, + }; + }, + }, + challenge: { + getters: { + participantsList: () => { + return []; + }, + challenge: () => { + return {}; + }, + }, + actions: { + getChallenge: jest.fn(), + getParticipants: jest.fn(), + }, + mutations: { + setParticipants: jest.fn(), + setChallenge: jest.fn(), + setLoading: jest.fn(), + }, + state: () => { + return { + loading: { + challenge: false, + participants: false, + }, + challenge: MockChallenge, + }; + }, + }, + }; + + it('is instantiated', async () => { + const wrapper = factory(mockStore); + expect(wrapper).toBeTruthy(); + await wrapper.vm.$nextTick(); + }); + + it('calls store actions on mount', () => { + const wrapper = factory(mockStore); + expect(wrapper).toBeTruthy(); + + expect(mockStore.challenge.actions.getChallenge).toHaveBeenCalled(); + }); + + /* + it('renders the bar-plot component with the right ID and JSON data structure for rendering', () => { + mockStore.challenge.getters.datasetsList = () => { + return MockChallengeDatasetsBarplot; + }; + const wrapper = factory(mockStore); + expect(wrapper).toBeTruthy(); + const barplot = wrapper.findComponent(ChartBarplotVisualizerWrapper); // => finds Bar by `name` + expect(barplot.exists()).toBe(true); + expect(barplot.props().id).toBe(MockChallengeDatasetsBarplot[0]._id); + expect(barplot.props().data).toBe( + MockChallengeDatasetsBarplot[0].graphData + ); + }); + + it('renders the scatter-plot component with the right ID', () => { + mockStore.challenge.getters.datasetsList = () => { + return MockChallengeDatasetsScatter; + }; + const wrapper = factory(mockStore); + expect(wrapper).toBeTruthy(); + const scatter = wrapper.findComponent(ChartScatterVisualizerWrapper); // => finds Bar by `name` + expect(scatter.exists()).toBe(true); + expect(scatter.props().id).toBe(MockChallengeDatasetsScatter[0]._id); + }); + */ +}); diff --git a/pages/benchmarking/_community/_id/participants.vue b/pages/benchmarking/_community/_id/participants.vue new file mode 100644 index 00000000..2fbea700 --- /dev/null +++ b/pages/benchmarking/_community/_id/participants.vue @@ -0,0 +1,214 @@ + + + diff --git a/store/challenge.js b/store/challenge.js index eb91208b..57fded33 100644 --- a/store/challenge.js +++ b/store/challenge.js @@ -1,10 +1,19 @@ +const PARTICIPANT_ID_KEY = 'level_2:participant_id'; +const EXCLUDE_PARTICIPANT_KEY = 'level_2:exclude_participant'; +const CHALLENGE_LABEL_KEY = 'level_2:challenge_id'; +const METRIC_ID_KEY = 'level_2:metric_id'; + export default { state: () => { return { datasets: [], + participants: {}, challenge: {}, + metrics: [], loading: { datasets: false, + participants: false, + metrics: false, challenge: false, }, }; @@ -14,6 +23,8 @@ export default { async getChallenge({ commit, dispatch }, params) { commit('setLoading', { challenge: true }); commit('setLoading', { datasets: true }); + commit('setLoading', { participants: true }); + commit('setLoading', { metrics: true }); const response = await this.$graphql.$post('/graphql', { query: ` @@ -28,6 +39,42 @@ export default { tool_id } } + participant_datasets: datasets(datasetFilters: {type: "participant"}) { + _id + orig_id + datalink { + inline_data + schema_url + uri + schema_uri + } + depends_on { + tool_id + metrics_id + rel_dataset_ids { + dataset_id + } + } + _metadata + } + assessment_datasets: datasets(datasetFilters: {type: "assessment"}) { + _id + orig_id + datalink { + inline_data + schema_url + uri + schema_uri + } + depends_on { + tool_id + metrics_id + rel_dataset_ids { + dataset_id + } + } + _metadata + } } getDatasets( @@ -38,6 +85,13 @@ export default { } _id } + + getMetrics { + _id + orig_id + representation_hints + _metadata + } } `, variables: { @@ -45,17 +99,95 @@ export default { }, }); + // Tidying up _metadata + response.data.getChallenges.forEach((challenge) => { + if ( + typeof challenge._metadata === 'string' || + challenge._metadata instanceof String + ) { + challenge._metadata = JSON.parse(challenge._metadata); + } + + challenge.participant_datasets.forEach((participant) => { + if ( + typeof participant.datalink.inline_data === 'string' || + participant.datalink.inline_data instanceof String + ) { + participant.datalink.inline_data = JSON.parse( + participant.datalink.inline_data + ); + } + if ( + typeof participant._metadata === 'string' || + participant._metadata instanceof String + ) { + participant._metadata = JSON.parse(participant._metadata); + } + }); + + challenge.assessment_datasets.forEach((assessment) => { + if ( + typeof assessment.datalink.inline_data === 'string' || + assessment.datalink.inline_data instanceof String + ) { + assessment.datalink.inline_data = JSON.parse( + assessment.datalink.inline_data + ); + } + if ( + typeof assessment._metadata === 'string' || + assessment._metadata instanceof String + ) { + assessment._metadata = JSON.parse(assessment._metadata); + } + }); + }); + response.data.getDatasets.forEach((aggregation) => { + if ( + typeof aggregation.datalink.inline_data === 'string' || + aggregation.datalink.inline_data instanceof String + ) { + aggregation.datalink.inline_data = JSON.parse( + aggregation.datalink.inline_data + ); + } + if ( + typeof aggregation._metadata === 'string' || + aggregation._metadata instanceof String + ) { + aggregation._metadata = JSON.parse(aggregation._metadata); + } + }); + response.data.getMetrics.forEach((metric) => { + if ( + typeof metric.representation_hints === 'string' || + metric.representation_hints instanceof String + ) { + metric.representation_hints = JSON.parse(metric.representation_hints); + } + if ( + typeof metric._metadata === 'string' || + metric._metadata instanceof String + ) { + metric._metadata = JSON.parse(metric._metadata); + } + }); + commit('setChallenge', response.data); commit('setLoading', { challenge: false }); - commit('setDatasets', response.data); + commit('setDatasets', response.data.getDatasets); + commit('setParticipants', response.data.getChallenges[0]); + commit('setLoading', { participants: false }); + + commit('setMetrics', response.data.getMetrics); + commit('setLoading', { metrics: false }); dispatch('getGraphData'); }, getGraphData({ state, commit }) { - let i = 0; - state.datasets.forEach(async (dataset) => { + state.datasets.forEach(async (dataset, i, datasets) => { const id = dataset._id; // TODO remove if statement, once more visualizations are consuming the widget endpoint const response = @@ -70,8 +202,7 @@ export default { : []; await commit('setDatasetGraphData', { id, response }); - i++; - if (i === state.datasets.length) + if (i + 1 === datasets.length) commit('setLoading', { datasets: false }); }); @@ -81,14 +212,119 @@ export default { }, mutations: { - setDatasets(state, payload) { - state.datasets = payload.getDatasets.map((dataset) => { - dataset.datalink.inline_data = JSON.parse(dataset.datalink.inline_data); - return dataset; + setDatasets(state, datasets) { + state.datasets = datasets; + }, + setMetrics(state, metrics) { + state.metrics = metrics.map((metric) => { + const allLabels = []; + let proposedLabel; + if (metric._metadata) { + proposedLabel = metric._metadata[METRIC_ID_KEY]; + if (proposedLabel !== undefined) { + allLabels.push(proposedLabel); + } + } + if (metric.orig_id) { + const colonPos = metric.orig_id.indexOf(':'); + if (colonPos >= 0) { + // Removing prefix + proposedLabel = metric.orig_id.substring(colonPos + 1); + if (proposedLabel !== undefined) { + allLabels.push(proposedLabel); + } + } + allLabels.push(metric.orig_id); + } + + metric.metrics_label = allLabels[0]; + + return metric; }); }, setChallenge(state, payload) { state.challenge = payload.getChallenges[0]; + + // Obtaining the challenge label + let challengeLabel; + if (state.challenge._metadata) { + challengeLabel = state.challenge._metadata[CHALLENGE_LABEL_KEY]; + } + + if (state.challenge.acronym) { + challengeLabel = state.challenge.acronym; + } + + if (challengeLabel === undefined) { + let chaOrigId = state.challenge.orig_id; + if (!chaOrigId) { + chaOrigId = state.challenge._id; + } + const colonPos = chaOrigId.indexOf(':'); + if (colonPos >= 0) { + // Removing prefix + challengeLabel = chaOrigId.substring(colonPos + 1); + } else { + challengeLabel = chaOrigId; + } + + // TODO: remove benchmarking event label prefix + } + + state.challenge.challenge_label = challengeLabel; + }, + setParticipants(state, challenge) { + // Store and index all the participant datasets, and their related + // assessments + state.participants = {}; + challenge.participant_datasets.forEach((participant) => { + // Skip excluded participants + if ( + participant._metadata && + participant._metadata[EXCLUDE_PARTICIPANT_KEY] + ) { + return; + } + + state.participants[participant._id] = participant; + participant.assessments = []; + + // Obtaining the participant label + let parLabel; + if (participant._metadata) { + parLabel = participant._metadata[PARTICIPANT_ID_KEY]; + } + + if (parLabel === undefined) { + let parOrigId = participant.orig_id; + if (!parOrigId) { + parOrigId = participant._id; + } + const colonPos = parOrigId.indexOf(':'); + if (colonPos >= 0) { + // Removing prefix + parLabel = parOrigId.substring(colonPos + 1); + } else { + parLabel = parOrigId; + } + + // Removing suffix + if (parLabel.endsWith('_P')) { + parLabel = parLabel.substring(0, parLabel.length - 2); + } + } + + participant.participant_label = parLabel; + }); + challenge.assessment_datasets.forEach((assessment) => { + assessment.depends_on.rel_dataset_ids.forEach((participantRef) => { + if (participantRef.dataset_id in state.participants) { + state.participants[participantRef.dataset_id].assessments.push( + assessment + ); + } + }); + }); }, setDatasetGraphData(state, { id, response }) { state.datasets.find((x) => x._id === id).graphData = response; @@ -100,6 +336,15 @@ export default { getters: { datasetsList: (state) => state.datasets, + participants: (state) => { + const participants = Object.values(state.participants).sort((a, b) => + a.participant_label.localeCompare(b.participant_label, { + sensitivity: 'base', + }) + ); + return participants; + }, + metrics: (state) => state.metrics, challenge: (state) => state.challenge, }, }; From 889f6f45238608f235b6b0a4ebacca534fac0e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=C2=AA=20Fern=C3=A1ndez?= Date: Thu, 11 Apr 2024 12:23:18 +0200 Subject: [PATCH 3/5] Updated package-lock.json --- package-lock.json | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 078c8b19..5e3c704b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openEBench-nuxt", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openEBench-nuxt", - "version": "1.0.3", + "version": "1.0.4", "dependencies": { "@inb/oeb-chart-barplot": "^1.2.0", "@inb/oeb-chart-scatter": "^1.1.4", @@ -11038,13 +11038,23 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001294", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz", - "integrity": "sha512-LiMlrs1nSKZ8qkNhpUf5KD0Al1KCBE3zaT7OLOwEkagXMEDij98SiOovn9wxVGQpklk9vVC/pUSqgYmkmKOS8g==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "version": "1.0.30001608", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", + "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] }, "node_modules/capture-exit": { "version": "2.0.0", From bd07b012ca95d33004cc31ab3226ad039452573b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=C2=AA=20Fern=C3=A1ndez?= Date: Thu, 11 Apr 2024 13:08:39 +0200 Subject: [PATCH 4/5] Fixed issues in tests, introduced by code reordering. --- pages/benchmarking/_community/_id/index.spec.js | 2 +- pages/benchmarking/_community/_id/participants.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/benchmarking/_community/_id/index.spec.js b/pages/benchmarking/_community/_id/index.spec.js index 330f3a6b..8d386741 100644 --- a/pages/benchmarking/_community/_id/index.spec.js +++ b/pages/benchmarking/_community/_id/index.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Challenge from './_id.vue'; +import Challenge from './index.vue'; import MockCommunity from '~/test/unit/mockData/Community'; import MockEvent from '~/test/unit/mockData/Event'; import MockEvents from '~/test/unit/mockData/Events'; diff --git a/pages/benchmarking/_community/_id/participants.spec.js b/pages/benchmarking/_community/_id/participants.spec.js index d485a610..9a1c951d 100644 --- a/pages/benchmarking/_community/_id/participants.spec.js +++ b/pages/benchmarking/_community/_id/participants.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Challenge from './_id.vue'; +import Challenge from './index.vue'; import MockCommunity from '~/test/unit/mockData/Community'; import MockEvent from '~/test/unit/mockData/Event'; import MockEvents from '~/test/unit/mockData/Events'; From 9cd0264aae5db1904b5f530b65560960df9ec0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=C2=AA=20Fern=C3=A1ndez?= Date: Thu, 11 Apr 2024 13:09:10 +0200 Subject: [PATCH 5/5] Fix issue in test code, due having integrated parts from community to project. --- pages/projects/_project/index.spec.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pages/projects/_project/index.spec.js b/pages/projects/_project/index.spec.js index c2fb6d30..3103f52d 100644 --- a/pages/projects/_project/index.spec.js +++ b/pages/projects/_project/index.spec.js @@ -1,6 +1,8 @@ import { shallowMount } from '@vue/test-utils'; import Index from './index.vue'; import MockCommunity from '~/test/unit/mockData/Community'; +import MockDatasets from '~/test/unit/mockData/Datasets'; +import MockTools from '~/test/unit/mockData/Tools'; jest.mock('marked', () => ({ marked: jest.fn(() => 'foo') })); const factory = (mockStore, route) => { @@ -13,7 +15,7 @@ const factory = (mockStore, route) => { }); }; -describe('Index.vue', () => { +describe('index.vue', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -27,6 +29,12 @@ describe('Index.vue', () => { community: () => { return MockCommunity; }, + datasets: () => { + return MockDatasets; + }, + tools: () => { + return MockTools; + }, }, actions: { getCommunity: jest.fn() }, state: () => { @@ -38,6 +46,8 @@ describe('Index.vue', () => { community: false, }, community: MockCommunity, + datasets: MockDatasets, + tools: MockTools, }; }, },