diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue
index 71fe3210173dec40229b9db6c39dba7a18888619..3c9f65eb83a3da77ee3af23938004bb821066001 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/dora_chart.vue
@@ -1,12 +1,12 @@
 <script>
 import { GlLoadingIcon } from '@gitlab/ui';
-import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue';
+import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue';
 import GroupOrProjectProvider from 'ee/analytics/dashboards/components/group_or_project_provider.vue';
 
 export default {
   name: 'DoraChart',
   components: {
-    ComparisonChart,
+    FilterableComparisonChart,
     GlLoadingIcon,
     GroupOrProjectProvider,
   },
@@ -22,23 +22,38 @@ export default {
       default: () => ({}),
     },
   },
+  computed: {
+    filters() {
+      const { filters: { labels = [], excludeMetrics = [] } = {} } = this.data;
+      return {
+        labels,
+        excludeMetrics,
+      };
+    },
+  },
+  methods: {
+    webUrl(group, project, isProject) {
+      return isProject ? project.webUrl : group.webUrl;
+    },
+  },
 };
 </script>
 
 <template>
   <group-or-project-provider
-    #default="{ isProject, isNamespaceLoading }"
+    #default="{ isProject, isNamespaceLoading, group, project }"
     :full-path="data.namespace"
   >
     <div v-if="isNamespaceLoading" class="gl--flex-center gl-h-full">
       <gl-loading-icon size="lg" />
     </div>
-    <comparison-chart
+    <filterable-comparison-chart
       v-else
-      :request-path="data.namespace"
+      :namespace="data.namespace"
+      :filters="filters"
       :is-project="isProject"
-      :exclude-metrics="data.excludeMetrics"
-      :filter-labels="data.filterLabels"
+      :is-loading="isNamespaceLoading"
+      :web-url="webUrl(group, project, isProject)"
       @set-errors="$emit('set-errors', $event)"
     />
   </group-or-project-provider>
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js
index 4a67cdbe766636c308583e9711fe6ffb81284805..1caae2f47c3a0f5856360810f7978aba32e6e37b 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/usage_overview.js
@@ -84,11 +84,10 @@ export const prepareQuery = (queryKeysToInclude = []) => {
  */
 export const fetch = async ({
   rootNamespace: { requestPath: fullPath },
-  query: { include = [] },
+  queryOverrides: { filters: { include = USAGE_OVERVIEW_IDENTIFIERS } = {} } = {},
 }) => {
   const variableOverrides = prepareQuery(include);
   const { startDate, endDate } = USAGE_OVERVIEW_DEFAULT_DATE_RANGE;
-
   try {
     const { data = {} } = await defaultClient.query({
       query: getUsageOverviewQuery,
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js
index 1635ed4bc19cffaa070b7a652d37f3b4729928f5..3510ed8727168d200834819de788b06eb5484a83 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/value_stream.js
@@ -1,4 +1,5 @@
 import { sprintf, s__ } from '~/locale';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 
 const I18N_VSD_DORA_METRICS_PANEL_TITLE = s__('DORA4Metrics|Metrics comparison for %{name}');
 
@@ -7,10 +8,13 @@ const generatePanelTitle = ({ namespace: { name } }) => {
 };
 
 export const fetch = ({ title, namespace, query, queryOverrides = {} }) => {
-  return {
-    namespace,
-    title: title || generatePanelTitle({ namespace }),
-    ...query,
-    ...queryOverrides,
-  };
+  return convertObjectPropsToCamelCase(
+    {
+      namespace,
+      title: title || generatePanelTitle({ namespace }),
+      ...query,
+      ...queryOverrides,
+    },
+    { deep: true },
+  );
 };
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js
index 77290952210332d0210339db99633bb930783007..dfd28c2d103beb0d1635db1abb5e2a46e7e912cb 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/index.js
@@ -44,6 +44,7 @@ export default () => {
     rootNamespaceFullPath,
     dataSourceClickhouse,
     aiGenerateCubeQueryEnabled,
+    topicsExploreProjectsPath,
   } = el.dataset;
 
   const analyticsDashboardPointer = buildAnalyticsDashboardPointer(analyticsDashboardPointerJSON);
@@ -122,6 +123,7 @@ export default () => {
       rootNamespaceFullPath,
       dataSourceClickhouse: parseBoolean(dataSourceClickhouse),
       currentUserId: window.gon?.current_user_id,
+      topicsExploreProjectsPath,
     },
     render(h) {
       return h(DashboardsApp);
diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue b/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue
index cc4c83b12e46614e85f77b7dcc236380f1cf551e..51b397778fa2dd40b9f1f0315abfc4906a7fa1a4 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue
+++ b/ee/app/assets/javascripts/analytics/dashboards/components/dora_performers_score_chart.vue
@@ -131,7 +131,8 @@ export default {
       );
     },
     filterProjectTopics() {
-      return validateProjectTopics(this.data?.filter_project_topics || []);
+      const { filters: { projectTopics = [] } = {} } = this.data;
+      return validateProjectTopics(projectTopics);
     },
     hasFilterProjectTopics() {
       return this.filterProjectTopics.length > 0;
diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue b/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue
new file mode 100644
index 0000000000000000000000000000000000000000..df39ee0e9dcd5592ad74213bdce33169b08d026a
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/dashboards/components/filterable_comparison_chart.vue
@@ -0,0 +1,120 @@
+<script>
+import { uniq, flatten, uniqBy } from 'lodash';
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import filterLabelsQueryBuilder, { LABEL_PREFIX } from '../graphql/filter_labels_query_builder';
+import { DASHBOARD_LABELS_LOAD_ERROR, METRICS_WITHOUT_LABEL_FILTERING } from '../constants';
+import ComparisonChart from './comparison_chart.vue';
+import ComparisonChartLabels from './comparison_chart_labels.vue';
+
+export default {
+  name: 'FilterableComparisonChart',
+  components: {
+    ComparisonChart,
+    ComparisonChartLabels,
+    GlSkeletonLoader,
+  },
+  props: {
+    namespace: {
+      type: String,
+      required: true,
+    },
+    webUrl: {
+      type: String,
+      required: true,
+    },
+    filters: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    isLoading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    isProject: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  apollo: {
+    filterLabelsResults: {
+      query() {
+        return filterLabelsQueryBuilder(this.filterLabelsQuery, this.isProject);
+      },
+      variables() {
+        return {
+          fullPath: this.namespace,
+        };
+      },
+      skip() {
+        return !this.hasFilterLabelsQuery || !this.namespace;
+      },
+      update(data) {
+        const labels = Object.entries(data.namespace || {})
+          .filter(([key]) => key.includes(LABEL_PREFIX))
+          .map(([, { nodes }]) => nodes);
+        return uniqBy(flatten(labels), ({ id }) => id);
+      },
+      error() {
+        this.hasLabelErrors = true;
+        const labels = this.filterLabelsQuery.join(', ');
+        this.$emit('set-errors', { errors: [sprintf(DASHBOARD_LABELS_LOAD_ERROR, { labels })] });
+      },
+    },
+  },
+  data() {
+    return {
+      filterLabelsResults: [],
+      hasLabelErrors: false,
+    };
+  },
+  computed: {
+    loading() {
+      return this.isLoading || this.$apollo.queries.filterLabelsResults.loading;
+    },
+    filterLabelsQuery() {
+      return this.filters?.labels;
+    },
+    hasFilterLabelsQuery() {
+      return this.filterLabelsQuery.length;
+    },
+    hasFilterLabels() {
+      return this.filterLabelsResults.length > 0;
+    },
+    filterLabelNames() {
+      return this.filterLabelsResults.map(({ title }) => title);
+    },
+    excludeMetrics() {
+      let metrics = this.filters?.excludeMetrics;
+      if (this.hasFilterLabels) {
+        metrics = [...metrics, ...METRICS_WITHOUT_LABEL_FILTERING];
+      }
+      return uniq(metrics);
+    },
+  },
+};
+</script>
+<template>
+  <div v-if="loading">
+    <gl-skeleton-loader :lines="1" />
+  </div>
+  <div v-else>
+    <div class="gl-text-right gl-py-2">
+      <comparison-chart-labels
+        v-if="filterLabelsResults.length"
+        :labels="filterLabelsResults"
+        :web-url="webUrl"
+      />
+    </div>
+    <comparison-chart
+      v-if="!hasLabelErrors"
+      :request-path="namespace"
+      :is-project="isProject"
+      :exclude-metrics="excludeMetrics"
+      :filter-labels="filterLabelNames"
+    />
+  </div>
+</template>
diff --git a/ee/app/views/groups/analytics/dashboards/index.html.haml b/ee/app/views/groups/analytics/dashboards/index.html.haml
index f5bdd595661735b72e085a039ef54bd49257c6fb..051a8e33efe1dc3cdc5eab16fdcd19260ab78550 100644
--- a/ee/app/views/groups/analytics/dashboards/index.html.haml
+++ b/ee/app/views/groups/analytics/dashboards/index.html.haml
@@ -1,4 +1,4 @@
 - page_title s_('Analytics|Analytics dashboards')
 - breadcrumb_title s_("Analytics|Analytics dashboards")
 
-#js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s }) }
+#js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s, topics_explore_projects_path: topics_explore_projects_path }) }
diff --git a/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml b/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml
index f962174871d2930b971cae9b0de8cec2e1f32c68..e9aaa22da76f698863cbe24a2582c89618d9efb6 100644
--- a/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml
+++ b/ee/app/views/groups/analytics/dashboards/value_streams_dashboard.html.haml
@@ -5,6 +5,6 @@
   - breadcrumb_title s_('Analytics|Analytics dashboards')
 
 - if Feature.enabled?(:group_analytics_dashboard_dynamic_vsd, @group)
-  #js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s }) }
+  #js-analytics-dashboards-list-app{ data: analytics_dashboards_list_app_data(@group).merge({ available_visualizations: @available_visualizations&.to_json, data_source_clickhouse: @data_source_clickhouse.to_s, topics_explore_projects_path: topics_explore_projects_path }) }
 - else
   #js-analytics-dashboards-app{ data: { full_path: @group.full_path, namespaces: @namespaces.to_json, pointer_project: @pointer_project&.to_json, available_visualizations: @available_visualizations&.to_json, topics_explore_projects_path: topics_explore_projects_path, data_source_clickhouse: @data_source_clickhouse.to_s } }
diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js
index 49c1217c955c70a55c87d04d585bb5d19fb55df9..c832d8322fe343718364a7dd51270981001516f0 100644
--- a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js
+++ b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/usage_overview_spec.js
@@ -23,7 +23,7 @@ describe('Usage overview Data Source', () => {
 
   const rootNamespace = { name: 'cool namespace', requestPath: 'some-group-path' };
   const queryKeys = [USAGE_OVERVIEW_IDENTIFIER_GROUPS, USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS];
-  const mockQuery = { include: queryKeys };
+  const mockQuery = { filters: { include: queryKeys } };
   const { group: mockGroupUsageMetricsQueryResponse } = mockUsageMetricsQueryResponse;
   const identifiers = [
     USAGE_OVERVIEW_IDENTIFIER_GROUPS,
@@ -91,7 +91,7 @@ describe('Usage overview Data Source', () => {
     `('$label queries the top level group', async ({ namespace }) => {
       jest.spyOn(defaultClient, 'query').mockResolvedValue({ data: {} });
 
-      obj = await fetch({ rootNamespace, namespace, query: mockQuery });
+      obj = await fetch({ rootNamespace, namespace, queryOverrides: mockQuery });
 
       expect(defaultClient.query).toHaveBeenCalledWith(
         expect.objectContaining({
@@ -114,7 +114,7 @@ describe('Usage overview Data Source', () => {
 
       obj = await fetch({
         rootNamespace,
-        query: { include: [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS] },
+        queryOverrides: { filters: { include: [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS] } },
       });
 
       expect(defaultClient.query).toHaveBeenCalledWith(
@@ -135,8 +135,8 @@ describe('Usage overview Data Source', () => {
 
     it.each`
       label                              | data                             | params
-      ${'with no data'}                  | ${{}}                            | ${{ rootNamespace, query: mockQuery }}
-      ${'with no namespace.requestPath'} | ${mockUsageMetricsQueryResponse} | ${{ rootNamespace: {}, query: mockQuery }}
+      ${'with no data'}                  | ${{}}                            | ${{ rootNamespace, queryOverrides: mockQuery }}
+      ${'with no namespace.requestPath'} | ${mockUsageMetricsQueryResponse} | ${{ rootNamespace: {}, queryOverrides: mockQuery }}
     `('$label returns the no data object', async ({ params }) => {
       jest.spyOn(defaultClient, 'query').mockResolvedValue({ data: {} });
 
@@ -149,7 +149,7 @@ describe('Usage overview Data Source', () => {
       beforeEach(() => {
         jest.spyOn(defaultClient, 'query').mockRejectedValue();
 
-        obj = fetch({ rootNamespace, query: mockQuery });
+        obj = fetch({ rootNamespace, queryOverrides: mockQuery });
       });
 
       it('returns the no data object', async () => {
@@ -163,7 +163,7 @@ describe('Usage overview Data Source', () => {
           .spyOn(defaultClient, 'query')
           .mockResolvedValue({ data: mockUsageMetricsQueryResponse });
 
-        obj = await fetch({ rootNamespace, query: mockQuery });
+        obj = await fetch({ rootNamespace, queryOverrides: mockQuery });
       });
 
       it('will fetch the usage metrics', () => {
diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js
index 3dd8768550be18395b762481cac1828b2c0817bf..aa8577480f71a1a9416f85549a3c8e76bd04b351 100644
--- a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js
+++ b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/value_stream_spec.js
@@ -3,9 +3,9 @@ import { fetch } from 'ee/analytics/analytics_dashboards/data_sources/value_stre
 describe('Value Stream Data Source', () => {
   let obj;
 
-  const query = { exclude_metrics: [] };
-  const queryOverrides = { exclude_metrics: ['some metric'] };
-  const namespace = { name: 'cool namespace' };
+  const query = { filters: { exclude_metrics: [] } };
+  const queryOverrides = { filters: { excludeMetrics: ['some metric'] } };
+  const namespace = 'cool namespace';
   const title = 'fake title';
 
   describe('fetch', () => {
@@ -14,20 +14,13 @@ describe('Value Stream Data Source', () => {
 
       expect(obj.namespace).toBe(namespace);
       expect(obj.title).toBe(title);
-      expect(obj).toMatchObject({ exclude_metrics: [] });
-    });
-
-    it('generates a default title from the namespace if there is none', () => {
-      obj = fetch({ namespace });
-
-      expect(obj.namespace).toBe(namespace);
-      expect(obj.title).toBe('Metrics comparison for cool namespace');
+      expect(obj).toMatchObject({ filters: { excludeMetrics: [] } });
     });
 
     it('applies the queryOverrides over any relevant query parameters', () => {
       obj = fetch({ namespace, query, queryOverrides });
 
-      expect(obj).not.toMatchObject({ exclude_metrics: [] });
+      expect(obj).not.toMatchObject({ filters: { excludeMetrics: [] } });
       expect(obj).toMatchObject(queryOverrides);
     });
   });
diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js
index cf87bf4f858d4d9e188bd52b8e8324de56daba0e..9b53c41ce7b7fb72f24bce768ccddaf9c87c3704 100644
--- a/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js
+++ b/ee/spec/frontend/analytics/analytics_dashboards/components/visualizations/dora_chart_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import DoraChart from 'ee/analytics/analytics_dashboards/components/visualizations/dora_chart.vue';
-import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue';
+import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue';
 import GroupOrProjectProvider from 'ee/analytics/dashboards/components/group_or_project_provider.vue';
 import GetGroupOrProjectQuery from 'ee/analytics/dashboards/graphql/get_group_or_project.query.graphql';
 import { mockGroup } from 'ee_jest/analytics/dashboards/mock_data';
@@ -23,11 +23,13 @@ describe('DoraChart Visualization', () => {
 
   const defaultData = {
     namespace,
-    excludeMetrics,
-    filterLabels,
+    filters: {
+      excludeMetrics,
+      labels: filterLabels,
+    },
   };
 
-  const findChart = () => wrapper.findComponent(ComparisonChart);
+  const findChart = () => wrapper.findComponent(FilterableComparisonChart);
   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
 
   const createWrapper = (props = {}) => {
@@ -83,9 +85,12 @@ describe('DoraChart Visualization', () => {
 
     it('renders the comparison chart component', () => {
       expect(findChart().props()).toMatchObject({
-        excludeMetrics,
-        filterLabels,
-        requestPath: 'some/fake/path',
+        namespace,
+        filters: {
+          excludeMetrics,
+          labels: filterLabels,
+        },
+        webUrl: 'gdk.test/groups/group-10',
       });
     });
 
diff --git a/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js b/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js
index dcb8f6d392ef24c7da77d7c434764bf5fb2d1371..497ebd19629a4a159538831ab9a54f565b68c572 100644
--- a/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js
+++ b/ee/spec/frontend/analytics/dashboards/components/dora_performers_score_chart_spec.js
@@ -201,7 +201,9 @@ describe('DoraPerformersScoreChart', () => {
 
     it('renders the filter badges when provided', async () => {
       const topics = ['one', 'two'];
-      await createWrapper({ props: { data: { ...mockData, filter_project_topics: topics } } });
+      await createWrapper({
+        props: { data: { ...mockData, filters: { projectTopics: topics } } },
+      });
       expect(findFilterBadges().exists()).toBe(true);
       expect(findFilterBadges().props('topics')).toEqual(topics);
     });
@@ -213,7 +215,9 @@ describe('DoraPerformersScoreChart', () => {
 
     it('filters out invalid project topics', async () => {
       const topics = ['one', 'two\n'];
-      await createWrapper({ props: { data: { ...mockData, filter_project_topics: topics } } });
+      await createWrapper({
+        props: { data: { ...mockData, filters: { projectTopics: topics } } },
+      });
       expect(findFilterBadges().exists()).toBe(true);
       expect(findFilterBadges().props('topics')).toEqual(['one']);
     });
diff --git a/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js b/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..43300455259c9dc90f73a90c98fea96ee0e75c6c
--- /dev/null
+++ b/ee/spec/frontend/analytics/dashboards/components/filterable_comparison_chart_spec.js
@@ -0,0 +1,234 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlSkeletonLoader } from '@gitlab/ui';
+import FilterableComparisonChart from 'ee/analytics/dashboards/components/filterable_comparison_chart.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { METRICS_WITHOUT_LABEL_FILTERING } from 'ee/analytics/dashboards/constants';
+import ComparisonChartLabels from 'ee/analytics/dashboards/components/comparison_chart_labels.vue';
+import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue';
+import filterLabelsQueryBuilder from 'ee/analytics/dashboards/graphql/filter_labels_query_builder';
+import { mockFilterLabelsResponse } from '../helpers';
+
+Vue.use(VueApollo);
+
+describe('FilterableComparisonChart', () => {
+  let wrapper;
+
+  const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+  const findComparisonChart = () => wrapper.findComponent(ComparisonChart);
+  const findComparisonChartLabels = () => wrapper.findComponent(ComparisonChartLabels);
+
+  const excludeMetrics = ['cycle_time'];
+  const labels = ['test::one', 'test::two'];
+
+  const groupWebUrl = 'group/web/url';
+  const projectWebUrl = 'project/web/url';
+
+  const groupNamespace = 'group-namespace';
+  const projectNamespace = 'group-namespace/project';
+
+  const createWrapper = async ({
+    namespace = groupNamespace,
+    isProject = false,
+    webUrl = groupWebUrl,
+    isLoading = false,
+    filters = {},
+    filterLabelsResolver = null,
+  } = {}) => {
+    const { labels: filterLabels = [] } = filters;
+    const apolloProvider = createMockApollo([
+      [
+        filterLabelsQueryBuilder(filterLabels, isProject),
+        filterLabelsResolver ||
+          jest.fn().mockResolvedValue({ data: mockFilterLabelsResponse(filterLabels) }),
+      ],
+    ]);
+
+    wrapper = shallowMountExtended(FilterableComparisonChart, {
+      apolloProvider,
+      propsData: {
+        namespace,
+        isProject,
+        webUrl,
+        isLoading,
+        filters: {
+          labels: filterLabels,
+          excludeMetrics: [],
+          ...filters,
+        },
+      },
+    });
+
+    await waitForPromises();
+  };
+
+  describe('default', () => {
+    beforeEach(async () => {
+      await createWrapper();
+    });
+
+    it('does not render the skeleton loader', () => {
+      expect(findSkeletonLoader().exists()).toBe(false);
+    });
+
+    it('does not emit an error', () => {
+      expect(wrapper.emitted('set-errors')).toBeUndefined();
+    });
+
+    it('does not render the chart labels', () => {
+      expect(findComparisonChartLabels().exists()).toBe(false);
+    });
+
+    it('renders the chart', () => {
+      expect(findComparisonChart().props()).toEqual({
+        excludeMetrics: [],
+        filterLabels: [],
+        isProject: false,
+        requestPath: 'group-namespace',
+      });
+    });
+  });
+
+  describe('with filters', () => {
+    describe('error loading filters', () => {
+      beforeEach(async () => {
+        await createWrapper({
+          filters: { labels },
+          filterLabelsResolver: jest.fn().mockRejectedValue(),
+        });
+      });
+
+      it('emits the `set-errors` event', () => {
+        expect(wrapper.emitted('set-errors')[0]).toEqual([
+          { errors: ['Failed to load labels matching the filter: test::one, test::two'] },
+        ]);
+      });
+
+      it('does not render the chart labels', () => {
+        expect(findComparisonChartLabels().exists()).toBe(false);
+      });
+    });
+
+    describe('labels', () => {
+      beforeEach(async () => {
+        await createWrapper({ filters: { labels } });
+      });
+
+      it('does not render the skeleton loader', () => {
+        expect(findSkeletonLoader().exists()).toBe(false);
+      });
+
+      it('renders the chart', () => {
+        expect(findComparisonChart().props()).toEqual({
+          excludeMetrics: METRICS_WITHOUT_LABEL_FILTERING,
+          filterLabels: labels,
+          isProject: false,
+          requestPath: 'group-namespace',
+        });
+      });
+
+      it('renders the chart labels', () => {
+        expect(findComparisonChartLabels().props()).toEqual({
+          webUrl: groupWebUrl,
+          labels: [
+            {
+              color: '#FFFFFF',
+              id: 'test::one',
+              title: 'test::one',
+            },
+            {
+              color: '#FFFFFF',
+              id: 'test::two',
+              title: 'test::two',
+            },
+          ],
+        });
+      });
+
+      describe('with duplicate labels', () => {
+        beforeEach(async () => {
+          await createWrapper({ filters: { labels: [...labels, ...labels] } });
+        });
+
+        it('removes duplicates result', () => {
+          expect(findComparisonChart().props('filterLabels').length).toBe(2);
+        });
+      });
+    });
+
+    describe('excludeMetrics', () => {
+      beforeEach(() => {
+        createWrapper({ filters: { excludeMetrics } });
+      });
+
+      it('does not render the chart labels', () => {
+        expect(findComparisonChartLabels().exists()).toBe(false);
+      });
+
+      it('renders the chart', () => {
+        expect(findComparisonChart().props()).toEqual({
+          excludeMetrics: ['cycle_time'],
+          filterLabels: [],
+          isProject: false,
+          requestPath: 'group-namespace',
+        });
+      });
+    });
+
+    describe('with excludeMetrics and labels', () => {
+      beforeEach(async () => {
+        await createWrapper({ filters: { excludeMetrics, labels } });
+      });
+
+      it('will exclude incompatible metrics', () => {
+        expect(findComparisonChart().props()).toEqual(
+          expect.objectContaining({
+            filterLabels: labels,
+            excludeMetrics: ['cycle_time', ...METRICS_WITHOUT_LABEL_FILTERING],
+            isProject: false,
+            requestPath: 'group-namespace',
+          }),
+        );
+      });
+    });
+  });
+
+  describe('while loading', () => {
+    beforeEach(() => {
+      createWrapper({ isLoading: true });
+    });
+
+    it('renders the skeleton loader', () => {
+      expect(findSkeletonLoader().exists()).toBe(true);
+    });
+
+    it('does not render the chart', () => {
+      expect(findComparisonChart().exists()).toBe(false);
+    });
+
+    it('does not render the chart labels', () => {
+      expect(findComparisonChartLabels().exists()).toBe(false);
+    });
+
+    it('does not emit an error', () => {
+      expect(wrapper.emitted('set-errors')).toBeUndefined();
+    });
+  });
+
+  describe('with a project', () => {
+    beforeEach(async () => {
+      await createWrapper({ webUrl: projectWebUrl, isProject: true, namespace: projectNamespace });
+    });
+
+    it('renders the chart for project', () => {
+      expect(findComparisonChart().props()).toEqual({
+        excludeMetrics: [],
+        filterLabels: [],
+        isProject: true,
+        requestPath: 'group-namespace/project',
+      });
+    });
+  });
+});
diff --git a/ee/spec/frontend/analytics/dashboards/helpers.js b/ee/spec/frontend/analytics/dashboards/helpers.js
index aa6616ffa1f1f22f09a160cc3a03a1da92df1e85..9b44c3a905d58c96ad847b96a7d1bd9337aae583 100644
--- a/ee/spec/frontend/analytics/dashboards/helpers.js
+++ b/ee/spec/frontend/analytics/dashboards/helpers.js
@@ -100,8 +100,8 @@ export const mockGraphqlContributorCountResponse = (
     },
   });
 
-export const mockFilterLabelsResponse = (mockLabels) => ({
-  namespace: mockLabels.reduce(
+export const mockFilterLabelsResponse = (mockLabels = []) => ({
+  namespace: mockLabels?.reduce(
     (acc, label, index) =>
       Object.assign(acc, {
         [`label_${index}`]: { nodes: [{ id: label, title: label, color: '#FFFFFF' }] },
diff --git a/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb b/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb
index 39017cbef62e65c7bafb05e9d02cdd65ff972aac..b08d146653a3df51d9c26402d5b1815633148986 100644
--- a/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb
+++ b/ee/spec/requests/groups/analytics/dashboards_controller_spec.rb
@@ -91,6 +91,14 @@ def build_dashboard_path(path, namespaces)
       expect(js_list_app_attributes).to include('data-data-source-clickhouse')
     end
 
+    it 'passes topics-explore-projects-path to data attributes' do
+      request
+
+      expect(response).to be_successful
+
+      expect(js_list_app_attributes).to include('data-topics-explore-projects-path')
+    end
+
     context 'when project_id outside of the group hierarchy was set' do
       it 'does not pass the project pointer' do
         project_outside_the_hierarchy = create(:project)