@buildcanada/charts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +113 -0
  3. package/package.json +137 -0
  4. package/src/components/BodyPortal/BodyPortal.tsx +40 -0
  5. package/src/components/Button/Button.scss +110 -0
  6. package/src/components/Button/Button.tsx +101 -0
  7. package/src/components/Checkbox.scss +93 -0
  8. package/src/components/Checkbox.tsx +47 -0
  9. package/src/components/ExpandableToggle/ExpandableToggle.scss +123 -0
  10. package/src/components/ExpandableToggle/ExpandableToggle.tsx +60 -0
  11. package/src/components/GrapherTabIcon.tsx +156 -0
  12. package/src/components/GrapherTrendArrow.scss +16 -0
  13. package/src/components/GrapherTrendArrow.tsx +30 -0
  14. package/src/components/Halo/Halo.tsx +44 -0
  15. package/src/components/LabeledSwitch/LabeledSwitch.scss +109 -0
  16. package/src/components/LabeledSwitch/LabeledSwitch.tsx +62 -0
  17. package/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx +1173 -0
  18. package/src/components/OverlayHeader.scss +18 -0
  19. package/src/components/OverlayHeader.tsx +29 -0
  20. package/src/components/RadioButton.scss +69 -0
  21. package/src/components/RadioButton.tsx +42 -0
  22. package/src/components/SimpleMarkdownText.tsx +89 -0
  23. package/src/components/TextInput.scss +17 -0
  24. package/src/components/TextInput.tsx +19 -0
  25. package/src/components/TextWrap/TextWrap.tsx +361 -0
  26. package/src/components/TextWrap/TextWrapUtils.ts +32 -0
  27. package/src/components/closeButton/CloseButton.scss +40 -0
  28. package/src/components/closeButton/CloseButton.tsx +27 -0
  29. package/src/components/index.ts +70 -0
  30. package/src/components/loadingIndicator/LoadingIndicator.scss +40 -0
  31. package/src/components/loadingIndicator/LoadingIndicator.tsx +28 -0
  32. package/src/components/markdown/remarkPlainLinks.ts +36 -0
  33. package/src/components/reactUtil.ts +20 -0
  34. package/src/components/stubs/CodeSnippet.tsx +19 -0
  35. package/src/components/stubs/DataCitation.tsx +16 -0
  36. package/src/components/stubs/IndicatorKeyData.tsx +45 -0
  37. package/src/components/stubs/IndicatorProcessing.tsx +15 -0
  38. package/src/components/stubs/IndicatorSources.tsx +15 -0
  39. package/src/components/styles/colors.scss +113 -0
  40. package/src/components/styles/mixins.scss +630 -0
  41. package/src/components/styles/typography.scss +579 -0
  42. package/src/components/styles/util.scss +89 -0
  43. package/src/components/styles/variables.scss +208 -0
  44. package/src/config/ChartsConfig.ts +163 -0
  45. package/src/config/ChartsProvider.tsx +157 -0
  46. package/src/config/index.ts +20 -0
  47. package/src/core-table/CoreTable.ts +1355 -0
  48. package/src/core-table/CoreTableColumns.ts +973 -0
  49. package/src/core-table/CoreTableUtils.ts +793 -0
  50. package/src/core-table/ErrorValues.ts +73 -0
  51. package/src/core-table/OwidTable.ts +1175 -0
  52. package/src/core-table/OwidTableSynthesizers.ts +272 -0
  53. package/src/core-table/OwidTableUtil.ts +76 -0
  54. package/src/core-table/Transforms.ts +484 -0
  55. package/src/core-table/index.ts +82 -0
  56. package/src/explorer/ColumnGrammar.ts +217 -0
  57. package/src/explorer/Explorer.sample.ts +212 -0
  58. package/src/explorer/Explorer.scss +148 -0
  59. package/src/explorer/Explorer.tsx +1283 -0
  60. package/src/explorer/ExplorerConstants.ts +85 -0
  61. package/src/explorer/ExplorerControls.scss +156 -0
  62. package/src/explorer/ExplorerControls.tsx +210 -0
  63. package/src/explorer/ExplorerDecisionMatrix.ts +471 -0
  64. package/src/explorer/ExplorerGrammar.ts +161 -0
  65. package/src/explorer/ExplorerProgram.ts +568 -0
  66. package/src/explorer/ExplorerUtils.ts +59 -0
  67. package/src/explorer/GrapherGrammar.ts +387 -0
  68. package/src/explorer/gridLang/GrammarUtils.ts +121 -0
  69. package/src/explorer/gridLang/GridCell.ts +298 -0
  70. package/src/explorer/gridLang/GridLangConstants.ts +255 -0
  71. package/src/explorer/gridLang/GridProgram.ts +311 -0
  72. package/src/explorer/gridLang/readme.md +17 -0
  73. package/src/explorer/index.ts +69 -0
  74. package/src/explorer/readme.md +19 -0
  75. package/src/explorer/urlMigrations/CO2UrlMigration.ts +46 -0
  76. package/src/explorer/urlMigrations/CovidUrlMigration.ts +37 -0
  77. package/src/explorer/urlMigrations/EnergyUrlMigration.ts +41 -0
  78. package/src/explorer/urlMigrations/ExplorerPageUrlMigrationSpec.ts +12 -0
  79. package/src/explorer/urlMigrations/ExplorerUrlMigrationUtils.ts +45 -0
  80. package/src/explorer/urlMigrations/ExplorerUrlMigrations.ts +33 -0
  81. package/src/explorer/urlMigrations/LegacyCovidUrlMigration.ts +144 -0
  82. package/src/explorer/urlMigrations/readme.md +39 -0
  83. package/src/grapher/axis/Axis.ts +973 -0
  84. package/src/grapher/axis/AxisConfig.ts +179 -0
  85. package/src/grapher/axis/AxisViews.tsx +597 -0
  86. package/src/grapher/barCharts/DiscreteBarChart.tsx +728 -0
  87. package/src/grapher/barCharts/DiscreteBarChartConstants.ts +60 -0
  88. package/src/grapher/barCharts/DiscreteBarChartHelpers.ts +338 -0
  89. package/src/grapher/barCharts/DiscreteBarChartState.ts +354 -0
  90. package/src/grapher/barCharts/DiscreteBarChartThumbnail.tsx +34 -0
  91. package/src/grapher/captionedChart/CaptionedChart.scss +61 -0
  92. package/src/grapher/captionedChart/CaptionedChart.tsx +523 -0
  93. package/src/grapher/captionedChart/Logos.tsx +141 -0
  94. package/src/grapher/captionedChart/LogosSVG.tsx +16 -0
  95. package/src/grapher/captionedChart/StaticChartRasterizer.tsx +178 -0
  96. package/src/grapher/captionedChart/assets/buildcanada-logo-square.svg +15 -0
  97. package/src/grapher/captionedChart/assets/buildcanada-logo.svg +15 -0
  98. package/src/grapher/captionedChart/assets/canadaspends.svg +7 -0
  99. package/src/grapher/captionedChart/readme.md +14 -0
  100. package/src/grapher/chart/Chart.tsx +62 -0
  101. package/src/grapher/chart/ChartAreaContent.tsx +172 -0
  102. package/src/grapher/chart/ChartDimension.ts +121 -0
  103. package/src/grapher/chart/ChartInterface.ts +83 -0
  104. package/src/grapher/chart/ChartManager.ts +113 -0
  105. package/src/grapher/chart/ChartTabs.ts +178 -0
  106. package/src/grapher/chart/ChartTypeMap.tsx +158 -0
  107. package/src/grapher/chart/ChartTypeSwitcher.tsx +26 -0
  108. package/src/grapher/chart/ChartUtils.tsx +364 -0
  109. package/src/grapher/chart/DimensionSlot.ts +45 -0
  110. package/src/grapher/chart/StaticChartWrapper.tsx +94 -0
  111. package/src/grapher/chart/guidedChartUtils.ts +82 -0
  112. package/src/grapher/color/BinningStrategies.ts +484 -0
  113. package/src/grapher/color/BinningStrategyEqualSizeBins.ts +132 -0
  114. package/src/grapher/color/BinningStrategyLogarithmic.ts +121 -0
  115. package/src/grapher/color/CategoricalColorAssigner.ts +97 -0
  116. package/src/grapher/color/ColorBrewerSchemes.ts +80 -0
  117. package/src/grapher/color/ColorConstants.ts +20 -0
  118. package/src/grapher/color/ColorScale.ts +339 -0
  119. package/src/grapher/color/ColorScaleBin.ts +147 -0
  120. package/src/grapher/color/ColorScaleConfig.ts +204 -0
  121. package/src/grapher/color/ColorScheme.ts +137 -0
  122. package/src/grapher/color/ColorSchemes.ts +149 -0
  123. package/src/grapher/color/ColorUtils.ts +86 -0
  124. package/src/grapher/color/CustomSchemes.ts +1772 -0
  125. package/src/grapher/color/readme.md +84 -0
  126. package/src/grapher/comparisonLine/ComparisonLine.tsx +31 -0
  127. package/src/grapher/comparisonLine/ComparisonLineConstants.ts +11 -0
  128. package/src/grapher/comparisonLine/ComparisonLineGenerator.ts +60 -0
  129. package/src/grapher/comparisonLine/ComparisonLineHelpers.ts +10 -0
  130. package/src/grapher/comparisonLine/CustomComparisonLine.tsx +159 -0
  131. package/src/grapher/comparisonLine/VerticalComparisonLine.tsx +208 -0
  132. package/src/grapher/controls/ActionButtons.scss +97 -0
  133. package/src/grapher/controls/ActionButtons.tsx +453 -0
  134. package/src/grapher/controls/CommandPalette.scss +50 -0
  135. package/src/grapher/controls/CommandPalette.tsx +74 -0
  136. package/src/grapher/controls/ContentSwitchers.scss +93 -0
  137. package/src/grapher/controls/ContentSwitchers.tsx +238 -0
  138. package/src/grapher/controls/Controls.scss +158 -0
  139. package/src/grapher/controls/DataTableFilterDropdown.scss +7 -0
  140. package/src/grapher/controls/DataTableFilterDropdown.tsx +168 -0
  141. package/src/grapher/controls/DataTableSearchField.scss +3 -0
  142. package/src/grapher/controls/DataTableSearchField.tsx +76 -0
  143. package/src/grapher/controls/Dropdown.scss +252 -0
  144. package/src/grapher/controls/Dropdown.tsx +235 -0
  145. package/src/grapher/controls/EntitySelectionToggle.tsx +135 -0
  146. package/src/grapher/controls/MapRegionDropdown.scss +3 -0
  147. package/src/grapher/controls/MapRegionDropdown.tsx +104 -0
  148. package/src/grapher/controls/MapResetButton.tsx +115 -0
  149. package/src/grapher/controls/MapZoomDropdown.scss +9 -0
  150. package/src/grapher/controls/MapZoomDropdown.tsx +270 -0
  151. package/src/grapher/controls/MapZoomToSelectionButton.tsx +87 -0
  152. package/src/grapher/controls/SearchField.scss +78 -0
  153. package/src/grapher/controls/SearchField.tsx +63 -0
  154. package/src/grapher/controls/SettingsMenu.scss +191 -0
  155. package/src/grapher/controls/SettingsMenu.tsx +399 -0
  156. package/src/grapher/controls/ShareMenu.scss +58 -0
  157. package/src/grapher/controls/ShareMenu.tsx +304 -0
  158. package/src/grapher/controls/SortIcon.tsx +39 -0
  159. package/src/grapher/controls/VerticalScrollContainer.tsx +263 -0
  160. package/src/grapher/controls/controlsRow/ControlsRow.tsx +168 -0
  161. package/src/grapher/controls/dropdown-icons.scss +4 -0
  162. package/src/grapher/controls/entityPicker/EntityPicker.scss +255 -0
  163. package/src/grapher/controls/entityPicker/EntityPicker.tsx +816 -0
  164. package/src/grapher/controls/entityPicker/EntityPickerConstants.ts +23 -0
  165. package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.scss +129 -0
  166. package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.tsx +463 -0
  167. package/src/grapher/controls/globalEntitySelector/GlobalEntitySelectorConstants.ts +3 -0
  168. package/src/grapher/controls/globalEntitySelector/readme.md +17 -0
  169. package/src/grapher/controls/settings/AbsRelToggle.tsx +64 -0
  170. package/src/grapher/controls/settings/AxisScaleToggle.tsx +53 -0
  171. package/src/grapher/controls/settings/FacetStrategySelector.tsx +110 -0
  172. package/src/grapher/controls/settings/FacetYDomainToggle.tsx +51 -0
  173. package/src/grapher/controls/settings/NoDataAreaToggle.tsx +38 -0
  174. package/src/grapher/controls/settings/ZoomToggle.tsx +36 -0
  175. package/src/grapher/core/EntitiesByRegionType.ts +174 -0
  176. package/src/grapher/core/EntityCodes.ts +19 -0
  177. package/src/grapher/core/EntityUrlBuilder.ts +200 -0
  178. package/src/grapher/core/FetchingGrapher.tsx +156 -0
  179. package/src/grapher/core/Grapher.tsx +760 -0
  180. package/src/grapher/core/GrapherAnalytics.ts +229 -0
  181. package/src/grapher/core/GrapherConstants.ts +173 -0
  182. package/src/grapher/core/GrapherState.tsx +3659 -0
  183. package/src/grapher/core/GrapherUrl.ts +184 -0
  184. package/src/grapher/core/GrapherUrlMigrations.ts +29 -0
  185. package/src/grapher/core/GrapherUseHelpers.tsx +147 -0
  186. package/src/grapher/core/LegacyToOwidTable.ts +841 -0
  187. package/src/grapher/core/grapher.entry.ts +5 -0
  188. package/src/grapher/core/grapher.scss +257 -0
  189. package/src/grapher/core/loadGrapherTableHelpers.ts +116 -0
  190. package/src/grapher/core/loadVariable.ts +104 -0
  191. package/src/grapher/core/relatedQuestion.ts +12 -0
  192. package/src/grapher/core/typography.scss +206 -0
  193. package/src/grapher/dataTable/DataTable.sample.ts +206 -0
  194. package/src/grapher/dataTable/DataTable.scss +249 -0
  195. package/src/grapher/dataTable/DataTable.tsx +1332 -0
  196. package/src/grapher/dataTable/DataTableConstants.ts +186 -0
  197. package/src/grapher/entitySelector/EntitySelector.scss +255 -0
  198. package/src/grapher/entitySelector/EntitySelector.tsx +1838 -0
  199. package/src/grapher/facet/FacetChart.tsx +943 -0
  200. package/src/grapher/facet/FacetChartConstants.ts +24 -0
  201. package/src/grapher/facet/FacetChartUtils.ts +51 -0
  202. package/src/grapher/facet/FacetMap.tsx +604 -0
  203. package/src/grapher/facet/FacetMapConstants.ts +23 -0
  204. package/src/grapher/facet/readme.md +13 -0
  205. package/src/grapher/focus/FocusArray.ts +79 -0
  206. package/src/grapher/footer/Footer.scss +63 -0
  207. package/src/grapher/footer/Footer.tsx +809 -0
  208. package/src/grapher/footer/FooterManager.ts +44 -0
  209. package/src/grapher/fullScreen/FullScreen.scss +11 -0
  210. package/src/grapher/fullScreen/FullScreen.tsx +61 -0
  211. package/src/grapher/header/Header.scss +35 -0
  212. package/src/grapher/header/Header.tsx +372 -0
  213. package/src/grapher/header/HeaderManager.ts +28 -0
  214. package/src/grapher/index.ts +157 -0
  215. package/src/grapher/interaction/InteractionState.ts +60 -0
  216. package/src/grapher/legend/HorizontalColorLegends.tsx +923 -0
  217. package/src/grapher/legend/LegendInteractionState.ts +40 -0
  218. package/src/grapher/legend/VerticalColorLegend.tsx +295 -0
  219. package/src/grapher/lineCharts/LineChart.tsx +968 -0
  220. package/src/grapher/lineCharts/LineChartConstants.ts +89 -0
  221. package/src/grapher/lineCharts/LineChartHelpers.ts +184 -0
  222. package/src/grapher/lineCharts/LineChartState.ts +394 -0
  223. package/src/grapher/lineCharts/LineChartThumbnail.tsx +437 -0
  224. package/src/grapher/lineCharts/Lines.tsx +258 -0
  225. package/src/grapher/lineLegend/LineLegend.tsx +723 -0
  226. package/src/grapher/lineLegend/LineLegendConstants.ts +9 -0
  227. package/src/grapher/lineLegend/LineLegendFilterAlgorithms.ts +143 -0
  228. package/src/grapher/lineLegend/LineLegendHelpers.ts +253 -0
  229. package/src/grapher/lineLegend/LineLegendTypes.ts +32 -0
  230. package/src/grapher/mapCharts/CanadaTopology.ts +17922 -0
  231. package/src/grapher/mapCharts/ChoroplethGlobe.tsx +949 -0
  232. package/src/grapher/mapCharts/ChoroplethMap.tsx +662 -0
  233. package/src/grapher/mapCharts/GeoFeatures.ts +184 -0
  234. package/src/grapher/mapCharts/GlobeController.ts +496 -0
  235. package/src/grapher/mapCharts/MapAnnotationPlacements.json +1040 -0
  236. package/src/grapher/mapCharts/MapAnnotationPlacements.ts +31 -0
  237. package/src/grapher/mapCharts/MapAnnotations.ts +723 -0
  238. package/src/grapher/mapCharts/MapChart.sample.ts +59 -0
  239. package/src/grapher/mapCharts/MapChart.scss +5 -0
  240. package/src/grapher/mapCharts/MapChart.tsx +720 -0
  241. package/src/grapher/mapCharts/MapChartConstants.ts +260 -0
  242. package/src/grapher/mapCharts/MapChartState.ts +416 -0
  243. package/src/grapher/mapCharts/MapChartThumbnail.tsx +25 -0
  244. package/src/grapher/mapCharts/MapComponents.tsx +338 -0
  245. package/src/grapher/mapCharts/MapConfig.ts +156 -0
  246. package/src/grapher/mapCharts/MapHelpers.ts +181 -0
  247. package/src/grapher/mapCharts/MapProjections.ts +49 -0
  248. package/src/grapher/mapCharts/MapSparkline.tsx +257 -0
  249. package/src/grapher/mapCharts/MapTooltip.scss +49 -0
  250. package/src/grapher/mapCharts/MapTooltip.tsx +409 -0
  251. package/src/grapher/mapCharts/MapTopology.ts +1766 -0
  252. package/src/grapher/mapCharts/d3-bboxCollide.js +204 -0
  253. package/src/grapher/mapCharts/d3-geo-projection.ts +198 -0
  254. package/src/grapher/modal/DownloadIcons.tsx +39 -0
  255. package/src/grapher/modal/DownloadModal.scss +300 -0
  256. package/src/grapher/modal/DownloadModal.tsx +1226 -0
  257. package/src/grapher/modal/EmbedModal.scss +40 -0
  258. package/src/grapher/modal/EmbedModal.tsx +160 -0
  259. package/src/grapher/modal/EntitySelectorModal.tsx +59 -0
  260. package/src/grapher/modal/Modal.scss +31 -0
  261. package/src/grapher/modal/Modal.tsx +90 -0
  262. package/src/grapher/modal/ModalHeader.scss +12 -0
  263. package/src/grapher/modal/ModalHeader.tsx +16 -0
  264. package/src/grapher/modal/SourcesDescriptions.scss +87 -0
  265. package/src/grapher/modal/SourcesDescriptions.tsx +89 -0
  266. package/src/grapher/modal/SourcesKeyDataTable.scss +49 -0
  267. package/src/grapher/modal/SourcesKeyDataTable.tsx +87 -0
  268. package/src/grapher/modal/SourcesModal.scss +301 -0
  269. package/src/grapher/modal/SourcesModal.tsx +568 -0
  270. package/src/grapher/noDataModal/NoDataModal.tsx +125 -0
  271. package/src/grapher/scatterCharts/ConnectedScatterLegend.tsx +143 -0
  272. package/src/grapher/scatterCharts/MultiColorPolyline.tsx +129 -0
  273. package/src/grapher/scatterCharts/NoDataSection.scss +14 -0
  274. package/src/grapher/scatterCharts/NoDataSection.tsx +56 -0
  275. package/src/grapher/scatterCharts/ScatterPlotChart.tsx +792 -0
  276. package/src/grapher/scatterCharts/ScatterPlotChartConstants.ts +157 -0
  277. package/src/grapher/scatterCharts/ScatterPlotChartState.ts +678 -0
  278. package/src/grapher/scatterCharts/ScatterPlotChartThumbnail.tsx +155 -0
  279. package/src/grapher/scatterCharts/ScatterPlotTooltip.tsx +560 -0
  280. package/src/grapher/scatterCharts/ScatterPoints.tsx +153 -0
  281. package/src/grapher/scatterCharts/ScatterPointsWithLabels.tsx +708 -0
  282. package/src/grapher/scatterCharts/ScatterSizeLegend.tsx +327 -0
  283. package/src/grapher/scatterCharts/ScatterUtils.ts +265 -0
  284. package/src/grapher/scatterCharts/Triangle.tsx +41 -0
  285. package/src/grapher/schema/README.md +33 -0
  286. package/src/grapher/schema/defaultGrapherConfig.ts +100 -0
  287. package/src/grapher/schema/grapher-schema.009.yaml +781 -0
  288. package/src/grapher/schema/migrations/helpers.ts +58 -0
  289. package/src/grapher/schema/migrations/migrate.ts +75 -0
  290. package/src/grapher/schema/migrations/migrations.ts +158 -0
  291. package/src/grapher/selection/MapSelectionArray.ts +99 -0
  292. package/src/grapher/selection/SelectionArray.ts +71 -0
  293. package/src/grapher/selection/readme.md +16 -0
  294. package/src/grapher/sidePanel/SidePanel.scss +10 -0
  295. package/src/grapher/sidePanel/SidePanel.tsx +23 -0
  296. package/src/grapher/slideInDrawer/SlideInDrawer.scss +57 -0
  297. package/src/grapher/slideInDrawer/SlideInDrawer.tsx +125 -0
  298. package/src/grapher/slideshowController/SlideShowController.tsx +43 -0
  299. package/src/grapher/slideshowController/readme.md +7 -0
  300. package/src/grapher/slopeCharts/MarkX.tsx +45 -0
  301. package/src/grapher/slopeCharts/Slope.tsx +102 -0
  302. package/src/grapher/slopeCharts/SlopeChart.tsx +1152 -0
  303. package/src/grapher/slopeCharts/SlopeChartConstants.ts +33 -0
  304. package/src/grapher/slopeCharts/SlopeChartHelpers.ts +73 -0
  305. package/src/grapher/slopeCharts/SlopeChartState.ts +392 -0
  306. package/src/grapher/slopeCharts/SlopeChartThumbnail.tsx +368 -0
  307. package/src/grapher/stackedCharts/AbstractStackedChartState.ts +370 -0
  308. package/src/grapher/stackedCharts/MarimekkoBars.tsx +190 -0
  309. package/src/grapher/stackedCharts/MarimekkoBarsForOneEntity.tsx +168 -0
  310. package/src/grapher/stackedCharts/MarimekkoChart.tsx +1144 -0
  311. package/src/grapher/stackedCharts/MarimekkoChartConstants.ts +112 -0
  312. package/src/grapher/stackedCharts/MarimekkoChartHelpers.ts +21 -0
  313. package/src/grapher/stackedCharts/MarimekkoChartState.ts +465 -0
  314. package/src/grapher/stackedCharts/MarimekkoChartThumbnail.tsx +168 -0
  315. package/src/grapher/stackedCharts/MarimekkoInternalLabels.tsx +124 -0
  316. package/src/grapher/stackedCharts/StackedAreaChart.tsx +678 -0
  317. package/src/grapher/stackedCharts/StackedAreaChartState.ts +34 -0
  318. package/src/grapher/stackedCharts/StackedAreaChartThumbnail.tsx +215 -0
  319. package/src/grapher/stackedCharts/StackedAreas.tsx +223 -0
  320. package/src/grapher/stackedCharts/StackedBarChart.tsx +619 -0
  321. package/src/grapher/stackedCharts/StackedBarChartState.ts +80 -0
  322. package/src/grapher/stackedCharts/StackedBarChartThumbnail.tsx +220 -0
  323. package/src/grapher/stackedCharts/StackedBarSegment.tsx +87 -0
  324. package/src/grapher/stackedCharts/StackedBars.tsx +102 -0
  325. package/src/grapher/stackedCharts/StackedConstants.ts +109 -0
  326. package/src/grapher/stackedCharts/StackedDiscreteBarChart.tsx +270 -0
  327. package/src/grapher/stackedCharts/StackedDiscreteBarChartState.ts +296 -0
  328. package/src/grapher/stackedCharts/StackedDiscreteBarChartThumbnail.tsx +27 -0
  329. package/src/grapher/stackedCharts/StackedDiscreteBars.tsx +648 -0
  330. package/src/grapher/stackedCharts/StackedUtils.ts +142 -0
  331. package/src/grapher/tabs/Tabs.scss +169 -0
  332. package/src/grapher/tabs/Tabs.tsx +54 -0
  333. package/src/grapher/tabs/TabsWithDropdown.scss +62 -0
  334. package/src/grapher/tabs/TabsWithDropdown.tsx +114 -0
  335. package/src/grapher/testData/OwidTestData.sample.ts +273 -0
  336. package/src/grapher/testData/OwidTestData.ts +64 -0
  337. package/src/grapher/timeline/TimelineComponent.scss +139 -0
  338. package/src/grapher/timeline/TimelineComponent.tsx +658 -0
  339. package/src/grapher/timeline/TimelineController.ts +368 -0
  340. package/src/grapher/timeline/readme.md +7 -0
  341. package/src/grapher/tooltip/Tooltip.scss +510 -0
  342. package/src/grapher/tooltip/Tooltip.tsx +294 -0
  343. package/src/grapher/tooltip/TooltipContents.tsx +383 -0
  344. package/src/grapher/tooltip/TooltipProps.ts +123 -0
  345. package/src/grapher/tooltip/TooltipState.ts +81 -0
  346. package/src/grapher/verticalLabels/VerticalLabels.tsx +31 -0
  347. package/src/grapher/verticalLabels/VerticalLabelsState.ts +154 -0
  348. package/src/index.ts +226 -0
  349. package/src/styles/charts.scss +15 -0
  350. package/src/types/NominalType.ts +30 -0
  351. package/src/types/OwidOrigin.ts +18 -0
  352. package/src/types/OwidSource.ts +9 -0
  353. package/src/types/OwidVariable.ts +133 -0
  354. package/src/types/OwidVariableDisplayConfigInterface.ts +49 -0
  355. package/src/types/analyticsTypes.ts +54 -0
  356. package/src/types/dbTypes/Tags.ts +11 -0
  357. package/src/types/domainTypes/Archive.ts +139 -0
  358. package/src/types/domainTypes/Author.ts +28 -0
  359. package/src/types/domainTypes/ContentGraph.ts +76 -0
  360. package/src/types/domainTypes/CoreTableTypes.ts +305 -0
  361. package/src/types/domainTypes/DeployStatus.ts +23 -0
  362. package/src/types/domainTypes/Layout.ts +34 -0
  363. package/src/types/domainTypes/Posts.ts +34 -0
  364. package/src/types/domainTypes/Search.ts +299 -0
  365. package/src/types/domainTypes/Site.ts +8 -0
  366. package/src/types/domainTypes/StaticViz.ts +64 -0
  367. package/src/types/domainTypes/Toc.ts +11 -0
  368. package/src/types/domainTypes/Tombstone.ts +19 -0
  369. package/src/types/domainTypes/Various.ts +79 -0
  370. package/src/types/gdocTypes/Gdoc.ts +280 -0
  371. package/src/types/grapherTypes/BinningStrategyTypes.ts +46 -0
  372. package/src/types/grapherTypes/GrapherConstants.ts +53 -0
  373. package/src/types/grapherTypes/GrapherTypes.ts +743 -0
  374. package/src/types/index.ts +316 -0
  375. package/src/types/wordpressTypes/WordpressTypes.ts +9 -0
  376. package/src/utils/Bounds.ts +439 -0
  377. package/src/utils/BrowserUtils.ts +12 -0
  378. package/src/utils/FuzzySearch.ts +74 -0
  379. package/src/utils/MultiDimDataPageConfig.ts +31 -0
  380. package/src/utils/OwidVariable.ts +82 -0
  381. package/src/utils/PointVector.ts +97 -0
  382. package/src/utils/PromiseCache.ts +36 -0
  383. package/src/utils/PromiseSwitcher.ts +52 -0
  384. package/src/utils/TimeBounds.ts +130 -0
  385. package/src/utils/Tippy.tsx +57 -0
  386. package/src/utils/Util.ts +2369 -0
  387. package/src/utils/archival/archivalDate.ts +48 -0
  388. package/src/utils/dayjs.ts +32 -0
  389. package/src/utils/formatValue.ts +242 -0
  390. package/src/utils/grapherConfigUtils.ts +81 -0
  391. package/src/utils/image.ts +225 -0
  392. package/src/utils/index.ts +318 -0
  393. package/src/utils/isPresent.ts +5 -0
  394. package/src/utils/metadataHelpers.ts +329 -0
  395. package/src/utils/persistable/Persistable.ts +82 -0
  396. package/src/utils/persistable/readme.md +50 -0
  397. package/src/utils/regions.json +5635 -0
  398. package/src/utils/regions.ts +463 -0
  399. package/src/utils/serializers.ts +16 -0
  400. package/src/utils/string.ts +42 -0
  401. package/src/utils/urls/Url.ts +195 -0
  402. package/src/utils/urls/UrlMigration.ts +10 -0
  403. package/src/utils/urls/UrlUtils.ts +54 -0
  404. package/src/utils/urls/readme.md +90 -0
@@ -0,0 +1,1838 @@
1
+ import * as _ from "lodash-es"
2
+ import * as React from "react"
3
+ import { observer } from "mobx-react"
4
+ import {
5
+ computed,
6
+ action,
7
+ reaction,
8
+ when,
9
+ IReactionDisposer,
10
+ makeObservable,
11
+ } from "mobx"
12
+ import cx from "classnames"
13
+ import a from "indefinite"
14
+ import {
15
+ isTouchDevice,
16
+ SortOrder,
17
+ isFiniteWithGuard,
18
+ CoreValueType,
19
+ getUserCountryInformation,
20
+ regions,
21
+ Tippy,
22
+ excludeUndefined,
23
+ FuzzySearch,
24
+ getUserNavigatorLanguagesNonEnglish,
25
+ getRegionAlternativeNames,
26
+ convertDaysSinceEpochToDate,
27
+ checkIsOwidIncomeGroupName,
28
+ checkHasMembers,
29
+ Region,
30
+ getRegionByName,
31
+ makeSafeForCSS,
32
+ } from "../../utils/index.js"
33
+ import {
34
+ Checkbox,
35
+ RadioButton,
36
+ OverlayHeader,
37
+ } from "../../components/index.js"
38
+ import {
39
+ faLocationArrow,
40
+ faArrowRightArrowLeft,
41
+ faFilter,
42
+ } from "@fortawesome/free-solid-svg-icons"
43
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
44
+ import { SelectionArray } from "../selection/SelectionArray"
45
+ import { Flipper, Flipped } from "react-flip-toolkit"
46
+ import {
47
+ combineHistoricalAndProjectionColumns,
48
+ makeSelectionArray,
49
+ } from "../chart/ChartUtils.js"
50
+ import {
51
+ DEFAULT_GRAPHER_ENTITY_TYPE,
52
+ DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL,
53
+ POPULATION_INDICATOR_ID_USED_IN_ENTITY_SELECTOR,
54
+ GDP_PER_CAPITA_INDICATOR_ID_USED_IN_ENTITY_SELECTOR,
55
+ isPopulationVariableETLPath,
56
+ isWorldEntityName,
57
+ } from "../core/GrapherConstants"
58
+ import { CoreColumn, OwidTable } from "../../core-table/index.js"
59
+ import { SortIcon } from "../controls/SortIcon"
60
+ import { Dropdown } from "../controls/Dropdown"
61
+ import { scaleLinear, type ScaleLinear } from "d3-scale"
62
+ import {
63
+ AdditionalGrapherDataFetchFn,
64
+ ColumnSlug,
65
+ EntityName,
66
+ OwidColumnDef,
67
+ ProjectionColumnInfo,
68
+ Time,
69
+ ToleranceStrategy,
70
+ type EntitySelectorEvent,
71
+ } from "../../types/index.js"
72
+ import { buildVariableTable } from "../core/LegacyToOwidTable"
73
+ import { DrawerContext } from "../slideInDrawer/SlideInDrawer.js"
74
+ import * as R from "remeda"
75
+ import { MapConfig } from "../mapCharts/MapConfig"
76
+ import { match } from "ts-pattern"
77
+ import {
78
+ entityRegionTypeLabels,
79
+ EntityNamesByRegionType,
80
+ EntityRegionType,
81
+ EntityRegionTypeGroup,
82
+ isAggregateSource,
83
+ } from "../core/EntitiesByRegionType"
84
+ import { SearchField } from "../controls/SearchField"
85
+ import { MAP_REGION_LABELS } from "../mapCharts/MapChartConstants.js"
86
+
87
+ export type CoreColumnBySlug = Record<ColumnSlug, CoreColumn>
88
+
89
+ type EntityFilter = EntityRegionType | "all"
90
+
91
+ type ValueBySlugAndTimeAndEntityName<T> = Map<
92
+ ColumnSlug,
93
+ Map<Time, Map<EntityName, T>>
94
+ >
95
+
96
+ export interface EntitySelectorState {
97
+ searchInput: string
98
+ sortConfig: SortConfig
99
+ entityFilter: EntityFilter
100
+ localEntityNames?: string[]
101
+ interpolatedSortColumnsBySlug?: CoreColumnBySlug
102
+ isProjectionBySlugAndTimeAndEntityName?: ValueBySlugAndTimeAndEntityName<boolean>
103
+ isLoadingExternalSortColumn?: boolean
104
+ }
105
+
106
+ export interface EntitySelectorManager {
107
+ entitySelectorState: Partial<EntitySelectorState>
108
+ table: OwidTable
109
+ tableForSelection: OwidTable
110
+ selection: SelectionArray
111
+ entityType?: string
112
+ entityTypePlural?: string
113
+ activeColumnSlugs?: string[]
114
+ isEntitySelectorModalOrDrawerOpen?: boolean
115
+ canChangeEntity?: boolean
116
+ canHighlightEntities?: boolean
117
+ endTime?: Time
118
+ isOnMapTab?: boolean
119
+ mapConfig?: MapConfig
120
+ mapColumnSlug?: ColumnSlug
121
+ isEntityMutedInSelector?: (entityName: EntityName) => boolean
122
+ onSelectEntity?: (entityName: EntityName) => void
123
+ onDeselectEntity?: (entityName: EntityName) => void
124
+ onClearEntities?: () => void
125
+ entityRegionTypeGroups?: EntityRegionTypeGroup[]
126
+ entityNamesByRegionType?: EntityNamesByRegionType
127
+ isReady?: boolean
128
+ logEntitySelectorEvent: (
129
+ action: EntitySelectorEvent,
130
+ target?: string
131
+ ) => void
132
+ additionalDataLoaderFn?: AdditionalGrapherDataFetchFn
133
+ projectionColumnInfoBySlug?: Map<ColumnSlug, ProjectionColumnInfo>
134
+ }
135
+
136
+ interface SortConfig {
137
+ slug: ColumnSlug
138
+ order: SortOrder
139
+ }
140
+
141
+ type SearchableEntity = {
142
+ name: string
143
+ sortColumnValues: Record<ColumnSlug, CoreValueType | undefined>
144
+ isLocal?: boolean
145
+ alternativeNames?: string[]
146
+ regionInfo?: Region
147
+ }
148
+
149
+ interface SortDropdownOption {
150
+ type:
151
+ | "name" // sorted by name
152
+ | "chart-indicator" // sorted by chart column
153
+ | "external-indicator" // sorted by an external indicator
154
+ value: string // slug
155
+ slug: string
156
+ label: string
157
+ formattedTime?: string
158
+ trackNote?: string // unused
159
+ }
160
+
161
+ interface FilterDropdownOption {
162
+ value: EntityFilter
163
+ label: string
164
+ count: number
165
+ trackNote?: string // unused
166
+ }
167
+
168
+ const EXTERNAL_SORT_INDICATOR_DEFINITIONS = [
169
+ {
170
+ key: "population",
171
+ label: "Population",
172
+ indicatorId: POPULATION_INDICATOR_ID_USED_IN_ENTITY_SELECTOR,
173
+ slug: indicatorIdToSlug(
174
+ POPULATION_INDICATOR_ID_USED_IN_ENTITY_SELECTOR
175
+ ),
176
+ // checks if a column has population data
177
+ isMatch: (column: CoreColumn): boolean => {
178
+ // check the slug first
179
+ const externalSlug = indicatorIdToSlug(
180
+ POPULATION_INDICATOR_ID_USED_IN_ENTITY_SELECTOR
181
+ )
182
+ if (column.slug === externalSlug) return true
183
+
184
+ // then check the catalog path
185
+ return isPopulationVariableETLPath(
186
+ (column.def as OwidColumnDef)?.catalogPath ?? ""
187
+ )
188
+ },
189
+ },
190
+ {
191
+ key: "gdpPerCapita",
192
+ label: "GDP per capita (int. $)",
193
+ indicatorId: GDP_PER_CAPITA_INDICATOR_ID_USED_IN_ENTITY_SELECTOR,
194
+ slug: indicatorIdToSlug(
195
+ GDP_PER_CAPITA_INDICATOR_ID_USED_IN_ENTITY_SELECTOR
196
+ ),
197
+ // checks if a column has GDP per capita data
198
+ isMatch: (column: CoreColumn): boolean => {
199
+ // check the slug first
200
+ const externalSlug = indicatorIdToSlug(
201
+ GDP_PER_CAPITA_INDICATOR_ID_USED_IN_ENTITY_SELECTOR
202
+ )
203
+ if (column.slug === externalSlug) return true
204
+
205
+ // then check the label
206
+ const label = getTitleForSortColumnLabel(column)
207
+ // matches "gdp per capita" and content within parentheses
208
+ const potentialMatches =
209
+ label.match(/\(.*?\)|(\bgdp per capita\b)/gi) ?? []
210
+ // filter for "gdp per capita" matches that are not within parentheses
211
+ const matches = potentialMatches.filter(
212
+ (match) => !match.includes("(")
213
+ )
214
+
215
+ return matches.length > 0
216
+ },
217
+ },
218
+ ] as const
219
+
220
+ type ExternalSortIndicatorDefinition =
221
+ (typeof EXTERNAL_SORT_INDICATOR_DEFINITIONS)[number]
222
+ type ExternalSortIndicatorKey = ExternalSortIndicatorDefinition["key"]
223
+
224
+ const regionNamesSet = new Set(regions.map((region) => region.name))
225
+
226
+ interface EntitySelectorProps {
227
+ manager: EntitySelectorManager
228
+ selection?: SelectionArray
229
+ autoFocus?: boolean
230
+ onDismiss?: () => void
231
+ }
232
+
233
+ @observer
234
+ export class EntitySelector extends React.Component<EntitySelectorProps> {
235
+ static override contextType = DrawerContext
236
+ declare context: React.ContextType<typeof DrawerContext>
237
+
238
+ scrollableContainer = React.createRef<HTMLDivElement>()
239
+ searchFieldRef = React.createRef<HTMLInputElement>()
240
+ contentRef = React.createRef<HTMLDivElement>()
241
+
242
+ private sortConfigByName: SortConfig = {
243
+ slug: this.table.entityNameSlug,
244
+ order: SortOrder.asc,
245
+ }
246
+
247
+ private disposers: IReactionDisposer[] = []
248
+
249
+ constructor(props: EntitySelectorProps) {
250
+ super(props)
251
+ makeObservable(this)
252
+ }
253
+
254
+ override componentDidMount(): void {
255
+ void this.populateLocalEntities()
256
+
257
+ if (this.props.autoFocus && !isTouchDevice())
258
+ this.searchFieldRef.current?.focus()
259
+
260
+ // scroll to the top when the search input changes
261
+ this.disposers.push(
262
+ reaction(
263
+ () => this.searchInput,
264
+ () => {
265
+ if (this.scrollableContainer.current)
266
+ this.scrollableContainer.current.scrollTop = 0
267
+ }
268
+ )
269
+ )
270
+
271
+ // the initial sorting strategy depends on data,
272
+ // which is why we wait for Grapher to be ready
273
+ this.disposers.push(
274
+ when(
275
+ () => !!this.manager.isReady,
276
+ () => this.initSortConfig()
277
+ )
278
+ )
279
+
280
+ // Mdims and explorers can change the columns available for sorting, and
281
+ // we need to change the sort config accordingly
282
+ this.disposers.push(
283
+ reaction(
284
+ () => this.sortOptions,
285
+ () => this.updateSortConfigIfOptionHasBecomeUnavailable()
286
+ )
287
+ )
288
+ }
289
+
290
+ override componentWillUnmount(): void {
291
+ if (this.timeoutId) clearTimeout(this.timeoutId)
292
+ this.disposers.forEach((dispose) => dispose())
293
+ }
294
+
295
+ @action.bound private set(newState: Partial<EntitySelectorState>): void {
296
+ const correctedState = { ...newState }
297
+
298
+ if (newState.sortConfig !== undefined) {
299
+ const correctedSortConfig = { ...newState.sortConfig }
300
+
301
+ const shouldBeSortedByName =
302
+ newState.sortConfig.slug === this.table.entityNameSlug
303
+
304
+ // sort names in ascending order by default
305
+ if (shouldBeSortedByName && !this.isSortedByName) {
306
+ correctedSortConfig.order = SortOrder.asc
307
+ }
308
+
309
+ // sort values in descending order by default
310
+ if (!shouldBeSortedByName && this.isSortedByName) {
311
+ correctedSortConfig.order = SortOrder.desc
312
+ }
313
+
314
+ correctedState.sortConfig = correctedSortConfig
315
+ }
316
+
317
+ this.manager.entitySelectorState = {
318
+ ...this.manager.entitySelectorState,
319
+ ...correctedState,
320
+ }
321
+ }
322
+
323
+ getDefaultSortConfig(): SortConfig {
324
+ const chartIndicatorSortOptions = this.sortOptions.filter(
325
+ (option) => option.type === "chart-indicator"
326
+ )
327
+
328
+ // default to sorting by the first chart column if there is only one
329
+ if (chartIndicatorSortOptions.length === 1) {
330
+ const { slug } = chartIndicatorSortOptions[0]
331
+ this.setInterpolatedSortColumnBySlug(slug)
332
+ return { slug, order: SortOrder.desc }
333
+ }
334
+
335
+ return this.sortConfigByName
336
+ }
337
+
338
+ updateSortConfigIfOptionHasBecomeUnavailable() {
339
+ // We don't want to update the sort config when `sortOptions` are not ready,
340
+ // because the new chart dimensions are currently loading
341
+ if (!this.manager.isReady) return
342
+ if (!this.manager.activeColumnSlugs?.length) return
343
+
344
+ // Check whether the current sort option is still available in the newly-updated
345
+ // sortOptions
346
+ if (
347
+ !this.sortOptions.find(
348
+ (option) => option.slug === this.sortConfig.slug
349
+ )
350
+ ) {
351
+ this.set({ sortConfig: this.getDefaultSortConfig() })
352
+ }
353
+ }
354
+
355
+ initSortConfig(): void {
356
+ this.set({ sortConfig: this.getDefaultSortConfig() })
357
+ }
358
+
359
+ resetInterpolatedMapColumn(): void {
360
+ const { mapColumnSlug } = this.manager
361
+ const sortSlug = this.sortConfig.slug
362
+
363
+ // no need to reset the map column slug if it doesn't exist or isn't set
364
+ if (
365
+ !mapColumnSlug ||
366
+ !this.interpolatedSortColumnsBySlug[mapColumnSlug]
367
+ )
368
+ return
369
+
370
+ if (sortSlug === mapColumnSlug) {
371
+ // if the map column slug is currently selected, re-calculate its
372
+ // tolerance because the map and chart tab might have different
373
+ // tolerance settings
374
+ this.setInterpolatedSortColumn(
375
+ this.interpolateSortColumn(mapColumnSlug)
376
+ )
377
+ } else {
378
+ // otherwise, delete it and it will be re-calculated when necessary
379
+ delete this.manager.entitySelectorState
380
+ .interpolatedSortColumnsBySlug?.[mapColumnSlug]
381
+ }
382
+ }
383
+
384
+ @action.bound async populateLocalEntities(): Promise<void> {
385
+ try {
386
+ const localCountryInfo = await getUserCountryInformation()
387
+ if (!localCountryInfo) return
388
+
389
+ const countryRegionsWithoutIncomeGroups = localCountryInfo.regions
390
+ ? localCountryInfo.regions.filter(
391
+ (region) => !checkIsOwidIncomeGroupName(region)
392
+ )
393
+ : []
394
+
395
+ const userEntityCodes = [
396
+ localCountryInfo.code,
397
+ ...countryRegionsWithoutIncomeGroups,
398
+ ]
399
+
400
+ const userRegions = regions.filter((region) =>
401
+ userEntityCodes.includes(region.code)
402
+ )
403
+
404
+ const sortedUserRegions = _.sortBy(userRegions, (region) =>
405
+ userEntityCodes.indexOf(region.code)
406
+ )
407
+
408
+ const localEntityNames = sortedUserRegions.map(
409
+ (region) => region.name
410
+ )
411
+
412
+ if (localEntityNames) this.set({ localEntityNames })
413
+ } catch {
414
+ // ignore
415
+ }
416
+ }
417
+
418
+ private setInterpolatedSortColumn(column: CoreColumn): void {
419
+ this.set({
420
+ interpolatedSortColumnsBySlug: {
421
+ ...this.interpolatedSortColumnsBySlug,
422
+ [column.slug]: column,
423
+ },
424
+ })
425
+ }
426
+
427
+ private setIsProjectionForSlug(
428
+ slug: ColumnSlug,
429
+ valuesByTimeAndEntityName: Map<Time, Map<EntityName, boolean>>
430
+ ): void {
431
+ const { isProjectionBySlugAndTimeAndEntityName } = this
432
+ isProjectionBySlugAndTimeAndEntityName.set(
433
+ slug,
434
+ valuesByTimeAndEntityName
435
+ )
436
+ this.set({ isProjectionBySlugAndTimeAndEntityName })
437
+ }
438
+
439
+ @computed private get toleranceOverride(): {
440
+ value?: number
441
+ strategy?: ToleranceStrategy
442
+ } {
443
+ // use map tolerance if on the map tab
444
+ const tolerance = this.manager.isOnMapTab
445
+ ? this.mapConfig.timeTolerance
446
+ : undefined
447
+ const toleranceStrategy = this.manager.isOnMapTab
448
+ ? this.mapConfig.toleranceStrategy
449
+ : undefined
450
+
451
+ return { value: tolerance, strategy: toleranceStrategy }
452
+ }
453
+
454
+ private interpolateSortColumn(slug: ColumnSlug): CoreColumn {
455
+ return this.inputTable
456
+ .interpolateColumnWithTolerance(slug, {
457
+ toleranceOverride: this.toleranceOverride.value,
458
+ toleranceStrategyOverride: this.toleranceOverride.strategy,
459
+ })
460
+ .get(slug)
461
+ }
462
+
463
+ private interpolateAndCombineSortColumns(
464
+ info: ProjectionColumnInfo
465
+ ): OwidTable {
466
+ const { projectedSlug, historicalSlug } = info
467
+
468
+ // Interpolate the historical and projected columns separately
469
+ const table = this.table
470
+ .interpolateColumnWithTolerance(historicalSlug, {
471
+ toleranceOverride: this.toleranceOverride.value,
472
+ toleranceStrategyOverride: this.toleranceOverride.strategy,
473
+ })
474
+ .interpolateColumnWithTolerance(projectedSlug, {
475
+ toleranceOverride: this.toleranceOverride.value,
476
+ toleranceStrategyOverride: this.toleranceOverride.strategy,
477
+ })
478
+
479
+ // Combine the interpolated columns
480
+ return combineHistoricalAndProjectionColumns(table, info, {
481
+ shouldAddIsProjectionColumn: true,
482
+ })
483
+ }
484
+
485
+ private setInterpolatedSortColumnBySlug(slug: ColumnSlug): void {
486
+ if (this.interpolatedSortColumnsBySlug[slug]) return
487
+
488
+ // If the column is a projection and has an historical counterpart,
489
+ // then combine the projected and historical data into a single column
490
+ const projectionInfo = this.projectionColumnInfoByCombinedSlug.get(slug)
491
+ if (projectionInfo) {
492
+ const table = this.interpolateAndCombineSortColumns(projectionInfo)
493
+
494
+ const combinedColumn = table.get(projectionInfo.combinedSlug)
495
+ const isProjectionValues = table.get(
496
+ projectionInfo.slugForIsProjectionColumn
497
+ ).valueByTimeAndEntityName
498
+
499
+ this.setInterpolatedSortColumn(combinedColumn)
500
+ this.setIsProjectionForSlug(
501
+ projectionInfo.combinedSlug,
502
+ isProjectionValues
503
+ )
504
+
505
+ return
506
+ }
507
+
508
+ const column = this.interpolateSortColumn(slug)
509
+ this.setInterpolatedSortColumn(column)
510
+ }
511
+
512
+ private clearSearchInput(): void {
513
+ this.set({ searchInput: "" })
514
+ }
515
+
516
+ private resetEntityFilter(): void {
517
+ this.set({ entityFilter: undefined })
518
+ }
519
+
520
+ private updateSortSlug(newSlug: ColumnSlug) {
521
+ this.set({
522
+ sortConfig: {
523
+ slug: newSlug,
524
+ order: this.sortConfig.order,
525
+ },
526
+ })
527
+ }
528
+
529
+ private toggleSortOrder() {
530
+ const newOrder =
531
+ this.sortConfig.order === SortOrder.asc
532
+ ? SortOrder.desc
533
+ : SortOrder.asc
534
+ this.set({
535
+ sortConfig: {
536
+ slug: this.sortConfig.slug,
537
+ order: newOrder,
538
+ },
539
+ })
540
+ }
541
+
542
+ @computed private get chartHasDailyData(): boolean {
543
+ return this.numericalChartColumns.some(
544
+ (column) => column.display?.yearIsDay
545
+ )
546
+ }
547
+
548
+ /**
549
+ * Converts the given time to be compatible with the time format
550
+ * of the given column.
551
+ *
552
+ * This is necessary for external sort indicators when they're loaded
553
+ * for charts with daily data.
554
+ */
555
+ private toColumnCompatibleTime(time: Time, column: CoreColumn): Time {
556
+ const isExternal = this.externalSortIndicatorDefinitions.some(
557
+ (external) => column.slug === external.slug
558
+ )
559
+
560
+ // if the column comes from the chart, no conversion is needed
561
+ if (!isExternal) return time
562
+
563
+ // assumes that external indicators have yearly data
564
+ const year = this.chartHasDailyData
565
+ ? convertDaysSinceEpochToDate(time).year()
566
+ : time
567
+
568
+ // clamping is necessary since external indicators might not cover
569
+ // the entire time range of the chart
570
+ return R.clamp(year, { min: column.minTime, max: column.maxTime })
571
+ }
572
+
573
+ private formatTimeForSortColumnLabel(
574
+ time: Time,
575
+ column: CoreColumn
576
+ ): string {
577
+ const compatibleTime = this.toColumnCompatibleTime(time, column)
578
+ return column.formatTime(compatibleTime)
579
+ }
580
+
581
+ @computed private get manager(): EntitySelectorManager {
582
+ return this.props.manager
583
+ }
584
+
585
+ @computed private get endTime(): Time {
586
+ return this.manager.endTime ?? this.table.maxTime!
587
+ }
588
+
589
+ @computed private get mapConfig(): MapConfig {
590
+ return this.manager.mapConfig ?? new MapConfig()
591
+ }
592
+
593
+ private isEntityMuted(entityName: EntityName): boolean {
594
+ return this.manager.isEntityMutedInSelector?.(entityName) ?? false
595
+ }
596
+
597
+ @computed private get title(): string {
598
+ return this.manager.isOnMapTab
599
+ ? `Select ${this.entityType.plural}`
600
+ : this.manager.canHighlightEntities
601
+ ? `Select ${this.entityType.plural}`
602
+ : this.manager.canChangeEntity
603
+ ? `Choose ${a(this.entityType.singular)}`
604
+ : `Add/remove ${this.entityType.plural}`
605
+ }
606
+
607
+ @computed private get searchPlaceholderEntityType(): string {
608
+ if (isAggregateSource(this.entityFilter)) return "region"
609
+
610
+ return match(this.entityFilter)
611
+ .with("all", () => this.entityType.singular)
612
+ .with("countries", () => "country")
613
+ .with("continents", () => "continent")
614
+ .with("incomeGroups", () => "income group")
615
+ .with("historicalCountries", () => "country or region")
616
+ .exhaustive()
617
+ }
618
+
619
+ @computed private get searchInput(): string {
620
+ return this.manager.entitySelectorState.searchInput ?? ""
621
+ }
622
+
623
+ @computed get sortConfig(): SortConfig {
624
+ return (
625
+ this.manager.entitySelectorState.sortConfig ?? this.sortConfigByName
626
+ )
627
+ }
628
+
629
+ isSortSlugValid(slug: ColumnSlug): boolean {
630
+ return this.sortOptions.some((option) => option.value === slug)
631
+ }
632
+
633
+ isEntityFilterValid(entityFilter: EntityFilter): boolean {
634
+ return this.filterOptions.some(
635
+ (option) => option.value === entityFilter
636
+ )
637
+ }
638
+
639
+ @computed private get entityFilter(): EntityFilter {
640
+ return (
641
+ this.manager.entitySelectorState.entityFilter ??
642
+ this.filterOptions[0]?.value ??
643
+ "all"
644
+ )
645
+ }
646
+
647
+ @computed private get localEntityNames(): string[] | undefined {
648
+ return this.manager.entitySelectorState.localEntityNames
649
+ }
650
+
651
+ @computed private get interpolatedSortColumnsBySlug(): CoreColumnBySlug {
652
+ return (
653
+ this.manager.entitySelectorState.interpolatedSortColumnsBySlug ?? {}
654
+ )
655
+ }
656
+
657
+ @computed
658
+ private get isProjectionBySlugAndTimeAndEntityName(): ValueBySlugAndTimeAndEntityName<boolean> {
659
+ return (
660
+ this.manager.entitySelectorState
661
+ .isProjectionBySlugAndTimeAndEntityName ?? new Map()
662
+ )
663
+ }
664
+
665
+ @computed private get interpolatedSortColumns(): CoreColumn[] {
666
+ return Object.values(this.interpolatedSortColumnsBySlug)
667
+ }
668
+
669
+ @computed private get isLoadingExternalSortColumn(): boolean {
670
+ return (
671
+ this.manager.entitySelectorState.isLoadingExternalSortColumn ??
672
+ false
673
+ )
674
+ }
675
+
676
+ @computed private get inputTable(): OwidTable {
677
+ return this.manager.table
678
+ }
679
+
680
+ @computed private get table(): OwidTable {
681
+ return this.manager.tableForSelection
682
+ }
683
+
684
+ @computed private get someEntitiesAreRegions(): boolean {
685
+ if (!this.entitiesAreCountriesOrRegions) return false
686
+ return this.availableEntities.some((entity) =>
687
+ checkHasMembers(entity.regionInfo)
688
+ )
689
+ }
690
+
691
+ @computed private get entitiesAreCountriesOrRegions(): boolean {
692
+ // Ignore the World entity since we have charts that only have the
693
+ // World entity but no other countries or regions (e.g. 'World',
694
+ // 'Northern Hemisphere' and 'Southern hemisphere')
695
+ return this.availableEntityNames.some(
696
+ (entityName) =>
697
+ regionNamesSet.has(entityName) && !isWorldEntityName(entityName)
698
+ )
699
+ }
700
+
701
+ @computed private get supportsSortingByExternalIndicators(): boolean {
702
+ // If we can't dynamically load variables, don't ever the option to sort
703
+ // by external indicators
704
+ if (!this.manager.additionalDataLoaderFn) return false
705
+
706
+ // Adding external indicators like population and gdp per capita
707
+ // only makes sense for charts with countries or regions
708
+ return this.entitiesAreCountriesOrRegions
709
+ }
710
+
711
+ @computed private get numericalChartColumns(): CoreColumn[] {
712
+ const {
713
+ activeColumnSlugs = [],
714
+ mapColumnSlug,
715
+ isOnMapTab,
716
+ } = this.manager
717
+
718
+ const activeSlugs = isOnMapTab ? [mapColumnSlug] : activeColumnSlugs
719
+
720
+ return activeSlugs
721
+ .map((slug) => this.table.get(slug))
722
+ .filter((column) => column.hasNumberFormatting)
723
+ }
724
+
725
+ /**
726
+ * Map of chart columns that match external sort indicators.
727
+ * For example, if the chart has a column with population data,
728
+ * it will be used instead of the "Population" external indicator.
729
+ */
730
+ @computed
731
+ private get chartColumnsByExternalSortIndicatorKey(): Partial<
732
+ Record<ExternalSortIndicatorKey, CoreColumn>
733
+ > {
734
+ const matchingColumns: Partial<
735
+ Record<ExternalSortIndicatorKey, CoreColumn>
736
+ > = {}
737
+ for (const external of EXTERNAL_SORT_INDICATOR_DEFINITIONS) {
738
+ const matchingColumn = this.numericalChartColumns.find((column) =>
739
+ external.isMatch(column)
740
+ )
741
+ if (matchingColumn) matchingColumns[external.key] = matchingColumn
742
+ }
743
+ return matchingColumns
744
+ }
745
+
746
+ @computed
747
+ private get externalSortIndicatorDefinitions(): ExternalSortIndicatorDefinition[] {
748
+ if (!this.supportsSortingByExternalIndicators) return []
749
+
750
+ // if the chart has a column that matches an external sort indicator,
751
+ // prefer the chart column over the external indicator
752
+ const matchingKeys = Object.keys(
753
+ this.chartColumnsByExternalSortIndicatorKey
754
+ )
755
+ return EXTERNAL_SORT_INDICATOR_DEFINITIONS.filter(
756
+ (external) => !matchingKeys.includes(external.key)
757
+ )
758
+ }
759
+
760
+ @computed private get projectionColumnInfoByCombinedSlug(): Map<
761
+ ColumnSlug,
762
+ ProjectionColumnInfo
763
+ > {
764
+ if (!this.manager.projectionColumnInfoBySlug) return new Map()
765
+
766
+ const projectionColumnInfoByCombinedSlug: Map<
767
+ ColumnSlug,
768
+ ProjectionColumnInfo
769
+ > = new Map()
770
+
771
+ for (const info of this.manager.projectionColumnInfoBySlug.values()) {
772
+ projectionColumnInfoByCombinedSlug.set(info.combinedSlug, info)
773
+ }
774
+
775
+ return projectionColumnInfoByCombinedSlug
776
+ }
777
+
778
+ private combinedColumnHasHistoricalDataForTime(
779
+ slug: ColumnSlug,
780
+ time: Time
781
+ ): boolean | null {
782
+ const isProjectionByTimeAndEntityName =
783
+ this.isProjectionBySlugAndTimeAndEntityName.get(slug)
784
+
785
+ // We don't have data and thus can't make a decision
786
+ if (!isProjectionByTimeAndEntityName) return null
787
+
788
+ const values = isProjectionByTimeAndEntityName.get(time)?.values() ?? []
789
+ return Array.from(values)?.some((isProjection) => !isProjection)
790
+ }
791
+
792
+ private makeSortColumnLabelForCombinedColumn(
793
+ info: ProjectionColumnInfo,
794
+ time: Time
795
+ ): string {
796
+ const { table, isProjectionBySlugAndTimeAndEntityName } = this
797
+
798
+ const projectedLabel = getTitleForSortColumnLabel(
799
+ table.get(info.projectedSlug)
800
+ )
801
+ const historicalLabel = getTitleForSortColumnLabel(
802
+ table.get(info.historicalSlug)
803
+ )
804
+
805
+ const hasHistoricalDataForTime =
806
+ this.combinedColumnHasHistoricalDataForTime(info.combinedSlug, time)
807
+
808
+ // If the data for this column hasn't been computed yet, we can't
809
+ // determine if it has historical data, and thus which label to show.
810
+ // As a workaround, we check if any other (arbitrary) combined column
811
+ // has historical data for this time point, based on the assumption that
812
+ // projection columns typically share the same cut-off time.
813
+ if (hasHistoricalDataForTime === null) {
814
+ const arbitrarySlug = isProjectionBySlugAndTimeAndEntityName
815
+ .keys()
816
+ .next().value
817
+
818
+ if (arbitrarySlug) {
819
+ const hasHistoricalValues =
820
+ this.combinedColumnHasHistoricalDataForTime(
821
+ arbitrarySlug,
822
+ time
823
+ )
824
+ return hasHistoricalValues ? historicalLabel : projectedLabel
825
+ }
826
+
827
+ return projectedLabel
828
+ }
829
+
830
+ // If there is any historical value for the given time,
831
+ // we choose to show the label of the historical column
832
+ return hasHistoricalDataForTime ? historicalLabel : projectedLabel
833
+ }
834
+
835
+ @computed get sortOptions(): SortDropdownOption[] {
836
+ let options: SortDropdownOption[] = []
837
+
838
+ // the first dropdown option is always the entity name
839
+ options.push({
840
+ type: "name",
841
+ value: this.table.entityNameSlug,
842
+ slug: this.table.entityNameSlug,
843
+ label: "Name",
844
+ })
845
+
846
+ // add external indicators as sort options if applicable
847
+ if (this.supportsSortingByExternalIndicators) {
848
+ EXTERNAL_SORT_INDICATOR_DEFINITIONS.forEach((external) => {
849
+ // if the chart has a column that matches the external
850
+ // indicator, prefer it over the external indicator
851
+ const chartColumn =
852
+ this.chartColumnsByExternalSortIndicatorKey[external.key]
853
+
854
+ if (chartColumn) {
855
+ options.push({
856
+ type: "chart-indicator",
857
+ value: chartColumn.slug,
858
+ slug: chartColumn.slug,
859
+ label: getTitleForSortColumnLabel(chartColumn),
860
+ formattedTime: this.formatTimeForSortColumnLabel(
861
+ this.endTime,
862
+ chartColumn
863
+ ),
864
+ })
865
+ } else {
866
+ const column =
867
+ this.interpolatedSortColumnsBySlug[external.slug]
868
+ options.push({
869
+ type: "external-indicator",
870
+ value: external.slug,
871
+ slug: external.slug,
872
+ label: external.label,
873
+ formattedTime: column
874
+ ? this.formatTimeForSortColumnLabel(
875
+ this.endTime,
876
+ column
877
+ )
878
+ : undefined,
879
+ })
880
+ }
881
+ })
882
+ }
883
+
884
+ // add the remaining numerical chart columns as sort options,
885
+ // excluding columns that match external indicators (since those
886
+ // have already been added)
887
+ const matchingSlugs = Object.values(
888
+ this.chartColumnsByExternalSortIndicatorKey
889
+ ).map((column) => column.slug)
890
+ const columns = this.numericalChartColumns.filter(
891
+ (column) => !matchingSlugs.includes(column.slug)
892
+ )
893
+
894
+ // If we add data columns that combine historical and projected data,
895
+ // then we want to exclude the individual columns from the sort options
896
+ const slugsToExclude: Set<ColumnSlug> = new Set()
897
+
898
+ for (const column of columns) {
899
+ const formattedTime = this.formatTimeForSortColumnLabel(
900
+ this.endTime,
901
+ column
902
+ )
903
+
904
+ const projectionInfo = this.manager.projectionColumnInfoBySlug?.get(
905
+ column.slug
906
+ )
907
+
908
+ // Combine projected and historical data
909
+ if (projectionInfo) {
910
+ const time = this.toColumnCompatibleTime(this.endTime, column)
911
+ const label = this.makeSortColumnLabelForCombinedColumn(
912
+ projectionInfo,
913
+ time
914
+ )
915
+
916
+ options.push({
917
+ type: "chart-indicator",
918
+ value: projectionInfo.combinedSlug,
919
+ slug: projectionInfo.combinedSlug,
920
+ label,
921
+ formattedTime,
922
+ })
923
+
924
+ // We don't need a separate option for the historical data
925
+ // if it's part of the projection series
926
+ slugsToExclude.add(projectionInfo.historicalSlug)
927
+ } else {
928
+ options.push({
929
+ type: "chart-indicator",
930
+ value: column.slug,
931
+ slug: column.slug,
932
+ label: getTitleForSortColumnLabel(column),
933
+ formattedTime,
934
+ })
935
+ }
936
+ }
937
+
938
+ options = options.filter((option) => !slugsToExclude.has(option.value))
939
+
940
+ return options
941
+ }
942
+
943
+ @computed get sortValue(): SortDropdownOption | null {
944
+ return (
945
+ this.sortOptions.find(
946
+ (option) => option.slug === this.sortConfig.slug
947
+ ) ?? null
948
+ )
949
+ }
950
+
951
+ private isEntityNameSlug(slug: ColumnSlug): boolean {
952
+ return slug === this.table.entityNameSlug
953
+ }
954
+
955
+ @computed private get isSortedByName(): boolean {
956
+ return this.isEntityNameSlug(this.sortConfig.slug)
957
+ }
958
+
959
+ @computed private get entityType(): { singular: string; plural: string } {
960
+ const entitiesAreCountriesOrRegions =
961
+ this.manager.isOnMapTab ||
962
+ (!this.manager.entityType && this.entitiesAreCountriesOrRegions)
963
+
964
+ if (entitiesAreCountriesOrRegions)
965
+ return this.someEntitiesAreRegions
966
+ ? {
967
+ singular: "country or region",
968
+ plural: "countries and regions",
969
+ }
970
+ : { singular: "country", plural: "countries" }
971
+
972
+ return {
973
+ singular: this.manager.entityType ?? DEFAULT_GRAPHER_ENTITY_TYPE,
974
+ plural:
975
+ this.manager.entityTypePlural ??
976
+ DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL,
977
+ }
978
+ }
979
+
980
+ @computed private get selectionArray(): SelectionArray {
981
+ return makeSelectionArray(
982
+ this.props.selection ?? this.manager.selection
983
+ )
984
+ }
985
+
986
+ @computed private get allEntitiesSelected(): boolean {
987
+ return (
988
+ this.selectionArray.numSelectedEntities ===
989
+ this.availableEntityNames.length
990
+ )
991
+ }
992
+
993
+ @computed private get availableEntityNames(): string[] {
994
+ return this.table.availableEntityNames
995
+ }
996
+
997
+ @computed private get availableEntityNameSet(): Set<string> {
998
+ return this.table.availableEntityNameSet
999
+ }
1000
+
1001
+ @computed private get availableEntities(): SearchableEntity[] {
1002
+ const langs = getUserNavigatorLanguagesNonEnglish()
1003
+
1004
+ return this.availableEntityNames.map((entityName) => {
1005
+ const searchableEntity: SearchableEntity = {
1006
+ name: entityName,
1007
+ sortColumnValues: {},
1008
+ alternativeNames: getRegionAlternativeNames(entityName, langs),
1009
+ regionInfo: getRegionByName(entityName),
1010
+ }
1011
+
1012
+ if (this.localEntityNames) {
1013
+ searchableEntity.isLocal =
1014
+ this.localEntityNames.includes(entityName)
1015
+ }
1016
+
1017
+ for (const column of this.interpolatedSortColumns) {
1018
+ const time = this.toColumnCompatibleTime(this.endTime, column)
1019
+
1020
+ // If we're dealing with a mixed column that has historical and
1021
+ // projected data for the given time, then we choose not to
1022
+ // show projected data since the dropdown is labelled with the
1023
+ // display name of the historical column.
1024
+ const projectionInfo =
1025
+ this.projectionColumnInfoByCombinedSlug.get(column.slug)
1026
+ if (projectionInfo) {
1027
+ const isProjectedValue =
1028
+ this.isProjectionBySlugAndTimeAndEntityName
1029
+ ?.get(projectionInfo.combinedSlug)
1030
+ ?.get(time)
1031
+ ?.get(entityName)
1032
+
1033
+ if (isProjectedValue) {
1034
+ const hasHistoricalValues =
1035
+ this.combinedColumnHasHistoricalDataForTime(
1036
+ projectionInfo.combinedSlug,
1037
+ time
1038
+ )
1039
+ if (hasHistoricalValues) continue
1040
+ }
1041
+ }
1042
+
1043
+ const row = column.owidRowByEntityNameAndTime
1044
+ .get(entityName)
1045
+ ?.get(time)
1046
+
1047
+ searchableEntity.sortColumnValues[column.slug] = row?.value
1048
+ }
1049
+
1050
+ return searchableEntity
1051
+ })
1052
+ }
1053
+
1054
+ @computed private get filteredAvailableEntities(): SearchableEntity[] {
1055
+ const { availableEntities, entityFilter } = this
1056
+
1057
+ // Sort locals and maybe World to the top if we are looking at all entites
1058
+ if (entityFilter === "all")
1059
+ return this.sortEntities(availableEntities, {
1060
+ sortLocalsToTop: true,
1061
+ })
1062
+
1063
+ const entityNameSet = new Set(
1064
+ this.manager.entityNamesByRegionType?.get(entityFilter) ?? []
1065
+ )
1066
+ const filteredAvailableEntities = availableEntities.filter((entity) =>
1067
+ entityNameSet.has(entity.name)
1068
+ )
1069
+
1070
+ return this.sortEntities(filteredAvailableEntities, {
1071
+ // Sort locals and maybe World to the top if looking at the long countries list, not for others
1072
+ sortLocalsToTop: entityFilter === "countries",
1073
+ })
1074
+ }
1075
+
1076
+ private sortEntities(
1077
+ entities: SearchableEntity[],
1078
+ options: { sortLocalsToTop: boolean } = {
1079
+ sortLocalsToTop: true,
1080
+ }
1081
+ ): SearchableEntity[] {
1082
+ const { sortConfig } = this
1083
+ const byName = (e: SearchableEntity) => e.name
1084
+ const byValue = (e: SearchableEntity) =>
1085
+ e.sortColumnValues[sortConfig.slug]
1086
+
1087
+ // Name sorting
1088
+ if (this.isSortedByName) {
1089
+ // Simple name sort without local/world prioritization
1090
+ if (!options.sortLocalsToTop) {
1091
+ return _.orderBy(entities, byName, sortConfig.order)
1092
+ }
1093
+
1094
+ // Name sort with locals on top and World between locals and others
1095
+ // We include "World" here (unlike when sorting by values, see notes below) because
1096
+ // here it is useful.
1097
+ const [[worldEntity], rest] = _.partition(entities, (e) =>
1098
+ isWorldEntityName(e.name)
1099
+ )
1100
+ const [locals, others] = _.partition(rest, (e) => e.isLocal)
1101
+
1102
+ const sortedLocals = _.sortBy(locals, (e) =>
1103
+ this.localEntityNames?.indexOf(e.name)
1104
+ )
1105
+ const sortedOthers = _.orderBy(others, byName, sortConfig.order)
1106
+
1107
+ return excludeUndefined([
1108
+ ...sortedLocals,
1109
+ worldEntity,
1110
+ ...sortedOthers,
1111
+ ])
1112
+ }
1113
+
1114
+ // Value sorting: missing values go last
1115
+ const [withValues, withoutValues] = _.partition(entities, (e) =>
1116
+ isFiniteWithGuard(byValue(e))
1117
+ )
1118
+
1119
+ let sortedWithValues: SearchableEntity[]
1120
+ if (options.sortLocalsToTop) {
1121
+ // We're not specially handling "World" here because we want to see the sorted
1122
+ // items and the user should understand that these are sorted. we already pull
1123
+ // up to three items (Germany, EU 27, Europe) up to the top and don't want to add
1124
+ // a fourth item in such cases that is obviously not sorted and that doesn't have
1125
+ // a "local" icon indicator
1126
+ const [localWith, otherWith] = _.partition(
1127
+ withValues,
1128
+ (e) => e.isLocal
1129
+ )
1130
+
1131
+ // Locals: keep user-preferred order (by localEntityNames index)
1132
+ const sortedLocalWith = _.sortBy(localWith, (e) =>
1133
+ this.localEntityNames?.indexOf(e.name)
1134
+ )
1135
+
1136
+ // Others: sort by value according to selected order
1137
+ const sortedOtherWith = _.orderBy(
1138
+ otherWith,
1139
+ byValue,
1140
+ sortConfig.order
1141
+ )
1142
+
1143
+ sortedWithValues = excludeUndefined([
1144
+ ...sortedLocalWith,
1145
+ ...sortedOtherWith,
1146
+ ])
1147
+ } else {
1148
+ sortedWithValues = _.orderBy(withValues, byValue, sortConfig.order)
1149
+ }
1150
+
1151
+ const sortedWithoutValues = _.orderBy(
1152
+ withoutValues,
1153
+ byName,
1154
+ SortOrder.asc
1155
+ )
1156
+
1157
+ return [...sortedWithValues, ...sortedWithoutValues]
1158
+ }
1159
+
1160
+ @computed get isMultiMode(): boolean {
1161
+ return !this.manager.canChangeEntity
1162
+ }
1163
+
1164
+ @computed get fuzzy(): FuzzySearch<SearchableEntity> {
1165
+ return FuzzySearch.withKeyArray(
1166
+ this.filteredAvailableEntities,
1167
+ (entity) => [entity.name, ...(entity.alternativeNames ?? [])],
1168
+ (entity) => entity.name
1169
+ )
1170
+ }
1171
+
1172
+ @computed get searchResults(): SearchableEntity[] | undefined {
1173
+ if (!this.searchInput) return undefined
1174
+ return this.fuzzy.search(this.searchInput)
1175
+ }
1176
+
1177
+ @computed get selectedEntities(): SearchableEntity[] {
1178
+ const selected = this.availableEntities.filter((entity) =>
1179
+ this.isEntitySelected(entity)
1180
+ )
1181
+ return this.sortEntities(selected, { sortLocalsToTop: false })
1182
+ }
1183
+
1184
+ @action.bound onTitleClick(): void {
1185
+ if (this.scrollableContainer.current)
1186
+ this.scrollableContainer.current.scrollTop = 0
1187
+ }
1188
+
1189
+ @action.bound onSearchKeyDown(e: KeyboardEvent): void {
1190
+ const { searchResults } = this
1191
+ if (e.key === "Enter" && searchResults && searchResults.length > 0) {
1192
+ this.onChange(searchResults[0].name)
1193
+ this.clearSearchInput()
1194
+ }
1195
+ }
1196
+
1197
+ @action.bound onDeselectEntities(entityNames: EntityName[]): void {
1198
+ for (const entityName of entityNames) {
1199
+ this.manager.onDeselectEntity?.(entityName)
1200
+ }
1201
+ }
1202
+
1203
+ private timeoutId?: number
1204
+ @action.bound onChange(entityName: EntityName): void {
1205
+ if (this.isMultiMode) {
1206
+ this.selectionArray.toggleSelection(entityName)
1207
+
1208
+ if (this.selectionArray.selectedSet.has(entityName)) {
1209
+ this.manager.onSelectEntity?.(entityName)
1210
+ this.manager.logEntitySelectorEvent?.("select", entityName)
1211
+ } else {
1212
+ this.manager.onDeselectEntity?.(entityName)
1213
+ this.manager.logEntitySelectorEvent?.("deselect", entityName)
1214
+ }
1215
+
1216
+ if (this.selectionArray.numSelectedEntities === 0) {
1217
+ this.manager.onClearEntities?.()
1218
+ }
1219
+ } else {
1220
+ const dropEntityNames = this.selectionArray.selectedEntityNames
1221
+ this.selectionArray.setSelectedEntities([entityName])
1222
+ this.manager.onSelectEntity?.(entityName)
1223
+ this.manager.logEntitySelectorEvent?.("select", entityName)
1224
+ this.onDeselectEntities(dropEntityNames)
1225
+
1226
+ // close the modal or drawer automatically after selection
1227
+ if (this.manager.isEntitySelectorModalOrDrawerOpen) {
1228
+ this.timeoutId = window.setTimeout(() => this.close(), 200)
1229
+ }
1230
+ }
1231
+
1232
+ this.clearSearchInput()
1233
+ }
1234
+
1235
+ @action.bound onClear(): void {
1236
+ const dropEntityNames = this.selectionArray.selectedEntityNames
1237
+ this.selectionArray.clearSelection()
1238
+ this.onDeselectEntities(dropEntityNames)
1239
+ this.manager.onClearEntities?.()
1240
+
1241
+ this.resetEntityFilter()
1242
+
1243
+ this.manager.logEntitySelectorEvent?.("clear")
1244
+ }
1245
+
1246
+ @action.bound async loadAndSetExternalSortColumn(
1247
+ external: ExternalSortIndicatorDefinition
1248
+ ): Promise<void> {
1249
+ const { slug, indicatorId } = external
1250
+ const { additionalDataLoaderFn } = this.manager
1251
+
1252
+ // the indicator has already been loaded
1253
+ if (this.interpolatedSortColumnsBySlug[slug]) return
1254
+
1255
+ // load the external indicator
1256
+ try {
1257
+ this.set({ isLoadingExternalSortColumn: true })
1258
+ if (additionalDataLoaderFn === undefined)
1259
+ throw new Error(
1260
+ "additionalDataLoaderFn is not set, can't load sort variables on demand"
1261
+ )
1262
+ const variable = await additionalDataLoaderFn(indicatorId)
1263
+ const variableTable = buildVariableTable(variable)
1264
+ const column = variableTable
1265
+ .filterByEntityNames(this.inputTable.availableEntityNames)
1266
+ .interpolateColumnWithTolerance(slug, {
1267
+ toleranceOverride: Infinity,
1268
+ })
1269
+ .get(slug)
1270
+ if (column) this.setInterpolatedSortColumn(column)
1271
+ } catch {
1272
+ console.error(`Failed to load variable with id ${indicatorId}`)
1273
+ } finally {
1274
+ this.set({ isLoadingExternalSortColumn: false })
1275
+ }
1276
+ }
1277
+
1278
+ @action.bound async onChangeSortSlug(
1279
+ selected: SortDropdownOption | null
1280
+ ): Promise<void> {
1281
+ if (selected) {
1282
+ const { slug } = selected
1283
+
1284
+ // if an external indicator has been selected, load it
1285
+ const external = this.externalSortIndicatorDefinitions.find(
1286
+ (external) => external.slug === slug
1287
+ )
1288
+ if (external) await this.loadAndSetExternalSortColumn(external)
1289
+
1290
+ // apply tolerance if an indicator is selected for the first time
1291
+ if (!external && !this.isEntityNameSlug(slug)) {
1292
+ this.setInterpolatedSortColumnBySlug(slug)
1293
+ }
1294
+
1295
+ this.updateSortSlug(slug)
1296
+
1297
+ const sortByTarget = this.isEntityNameSlug(slug)
1298
+ ? "name"
1299
+ : external
1300
+ ? external.key
1301
+ : "value"
1302
+ this.manager.logEntitySelectorEvent("sortBy", sortByTarget)
1303
+ }
1304
+ }
1305
+
1306
+ @action.bound onChangeSortOrder(): void {
1307
+ this.toggleSortOrder()
1308
+ this.manager.logEntitySelectorEvent("sortOrder")
1309
+ }
1310
+
1311
+ @action.bound private close(): void {
1312
+ // if rendered into a drawer, we use a method provided by the
1313
+ // `<SlideInDrawer />` component so that closing the drawer is animated
1314
+ if (this.context.toggleDrawerVisibility) {
1315
+ this.context.toggleDrawerVisibility()
1316
+ } else {
1317
+ this.manager.isEntitySelectorModalOrDrawerOpen = false
1318
+ }
1319
+ }
1320
+
1321
+ @computed get filterOptions(): FilterDropdownOption[] {
1322
+ const { entityRegionTypeGroups = [] } = this.manager
1323
+
1324
+ const options: FilterDropdownOption[] = entityRegionTypeGroups
1325
+ .map(({ regionType, entityNames }) => ({
1326
+ value: regionType,
1327
+ label: entityRegionTypeLabels[regionType],
1328
+ count: entityNames.filter((entityName) =>
1329
+ this.availableEntityNameSet.has(entityName)
1330
+ ).length,
1331
+ }))
1332
+ .filter(({ count }) => count > 0)
1333
+
1334
+ return [
1335
+ {
1336
+ value: "all",
1337
+ label: "All",
1338
+ count: this.availableEntities.length,
1339
+ },
1340
+ ...options,
1341
+ ]
1342
+ }
1343
+
1344
+ @action.bound private onChangeEntityFilter(
1345
+ selected: FilterDropdownOption | null
1346
+ ): void {
1347
+ if (selected) {
1348
+ const option = selected
1349
+ this.set({ entityFilter: option.value })
1350
+
1351
+ this.manager.logEntitySelectorEvent("filterBy", option.value)
1352
+ }
1353
+ }
1354
+
1355
+ @computed private get filterValue(): FilterDropdownOption {
1356
+ return (
1357
+ this.filterOptions.find(
1358
+ (option) => option.value === this.entityFilter
1359
+ ) ?? this.filterOptions[0]
1360
+ )
1361
+ }
1362
+
1363
+ @computed get shouldShowFilterBar(): boolean {
1364
+ return (
1365
+ this.filterOptions.length > 1 &&
1366
+ this.filterOptions[0].count !== this.filterOptions[1].count
1367
+ )
1368
+ }
1369
+
1370
+ private renderFilterBar(): React.ReactElement {
1371
+ return (
1372
+ <div className="entity-selector__filter-bar">
1373
+ <Dropdown<FilterDropdownOption>
1374
+ options={this.filterOptions}
1375
+ onChange={this.onChangeEntityFilter}
1376
+ value={this.filterValue}
1377
+ renderTriggerValue={renderFilterTriggerValue}
1378
+ renderMenuOption={renderFilterMenuOption}
1379
+ aria-label="Filter by type"
1380
+ portalContainer={
1381
+ this.scrollableContainer.current ?? undefined
1382
+ }
1383
+ />
1384
+ </div>
1385
+ )
1386
+ }
1387
+
1388
+ private renderSearchBar(): React.ReactElement {
1389
+ return (
1390
+ <div className="entity-selector__search-bar">
1391
+ <SearchField
1392
+ ref={this.searchFieldRef}
1393
+ value={this.searchInput}
1394
+ onChange={(value) => this.set({ searchInput: value })}
1395
+ onClear={() => this.clearSearchInput()}
1396
+ placeholder={`Search for ${a(
1397
+ this.searchPlaceholderEntityType
1398
+ )}`}
1399
+ trackNote="entity_selector_search"
1400
+ onKeyDown={this.onSearchKeyDown}
1401
+ />
1402
+ </div>
1403
+ )
1404
+ }
1405
+
1406
+ private renderSortBar(): React.ReactElement {
1407
+ return (
1408
+ <div className="entity-selector__sort-bar">
1409
+ <div className="entity-selector__sort-dropdown-and-button">
1410
+ <Dropdown<SortDropdownOption>
1411
+ className="entity-selector__sort-dropdown"
1412
+ menuClassName="entity-selector__sort-dropdown-menu"
1413
+ options={this.sortOptions}
1414
+ onChange={this.onChangeSortSlug}
1415
+ value={this.sortValue}
1416
+ isLoading={this.isLoadingExternalSortColumn}
1417
+ renderTriggerValue={renderSortTriggerValue}
1418
+ renderMenuOption={renderSortMenuOption}
1419
+ aria-label="Sort by"
1420
+ portalContainer={
1421
+ this.scrollableContainer.current ?? undefined
1422
+ }
1423
+ />
1424
+ <button
1425
+ type="button"
1426
+ className="sort"
1427
+ onClick={this.onChangeSortOrder}
1428
+ >
1429
+ <SortIcon
1430
+ type={this.isSortedByName ? "text" : "numeric"}
1431
+ order={this.sortConfig.order}
1432
+ />
1433
+ </button>
1434
+ </div>
1435
+ </div>
1436
+ )
1437
+ }
1438
+
1439
+ private renderSearchResults(): React.ReactElement {
1440
+ if (!this.searchResults || this.searchResults.length === 0) {
1441
+ return (
1442
+ <div className="entity-search-results grapher_body-3-regular grapher_light">
1443
+ There is no data for the {this.entityType.singular} you are
1444
+ looking for. You may want to try using different keywords or
1445
+ checking for typos.
1446
+ </div>
1447
+ )
1448
+ }
1449
+
1450
+ return (
1451
+ <ul className="entity-search-results">
1452
+ {this.searchResults.map((entity) => (
1453
+ <li key={entity.name}>
1454
+ <SelectableEntity
1455
+ name={entity.name}
1456
+ type={this.isMultiMode ? "checkbox" : "radio"}
1457
+ checked={this.isEntitySelected(entity)}
1458
+ bar={this.getBarConfigForEntity(entity)}
1459
+ onChange={this.onChange}
1460
+ isLocal={entity.isLocal}
1461
+ isMuted={this.isEntityMuted(entity.name)}
1462
+ />
1463
+ </li>
1464
+ ))}
1465
+ </ul>
1466
+ )
1467
+ }
1468
+
1469
+ private renderAllEntitiesInSingleMode(): React.ReactElement {
1470
+ const { filteredAvailableEntities, shouldShowFilterBar } = this
1471
+
1472
+ return (
1473
+ <>
1474
+ {shouldShowFilterBar && this.renderFilterBar()}
1475
+ <ul className={cx({ "hide-top-border": shouldShowFilterBar })}>
1476
+ {filteredAvailableEntities.map((entity) => (
1477
+ <li key={entity.name}>
1478
+ <SelectableEntity
1479
+ name={entity.name}
1480
+ type="radio"
1481
+ checked={this.isEntitySelected(entity)}
1482
+ bar={this.getBarConfigForEntity(entity)}
1483
+ onChange={this.onChange}
1484
+ isLocal={entity.isLocal}
1485
+ isMuted={this.isEntityMuted(entity.name)}
1486
+ />
1487
+ </li>
1488
+ ))}
1489
+ </ul>
1490
+ </>
1491
+ )
1492
+ }
1493
+
1494
+ @computed private get selectedSortColumn(): CoreColumn | undefined {
1495
+ const { sortConfig } = this
1496
+ if (this.isSortedByName) return undefined
1497
+ return this.interpolatedSortColumnsBySlug[sortConfig.slug]
1498
+ }
1499
+
1500
+ @computed private get selectedSortColumnMaxValue(): number | undefined {
1501
+ const { selectedSortColumn, endTime } = this
1502
+ if (!selectedSortColumn) return undefined
1503
+ const time = this.toColumnCompatibleTime(endTime, selectedSortColumn)
1504
+ const values = selectedSortColumn.valuesByTime.get(time)
1505
+ return _.max(values)
1506
+ }
1507
+
1508
+ @computed private get barScale(): ScaleLinear<number, number> {
1509
+ return scaleLinear()
1510
+ .domain([0, this.selectedSortColumnMaxValue ?? 1])
1511
+ .range([0, 1])
1512
+ }
1513
+
1514
+ private getBarConfigForEntity(
1515
+ entity: SearchableEntity
1516
+ ): BarConfig | undefined {
1517
+ const { selectedSortColumn, barScale } = this
1518
+
1519
+ if (!selectedSortColumn) return undefined
1520
+
1521
+ const value = entity.sortColumnValues[selectedSortColumn.slug]
1522
+
1523
+ if (!isFiniteWithGuard(value)) return { formattedValue: "No data" }
1524
+
1525
+ const formattedValue =
1526
+ selectedSortColumn.formatValueShortWithAbbreviations(value)
1527
+
1528
+ if (value < 0) return { formattedValue, width: 0 }
1529
+
1530
+ return {
1531
+ formattedValue:
1532
+ selectedSortColumn.formatValueShortWithAbbreviations(value),
1533
+ width: R.clamp(barScale(value), { min: 0, max: 1 }),
1534
+ }
1535
+ }
1536
+
1537
+ private isEntitySelected(entity: SearchableEntity): boolean {
1538
+ return this.selectionArray.selectedSet.has(entity.name)
1539
+ }
1540
+
1541
+ private renderAllEntitiesInMultiMode(): React.ReactElement {
1542
+ const {
1543
+ filteredAvailableEntities,
1544
+ selectedEntities,
1545
+ shouldShowFilterBar,
1546
+ } = this
1547
+ const { numSelectedEntities, selectedEntityNames } = this.selectionArray
1548
+
1549
+ // having a "Selection" and "Available entities" section both looks odd
1550
+ // when all entities are currently selected and there are only a few of them
1551
+ const hasFewEntities = filteredAvailableEntities.length < 10
1552
+ const shouldHideAvailableEntities =
1553
+ !shouldShowFilterBar && hasFewEntities && this.allEntitiesSelected
1554
+
1555
+ const availableEntitiesTitle = this.mapConfig.is2dContinentActive()
1556
+ ? `Countries in ${MAP_REGION_LABELS[this.mapConfig.region]}`
1557
+ : `All ${this.entityType.plural}`
1558
+
1559
+ return (
1560
+ <Flipper
1561
+ spring={{ stiffness: 300, damping: 33 }}
1562
+ flipKey={selectedEntityNames.join(",")}
1563
+ >
1564
+ <div className="entity-section">
1565
+ {selectedEntities.length > 0 && (
1566
+ <Flipped flipId="__selection" translate opacity>
1567
+ <div className="entity-section__header">
1568
+ <div className="entity-section__title grapher_body-3-regular-italic grapher_light">
1569
+ Selection{" "}
1570
+ {numSelectedEntities > 0 &&
1571
+ `(${numSelectedEntities})`}
1572
+ </div>
1573
+ <button type="button" onClick={this.onClear}>
1574
+ Clear
1575
+ </button>
1576
+ </div>
1577
+ </Flipped>
1578
+ )}
1579
+ <ul>
1580
+ {selectedEntities.map((entity, entityIndex) => (
1581
+ <FlippedListItem
1582
+ index={entityIndex}
1583
+ key={entity.name}
1584
+ flipId={`selected_${makeSafeForCSS(
1585
+ entity.name
1586
+ )}`}
1587
+ >
1588
+ <SelectableEntity
1589
+ name={entity.name}
1590
+ type="checkbox"
1591
+ checked={true}
1592
+ bar={this.getBarConfigForEntity(entity)}
1593
+ onChange={this.onChange}
1594
+ isLocal={entity.isLocal}
1595
+ isMuted={this.isEntityMuted(entity.name)}
1596
+ />
1597
+ </FlippedListItem>
1598
+ ))}
1599
+ </ul>
1600
+ </div>
1601
+
1602
+ {shouldShowFilterBar && (
1603
+ <Flipped flipId="__filter-bar" translate opacity>
1604
+ {this.renderFilterBar()}
1605
+ </Flipped>
1606
+ )}
1607
+
1608
+ {!shouldHideAvailableEntities && (
1609
+ <div
1610
+ className={cx("entity-section", {
1611
+ "hide-top-border": shouldShowFilterBar,
1612
+ })}
1613
+ >
1614
+ {!shouldShowFilterBar && (
1615
+ <Flipped flipId="__available" translate opacity>
1616
+ <div className="entity-section__title grapher_body-3-regular-italic grapher_light">
1617
+ {availableEntitiesTitle}
1618
+ </div>
1619
+ </Flipped>
1620
+ )}
1621
+
1622
+ <ul>
1623
+ {filteredAvailableEntities.map(
1624
+ (entity, entityIndex) => (
1625
+ <FlippedListItem
1626
+ index={entityIndex}
1627
+ key={entity.name}
1628
+ flipId={`available_${makeSafeForCSS(
1629
+ entity.name
1630
+ )}`}
1631
+ >
1632
+ <SelectableEntity
1633
+ name={entity.name}
1634
+ type="checkbox"
1635
+ checked={this.isEntitySelected(
1636
+ entity
1637
+ )}
1638
+ bar={this.getBarConfigForEntity(
1639
+ entity
1640
+ )}
1641
+ onChange={this.onChange}
1642
+ isLocal={entity.isLocal}
1643
+ isMuted={this.isEntityMuted(
1644
+ entity.name
1645
+ )}
1646
+ />
1647
+ </FlippedListItem>
1648
+ )
1649
+ )}
1650
+ </ul>
1651
+ </div>
1652
+ )}
1653
+ </Flipper>
1654
+ )
1655
+ }
1656
+
1657
+ override render(): React.ReactElement {
1658
+ return (
1659
+ <div className="entity-selector">
1660
+ <OverlayHeader
1661
+ title={this.title}
1662
+ onTitleClick={this.onTitleClick}
1663
+ onDismiss={this.close}
1664
+ />
1665
+
1666
+ {this.renderSearchBar()}
1667
+
1668
+ <div ref={this.scrollableContainer} className="scrollable">
1669
+ {!this.searchInput &&
1670
+ this.sortOptions.length > 1 &&
1671
+ this.renderSortBar()}
1672
+
1673
+ <div
1674
+ ref={this.contentRef}
1675
+ className="entity-selector__content"
1676
+ >
1677
+ {this.searchInput
1678
+ ? this.renderSearchResults()
1679
+ : this.isMultiMode
1680
+ ? this.renderAllEntitiesInMultiMode()
1681
+ : this.renderAllEntitiesInSingleMode()}
1682
+ </div>
1683
+ </div>
1684
+ </div>
1685
+ )
1686
+ }
1687
+ }
1688
+
1689
+ type BarConfig = { formattedValue: string; width?: number }
1690
+
1691
+ function SelectableEntity({
1692
+ name,
1693
+ checked,
1694
+ type,
1695
+ bar,
1696
+ onChange,
1697
+ isLocal,
1698
+ isMuted,
1699
+ }: {
1700
+ name: string
1701
+ checked: boolean
1702
+ type: "checkbox" | "radio"
1703
+ bar?: BarConfig
1704
+ onChange: (entityName: EntityName) => void
1705
+ isLocal?: boolean
1706
+ isMuted?: boolean
1707
+ }) {
1708
+ const Input = {
1709
+ checkbox: Checkbox,
1710
+ radio: RadioButton,
1711
+ }[type]
1712
+
1713
+ const nameWords = name.split(" ")
1714
+ const label = isLocal ? (
1715
+ <span className="label-with-location-icon">
1716
+ {nameWords.slice(0, -1).join(" ")}{" "}
1717
+ <span className="label-with-location-icon label-with-location-icon--no-line-break">
1718
+ {nameWords[nameWords.length - 1]}
1719
+ <Tippy
1720
+ content="Your current location"
1721
+ theme="grapher-explanation--short"
1722
+ placement="top"
1723
+ >
1724
+ <FontAwesomeIcon icon={faLocationArrow} />
1725
+ </Tippy>
1726
+ </span>
1727
+ </span>
1728
+ ) : (
1729
+ name
1730
+ )
1731
+
1732
+ return (
1733
+ <div
1734
+ className={cx("selectable-entity", {
1735
+ "selectable-entity--with-bar": bar && bar.width !== undefined,
1736
+ "selectable-entity--muted": isMuted,
1737
+ })}
1738
+ >
1739
+ {bar && bar.width !== undefined && (
1740
+ <div className="bar" style={{ width: `${bar.width * 100}%` }} />
1741
+ )}
1742
+ <Input
1743
+ label={label}
1744
+ checked={checked}
1745
+ onChange={() => onChange(name)}
1746
+ />
1747
+ {bar && (
1748
+ <span className="value grapher_label-1-regular">
1749
+ {bar.formattedValue}
1750
+ </span>
1751
+ )}
1752
+ </div>
1753
+ )
1754
+ }
1755
+
1756
+ function FlippedListItem({
1757
+ flipId,
1758
+ index = 0,
1759
+ children,
1760
+ }: {
1761
+ flipId: string
1762
+ index?: number
1763
+ children: React.ReactNode
1764
+ }) {
1765
+ return (
1766
+ <Flipped
1767
+ flipId={flipId}
1768
+ translate
1769
+ opacity
1770
+ spring={{
1771
+ stiffness: Math.max(300 - index, 180),
1772
+ damping: 33,
1773
+ }}
1774
+ >
1775
+ <li>{children}</li>
1776
+ </Flipped>
1777
+ )
1778
+ }
1779
+
1780
+ function renderSortTriggerValue(
1781
+ option: SortDropdownOption | null
1782
+ ): React.ReactNode | undefined {
1783
+ if (!option) return undefined
1784
+ return (
1785
+ <>
1786
+ <span className="label">
1787
+ <FontAwesomeIcon icon={faArrowRightArrowLeft} size="sm" />
1788
+ {"Sort by: "}
1789
+ </span>
1790
+ {option.label}
1791
+ {option.formattedTime && (
1792
+ <span className="detail">, {option.formattedTime}</span>
1793
+ )}
1794
+ </>
1795
+ )
1796
+ }
1797
+
1798
+ function renderSortMenuOption(option: SortDropdownOption): React.ReactNode {
1799
+ return (
1800
+ <>
1801
+ {option.label}
1802
+ {option.formattedTime && (
1803
+ <span className="detail">, {option.formattedTime}</span>
1804
+ )}
1805
+ </>
1806
+ )
1807
+ }
1808
+
1809
+ function renderFilterTriggerValue(
1810
+ option: FilterDropdownOption | null
1811
+ ): React.ReactNode | undefined {
1812
+ if (!option) return undefined
1813
+ return (
1814
+ <>
1815
+ <span className="label">
1816
+ <FontAwesomeIcon icon={faFilter} size="sm" />
1817
+ {"Filter by type: "}
1818
+ </span>
1819
+ {option.label} <span className="detail">({option.count})</span>
1820
+ </>
1821
+ )
1822
+ }
1823
+
1824
+ function renderFilterMenuOption(option: FilterDropdownOption): React.ReactNode {
1825
+ return (
1826
+ <>
1827
+ {option.label} <span className="detail">({option.count})</span>
1828
+ </>
1829
+ )
1830
+ }
1831
+
1832
+ function getTitleForSortColumnLabel(column: CoreColumn): string {
1833
+ return column.titlePublicOrDisplayName.title
1834
+ }
1835
+
1836
+ function indicatorIdToSlug(indicatorId: number): ColumnSlug {
1837
+ return indicatorId.toString()
1838
+ }