@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,949 @@
1
+ import * as _ from "lodash-es"
2
+ import React from "react"
3
+ import { computed, action, observable, makeObservable } from "mobx"
4
+ import { observer } from "mobx-react"
5
+ import {
6
+ geoGraticule,
7
+ geoOrthographic,
8
+ GeoPath,
9
+ geoPath,
10
+ GeoPermissibleObjects,
11
+ } from "d3-geo"
12
+ import { Quadtree, quadtree } from "d3-quadtree"
13
+ import { select } from "d3-selection"
14
+ import { zoom } from "d3-zoom"
15
+ // no types available for versor
16
+ import versor from "versor"
17
+ import {
18
+ makeIdForHumanConsumption,
19
+ Bounds,
20
+ isTouchDevice,
21
+ getRelativeMouse,
22
+ checkIsTouchEvent,
23
+ PointVector,
24
+ MapRegionName,
25
+ excludeUndefined,
26
+ EntityName,
27
+ } from "../../utils/index.js"
28
+ import {
29
+ Annotation,
30
+ ANNOTATION_COLOR_DARK,
31
+ ANNOTATION_COLOR_LIGHT,
32
+ ChoroplethMapManager,
33
+ ChoroplethSeriesByName,
34
+ DEFAULT_GLOBE_SIZE,
35
+ ExternalAnnotation,
36
+ GEO_FEATURES_CLASSNAME,
37
+ GLOBE_COUNTRY_ZOOM,
38
+ GLOBE_LATITUDE_MAX,
39
+ GLOBE_LATITUDE_MIN,
40
+ GLOBE_MAX_ZOOM,
41
+ GLOBE_MIN_ZOOM,
42
+ GlobeRenderFeature,
43
+ InternalAnnotation,
44
+ MAP_HOVER_TARGET_RANGE,
45
+ PROJECTED_DATA_LEGEND_COLOR,
46
+ RenderFeature,
47
+ SVGMouseEvent,
48
+ } from "./MapChartConstants"
49
+ import { MapConfig } from "./MapConfig"
50
+ import { getGeoFeaturesForGlobe } from "./GeoFeatures"
51
+ import {
52
+ BackgroundCountry,
53
+ CountryWithData,
54
+ CountryWithNoData,
55
+ ExternalValueAnnotation,
56
+ InternalValueAnnotation,
57
+ NoDataPattern,
58
+ ProjectedDataPattern,
59
+ } from "./MapComponents"
60
+ import { Patterns } from "../core/GrapherConstants"
61
+ import {
62
+ calculateDistance,
63
+ detectNearbyFeature,
64
+ isPointPlacedOnVisibleHemisphere,
65
+ sortFeaturesByInteractionStateAndSize,
66
+ getForegroundFeatures,
67
+ isValidGlobeRegionName,
68
+ } from "./MapHelpers"
69
+ import {
70
+ makeInternalAnnotationForFeature,
71
+ makeExternalAnnotationForFeature,
72
+ repositionAndFilterExternalAnnotations,
73
+ } from "./MapAnnotations"
74
+ import * as R from "remeda"
75
+ import { GlobeController } from "./GlobeController"
76
+ import { isDarkColor } from "../color/ColorUtils"
77
+ import { MapSelectionArray } from "../selection/MapSelectionArray"
78
+
79
+ const DEFAULT_SCALE = geoOrthographic().scale()
80
+
81
+ @observer
82
+ export class ChoroplethGlobe extends React.Component<{
83
+ manager: ChoroplethMapManager
84
+ }> {
85
+ base = React.createRef<SVGGElement>()
86
+
87
+ private hoverEnterFeature: GlobeRenderFeature | undefined = undefined
88
+ private hoverNearbyFeature: GlobeRenderFeature | undefined = undefined
89
+
90
+ private isPanningOrZooming = false
91
+
92
+ constructor(props: { manager: ChoroplethMapManager }) {
93
+ super(props)
94
+
95
+ makeObservable<
96
+ ChoroplethGlobe,
97
+ "hoverEnterFeature" | "hoverNearbyFeature"
98
+ >(this, {
99
+ hoverEnterFeature: observable,
100
+ hoverNearbyFeature: observable,
101
+ })
102
+ }
103
+
104
+ @computed private get isTouchDevice(): boolean {
105
+ return isTouchDevice()
106
+ }
107
+
108
+ @computed private get manager(): ChoroplethMapManager {
109
+ return this.props.manager
110
+ }
111
+
112
+ @computed get mapConfig(): MapConfig {
113
+ return this.manager.mapConfig
114
+ }
115
+
116
+ @computed private get globeController(): GlobeController {
117
+ return this.manager.globeController ?? new GlobeController(this)
118
+ }
119
+
120
+ @computed.struct private get bounds(): Bounds {
121
+ return this.manager.choroplethMapBounds
122
+ }
123
+
124
+ @computed private get selectionArray(): MapSelectionArray {
125
+ return this.manager.selectionArray ?? new MapSelectionArray()
126
+ }
127
+
128
+ @computed.struct private get choroplethData(): ChoroplethSeriesByName {
129
+ return this.manager.choroplethData
130
+ }
131
+
132
+ @computed private get features(): GlobeRenderFeature[] {
133
+ return getGeoFeaturesForGlobe()
134
+ }
135
+
136
+ @computed private get featuresById(): Map<string, GlobeRenderFeature> {
137
+ return new Map(this.features.map((feature) => [feature.id, feature]))
138
+ }
139
+
140
+ @computed private get foregroundFeatures(): GlobeRenderFeature[] {
141
+ return getForegroundFeatures(this.features, this.selectionArray)
142
+ }
143
+
144
+ @computed
145
+ private get backgroundFeatures(): GlobeRenderFeature[] {
146
+ return _.difference(this.features, this.foregroundFeatures)
147
+ }
148
+
149
+ @computed private get backgroundFeatureIdSet(): Set<EntityName> {
150
+ return new Set(this.backgroundFeatures.map((feature) => feature.id))
151
+ }
152
+
153
+ @computed private get featuresWithData(): GlobeRenderFeature[] {
154
+ return this.foregroundFeatures.filter((feature) =>
155
+ this.choroplethData.has(feature.id)
156
+ )
157
+ }
158
+
159
+ @computed private get sortedFeaturesWithData(): GlobeRenderFeature[] {
160
+ // sort features so that hovered or selected features are rendered last
161
+ // and smaller countries are rendered on top of bigger ones
162
+ return sortFeaturesByInteractionStateAndSize(this.featuresWithData, {
163
+ isHovered: (featureId: string) =>
164
+ this.manager.getHoverState?.(featureId).active ?? false,
165
+ isSelected: (featureId) =>
166
+ this.manager.isSelected?.(featureId) ?? false,
167
+ })
168
+ }
169
+
170
+ @computed private get featuresWithNoData(): GlobeRenderFeature[] {
171
+ return _.difference(this.foregroundFeatures, this.featuresWithData)
172
+ }
173
+
174
+ @computed private get binColors(): string[] {
175
+ return this.manager.binColors ?? []
176
+ }
177
+
178
+ // Map uses a hybrid approach to mouseover
179
+ // If mouse is inside an element, that is prioritized
180
+ // Otherwise we do a quadtree search for the closest center point of a feature bounds,
181
+ // so that we can hover very small countries without trouble
182
+ @action.bound private detectNearbyFeature(
183
+ event: MouseEvent | TouchEvent,
184
+ maxDistance = MAP_HOVER_TARGET_RANGE
185
+ ): GlobeRenderFeature | undefined {
186
+ if (this.hoverEnterFeature || !this.base.current) return
187
+
188
+ const nearbyFeature = detectNearbyFeature({
189
+ quadtree: this.quadtree,
190
+ element: this.base.current,
191
+ event,
192
+ distance: maxDistance,
193
+ })
194
+
195
+ if (!nearbyFeature) {
196
+ this.hoverNearbyFeature = undefined
197
+ this.manager.onMapMouseLeave?.()
198
+ return
199
+ }
200
+
201
+ if (nearbyFeature.id !== this.hoverNearbyFeature?.id) {
202
+ this.hoverNearbyFeature = nearbyFeature
203
+ this.manager.onMapMouseOver?.(nearbyFeature.geo)
204
+ }
205
+
206
+ return nearbyFeature
207
+ }
208
+
209
+ @computed private get globeSize(): number {
210
+ return Math.min(this.bounds.width, this.bounds.height)
211
+ }
212
+
213
+ @computed private get globeCenter(): [number, number] {
214
+ return [
215
+ this.bounds.left + this.bounds.width / 2,
216
+ this.bounds.top + this.bounds.height / 2,
217
+ ]
218
+ }
219
+
220
+ private computeProjectionScale(zoomScale: number): number {
221
+ return zoomScale * DEFAULT_SCALE * (this.globeSize / DEFAULT_GLOBE_SIZE)
222
+ }
223
+
224
+ @computed private get minScale(): number {
225
+ return this.computeProjectionScale(GLOBE_MIN_ZOOM)
226
+ }
227
+
228
+ @computed private get maxScale(): number {
229
+ return this.computeProjectionScale(GLOBE_MAX_ZOOM)
230
+ }
231
+
232
+ @computed private get globeScale(): number {
233
+ return this.computeProjectionScale(this.zoomScale)
234
+ }
235
+
236
+ @computed private get globeRadius(): number {
237
+ return (this.globeSize / 2) * this.zoomScale
238
+ }
239
+
240
+ @computed private get globeRotation(): [number, number] {
241
+ // d3 projections expect [-lon, -lat] to rotate to [lon, lat]
242
+ return [
243
+ -this.mapConfig.globe.rotation[0],
244
+ -this.mapConfig.globe.rotation[1],
245
+ ]
246
+ }
247
+
248
+ @computed private get zoomScale(): number {
249
+ return this.mapConfig.globe.zoom
250
+ }
251
+
252
+ @computed private get projection(): any {
253
+ return geoOrthographic()
254
+ .scale(this.globeScale)
255
+ .translate(this.globeCenter)
256
+ .rotate(this.globeRotation)
257
+ }
258
+
259
+ @computed private get globePath(): GeoPath<any, GeoPermissibleObjects> {
260
+ return geoPath().digits(1).projection(this.projection)
261
+ }
262
+
263
+ private getPath(feature: GlobeRenderFeature): string {
264
+ return this.globePath(feature.geo) ?? ""
265
+ }
266
+
267
+ private isFeatureCentroidVisibleOnGlobe(
268
+ feature: GlobeRenderFeature,
269
+ threshold = 0 // 1 = at the exact center, 0 = anywhere on the visible hemisphere
270
+ ): boolean {
271
+ return isPointPlacedOnVisibleHemisphere(
272
+ feature.geoCentroid,
273
+ this.mapConfig.globe.rotation,
274
+ threshold
275
+ )
276
+ }
277
+
278
+ @computed private get graticulePath(): string {
279
+ const graticule = geoGraticule().step([10, 10])()
280
+ return this.globePath(graticule) ?? ""
281
+ }
282
+
283
+ @computed private get equatorPath(): string {
284
+ const equator = geoGraticule().step([0, 360])()
285
+ return this.globePath(equator) ?? ""
286
+ }
287
+
288
+ @computed private get visibleFeatures(): GlobeRenderFeature[] {
289
+ return this.foregroundFeatures.filter((feature) =>
290
+ this.isFeatureCentroidVisibleOnGlobe(feature)
291
+ )
292
+ }
293
+
294
+ @computed private get quadtree(): Quadtree<GlobeRenderFeature> {
295
+ return quadtree<GlobeRenderFeature>()
296
+ .x((feature) => this.projection(feature.geoCentroid)[0])
297
+ .y((feature) => this.projection(feature.geoCentroid)[1])
298
+ .addAll(this.visibleFeatures)
299
+ }
300
+
301
+ @computed private get shouldShowAnnotations(): boolean {
302
+ return !!(
303
+ this.manager.mapColumn.hasNumberFormatting &&
304
+ !this.mapConfig.tooltipUseCustomLabels
305
+ )
306
+ }
307
+
308
+ private formatAnnotationLabel(value: string | number): string {
309
+ return this.manager.mapColumn.formatValueShortWithAbbreviations(value)
310
+ }
311
+
312
+ @computed private get annotationCandidateFeatures(): GlobeRenderFeature[] {
313
+ if (!this.shouldShowAnnotations) return []
314
+
315
+ return excludeUndefined(
316
+ this.mapConfig.selection.selectedCountryNamesInForeground.map(
317
+ (name) => this.featuresById.get(name)
318
+ )
319
+ ).filter((feature) => this.foregroundFeatures.includes(feature))
320
+ }
321
+
322
+ /* Naively placed annotations that might be overlapping */
323
+ @computed
324
+ private get annotationCandidates(): Annotation[] {
325
+ return excludeUndefined(
326
+ this.annotationCandidateFeatures
327
+ .filter(
328
+ (feature): feature is GlobeRenderFeature =>
329
+ feature !== undefined &&
330
+ // don't show annotations for countries that are currently
331
+ // on the back side of the globe or at the edge
332
+ this.isFeatureCentroidVisibleOnGlobe(feature, 0.3)
333
+ )
334
+ .map((feature) => {
335
+ const series = this.choroplethData.get(feature.id)
336
+ if (!series) return
337
+
338
+ const labelColor = isDarkColor(series.color)
339
+ ? ANNOTATION_COLOR_LIGHT
340
+ : ANNOTATION_COLOR_DARK
341
+ const fontSizeScale = 1 / this.zoomScale
342
+
343
+ const args = {
344
+ feature,
345
+ projection: this.projection,
346
+ formattedValue: this.formatAnnotationLabel(
347
+ series.value
348
+ ),
349
+ color: labelColor,
350
+ }
351
+
352
+ // try to fit the annotation inside the feature
353
+ const internalAnnotation =
354
+ makeInternalAnnotationForFeature(args)
355
+ if (internalAnnotation) return internalAnnotation
356
+
357
+ // place the annotation outside of the feature
358
+ const externalAnnotation = makeExternalAnnotationForFeature(
359
+ { ...args, fontSizeScale }
360
+ )
361
+ if (externalAnnotation) return externalAnnotation
362
+
363
+ return undefined
364
+ })
365
+ )
366
+ }
367
+
368
+ @computed
369
+ private get internalAnnotations(): InternalAnnotation[] {
370
+ return this.annotationCandidates.filter(
371
+ (annotation) => annotation.type === "internal"
372
+ )
373
+ }
374
+
375
+ @computed
376
+ private get externalAnnotations(): ExternalAnnotation[] {
377
+ const { projection, backgroundFeatureIdSet } = this
378
+ const annotations = this.annotationCandidates.filter(
379
+ (annotation) => annotation.type === "external"
380
+ )
381
+ return repositionAndFilterExternalAnnotations({
382
+ annotations,
383
+ projection,
384
+ backgroundFeatureIdSet,
385
+ })
386
+ }
387
+
388
+ @action.bound private rotateGlobe(targetCoords: [number, number]): void {
389
+ this.mapConfig.globe.rotation = [
390
+ -targetCoords[0],
391
+ // Clamping the latitude to [-90, 90] would allow rotation up to the poles.
392
+ // However, the panning strategy used doesn't work well around the poles.
393
+ // That's why we clamp the latitude to a narrower range.
394
+ -R.clamp(targetCoords[1], {
395
+ min: GLOBE_LATITUDE_MIN,
396
+ max: GLOBE_LATITUDE_MAX,
397
+ }),
398
+ ]
399
+ }
400
+
401
+ @action.bound private zoomGlobe(delta: number): void {
402
+ const sensitivity = 0.01
403
+ const newZoom = this.zoomScale * (1 + delta * sensitivity)
404
+ this.mapConfig.globe.zoom = R.clamp(newZoom, {
405
+ min: GLOBE_MIN_ZOOM,
406
+ max: GLOBE_MAX_ZOOM,
407
+ })
408
+ }
409
+
410
+ @computed private get hoverFeature(): GlobeRenderFeature | undefined {
411
+ return this.hoverEnterFeature || this.hoverNearbyFeature
412
+ }
413
+
414
+ @action.bound private clearHover(): void {
415
+ this.hoverEnterFeature = undefined
416
+ this.hoverNearbyFeature = undefined
417
+ this.manager.onMapMouseLeave?.()
418
+ this.globeController.dismissCountryFocus()
419
+ }
420
+
421
+ @action.bound private onMouseMove(event: MouseEvent): void {
422
+ this.detectNearbyFeature(event)
423
+ }
424
+
425
+ @action.bound private onMouseEnterFeature(
426
+ feature: GlobeRenderFeature
427
+ ): void {
428
+ // ignore mouse enter if panning or zooming
429
+ if (this.isPanningOrZooming) return
430
+ this.setHoverEnterFeature(feature)
431
+ }
432
+
433
+ @action.bound private onMouseLeaveFeature(): void {
434
+ // Fixes an issue where clicking on a country that overlaps with the
435
+ // tooltip causes the tooltip to disappear shortly after being rendered
436
+ if (this.isTouchDevice) return
437
+
438
+ this.clearHoverEnterFeature()
439
+ }
440
+
441
+ @action.bound private setHoverEnterFeature(
442
+ feature: GlobeRenderFeature
443
+ ): void {
444
+ if (this.hoverEnterFeature?.id === feature.id) return
445
+
446
+ this.hoverEnterFeature = feature
447
+ this.manager.onMapMouseOver?.(feature.geo)
448
+ }
449
+
450
+ @action.bound private clearHoverEnterFeature(): void {
451
+ this.hoverEnterFeature = undefined
452
+ this.manager.onMapMouseLeave?.()
453
+ }
454
+
455
+ @action.bound private onClick(feature: GlobeRenderFeature): void {
456
+ this.setHoverEnterFeature(feature)
457
+
458
+ // reset the region if necessary
459
+ this.mapConfig.region = MapRegionName.World
460
+
461
+ // select/deselect the country if allowed
462
+ const country = feature.id
463
+ if (this.manager.isMapSelectionEnabled) {
464
+ this.mapConfig.selection.toggleSelection(country)
465
+
466
+ // make sure country focus is dismissed for unselected countries
467
+ if (!this.mapConfig.selection.selectedSet.has(country))
468
+ this.globeController.dismissCountryFocus()
469
+ }
470
+
471
+ // rotate to the selected country on mobile
472
+ if (!this.manager.isMapSelectionEnabled) {
473
+ // only zoom in on click, never zoom out
474
+ const zoom = Math.max(GLOBE_COUNTRY_ZOOM, this.mapConfig.globe.zoom)
475
+ this.globeController.rotateToCountry(country, zoom)
476
+ }
477
+ }
478
+
479
+ @action.bound private onTouchStart(feature: GlobeRenderFeature): void {
480
+ this.setHoverEnterFeature(feature)
481
+ }
482
+
483
+ @action.bound private onDocumentClick(): void {
484
+ this.clearHover()
485
+ }
486
+
487
+ private setUpPanningAndZooming(): void {
488
+ const base = this.base.current
489
+ if (!base) return
490
+
491
+ // Possible interaction types are
492
+ // - zoom-scroll: zooming by scrolling via the wheel event
493
+ // - zoom-pinch: zooming by pinching using two fingers on touch devices
494
+ // - pan: panning by dragging the mouse or using a finger on touch devices
495
+ type InteractionType = "zoom-scroll" | "zoom-pinch" | "pan"
496
+
497
+ // Panning and zooming are powered by D3.
498
+ //
499
+ // Panning is adapted from https://observablehq.com/d/569d101dd5bd332b.
500
+ // The strategy ensures the geographic start point remains under the cursor
501
+ // where possible. See https://www.jasondavies.com/maps/rotate/ for more
502
+ // details.
503
+ //
504
+ // We could rely on D3's event.transform.k for zooming, but the
505
+ // transform value might be out of sync with the actual zoom level if
506
+ // the zoom level was changed elsewhere (e.g. by automatically zooming
507
+ // in on a country). That's why we compute the target zoom level ourselves.
508
+
509
+ const panAndZoom = (): any => {
510
+ let previousType: InteractionType | undefined
511
+
512
+ // for panning
513
+ let startCoords: [number, number, number],
514
+ startQuat: [number, number, number, number],
515
+ startRot: [number, number, number],
516
+ previousPos: [number, number]
517
+
518
+ // for zooming
519
+ let startDistance: number | undefined
520
+
521
+ const getInteractionType = (event: any): InteractionType => {
522
+ if (event.sourceEvent.type === "wheel") return "zoom-scroll"
523
+ if (isMultiTouchEvent(event.sourceEvent)) return "zoom-pinch"
524
+ return "pan"
525
+ }
526
+
527
+ const panningOrZoomingStart = (event: any): void => {
528
+ const type = getInteractionType(event)
529
+
530
+ const startPinching = (): void => {
531
+ startDistance = calculatePinchDistance(event.sourceEvent)
532
+ }
533
+
534
+ const startPanning = (): void => {
535
+ const posVector = getRelativeMouse(base, event.sourceEvent)
536
+ const pos: [number, number] = [posVector.x, posVector.y]
537
+
538
+ startCoords = versor.cartesian(this.projection.invert(pos))
539
+ startRot = this.projection.rotate()
540
+ startQuat = versor(startRot)
541
+ previousPos = pos
542
+ }
543
+
544
+ if (type === "zoom-pinch") startPinching()
545
+ else if (type === "pan") startPanning()
546
+
547
+ previousType = type
548
+ }
549
+
550
+ const panningOrZooming = action((event: any): void => {
551
+ this.isPanningOrZooming = true
552
+
553
+ this.clearHover() // dismiss the tooltip
554
+ this.mapConfig.region = MapRegionName.World // reset region
555
+
556
+ const wheeling = (): void => {
557
+ this.zoomGlobe(-event.sourceEvent.deltaY)
558
+ }
559
+
560
+ const pinching = (): void => {
561
+ const distance = calculatePinchDistance(event.sourceEvent)
562
+
563
+ if (!startDistance) {
564
+ startDistance = distance
565
+ return
566
+ }
567
+
568
+ const delta = distance - startDistance
569
+
570
+ // We sometimes get two events for the same pinch gesture,
571
+ // with one of the events having a delta of 0. We simply
572
+ // ignore the delta-0 events. This fixes a bug where the
573
+ // rendered SVG country paths would interfere with the
574
+ // pinch-to-zoom gesture.
575
+ if (delta === 0) return
576
+
577
+ this.zoomGlobe(delta)
578
+ startDistance = distance
579
+ }
580
+
581
+ const panning = (): void => {
582
+ const posVector = getRelativeMouse(base, event.sourceEvent)
583
+ const pos: [number, number] = [posVector.x, posVector.y]
584
+
585
+ // True if the cursor is currently over the globe
586
+ const isDraggingGlobe = isPointInCircle(posVector, {
587
+ cx: this.globeCenter[0],
588
+ cy: this.globeCenter[1],
589
+ r: this.globeRadius,
590
+ })
591
+
592
+ if (isDraggingGlobe) {
593
+ // If the user is dragging the globe, then use a
594
+ // rotation strategy that ensures the geographic start
595
+ // point remains under the cursor where possible
596
+
597
+ const currCoords = versor.cartesian(
598
+ this.projection.rotate(startRot).invert(pos)
599
+ )
600
+ const delta = versor.delta(startCoords, currCoords)
601
+ const quat = versor.multiply(startQuat, delta)
602
+ const rotation = versor.rotation(quat)
603
+
604
+ // Ignore the gamma channel for more intuitive rotation
605
+ // see https://observablehq.com/@d3/three-axis-rotation
606
+ // for a visual explanation of three-axis rotation.
607
+ // As a side effect, rotation around the poles feels off.
608
+ this.rotateGlobe([rotation[0], rotation[1]])
609
+ } else {
610
+ // If the user's cursor is outside of the globe, then
611
+ // adjust the globe's rotation based on the cursor's
612
+ // movement, applying a sensitivity factor to control
613
+ // the speed of rotation
614
+
615
+ const sensitivity = 0.8
616
+ const r = this.globeRotation
617
+ const dx = pos[0] - previousPos[0]
618
+ const dy = pos[1] - previousPos[1]
619
+ this.rotateGlobe([
620
+ r[0] + sensitivity * dx,
621
+ r[1] - sensitivity * dy,
622
+ ])
623
+ }
624
+
625
+ previousPos = pos
626
+ }
627
+
628
+ const type = getInteractionType(event)
629
+
630
+ // bail if a zoom-pinch gesture turned into a pan
631
+ // because this might lead to erratic jumps
632
+ if (previousType === "zoom-pinch" && type === "pan") return
633
+
634
+ if (type === "zoom-scroll") wheeling()
635
+ else if (type === "zoom-pinch") pinching()
636
+ else if (type === "pan") panning()
637
+
638
+ previousType = type
639
+ })
640
+
641
+ const panningOrZoomingEnd = (): void => {
642
+ this.isPanningOrZooming = false
643
+ startDistance = undefined
644
+ previousType = undefined
645
+ }
646
+
647
+ return zoom()
648
+ .scaleExtent([this.minScale, this.maxScale])
649
+ .touchable(() => this.isTouchDevice)
650
+ .on("start", panningOrZoomingStart)
651
+ .on("zoom", panningOrZooming)
652
+ .on("end", panningOrZoomingEnd)
653
+ }
654
+
655
+ select(base).call(panAndZoom())
656
+ }
657
+
658
+ override componentDidMount(): void {
659
+ // rotate to the selected region
660
+ if (isValidGlobeRegionName(this.mapConfig.region)) {
661
+ this.globeController.jumpToOwidContinent(this.mapConfig.region)
662
+ }
663
+
664
+ document.addEventListener("touchstart", this.onDocumentClick, {
665
+ capture: true,
666
+ passive: true,
667
+ })
668
+
669
+ this.setUpPanningAndZooming()
670
+ }
671
+
672
+ override componentWillUnmount(): void {
673
+ document.removeEventListener("touchstart", this.onDocumentClick, {
674
+ capture: true,
675
+ })
676
+ }
677
+
678
+ renderGlobeOutline(): React.ReactElement {
679
+ return (
680
+ <>
681
+ <circle
682
+ id={makeIdForHumanConsumption("globe-sphere")}
683
+ cx={this.globeCenter[0]}
684
+ cy={this.globeCenter[1]}
685
+ r={this.globeRadius}
686
+ fill="#fafafa"
687
+ />
688
+ <path
689
+ id={makeIdForHumanConsumption("globe-graticule")}
690
+ d={this.graticulePath}
691
+ stroke="#e7e7e7"
692
+ strokeWidth={1}
693
+ fill="none"
694
+ style={{ pointerEvents: "none" }}
695
+ />
696
+ <path
697
+ id={makeIdForHumanConsumption("globe-equator")}
698
+ d={this.equatorPath}
699
+ stroke="#dadada"
700
+ strokeWidth={1}
701
+ fill="none"
702
+ style={{ pointerEvents: "none" }}
703
+ />
704
+ </>
705
+ )
706
+ }
707
+
708
+ renderFeaturesInBackground(): React.ReactElement | undefined {
709
+ if (this.backgroundFeatures.length === 0) return
710
+
711
+ return (
712
+ <g id={makeIdForHumanConsumption("countries-background")}>
713
+ {this.backgroundFeatures.map((feature) => (
714
+ <BackgroundCountry
715
+ key={feature.id}
716
+ feature={feature}
717
+ path={this.getPath(feature)}
718
+ />
719
+ ))}
720
+ </g>
721
+ )
722
+ }
723
+
724
+ renderFeaturesWithNoData(): React.ReactElement | undefined {
725
+ if (this.featuresWithNoData.length === 0) return
726
+
727
+ const patternId = Patterns.noDataPatternForGlobe
728
+
729
+ return (
730
+ <g
731
+ id={makeIdForHumanConsumption("countries-without-data")}
732
+ className="noDataFeatures"
733
+ >
734
+ <defs>
735
+ <NoDataPattern patternId={patternId} />
736
+ </defs>
737
+
738
+ {this.featuresWithNoData.map((feature) => (
739
+ <CountryWithNoData
740
+ key={feature.id}
741
+ feature={feature}
742
+ path={this.getPath(feature)}
743
+ patternId={patternId}
744
+ isSelected={this.manager.isSelected?.(feature.id)}
745
+ hover={this.manager.getHoverState?.(feature.id)}
746
+ onClick={(event) => {
747
+ // don't invoke a second click on parent that
748
+ // catches clicks on 'nearby' features
749
+ event.stopPropagation()
750
+
751
+ this.onClick(feature)
752
+ }}
753
+ onTouchStart={() => this.onTouchStart(feature)}
754
+ onMouseEnter={this.onMouseEnterFeature}
755
+ onMouseLeave={this.onMouseLeaveFeature}
756
+ />
757
+ ))}
758
+ </g>
759
+ )
760
+ }
761
+
762
+ renderFeaturesWithData(): React.ReactElement | undefined {
763
+ if (this.sortedFeaturesWithData.length === 0) return
764
+
765
+ return (
766
+ <g id={makeIdForHumanConsumption("countries-with-data")}>
767
+ {this.manager.hasProjectedData && (
768
+ <defs>
769
+ {/* Pattern used by the map legend for the projected data bin */}
770
+ <ProjectedDataPattern
771
+ key={PROJECTED_DATA_LEGEND_COLOR}
772
+ color={PROJECTED_DATA_LEGEND_COLOR}
773
+ forLegend
774
+ />
775
+
776
+ {/* Patterns used by the map legend. These duplicate the patterns below,
777
+ but use a legend-specific id */}
778
+ {this.binColors.map((color, index) => (
779
+ <ProjectedDataPattern
780
+ key={`${color}-${index}`}
781
+ color={color}
782
+ forLegend
783
+ />
784
+ ))}
785
+
786
+ {/* Pattern used by features */}
787
+ {this.binColors.map((color, index) => (
788
+ <ProjectedDataPattern
789
+ key={`${color}-${index}`}
790
+ color={color}
791
+ />
792
+ ))}
793
+ </defs>
794
+ )}
795
+
796
+ {this.sortedFeaturesWithData.map((feature) => {
797
+ const series = this.choroplethData.get(feature.id)
798
+ if (!series) return null
799
+ return (
800
+ <CountryWithData
801
+ key={feature.id}
802
+ feature={feature}
803
+ series={series}
804
+ path={this.getPath(feature)}
805
+ isSelected={this.manager.isSelected?.(feature.id)}
806
+ hover={this.manager.getHoverState?.(feature.id)}
807
+ onClick={(event) => {
808
+ // don't invoke a second click on parent that
809
+ // catches clicks on 'nearby' features
810
+ event.stopPropagation()
811
+
812
+ this.onClick(feature)
813
+ }}
814
+ onTouchStart={() => this.onTouchStart(feature)}
815
+ onMouseEnter={this.onMouseEnterFeature}
816
+ onMouseLeave={this.onMouseLeaveFeature}
817
+ />
818
+ )
819
+ })}
820
+ </g>
821
+ )
822
+ }
823
+
824
+ renderInternalAnnotations(): React.ReactElement | undefined {
825
+ if (this.internalAnnotations.length === 0) return
826
+
827
+ return (
828
+ <g id={makeIdForHumanConsumption("annotations-internal")}>
829
+ {this.internalAnnotations.map((annotation) => (
830
+ <InternalValueAnnotation
831
+ key={annotation.id}
832
+ annotation={annotation}
833
+ showOutline={
834
+ this.choroplethData.get(annotation.id)?.isProjection
835
+ }
836
+ />
837
+ ))}
838
+ </g>
839
+ )
840
+ }
841
+
842
+ renderExternalAnnotations(): React.ReactElement | undefined {
843
+ if (this.externalAnnotations.length === 0) return
844
+
845
+ return (
846
+ <g
847
+ id={makeIdForHumanConsumption("annotations-external")}
848
+ className="ExternalAnnotations"
849
+ >
850
+ {this.externalAnnotations.map((annotation) => (
851
+ <ExternalValueAnnotation
852
+ key={annotation.id}
853
+ annotation={annotation}
854
+ onMouseEnter={action((feature: RenderFeature) =>
855
+ this.setHoverEnterFeature(
856
+ feature as GlobeRenderFeature
857
+ )
858
+ )}
859
+ onMouseLeave={action(() =>
860
+ this.clearHoverEnterFeature()
861
+ )}
862
+ />
863
+ ))}
864
+ </g>
865
+ )
866
+ }
867
+
868
+ renderStatic(): React.ReactElement {
869
+ return (
870
+ <>
871
+ {this.renderGlobeOutline()}
872
+ <g id={makeIdForHumanConsumption("globe")}>
873
+ {this.renderFeaturesInBackground()}
874
+ {this.renderFeaturesWithNoData()}
875
+ {this.renderFeaturesWithData()}
876
+ {this.renderInternalAnnotations()}
877
+ {this.renderExternalAnnotations()}
878
+ </g>
879
+ </>
880
+ )
881
+ }
882
+
883
+ renderInteractive(): React.ReactElement {
884
+ // this needs to be referenced here or it will be recomputed on every mousemove
885
+ const _cachedCentroids = this.quadtree
886
+
887
+ return (
888
+ <g
889
+ ref={this.base}
890
+ onMouseDown={
891
+ (ev: SVGMouseEvent): void =>
892
+ ev.preventDefault() /* Without this, title may get selected while shift clicking */
893
+ }
894
+ onMouseMove={(ev: SVGMouseEvent): void =>
895
+ this.onMouseMove(ev.nativeEvent)
896
+ }
897
+ onMouseLeave={this.onMouseLeaveFeature}
898
+ onClick={() => {
899
+ // invoke a click on a feature when clicking nearby one
900
+ if (this.hoverNearbyFeature)
901
+ this.onClick(this.hoverNearbyFeature)
902
+ }}
903
+ style={{ cursor: this.hoverFeature ? "pointer" : undefined }}
904
+ >
905
+ {this.renderGlobeOutline()}
906
+ <g className={GEO_FEATURES_CLASSNAME}>
907
+ {this.renderFeaturesInBackground()}
908
+ {this.renderFeaturesWithNoData()}
909
+ {this.renderFeaturesWithData()}
910
+ {this.renderInternalAnnotations()}
911
+ {this.renderExternalAnnotations()}
912
+ </g>
913
+ </g>
914
+ )
915
+ }
916
+
917
+ override render(): React.ReactElement {
918
+ return this.manager.isStatic
919
+ ? this.renderStatic()
920
+ : this.renderInteractive()
921
+ }
922
+ }
923
+
924
+ const isMultiTouchEvent = (
925
+ event: MouseEvent | TouchEvent
926
+ ): event is TouchEvent => {
927
+ return checkIsTouchEvent(event) && event.touches.length >= 2
928
+ }
929
+
930
+ const calculatePinchDistance = (event: TouchEvent): number => {
931
+ const { touches } = event
932
+ return calculateDistance(
933
+ [touches[0].clientX, touches[0].clientY],
934
+ [touches[1].clientX, touches[1].clientY]
935
+ )
936
+ }
937
+
938
+ function isPointInCircle(
939
+ point: PointVector,
940
+ circle: { cx: number; cy: number; r: number }
941
+ ): boolean {
942
+ const { x, y } = point
943
+ const { cx, cy, r } = circle
944
+
945
+ const distanceSquared = (x - cx) ** 2 + (y - cy) ** 2
946
+ const radiusSquared = r ** 2
947
+
948
+ return distanceSquared <= radiusSquared
949
+ }