Skip to content

Commit cf51a04

Browse files
committed
feat: display all countries in geographical distribution widget IN-1225
Signed-off-by: Efren Lim <elim@linuxfoundation.org>
1 parent 2004e6f commit cf51a04

3 files changed

Lines changed: 278 additions & 59 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!--
2+
Copyright (c) 2025 The Linux Foundation and each contributor.
3+
SPDX-License-Identifier: MIT
4+
-->
5+
<template>
6+
<div class="w-full h-[330px]">
7+
<client-only>
8+
<lfx-chart
9+
:config="getGeoMapChartConfig(chartData, chartSeries, getMaxValue(chartData))"
10+
:animation="props.animation"
11+
/>
12+
</client-only>
13+
</div>
14+
<div class="flex flex-col gap-5">
15+
<div
16+
v-for="item in listData"
17+
:key="item.name"
18+
class="flex flex-row justify-between items-center text-sm"
19+
>
20+
<div class="flex flex-row gap-4 items-center">
21+
<span class="text-base">
22+
{{ item.flag }}
23+
</span>
24+
<span class="font-medium">
25+
{{ item.name }}
26+
</span>
27+
</div>
28+
<span>
29+
{{ formatNumber(item.count) }} {{ pluralize(props.label.toLowerCase(), item.count) }} ・ {{ item.percentage }}%
30+
</span>
31+
</div>
32+
</div>
33+
</template>
34+
35+
<script setup lang="ts">
36+
import { computed } from 'vue';
37+
import pluralize from 'pluralize';
38+
import LfxChart from '~/components/uikit/chart/chart.vue';
39+
import { convertToChartData, getMaxValue } from '~/components/uikit/chart/helpers/chart-helpers';
40+
import type { ChartData, RawChartData, ChartSeries } from '~/components/uikit/chart/types/ChartTypes';
41+
import { getGeoMapChartConfig } from '~/components/uikit/chart/configs/geo-map.chart';
42+
import { formatNumber } from '~/components/shared/utils/formatter';
43+
import type { GeoMapData } from '~/components/modules/widget/components/contributors/types/geo-map.types';
44+
45+
const props = withDefaults(
46+
defineProps<{
47+
geoMapData?: GeoMapData[];
48+
label: string;
49+
limit?: number;
50+
animation?: boolean;
51+
}>(),
52+
{
53+
geoMapData: () => [],
54+
limit: undefined,
55+
animation: true,
56+
},
57+
);
58+
59+
// "Unknown" location is intentionally hidden from the list and chart (see IN-1225 / #2062).
60+
const knownGeoMapData = computed(() => props.geoMapData.filter((item) => item.name !== 'Unknown'));
61+
62+
const listData = computed(() => (props.limit ? knownGeoMapData.value.slice(0, props.limit) : knownGeoMapData.value));
63+
64+
const chartData = computed<ChartData[]>(() =>
65+
convertToChartData(knownGeoMapData.value as unknown as RawChartData[], 'name', ['count', 'percentage']).map(
66+
(item) => ({
67+
...item,
68+
key: item.key === 'United States' ? 'United States of America' : item.key,
69+
}),
70+
),
71+
);
72+
73+
const chartSeries = computed<ChartSeries[]>(() => [
74+
{
75+
name: props.label,
76+
type: 'map',
77+
yAxisIndex: 0,
78+
dataIndex: 0,
79+
},
80+
]);
81+
</script>
82+
83+
<script lang="ts">
84+
export default {
85+
name: 'LfxGeoDistributionView',
86+
};
87+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<!--
2+
Copyright (c) 2025 The Linux Foundation and each contributor.
3+
SPDX-License-Identifier: MIT
4+
-->
5+
<template>
6+
<lfx-drawer
7+
v-model="isDrawerOpen"
8+
position="right"
9+
>
10+
<div class="relative flex flex-col justify-start h-full">
11+
<div class="pt-4 sm:pt-6 px-4 sm:px-6">
12+
<h3 class="text-heading-3 font-semibold font-secondary pb-3">
13+
{{ geographicalDistribution.name }}
14+
</h3>
15+
<p class="text-body-2 text-neutral-500 mb-6">
16+
{{ geographicalDistribution.description(project!, props.model) }}
17+
<a
18+
:href="geographicalDistribution.learnMoreLink"
19+
class="text-brand-500"
20+
target="_blank"
21+
>Learn more</a
22+
>
23+
</p>
24+
25+
<hr />
26+
</div>
27+
<section class="mt-5 flex flex-col flex-grow overflow-auto">
28+
<div class="flex flex-wrap gap-4 items-center justify-between mb-6 px-4 sm:px-6 pt-[1px]">
29+
<lfx-tabs
30+
:tabs="tabs"
31+
:model-value="activeTab"
32+
width-type="inline"
33+
@update:model-value="activeTab = $event"
34+
/>
35+
<div class="max-w-max">
36+
<lfx-activities-dropdown
37+
v-model="metric"
38+
placement="bottom-end"
39+
:full-width="false"
40+
:match-width="false"
41+
width="25rem"
42+
:include-collaborations="props.model.includeCollaborations"
43+
/>
44+
</div>
45+
</div>
46+
47+
<div class="px-4 sm:px-6">
48+
<lfx-project-load-state
49+
:status="status"
50+
:error="error"
51+
error-message="Error fetching geographical distribution"
52+
:is-empty="isEmpty"
53+
>
54+
<lfx-geo-distribution-view
55+
:geo-map-data="geoMapData"
56+
:label="label"
57+
/>
58+
</lfx-project-load-state>
59+
</div>
60+
</section>
61+
</div>
62+
</lfx-drawer>
63+
</template>
64+
65+
<script setup lang="ts">
66+
import { useRoute } from 'nuxt/app';
67+
import { ref, computed, watch } from 'vue';
68+
import { storeToRefs } from 'pinia';
69+
import LfxGeoDistributionView from './geo-distribution-view.vue';
70+
import LfxDrawer from '~/components/uikit/drawer/drawer.vue';
71+
import LfxTabs from '~/components/uikit/tabs/tabs.vue';
72+
import { useProjectStore } from '~/components/modules/project/store/project.store';
73+
import { isEmptyData } from '~/components/shared/utils/helper';
74+
import LfxActivitiesDropdown from '~/components/modules/widget/components/contributors/fragments/activities-dropdown.vue';
75+
import LfxProjectLoadState from '~/components/modules/project/components/shared/load-state.vue';
76+
import geographicalDistribution from '~/components/modules/widget/config/contributor/geographical-distribution/geographical-distribution.config';
77+
import type {
78+
GeoMapData,
79+
GeoMapResponse,
80+
} from '~/components/modules/widget/components/contributors/types/geo-map.types';
81+
import {
82+
CONTRIBUTORS_API_SERVICE,
83+
type GeographicalDistributionQueryParams,
84+
} from '~~/app/components/modules/widget/services/contributors.api.service';
85+
86+
const props = withDefaults(
87+
defineProps<{
88+
modelValue: boolean;
89+
selectedTab?: string;
90+
selectedMetric?: string;
91+
model: { includeCollaborations?: boolean };
92+
}>(),
93+
{
94+
modelValue: false,
95+
selectedTab: 'organizations',
96+
selectedMetric: 'all:all',
97+
model: () => ({ includeCollaborations: false }),
98+
},
99+
);
100+
101+
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>();
102+
103+
const isDrawerOpen = computed({
104+
get: () => props.modelValue,
105+
set: (value: boolean) => emit('update:modelValue', value),
106+
});
107+
108+
const route = useRoute();
109+
const activeTab = ref(props.selectedTab);
110+
const metric = ref(props.selectedMetric);
111+
const platform = computed(() => metric.value.split(':')[0]);
112+
const activityType = computed(() => metric.value.split(':')[1]);
113+
114+
const { isCollectionScope, startDate, endDate, selectedReposValues, project } = storeToRefs(useProjectStore());
115+
116+
const params = computed<GeographicalDistributionQueryParams>(() => ({
117+
projectSlug: isCollectionScope.value ? undefined : (route.params.slug as string),
118+
collectionSlug: isCollectionScope.value ? (route.params.slug as string) : undefined,
119+
type: activeTab.value,
120+
platform: platform.value,
121+
activityType: activityType.value,
122+
repos: selectedReposValues.value,
123+
startDate: startDate.value,
124+
endDate: endDate.value,
125+
includeCollaborations: props.model.includeCollaborations,
126+
}));
127+
128+
const { data, status, error } = CONTRIBUTORS_API_SERVICE.fetchGeographicalDistribution(params);
129+
130+
const geoMapData = computed<GeoMapData[] | undefined>(() => (data.value as GeoMapResponse)?.data);
131+
132+
const isEmpty = computed(() => isEmptyData(geoMapData.value as unknown as Record<string, unknown>[]));
133+
134+
const tabs = [
135+
{
136+
label: 'Organizations',
137+
value: 'organizations',
138+
},
139+
{
140+
label: 'Contributors',
141+
value: 'contributors',
142+
},
143+
];
144+
145+
const label = computed(() => (activeTab.value === 'contributors' ? 'Contributor' : 'Organization'));
146+
147+
watch(isDrawerOpen, (isOpen) => {
148+
if (isOpen) {
149+
activeTab.value = props.selectedTab;
150+
metric.value = props.selectedMetric;
151+
}
152+
});
153+
</script>
154+
155+
<script lang="ts">
156+
export default {
157+
name: 'LfxGeographicalDistributionDrawer',
158+
};
159+
</script>

frontend/app/components/modules/widget/config/contributor/geographical-distribution/geographical-distribution.vue

Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -36,53 +36,44 @@ SPDX-License-Identifier: MIT
3636
error-message="Error fetching geographical distribution"
3737
:is-empty="isEmpty"
3838
>
39-
<div class="w-full h-[330px]">
40-
<client-only>
41-
<lfx-chart
42-
:config="getGeoMapChartConfig(chartData, chartSeries, getMaxValue(chartData))"
43-
:animation="!props.snapshot"
44-
/>
45-
</client-only>
46-
</div>
47-
<div>
48-
<div
49-
v-if="status !== 'pending'"
50-
class="flex flex-col gap-5"
39+
<lfx-geo-distribution-view
40+
v-if="status !== 'pending'"
41+
:geo-map-data="geoMapData"
42+
:label="label"
43+
:limit="5"
44+
:animation="!props.snapshot"
45+
/>
46+
<div
47+
v-if="!props.snapshot && showAllCountriesButton"
48+
class="mt-5 flex flex-row justify-center"
49+
>
50+
<lfx-button
51+
type="transparent"
52+
@click="isDrawerOpened = true"
5153
>
52-
<div
53-
v-for="item in geoMapDataCountries"
54-
:key="item.name"
55-
class="flex flex-row justify-between items-center text-sm"
56-
>
57-
<div class="flex flex-row gap-4 items-center">
58-
<span class="text-base">
59-
{{ item.flag }}
60-
</span>
61-
<span class="font-medium">
62-
{{ item.name }}
63-
</span>
64-
</div>
65-
<span>
66-
{{ formatNumber(item.count) }} {{ pluralize(label.toLowerCase(), item.count) }} ・ {{ item.percentage }}%
67-
</span>
68-
</div>
69-
</div>
54+
All countries
55+
</lfx-button>
7056
</div>
7157
</lfx-project-load-state>
7258
</section>
59+
<client-only>
60+
<lfx-geographical-distribution-drawer
61+
v-model="isDrawerOpened"
62+
:selected-tab="model.activeTab"
63+
:selected-metric="model.metric"
64+
:model="model"
65+
/>
66+
</client-only>
7367
</template>
7468

7569
<script setup lang="ts">
76-
import { computed, watch } from 'vue';
70+
import { computed, ref, watch } from 'vue';
7771
import { useRoute } from 'vue-router';
7872
import { storeToRefs } from 'pinia';
79-
import pluralize from 'pluralize';
73+
import LfxGeoDistributionView from './fragments/geo-distribution-view.vue';
74+
import LfxGeographicalDistributionDrawer from './fragments/geographical-distribution-drawer.vue';
8075
import LfxTabs from '~/components/uikit/tabs/tabs.vue';
81-
import LfxChart from '~/components/uikit/chart/chart.vue';
82-
import { convertToChartData, getMaxValue } from '~/components/uikit/chart/helpers/chart-helpers';
83-
import type { ChartData, RawChartData, ChartSeries } from '~/components/uikit/chart/types/ChartTypes';
84-
import { getGeoMapChartConfig } from '~/components/uikit/chart/configs/geo-map.chart';
85-
import { formatNumber } from '~/components/shared/utils/formatter';
76+
import LfxButton from '~/components/uikit/button/button.vue';
8677
import { useProjectStore } from '~/components/modules/project/store/project.store';
8778
import { isEmptyData } from '~/components/shared/utils/helper';
8879
import type {
@@ -137,22 +128,13 @@ const params = computed<GeographicalDistributionQueryParams>(() => ({
137128
}));
138129
139130
const { data, status, error } = CONTRIBUTORS_API_SERVICE.fetchGeographicalDistribution(params);
131+
const isDrawerOpened = ref(false);
140132
141133
const geoMapData = computed<GeoMapData[] | undefined>(() => (data.value as GeoMapResponse)?.data);
142-
const geoMapDataCountries = computed<GeoMapData[] | undefined>(() =>
143-
geoMapData.value ? geoMapData.value.filter((item) => item.name !== 'Unknown').slice(0, 5) : undefined,
144-
);
145-
146-
const chartData = computed<ChartData[]>(
147-
// convert the data to chart data
148-
() =>
149-
convertToChartData(geoMapData.value as unknown as RawChartData[], 'name', ['count', 'percentage']).map((item) => ({
150-
...item,
151-
key: item.key === 'United States' ? 'United States of America' : item.key,
152-
})),
153-
);
134+
const knownGeoMapData = computed(() => geoMapData.value?.filter((item) => item.name !== 'Unknown') ?? []);
135+
const showAllCountriesButton = computed(() => knownGeoMapData.value.length > 5);
154136
155-
const isEmpty = computed(() => isEmptyData(chartData.value as unknown as Record<string, unknown>[]));
137+
const isEmpty = computed(() => isEmptyData(knownGeoMapData.value as unknown as Record<string, unknown>[]));
156138
157139
const tabs = [
158140
{
@@ -167,15 +149,6 @@ const tabs = [
167149
168150
const label = computed(() => (model.value.activeTab === 'contributors' ? 'Contributor' : 'Organization'));
169151
170-
const chartSeries = computed<ChartSeries[]>(() => [
171-
{
172-
name: label.value,
173-
type: 'map',
174-
yAxisIndex: 0,
175-
dataIndex: 0,
176-
},
177-
]);
178-
179152
watch(
180153
status,
181154
(value) => {

0 commit comments

Comments
 (0)