From f6afff7453396c8ce3dc637de2fb2e9142ef1c0a Mon Sep 17 00:00:00 2001 From: Conrad Nied Date: Sat, 27 Jun 2026 10:09:40 -0700 Subject: [PATCH 1/2] Language Details: Update location display --- src/entities/language/LanguageTypes.ts | 1 + .../compute/computeRecursiveLanguageData.ts | 3 +- .../load/extra_entities/GlottologData.tsx | 4 + .../selectors/LanguageScopeSelector.tsx | 6 +- .../details/sections/LanguageConnections.tsx | 18 +-- .../details/sections/LanguageLocation.tsx | 109 ++++++++++++------ 6 files changed, 96 insertions(+), 45 deletions(-) diff --git a/src/entities/language/LanguageTypes.ts b/src/entities/language/LanguageTypes.ts index 7ed220fbe..be8a1c714 100644 --- a/src/entities/language/LanguageTypes.ts +++ b/src/entities/language/LanguageTypes.ts @@ -122,6 +122,7 @@ export interface LanguageData extends ObjectBase { latitude?: number; longitude?: number; + coordsSource?: LanguageSource; depth?: number; // Computed depth in the language family tree, with 0 being a root language // References to other objects, filled in after loading the TSV diff --git a/src/features/data/compute/computeRecursiveLanguageData.ts b/src/features/data/compute/computeRecursiveLanguageData.ts index b12ee8722..3d8feb48c 100644 --- a/src/features/data/compute/computeRecursiveLanguageData.ts +++ b/src/features/data/compute/computeRecursiveLanguageData.ts @@ -1,4 +1,4 @@ -import { LanguageData } from '@entities/language/LanguageTypes'; +import { LanguageData, LanguageSource } from '@entities/language/LanguageTypes'; import { getVitalityMetascore } from '@entities/language/vitality/LanguageVitalityComputation'; import averageCoordinates from '@shared/lib/averageCoordinates'; @@ -73,6 +73,7 @@ function computeCoordinates(lang: LanguageData): void { const { latitude, longitude } = averageCoordinates(children); lang.latitude = latitude; lang.longitude = longitude; + if (lang.latitude != null && lang.longitude != null) lang.coordsSource = LanguageSource.Combined; } export default computeRecursiveLanguageData; diff --git a/src/features/data/load/extra_entities/GlottologData.tsx b/src/features/data/load/extra_entities/GlottologData.tsx index 193ff2899..cd1cdd351 100644 --- a/src/features/data/load/extra_entities/GlottologData.tsx +++ b/src/features/data/load/extra_entities/GlottologData.tsx @@ -5,6 +5,7 @@ import { LanguageData, LanguagesBySource, LanguageScope, + LanguageSource, } from '@entities/language/LanguageTypes'; import { setLanguageNames } from '@entities/language/setLanguageNames'; @@ -130,6 +131,9 @@ export function addGlottologLanguages( lang.Glottolog.name = name; lang.latitude = latitude; lang.longitude = longitude; + if (lang.latitude != null && lang.longitude != null) + lang.coordsSource = LanguageSource.Glottolog; + setLanguageNames(lang); if (lang.scope == null) { lang.scope = scope; diff --git a/src/features/transforms/filtering/selectors/LanguageScopeSelector.tsx b/src/features/transforms/filtering/selectors/LanguageScopeSelector.tsx index e64da2dc1..33fe896cb 100644 --- a/src/features/transforms/filtering/selectors/LanguageScopeSelector.tsx +++ b/src/features/transforms/filtering/selectors/LanguageScopeSelector.tsx @@ -1,13 +1,16 @@ import React from 'react'; import Selector from '@features/params/ui/Selector'; +import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext'; import usePageParams from '@features/params/usePageParams'; import { LanguageScope } from '@entities/language/LanguageTypes'; import { getLanguageScopeDescription, getLanguageScopeLabel } from '@strings/LanguageScopeStrings'; -const LanguageScopeSelector: React.FC = () => { +type Props = { display?: SelectorDisplay }; + +const LanguageScopeSelector: React.FC = ({ display }) => { const { languageScopes, updatePageParams } = usePageParams(); const selectorDescription = @@ -27,6 +30,7 @@ const LanguageScopeSelector: React.FC = () => { selected={languageScopes} getOptionLabel={getLanguageScopeLabel} getOptionDescription={getLanguageScopeDescription} + display={display} /> ); }; diff --git a/src/widgets/details/sections/LanguageConnections.tsx b/src/widgets/details/sections/LanguageConnections.tsx index cbd4b5deb..497da1abc 100644 --- a/src/widgets/details/sections/LanguageConnections.tsx +++ b/src/widgets/details/sections/LanguageConnections.tsx @@ -7,7 +7,8 @@ import { useDataContext } from '@features/data/context/useDataContext'; import HoverableButton from '@features/layers/hovercard/HoverableButton'; import HoverableObjectName from '@features/layers/hovercard/HoverableObjectName'; import usePageParams from '@features/params/usePageParams'; -import { getScopeFilter } from '@features/transforms/filtering/filter'; +import Field from '@features/transforms/fields/Field'; +import useFilters from '@features/transforms/filtering/useFilters'; import { getSortFunction } from '@features/transforms/sorting/sort'; import { TreeNodeData } from '@features/treelist/TreeListNode'; import TreeListRoot from '@features/treelist/TreeListRoot'; @@ -23,7 +24,7 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => { const { languageSource } = usePageParams(); const { getCLDRLanguage } = useDataContext(); const sortFunction = getSortFunction(); - const filterByScope = getScopeFilter(); + const filterByScope = useFilters()[Field.TerritoryScope]; const { childLanguages, ISO, Glottolog, variants, equivalentVariant } = lang; const relatedLanguages = (lang.CLDR.languageMatch ?? []) .map((match) => ({ @@ -72,14 +73,14 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => { )} - + 0 ? getLanguageTreeNodes([lang], languageSource, sortFunction) : [] } - listNodes={childLanguages.map((l) => ( + listNodes={childLanguages.sort(sortFunction).map((l) => ( ))} emptyMessage="No languages come from this language." @@ -90,9 +91,12 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => { treeNodes={ lang.locales.length > 0 ? getLocaleTreeNodes([lang], sortFunction, filterByScope) : [] } - listNodes={(lang.locales ?? []).map((v) => ( - - ))} + listNodes={(lang.locales ?? []) + .filter(filterByScope) + .sort(sortFunction) + .map((v) => ( + + ))} emptyMessage="No locales available for this language." /> diff --git a/src/widgets/details/sections/LanguageLocation.tsx b/src/widgets/details/sections/LanguageLocation.tsx index ac661924e..a2042b437 100644 --- a/src/widgets/details/sections/LanguageLocation.tsx +++ b/src/widgets/details/sections/LanguageLocation.tsx @@ -1,14 +1,17 @@ -import React from 'react'; +import { CheckSquare2Icon, SquareArrowUpLeftIcon, SquareIcon } from 'lucide-react'; +import React, { useMemo } from 'react'; import HoverableButton from '@features/layers/hovercard/HoverableButton'; import ObjectMap from '@features/map/EntityMap'; import LocalParamsProvider from '@features/params/LocalParamsProvider'; import { ObjectType, PageParamsOptional, View } from '@features/params/PageParamTypes'; +import { SelectorDisplay } from '@features/params/ui/SelectorDisplayContext'; import usePageParams from '@features/params/usePageParams'; import Field from '@features/transforms/fields/Field'; +import LanguageScopeSelector from '@features/transforms/filtering/selectors/LanguageScopeSelector'; import useFilteredEntities from '@features/transforms/filtering/useFilteredEntities'; -import { LanguageData } from '@entities/language/LanguageTypes'; +import { LanguageData, LanguageSource } from '@entities/language/LanguageTypes'; import DetailsField from '@shared/containers/DetailsField'; import DetailsSection from '@shared/containers/DetailsSection'; @@ -17,7 +20,7 @@ import Pill from '@shared/ui/Pill'; import { getLanguageScopeLabel } from '@strings/LanguageScopeStrings'; -const MAP_CIRCLE_LIMIT = 200; +const MAP_CIRCLE_LIMIT = 100; const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => { const { updatePageParams } = usePageParams(); @@ -27,7 +30,24 @@ const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => { {lang.latitude && lang.longitude ? ( <> - {lang.latitude.toFixed(4)}°, {lang.longitude.toFixed(4)}° Glottolog + {lang.latitude.toFixed(4)}°, {lang.longitude.toFixed(4)}°{' '} + {lang.coordsSource && {lang.coordsSource}} + {lang.coordsSource === LanguageSource.Glottolog && ( + <> + {' '} + These coordinates represent the "primary" location of the{' '} + {getLanguageScopeLabel(lang.scope).toLowerCase()}. This could be the centroid of the + area where the language is spoken or a significant location such as a major city for + which the language is known. + + )} + {lang.coordsSource === LanguageSource.Combined && ( + <> + {' '} + These coordinates represent the average location of the constituents of this{' '} + {getLanguageScopeLabel(lang.scope).toLowerCase()}. + + )} ) : ( @@ -43,6 +63,7 @@ const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => { objectType: ObjectType.Language, languageFilter: lang.nameCanonical + ' [' + lang.ID + ']', sortBy: Field.Population, + searchString: '', }} > @@ -57,44 +78,60 @@ type MapsProps = { updatePageParams: (newParams: PageParamsOptional) => void; }; function Maps({ lang, updatePageParams }: MapsProps) { - const descendants = useFilteredEntities({}).filteredEntities.filter( - (l) => l.type == ObjectType.Language && l.latitude && l.longitude, + const [showConstituents, setShowConstituents] = React.useState(true); + const nodes = useFilteredEntities({}).filteredEntities; // Use a LocalParamsProvider to limit the visible entities + const drawableNodes = useMemo( + () => [ + lang, // always show the selected language + ...nodes.filter((l) => l.ID !== lang.ID && l.latitude != null && l.longitude != null), + ], + [nodes], ); + if (nodes.length === 0) return null; return ( <> - {lang.latitude && lang.longitude ? ( - <> - These coordinates show the "primary" location of the language, as defined by - Glottolog. This could be the centroid of the area where the language is spoken, or a - significant location such as a major city where the language has a presence. It does not - represent all the locations where the language is spoken. - - - ) : null} - {descendants.length > 0 && ( - <> -
- These are the locations of descendant languages/dialects. - {descendants.length > MAP_CIRCLE_LIMIT && ( +
+ This map shows the center of the {getLanguageScopeLabel(lang.scope).toLowerCase()} + {drawableNodes.length > 1 ? ' and its constituents' : ''}. It does not capture every + location that the {getLanguageScopeLabel(lang.scope).toLowerCase()} is used.{' '} + + updatePageParams({ + view: View.Map, + languageFilter: lang.nameCanonical + ' [' + lang.ID + ']', + }) + } + > + See the full map in the explore panel + {' '} + {drawableNodes.length > 1 && ( + setShowConstituents((prev) => !prev)} + hoverContent={showConstituents ? 'Hide constituents' : 'Show constituents'} + > + {showConstituents ? : } + {showConstituents ? ' Showing constituents' : ' Not showing constituents'} + {showConstituents && drawableNodes.length > MAP_CIRCLE_LIMIT && ( {' '} - Showing first {MAP_CIRCLE_LIMIT} of {descendants.length}. + (first {MAP_CIRCLE_LIMIT} of {drawableNodes.length}) - )}{' '} - - updatePageParams({ - view: View.Map, - languageFilter: lang.nameCanonical + ' [' + lang.ID + ']', - }) - } - > - See the full map in explore panel - -
- - + )} + + )} +
+
+ +
+ {showConstituents && drawableNodes.length > 1 && ( + )} ); From d8d2733a27b98a23192081a1f1097de0e36caa06 Mon Sep 17 00:00:00 2001 From: Conrad Nied Date: Sat, 27 Jun 2026 11:13:41 -0700 Subject: [PATCH 2/2] Apply comments --- src/widgets/details/sections/LanguageConnections.tsx | 4 ++-- src/widgets/details/sections/LanguageLocation.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widgets/details/sections/LanguageConnections.tsx b/src/widgets/details/sections/LanguageConnections.tsx index 497da1abc..b63219efc 100644 --- a/src/widgets/details/sections/LanguageConnections.tsx +++ b/src/widgets/details/sections/LanguageConnections.tsx @@ -80,10 +80,10 @@ const LanguageConnections: React.FC<{ lang: LanguageData }> = ({ lang }) => { ? getLanguageTreeNodes([lang], languageSource, sortFunction) : [] } - listNodes={childLanguages.sort(sortFunction).map((l) => ( + listNodes={[...childLanguages].sort(sortFunction).map((l) => ( ))} - emptyMessage="No languages come from this language." + emptyMessage={`${lang.nameDisplay} has no constituent languages or dialects.`} />
diff --git a/src/widgets/details/sections/LanguageLocation.tsx b/src/widgets/details/sections/LanguageLocation.tsx index a2042b437..360c3969e 100644 --- a/src/widgets/details/sections/LanguageLocation.tsx +++ b/src/widgets/details/sections/LanguageLocation.tsx @@ -28,7 +28,7 @@ const LanguageLocation: React.FC<{ lang: LanguageData }> = ({ lang }) => { return ( - {lang.latitude && lang.longitude ? ( + {lang.latitude != null && lang.longitude != null ? ( <> {lang.latitude.toFixed(4)}°, {lang.longitude.toFixed(4)}°{' '} {lang.coordsSource && {lang.coordsSource}} @@ -85,7 +85,7 @@ function Maps({ lang, updatePageParams }: MapsProps) { lang, // always show the selected language ...nodes.filter((l) => l.ID !== lang.ID && l.latitude != null && l.longitude != null), ], - [nodes], + [lang, nodes], ); if (nodes.length === 0) return null; return (