@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,184 @@
1
+ import * as topojson from "topojson-client"
2
+ import {
3
+ GeoFeature,
4
+ GlobeRenderFeature,
5
+ MapRenderFeature,
6
+ RenderFeatureType,
7
+ } from "./MapChartConstants"
8
+ import { Bounds, lazy, MapRegionName, PointVector } from "../../utils/index.js"
9
+ import { CanadaTopology } from "./CanadaTopology"
10
+ import { MapTopology } from "./MapTopology"
11
+ import { geoBounds, geoCentroid, geoPath } from "d3-geo"
12
+ import { MAP_PROJECTIONS } from "./MapProjections"
13
+
14
+ // Get the underlying geographical topology elements we're going to display
15
+ export const GeoFeatures: GeoFeature[] = (
16
+ topojson.feature(
17
+ MapTopology as any,
18
+ MapTopology.objects.world as any
19
+ ) as any
20
+ ).features
21
+
22
+ export const GeoFeaturesById = new Map(
23
+ GeoFeatures.map((feature) => [feature.id, feature])
24
+ )
25
+
26
+ // Canadian province features
27
+ export const CanadaGeoFeatures: GeoFeature[] = (
28
+ topojson.feature(
29
+ CanadaTopology as any,
30
+ CanadaTopology.objects.canada as any
31
+ ) as any
32
+ ).features
33
+
34
+ export const CanadaGeoFeaturesById = new Map(
35
+ CanadaGeoFeatures.map((feature) => [feature.id, feature])
36
+ )
37
+
38
+ // Get the svg path specification string for every feature
39
+ const geoPathCache = new Map<MapRegionName, string[]>()
40
+ const geoPathsFor = (regionName: MapRegionName): string[] => {
41
+ if (geoPathCache.has(regionName)) return geoPathCache.get(regionName)!
42
+
43
+ const projectionGeo = geoPath()
44
+ .digits(1)
45
+ .projection(MAP_PROJECTIONS[regionName])
46
+ const strs = GeoFeatures.map((feature) => projectionGeo(feature) ?? "")
47
+
48
+ geoPathCache.set(regionName, strs)
49
+ return geoPathCache.get(regionName)!
50
+ }
51
+
52
+ // Get the bounding box for every geographical feature
53
+ const geoBoundsCache = new Map<MapRegionName, Bounds[]>()
54
+ const geoBoundsFor = (regionName: MapRegionName): Bounds[] => {
55
+ if (geoBoundsCache.has(regionName)) return geoBoundsCache.get(regionName)!
56
+
57
+ const projectionGeo = geoPath().projection(MAP_PROJECTIONS[regionName])
58
+ const bounds = GeoFeatures.map((feature) => {
59
+ const corners = projectionGeo.bounds(feature)
60
+
61
+ const bounds = Bounds.fromCorners(
62
+ new PointVector(...corners[0]),
63
+ new PointVector(...corners[1])
64
+ )
65
+
66
+ // HACK (Mispy): The path generator calculates weird bounds for Fiji (probably it wraps around the map)
67
+ if (feature.id === "Fiji")
68
+ return bounds.set({
69
+ x: bounds.right - bounds.height,
70
+ width: bounds.height,
71
+ })
72
+
73
+ return bounds
74
+ })
75
+
76
+ geoBoundsCache.set(regionName, bounds)
77
+ return geoBoundsCache.get(regionName)!
78
+ }
79
+
80
+ const geoCentroidsForFeatures = GeoFeatures.map((feature) =>
81
+ geoCentroid(feature.geometry)
82
+ )
83
+
84
+ const geoBoundsForFeatures = GeoFeatures.map((feature) => {
85
+ const corners = geoBounds(feature)
86
+ return Bounds.fromCorners(
87
+ new PointVector(...corners[0]),
88
+ new PointVector(...corners[1])
89
+ )
90
+ })
91
+
92
+ // Canada-specific computed values
93
+ const canadaGeoCentroidsForFeatures = CanadaGeoFeatures.map((feature) =>
94
+ geoCentroid(feature.geometry)
95
+ )
96
+
97
+ const canadaGeoBoundsForFeatures = CanadaGeoFeatures.map((feature) => {
98
+ const corners = geoBounds(feature)
99
+ return Bounds.fromCorners(
100
+ new PointVector(...corners[0]),
101
+ new PointVector(...corners[1])
102
+ )
103
+ })
104
+
105
+ // Canada-specific path and bounds functions
106
+ const canadaGeoPathsFor = (): string[] => {
107
+ const projectionGeo = geoPath()
108
+ .digits(1)
109
+ .projection(MAP_PROJECTIONS[MapRegionName.Canada])
110
+ return CanadaGeoFeatures.map((feature) => projectionGeo(feature) ?? "")
111
+ }
112
+
113
+ const canadaGeoBoundsFor = (): Bounds[] => {
114
+ const projectionGeo = geoPath().projection(MAP_PROJECTIONS[MapRegionName.Canada])
115
+ return CanadaGeoFeatures.map((feature) => {
116
+ const corners = projectionGeo.bounds(feature)
117
+ return Bounds.fromCorners(
118
+ new PointVector(...corners[0]),
119
+ new PointVector(...corners[1])
120
+ )
121
+ })
122
+ }
123
+
124
+ const geoFeaturesForMapCache = new Map<MapRegionName, MapRenderFeature[]>()
125
+ export const getGeoFeaturesForMap = (
126
+ regionName: MapRegionName
127
+ ): MapRenderFeature[] => {
128
+ if (geoFeaturesForMapCache.has(regionName))
129
+ return geoFeaturesForMapCache.get(regionName)!
130
+
131
+ // Handle Canada region separately using Canadian province features
132
+ if (regionName === MapRegionName.Canada) {
133
+ const projBounds = canadaGeoBoundsFor()
134
+ const projPaths = canadaGeoPathsFor()
135
+
136
+ const features = CanadaGeoFeatures.map((geo, index) => ({
137
+ type: RenderFeatureType.Map,
138
+ id: geo.id as string,
139
+ geo: geo,
140
+ projBounds: projBounds[index],
141
+ geoBounds: canadaGeoBoundsForFeatures[index],
142
+ geoCentroid: canadaGeoCentroidsForFeatures[index],
143
+ path: projPaths[index],
144
+ })) satisfies MapRenderFeature[]
145
+
146
+ geoFeaturesForMapCache.set(regionName, features)
147
+ return geoFeaturesForMapCache.get(regionName)!
148
+ }
149
+
150
+ const projBounds = geoBoundsFor(regionName)
151
+ const projPaths = geoPathsFor(regionName)
152
+
153
+ const features = (
154
+ GeoFeatures.map((geo, index) => ({
155
+ type: RenderFeatureType.Map,
156
+ id: geo.id as string,
157
+ geo: geo,
158
+ projBounds: projBounds[index], // projected
159
+ geoBounds: geoBoundsForFeatures[index], // unprojected
160
+ geoCentroid: geoCentroidsForFeatures[index], // unprojected
161
+ path: projPaths[index],
162
+ })) satisfies MapRenderFeature[]
163
+ ).filter((feature) => feature.id !== "Antarctica") // exclude Antarctica since it's distorted and uses up too much space
164
+
165
+ geoFeaturesForMapCache.set(regionName, features)
166
+ return geoFeaturesForMapCache.get(regionName)!
167
+ }
168
+
169
+ export const getGeoFeaturesForGlobe = lazy((): GlobeRenderFeature[] => {
170
+ return GeoFeatures.map((geo, index) => {
171
+ const corners = geoBounds(geo)
172
+ const bounds = Bounds.fromCorners(
173
+ new PointVector(...corners[0]),
174
+ new PointVector(...corners[1])
175
+ )
176
+ return {
177
+ type: RenderFeatureType.Globe,
178
+ id: geo.id as string,
179
+ geo: geo,
180
+ geoCentroid: geoCentroidsForFeatures[index],
181
+ geoBounds: bounds,
182
+ }
183
+ }) satisfies GlobeRenderFeature[]
184
+ })
@@ -0,0 +1,496 @@
1
+ import { FeatureCollection } from "geojson"
2
+ import { geoInterpolate, geoOrthographic, geoPath } from "d3-geo"
3
+ import { interpolateNumber } from "d3-interpolate"
4
+ import { easeCubicOut } from "d3-ease"
5
+ import * as R from "remeda"
6
+ import { EntityName, GlobeConfig, GlobeRegionName } from "../../types/index.js"
7
+ import {
8
+ Bounds,
9
+ excludeUndefined,
10
+ PartialBy,
11
+ PointVector,
12
+ checkIsOwidContinent,
13
+ getCountryNamesForRegion,
14
+ getRegionByName,
15
+ checkHasMembers,
16
+ } from "../../utils/index.js"
17
+ import { MapConfig } from "./MapConfig"
18
+ import { getGeoFeaturesForGlobe } from "./GeoFeatures"
19
+ import {
20
+ DEFAULT_GLOBE_ROTATION,
21
+ DEFAULT_GLOBE_ROTATIONS_FOR_TIME,
22
+ DEFAULT_GLOBE_SIZE,
23
+ GeoFeature,
24
+ GLOBE_COUNTRY_ZOOM,
25
+ GLOBE_LATITUDE_MAX,
26
+ GLOBE_LATITUDE_MIN,
27
+ GLOBE_MAX_ZOOM,
28
+ GLOBE_MIN_ZOOM,
29
+ GLOBE_VIEWPORTS,
30
+ GlobeRenderFeature,
31
+ MAP_REGION_NAMES,
32
+ } from "./MapChartConstants"
33
+ import { isPointPlacedOnVisibleHemisphere } from "./MapHelpers"
34
+ import { ckmeans } from "simple-statistics"
35
+ import { MapSelectionArray } from "../selection/MapSelectionArray"
36
+ import { center } from "@turf/center"
37
+ import { action } from "mobx"
38
+
39
+ const geoFeaturesById = new Map<string, GlobeRenderFeature>(
40
+ getGeoFeaturesForGlobe().map((f: GlobeRenderFeature) => [f.id, f])
41
+ )
42
+
43
+ const LONGITUDE_OFFSET = 40
44
+ const ANIMATION_DURATION = 600
45
+
46
+ interface Target {
47
+ coords: [number, number]
48
+ zoom: number
49
+ }
50
+
51
+ interface GlobeControllerManager {
52
+ mapConfig: MapConfig
53
+ }
54
+
55
+ export class GlobeController {
56
+ private manager: GlobeControllerManager
57
+
58
+ constructor(manager: GlobeControllerManager) {
59
+ this.manager = manager
60
+ }
61
+
62
+ private get globeConfig(): GlobeConfig {
63
+ return this.manager.mapConfig.globe
64
+ }
65
+
66
+ @action.bound showGlobe(): void {
67
+ this.globeConfig.isActive = true
68
+ }
69
+
70
+ @action.bound hideGlobe(): void {
71
+ this.globeConfig.isActive = false
72
+ }
73
+
74
+ @action.bound toggleGlobe(): void {
75
+ this.globeConfig.isActive = !this.globeConfig.isActive
76
+ }
77
+
78
+ @action.bound resetGlobe(): void {
79
+ this.globeConfig.rotation = DEFAULT_GLOBE_ROTATION
80
+ this.globeConfig.zoom = 1
81
+ this.globeConfig.focusCountry = undefined
82
+ }
83
+
84
+ @action.bound setFocusCountry(country: EntityName): void {
85
+ this.globeConfig.focusCountry = country
86
+ }
87
+
88
+ @action.bound dismissCountryFocus(): void {
89
+ this.globeConfig.focusCountry = undefined
90
+ }
91
+
92
+ @action.bound private jumpTo(target: Partial<Target>): void {
93
+ if (target.coords) this.globeConfig.rotation = target.coords
94
+ if (target.zoom) this.globeConfig.zoom = target.zoom
95
+ }
96
+
97
+ private showGlobeAndRotateTo(target: Target): void {
98
+ // if the globe isn't currently shown, jump to the offset position
99
+ // before switching to it so that rotating is predictable
100
+ if (!this.globeConfig.isActive) {
101
+ this.jumpTo({ coords: addLongitudeOffset(target.coords) })
102
+ this.showGlobe()
103
+ }
104
+
105
+ void this.rotateTo(target)
106
+ }
107
+
108
+ jumpToOwidContinent(continent: GlobeRegionName): void {
109
+ const target = calculateTargetForOwidContinent(continent)
110
+ this.jumpTo(target)
111
+ }
112
+
113
+ rotateToCountry(country: EntityName, zoom?: number): void {
114
+ const target = calculateTargetForCountry(country, zoom)
115
+ if (target) this.showGlobeAndRotateTo(target)
116
+ }
117
+
118
+ rotateToOwidContinent(continent: GlobeRegionName): void {
119
+ const target = calculateTargetForOwidContinent(continent)
120
+ this.showGlobeAndRotateTo(target)
121
+ }
122
+
123
+ rotateToDefaultBasedOnTime(): void {
124
+ const target = calculateTargetBasedOnTime()
125
+ this.showGlobeAndRotateTo(target)
126
+ }
127
+
128
+ rotateToSelection(): void {
129
+ const target = calculateTargetForSelection(
130
+ this.manager.mapConfig.selection
131
+ )
132
+ if (target) this.showGlobeAndRotateTo(target)
133
+ }
134
+
135
+ rotateToRegion(regionName: string): void {
136
+ const target = calculateTargetForRegion(regionName)
137
+ if (target) this.showGlobeAndRotateTo(target)
138
+ }
139
+
140
+ private currentAnimation?: AbortController
141
+ private async rotateTo(target: PartialBy<Target, "zoom">): Promise<void> {
142
+ // cancel any ongoing rotation
143
+ if (this.currentAnimation) {
144
+ this.currentAnimation.abort()
145
+ this.currentAnimation = undefined
146
+ }
147
+
148
+ // set up a new abort controller
149
+ const controller = new AbortController()
150
+ this.currentAnimation = controller
151
+
152
+ try {
153
+ await this._rotateTo(controller.signal, target.coords, target.zoom)
154
+ } catch {
155
+ // aborted
156
+ } finally {
157
+ if (this.currentAnimation === controller) {
158
+ this.currentAnimation = undefined
159
+ }
160
+ }
161
+ }
162
+
163
+ private async _rotateTo(
164
+ signal: AbortSignal,
165
+ targetCoords: [number, number],
166
+ targetZoom?: number
167
+ ): Promise<void> {
168
+ const currentCoords = this.globeConfig.rotation
169
+ const animatedCoords = geoInterpolate(currentCoords, targetCoords)
170
+
171
+ const currentZoom = this.globeConfig.zoom
172
+ const animatedZoom =
173
+ targetZoom !== undefined
174
+ ? interpolateNumber(currentZoom, targetZoom)
175
+ : undefined
176
+
177
+ const animPromise = new Promise<void>((resolve, reject) => {
178
+ const now = Date.now()
179
+ const step = action((): void => {
180
+ const elapsed = Date.now() - now
181
+ const t = Math.min(1, elapsed / ANIMATION_DURATION)
182
+
183
+ // Check if the animation was canceled
184
+ if (signal.aborted) {
185
+ reject()
186
+ return
187
+ }
188
+
189
+ // animate globe rotation
190
+ this.globeConfig.rotation = animatedCoords(easeCubicOut(t))
191
+
192
+ // animate zoom
193
+ if (animatedZoom)
194
+ this.globeConfig.zoom = animatedZoom(easeCubicOut(t))
195
+
196
+ if (t < 1) {
197
+ requestAnimationFrame(step)
198
+ } else {
199
+ resolve()
200
+ }
201
+ })
202
+ requestAnimationFrame(step)
203
+ })
204
+
205
+ await animPromise
206
+ .catch(() => {
207
+ // ignore
208
+ })
209
+ .then(
210
+ action(() => {
211
+ // ensure we end exactly at the target values
212
+ this.globeConfig.rotation = targetCoords
213
+ if (targetZoom !== undefined)
214
+ this.globeConfig.zoom = targetZoom
215
+ })
216
+ )
217
+ }
218
+ }
219
+
220
+ function calculateTargetForCountry(
221
+ country: EntityName,
222
+ zoom?: number
223
+ ): Target | undefined {
224
+ const geoFeature = geoFeaturesById.get(country)
225
+ if (!geoFeature) return
226
+
227
+ const coords: [number, number] = [
228
+ geoFeature.geoCentroid[0],
229
+ R.clamp(geoFeature.geoCentroid[1], {
230
+ min: GLOBE_LATITUDE_MIN,
231
+ max: GLOBE_LATITUDE_MAX,
232
+ }),
233
+ ]
234
+
235
+ // make sure the whole country is visible after zooming
236
+ const zoomToFit = calculateZoomToFitForGeoFeature(geoFeature.geo)
237
+ const targetZoom = Math.min(zoom ?? GLOBE_COUNTRY_ZOOM, zoomToFit)
238
+
239
+ return { coords, zoom: targetZoom }
240
+ }
241
+
242
+ function calculateZoomToFitForGeoFeature(geoFeature: GeoFeature): number {
243
+ const centerPoint = getCenterPoint(geoFeature)
244
+ const projection = geoOrthographic().rotate(negateCoords(centerPoint))
245
+
246
+ const corners = geoPath().projection(projection).bounds(geoFeature)
247
+ const bounds = Bounds.fromCorners(
248
+ new PointVector(...corners[0]),
249
+ new PointVector(...corners[1])
250
+ )
251
+
252
+ return calculateZoomToFitForBounds(bounds)
253
+ }
254
+
255
+ function calculateZoomToFitForBounds(bounds: Bounds): number {
256
+ // calculate the zoom needed for the bounds to be visible
257
+ let zoom = Math.min(
258
+ DEFAULT_GLOBE_SIZE / bounds.width,
259
+ DEFAULT_GLOBE_SIZE / bounds.height
260
+ )
261
+ if (Number.isNaN(zoom)) zoom = 1
262
+
263
+ // it's nicer to have a bit of padding around the zoomed-to area
264
+ zoom = zoom - 0.05
265
+
266
+ // clamp the zoom to the allowed range
267
+ zoom = R.clamp(zoom, { min: GLOBE_MIN_ZOOM, max: GLOBE_MAX_ZOOM })
268
+
269
+ return zoom
270
+ }
271
+
272
+ function calculateTargetForOwidContinent(continent: GlobeRegionName): Target {
273
+ const viewport = GLOBE_VIEWPORTS[continent]
274
+ return { coords: viewport.rotation, zoom: viewport.zoom }
275
+ }
276
+
277
+ function calculateTargetBasedOnTime(): Target {
278
+ const coords = getCoordsBasedOnTime()
279
+ return { coords, zoom: 1 }
280
+ }
281
+
282
+ function calculateTargetForRegion(regionName: string): Target | undefined {
283
+ const region = getRegionByName(regionName)
284
+ if (!region || !checkHasMembers(region)) return
285
+ const countryNames = getCountryNamesForRegion(region)
286
+ return calculateTargetForCountryCollection(countryNames)
287
+ }
288
+
289
+ function calculateTargetForSelection(
290
+ selection: MapSelectionArray
291
+ ): Target | undefined {
292
+ // if at least one country is selected, then rotate to the countries (and ignore the regions)
293
+ if (selection.selectedCountryNamesInForeground.length > 0) {
294
+ return calculateTargetForCountryCollection(
295
+ selection.selectedCountryNamesInForeground
296
+ )
297
+ }
298
+
299
+ // if a single owid continent is selected, then rotate to it
300
+ // (the hard-coded coords/zoom values are nicer than dynamically computing it)
301
+ if (
302
+ selection.selectedRegions.length === 1 &&
303
+ checkIsOwidContinent(selection.selectedRegions[0])
304
+ ) {
305
+ return calculateTargetForOwidContinent(
306
+ MAP_REGION_NAMES[
307
+ selection.selectedRegions[0].name
308
+ ] as GlobeRegionName
309
+ )
310
+ }
311
+
312
+ // rotate and zoom to all countries in the selected regions
313
+ const countryNames = [
314
+ ...selection.selectedCountryNamesInForeground,
315
+ ...selection.countryNamesForSelectedRegions,
316
+ ]
317
+ return calculateTargetForCountryCollection(countryNames)
318
+ }
319
+
320
+ function calculateTargetForCountryCollection(
321
+ countryNames: string[]
322
+ ): Target | undefined {
323
+ // early return if the selection is empty or a single country is selected
324
+ if (countryNames.length === 0) return
325
+ if (countryNames.length === 1) {
326
+ return calculateTargetForCountry(countryNames[0])
327
+ }
328
+
329
+ // find a subset of countries that can be shown on the globe,
330
+ // e.g. if 'Mexico', 'Guatemala' and 'Australia' are selected, then
331
+ // 'Australia' is dropped as it's on the opposite site from South America
332
+ const visibleCountries = findVisibleCountrySubset(countryNames)
333
+
334
+ // early return if no country or a single country is visible
335
+ if (visibleCountries.length === 0) return
336
+ if (visibleCountries.length === 1) {
337
+ return calculateTargetForCountry(visibleCountries[0])
338
+ }
339
+
340
+ // calculate target coords and zoom for two or more countries
341
+ return getCoordsAndZoomForCountryCollection(visibleCountries)
342
+ }
343
+
344
+ function getCoordsBasedOnTime(): [number, number] {
345
+ const date = new Date()
346
+ const hours = date.getUTCHours()
347
+
348
+ if (hours <= 7) {
349
+ return DEFAULT_GLOBE_ROTATIONS_FOR_TIME.UTC_MORNING
350
+ } else if (hours <= 15) {
351
+ return DEFAULT_GLOBE_ROTATIONS_FOR_TIME.UTC_MIDDAY
352
+ } else {
353
+ return DEFAULT_GLOBE_ROTATIONS_FOR_TIME.UTC_EVENING
354
+ }
355
+ }
356
+
357
+ function getCenterForCountryCollection(
358
+ countryNames: string[]
359
+ ): [number, number] {
360
+ const featureCollection = makeFeatureCollectionForCountries(countryNames)
361
+ return getCenterPoint(featureCollection)
362
+ }
363
+
364
+ function getCenterPoint(
365
+ geojson: GeoFeature | FeatureCollection
366
+ ): [number, number] {
367
+ const centerPoint = center(geojson)
368
+
369
+ return [
370
+ centerPoint.geometry.coordinates[0],
371
+ R.clamp(centerPoint.geometry.coordinates[1], {
372
+ min: GLOBE_LATITUDE_MIN,
373
+ max: GLOBE_LATITUDE_MAX,
374
+ }),
375
+ ]
376
+ }
377
+
378
+ function getCoordsAndZoomForCountryCollection(countryNames: string[]): {
379
+ coords: [number, number]
380
+ zoom: number
381
+ } {
382
+ const centerPoint = getCenterForCountryCollection(countryNames)
383
+ const projection = geoOrthographic().rotate(negateCoords(centerPoint))
384
+
385
+ const bounds = excludeUndefined(
386
+ countryNames.map((countryName) => {
387
+ const feature = geoFeaturesById.get(countryName)
388
+ if (!feature) return
389
+ const corners = geoPath().projection(projection).bounds(feature.geo)
390
+ if (corners[0][0] === Number.POSITIVE_INFINITY) return
391
+ return Bounds.fromCorners(
392
+ new PointVector(...corners[0]),
393
+ new PointVector(...corners[1])
394
+ )
395
+ })
396
+ )
397
+
398
+ // merge bounds and calculate the zoom needed for the countries to be visible
399
+ const mergedBounds = Bounds.merge(bounds)
400
+ const zoom = calculateZoomToFitForBounds(mergedBounds)
401
+
402
+ return { coords: centerPoint, zoom }
403
+ }
404
+
405
+ function findVisibleCountrySubset(countryNames: string[]): string[] {
406
+ // rotate the globe to the center point of all given countries,
407
+ // and find all countries that are then visible on the globe
408
+ const centerPoint = getCenterForCountryCollection(countryNames)
409
+ const projection = geoOrthographic().rotate(negateCoords(centerPoint))
410
+ const visibleCountries = countryNames.filter((countryName) => {
411
+ const feature = geoFeaturesById.get(countryName)
412
+ if (!feature) return false
413
+
414
+ // check if the centroid is visible
415
+ const isCentroidVisible = isPointPlacedOnVisibleHemisphere(
416
+ feature.geoCentroid,
417
+ centerPoint
418
+ )
419
+ if (!isCentroidVisible) return false
420
+
421
+ // if the centroid is visible, then also check if the bounds are
422
+ // visible (if they're infinite, then they're not)
423
+ const corners = geoPath().projection(projection).bounds(feature.geo)
424
+ if (corners[0][0] === Number.POSITIVE_INFINITY) return false
425
+
426
+ return true
427
+ })
428
+
429
+ // it's possible for no country to be visible if the countries are on opposite
430
+ // sides from the globe. in that case, we need to drop a subset of countries
431
+ if (visibleCountries.length === 0) {
432
+ // cluster countries into two groups based on their centroid longitude
433
+ const clusters = clusterCountriesByCentroidLongitude(countryNames)
434
+
435
+ // keep the bigger cluster
436
+ // (if both clusters have the same number of countries, keep any)
437
+ return clusters[0].length > clusters[1].length
438
+ ? clusters[0]
439
+ : clusters[1]
440
+ }
441
+
442
+ return visibleCountries
443
+ }
444
+
445
+ function clusterCountriesByCentroidLongitude(
446
+ countryNames: string[]
447
+ ): string[][] {
448
+ const nameToLon: Record<string, number> = {}
449
+ const lonToName: Record<number, string> = {}
450
+
451
+ // map country names to their centroid's longitude and vice versa
452
+ // (assumes that no two countries have the same longitude)
453
+ countryNames.forEach((countryName) => {
454
+ const feature = geoFeaturesById.get(countryName)
455
+ if (!feature) return
456
+
457
+ const lon = R.round(feature.geoCentroid[0], 5)
458
+ nameToLon[countryName] = lon
459
+ lonToName[lon] = countryName
460
+ })
461
+
462
+ // cluster longitudes into two groups
463
+ const clusters = ckmeans(
464
+ excludeUndefined(countryNames.map((name) => nameToLon[name])),
465
+ 2
466
+ )
467
+
468
+ // map longitudes back to country names
469
+ return clusters.map((cluster) =>
470
+ cluster.map((centroidLon) => lonToName[centroidLon])
471
+ )
472
+ }
473
+
474
+ function makeFeatureCollectionForCountries(
475
+ countryNames: string[]
476
+ ): FeatureCollection {
477
+ const features = excludeUndefined(
478
+ countryNames.map((name) => geoFeaturesById.get(name))
479
+ )
480
+
481
+ return {
482
+ type: "FeatureCollection",
483
+ features: features.map((feature) => feature.geo),
484
+ }
485
+ }
486
+
487
+ function addLongitudeOffset(
488
+ coords: [number, number],
489
+ offset = LONGITUDE_OFFSET
490
+ ): [number, number] {
491
+ return [coords[0] + offset, coords[1]]
492
+ }
493
+
494
+ function negateCoords(coords: [number, number]): [number, number] {
495
+ return [-coords[0], -coords[1]]
496
+ }