diff --git a/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx index 19902444d..343ee537e 100644 --- a/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx +++ b/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx @@ -28,11 +28,14 @@ import { import * as React from 'react'; import { useTheme } from '@mui/material/styles'; import { InfoOutlined, ListAltOutlined } from '@mui/icons-material'; -import { featureGroups, getGroupColor } from '../../../utils/analytics'; import { type FeatureMetrics } from '../types'; import { useRemoteConfig } from '../../../context/RemoteConfigProvider'; import MUITooltip from '@mui/material/Tooltip'; import { GTFS_ORG_LINK } from '../../../constants/Navigation'; +import { + DATASET_FEATURES, + getComponentDecorators, +} from '../../../utils/consts'; export default function GTFSFeatureAnalytics(): React.ReactElement { const navigateTo = useNavigate(); @@ -44,6 +47,16 @@ export default function GTFSFeatureAnalytics(): React.ReactElement { const [error, setError] = useState(null); const { config } = useRemoteConfig(); + const getUniqueKeyStringValues = (key: keyof FeatureMetrics): string[] => { + const subGroups = new Set(); + data.forEach((item) => { + if (item[key] !== undefined) { + subGroups.add(item[key] as string); + } + }); + return Array.from(subGroups); + }; + useEffect(() => { const fetchData = async (): Promise => { try { @@ -54,13 +67,15 @@ export default function GTFSFeatureAnalytics(): React.ReactElement { throw new Error('Network response was not ok'); } const fetchedData = await response.json(); - const dataWithGroups = fetchedData.map((feature: FeatureMetrics) => ({ - ...feature, - latest_feed_count: feature.feeds_count.slice(-1)[0], - feature_group: Object.keys(featureGroups).find((group) => - featureGroups[group].includes(feature.feature), - ), - })); + const dataWithGroups = fetchedData.map((feature: FeatureMetrics) => { + return { + ...feature, + latest_feed_count: feature.feeds_count.slice(-1)[0], + feature_group: DATASET_FEATURES[feature.feature]?.component, + feature_sub_group: + DATASET_FEATURES[feature.feature]?.componentSubgroup, + }; + }); setData(dataWithGroups); } catch (error) { if (error instanceof Error) { @@ -118,13 +133,34 @@ export default function GTFSFeatureAnalytics(): React.ReactElement { header: 'Feature Group', size: 200, filterVariant: 'multi-select', - filterSelectOptions: Object.keys(featureGroups), + filterSelectOptions: getUniqueKeyStringValues('feature_group'), + Cell: ({ cell }: { cell: MRT_Cell }) => { + const group = cell.getValue(); + return group == null ? null : ( + + {group} + + ); + }, + }, + { + accessorKey: 'feature_sub_group', + header: 'Feature Sub Group', + size: 200, + filterVariant: 'multi-select', + filterSelectOptions: getUniqueKeyStringValues('feature_sub_group'), Cell: ({ cell }: { cell: MRT_Cell }) => { const group = cell.getValue(); return group == null ? null : ( }) => { - const { groupedFeatures, otherFeatures } = groupFeatures( + const groupedFeatures = groupFeaturesByComponent( cell.getValue(), ); return (
- {Object.entries(groupedFeatures).map( - ([group, features], index) => ( -
-
- {group}: -
- {features.map((feature, index) => ( + {Object.entries(groupedFeatures) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .map(([group, features], index) => { + const componentDecorator = getComponentDecorators(group); + return ( +
{ - navigate( - `/metrics/gtfs/features?featureName=${feature}`, - ); + style={{ + background: componentDecorator.color, + color: 'black', + borderRadius: '5px', + padding: 5, + marginLeft: 5, + marginBottom: 5, + width: 'fit-content', }} > - {feature} + {group}
- ))} -
- ), - )} - {otherFeatures.length > 0 && ( -
-
- Empty Group: -
- {otherFeatures.map((feature, index) => ( -
{ - navigate( - `/metrics/gtfs/features?featureName=${feature}`, - ); - }} - > - {feature} + {features.map((featureData, index) => ( +
{ + navigate( + `/metrics/gtfs/features?featureName=${featureData.feature}`, + ); + }} + > + {featureData.feature} + {featureData.componentSubgroup !== undefined && ( + + {componentDecorator.icon} + + )} +
+ ))}
- ))} -
- )} + ); + })}
); }, diff --git a/web-app/src/app/screens/Analytics/types.ts b/web-app/src/app/screens/Analytics/types.ts index bacabed3e..0c6cc92d3 100644 --- a/web-app/src/app/screens/Analytics/types.ts +++ b/web-app/src/app/screens/Analytics/types.ts @@ -41,6 +41,7 @@ export interface FeatureMetrics { feeds_count: number[]; latest_feed_count: number; feature_group?: string; // Add a property to handle feature grouping + feature_sub_group?: string; } export interface AnalyticsFile { diff --git a/web-app/src/app/utils/analytics.ts b/web-app/src/app/utils/analytics.ts deleted file mode 100644 index c5bd64cb2..000000000 --- a/web-app/src/app/utils/analytics.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const featureGroups: Record = { - 'Fares v2': [ - 'Fare Products', - 'Route-Based Fares', - 'Fare Media', - 'Zone-Based Fares', - 'Time-Based Fares', - 'Transfer Fares', - ], - Pathways: ['Pathways', 'Pathways Directions', 'Levels'], - 'Flexible Services': ['Continuous Stops', 'Flex'], -}; - -/** - * Groups the features based on the feature groups - * @param features List of features - * @returns Object with grouped features and other features - */ -export function groupFeatures(features: string[]): { - groupedFeatures: Record; - otherFeatures: string[]; -} { - const groupedFeatures: Record = {}; - const otherFeatures: string[] = []; - - features?.forEach((feature) => { - let found = false; - for (const [group, groupFeatures] of Object.entries(featureGroups)) { - if (groupFeatures.includes(feature)) { - if (groupedFeatures[group] === undefined) { - groupedFeatures[group] = []; - } - groupedFeatures[group].push(feature); - found = true; - break; - } - } - if (!found) { - otherFeatures.push(feature); - } - }); - - return { groupedFeatures, otherFeatures }; -} - -/** - * Returns the color for the feature group - * @param group Feature group - * @returns Color for the feature group - */ -export function getGroupColor(group: string): string { - if (group === 'Fares v2') { - return '#d1e2ff'; - } - if (group === 'Pathways') { - return '#fdd4e0'; - } - if (group === 'Flexible Services') { - return '#fcb68e'; - } - return '#f7f7f7'; -} diff --git a/web-app/src/app/utils/consts.ts b/web-app/src/app/utils/consts.tsx similarity index 74% rename from web-app/src/app/utils/consts.ts rename to web-app/src/app/utils/consts.tsx index 0023af968..0dde5e15a 100644 --- a/web-app/src/app/utils/consts.ts +++ b/web-app/src/app/utils/consts.tsx @@ -1,5 +1,12 @@ +import AccessibleIcon from '@mui/icons-material/Accessible'; +import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; +import EscalatorIcon from '@mui/icons-material/Escalator'; +import AltRouteIcon from '@mui/icons-material/AltRoute'; +import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'; + interface DatasetFeature { component: string; + componentSubgroup?: string; fileName: string; linkToInfo: string; } @@ -13,6 +20,53 @@ export function getDataFeatureUrl(feature: string): string { ); } +export interface DatasetComponentFeature extends DatasetFeature { + feature: string; +} + +export function groupFeaturesByComponent( + features: string[] = Object.keys(DATASET_FEATURES), +): Record { + const groupedFeatures: Record = {}; + + features.forEach((feature) => { + const featureData = DATASET_FEATURES[feature]; + if (featureData !== undefined) { + const component = featureData.component ?? 'Other'; + if (groupedFeatures[component] === undefined) { + groupedFeatures[component] = []; + } + groupedFeatures[component].push({ ...featureData, feature }); + } + }); + + return groupedFeatures; +} + +export function getComponentDecorators(component: string): { + color: string; + icon: JSX.Element; +} { + switch (component) { + case 'Accessibility': + return { color: '#BDE4A7', icon: }; + case 'Base add-ons': + return { color: '#f0f0f0', icon: }; + case 'Fares v2': + return { color: '#C2D6FF', icon: }; + case 'Fares': + return { color: '#d1e4ff', icon: }; + case 'Pathways': + return { color: '#fdd4e0', icon: }; + case 'Flexible Services': + return { color: '#fcb68e', icon: }; + case 'Flex': + return { color: '#FBA674', icon: }; + default: + return { color: '#f7f7f7', icon: <> }; + } +} + export const DATASET_FEATURES: DatasetFeatures = { overview: { component: '', @@ -63,35 +117,41 @@ export const DATASET_FEATURES: DatasetFeatures = { }, 'Fare Products': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'fare_products.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#fare-products', }, 'Fare Media': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'fare_media.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#fare-media', }, 'Route-Based Fares': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'routes.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#route-based-fares', }, 'Time-Based Fares': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'timeframes.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#time-based-fares', }, 'Zone-Based Fares': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'areas.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#zone-based-fares', }, 'Fare Transfers': { component: 'Fares', + componentSubgroup: 'Fares v2', fileName: 'fare_transfer_rules.txt', linkToInfo: 'https://gtfs.org/getting-started/features/fares/#fare-transfers', @@ -156,24 +216,28 @@ export const DATASET_FEATURES: DatasetFeatures = { }, 'Booking Rules': { component: 'Flexible Services', + componentSubgroup: 'Flex', fileName: 'routes.txt', linkToInfo: 'https://gtfs.org/getting-started/features/flexible-services/#booking-rules', }, - 'Fixed-Stops Demand Responsive Services': { + 'Fixed-Stops Demand Responsive Transit': { component: 'Flexible Services', + componentSubgroup: 'Flex', fileName: 'location_groups.txt', linkToInfo: 'https://gtfs.org/getting-started/features/flexible-services/#fixed-stops-demand-responsive-services', }, 'Zone-Based Demand Responsive Services': { component: 'Flexible Services', + componentSubgroup: 'Flex', fileName: 'stop_times.txt', linkToInfo: 'https://gtfs.org/getting-started/features/flexible-services/#zone-based-demand-responsive-services', }, 'Predefined Routes with Deviation': { component: 'Flexible Services', + componentSubgroup: 'Flex', fileName: 'stop_times.txt', linkToInfo: 'https://gtfs.org/getting-started/features/flexible-services/#predefined-routes-with-deviation', @@ -197,6 +261,8 @@ export const DATASET_FEATURES: DatasetFeatures = { 'https://gtfs.org/getting-started/features/base-add-ons/#frequency-based-service ', }, }; +// SPELLING CORRECTIONS +DATASET_FEATURES['Text-to-Speech'] = DATASET_FEATURES['Text-To-Speech']; // DEPRECATED FEATURES DATASET_FEATURES['Wheelchair Accessibility'] = { @@ -210,7 +276,7 @@ DATASET_FEATURES['Transfer Fares'] = DATASET_FEATURES['Fare Transfers']; // as o DATASET_FEATURES['Pathways (basic)'] = DATASET_FEATURES['Pathway Connections']; // as of 6.0 DATASET_FEATURES['Pathways (extra)'] = DATASET_FEATURES['Pathway Details']; // as of 6.0 DATASET_FEATURES['Traversal Time'] = - DATASET_FEATURES['In-station traversal time']; + DATASET_FEATURES['In-station Traversal Time']; DATASET_FEATURES['Pathways Directions'] = { // as of 6.0 component: 'Pathways',