@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,1283 @@
1
+ // @ts-nocheck
2
+ import * as _ from "lodash-es"
3
+ import { faChartLine } from "@fortawesome/free-solid-svg-icons"
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5
+ import {
6
+ ArchiveContext,
7
+ ColumnTypeNames,
8
+ CoreColumnDef,
9
+ OwidColumnDef,
10
+ SortOrder,
11
+ TableSlug,
12
+ GrapherInterface,
13
+ GrapherQueryParams,
14
+ EntityName,
15
+ GRAPHER_TAB_QUERY_PARAMS,
16
+ } from "../types/index.js"
17
+ import {
18
+ OwidTable,
19
+ BlankOwidTable,
20
+ extractPotentialDataSlugsFromTransform,
21
+ } from "../core-table/index.js"
22
+ import {
23
+ EntityPicker,
24
+ EntityPickerManager,
25
+ Grapher,
26
+ GrapherManager,
27
+ GrapherProgrammaticInterface,
28
+ SelectionArray,
29
+ setSelectedEntityNamesParam,
30
+ SlideShowController,
31
+ SlideShowManager,
32
+ DEFAULT_GRAPHER_ENTITY_TYPE,
33
+ GrapherAnalytics,
34
+ GrapherState,
35
+ fetchInputTableForConfig,
36
+ loadVariableDataAndMetadata,
37
+ FetchInputTableForConfigFn,
38
+ } from "../grapher/index.js"
39
+ import {
40
+ Bounds,
41
+ ColumnSlug,
42
+ DimensionProperty,
43
+ excludeUndefined,
44
+ exposeInstanceOnWindow,
45
+ isInIFrame,
46
+ keyMap,
47
+ mergeGrapherConfigs,
48
+ omitUndefinedValues,
49
+ parseIntOrUndefined,
50
+ PromiseCache,
51
+ PromiseSwitcher,
52
+ SerializedGridProgram,
53
+ setWindowUrl,
54
+ Tippy,
55
+ Url,
56
+ } from "../utils/index.js"
57
+ import { MarkdownTextWrap } from "../components/index.js"
58
+ import classNames from "classnames"
59
+ import { action, computed, makeObservable, observable, reaction } from "mobx"
60
+ import { observer } from "mobx-react"
61
+ import React, { useCallback, useEffect, useState } from "react"
62
+ import { createRoot } from "react-dom/client"
63
+ import { ExplorerControlBar, ExplorerControlPanel } from "./ExplorerControls.js"
64
+ import { ExplorerProgram } from "./ExplorerProgram.js"
65
+ import {
66
+ ExplorerChartCreationMode,
67
+ ExplorerChoiceParams,
68
+ ExplorerContainerId,
69
+ ExplorerFullQueryParams,
70
+ EXPLORERS_PREVIEW_ROUTE,
71
+ EXPLORERS_ROUTE_FOLDER,
72
+ UNSAVED_EXPLORER_DRAFT,
73
+ UNSAVED_EXPLORER_PREVIEW_QUERYPARAMS,
74
+ } from "./ExplorerConstants.js"
75
+ import { ExplorerPageUrlMigrationSpec } from "./urlMigrations/ExplorerPageUrlMigrationSpec.js"
76
+ import {
77
+ explorerUrlMigrationsById,
78
+ migrateExplorerUrl,
79
+ } from "./urlMigrations/ExplorerUrlMigrations.js"
80
+
81
+ export interface ExplorerProps extends SerializedGridProgram {
82
+ grapherConfigs?: GrapherInterface[]
83
+ partialGrapherConfigs?: GrapherInterface[]
84
+ queryStr?: string
85
+ isEmbeddedInAnOwidPage?: boolean
86
+ isInStandalonePage?: boolean
87
+ isPreview?: boolean
88
+ canonicalUrl?: string
89
+ selection?: SelectionArray
90
+ adminBaseUrl: string
91
+ bakedBaseUrl: string
92
+ bakedGrapherUrl: string
93
+ dataApiUrl: string
94
+ bounds?: Bounds
95
+ staticBounds?: Bounds
96
+ loadMetadataOnly?: boolean
97
+ throwOnMissingGrapher?: boolean
98
+ setupGrapher?: boolean
99
+ archiveContext?: ArchiveContext
100
+ loadInputTableForConfig?: FetchInputTableForConfigFn
101
+ /** Force wide mode (sidebar layout) regardless of viewport width. Useful for Storybook testing. */
102
+ forceWideMode?: boolean
103
+ }
104
+
105
+ const LivePreviewComponent = (props: ExplorerProps) => {
106
+ const [useLocalStorage, setUseLocalStorage] = useState(true)
107
+ const [renderedProgram, setRenderedProgram] = useState("")
108
+ const [hasLocalStorage, setHasLocalStorage] = useState(false)
109
+
110
+ const updateProgram = useCallback(() => {
111
+ const localStorageProgram = localStorage.getItem(
112
+ UNSAVED_EXPLORER_DRAFT + props.slug
113
+ )
114
+ let program: string
115
+ if (useLocalStorage) program = localStorageProgram ?? props.program
116
+ else program = props.program
117
+
118
+ setHasLocalStorage(!!localStorageProgram)
119
+ setRenderedProgram((previousProgram) => {
120
+ if (program === previousProgram) return previousProgram
121
+ return program
122
+ })
123
+ }, [props.program, props.slug, useLocalStorage])
124
+
125
+ useEffect(() => {
126
+ updateProgram()
127
+ const interval = setInterval(updateProgram, 1000)
128
+ return () => clearInterval(interval)
129
+ }, [updateProgram])
130
+
131
+ const newProps = { ...props, program: renderedProgram }
132
+
133
+ return (
134
+ <>
135
+ {hasLocalStorage && (
136
+ <div className="admin-only-locally-edited-checkbox">
137
+ <Tippy
138
+ content={
139
+ <span>
140
+ <p>
141
+ <b>Checked</b>: Use the explorer version
142
+ with changes as present in the admin.
143
+ </p>
144
+ <p>
145
+ <b>Unchecked</b>: Use the currently-saved
146
+ version.
147
+ </p>
148
+ <hr />
149
+ <p>
150
+ Note that some features may only work
151
+ correctly when this checkbox is unchecked:
152
+ in particular, using <kbd>catalogPath</kbd>s
153
+ and <kbd>grapherId</kbd>s and variable IDs.
154
+ </p>
155
+ </span>
156
+ }
157
+ placement="bottom"
158
+ >
159
+ <label>
160
+ <input
161
+ type="checkbox"
162
+ id="useLocalStorage"
163
+ onChange={(e) =>
164
+ setUseLocalStorage(e.target.checked)
165
+ }
166
+ checked={useLocalStorage}
167
+ />
168
+ Display locally edited explorer
169
+ </label>
170
+ </Tippy>
171
+ </div>
172
+ )}
173
+ <Explorer
174
+ {...newProps}
175
+ queryStr={window?.location?.search ?? ""}
176
+ key={Date.now()}
177
+ isPreview={true}
178
+ />
179
+ </>
180
+ )
181
+ }
182
+
183
+ const renderLivePreviewVersion = (props: ExplorerProps) => {
184
+ const elem = document.getElementById(ExplorerContainerId)
185
+ if (!elem) throw new Error("ExplorerContainerId not found in DOM")
186
+ const root = createRoot(elem)
187
+ root.render(<LivePreviewComponent {...props} />)
188
+ }
189
+
190
+ const isNarrow = () =>
191
+ typeof window === "undefined"
192
+ ? false
193
+ : window.screen.width < 450 ||
194
+ document.documentElement.clientWidth <= 800
195
+
196
+ @observer
197
+ export class Explorer
198
+ extends React.Component<ExplorerProps>
199
+ implements
200
+ SlideShowManager<ExplorerChoiceParams>,
201
+ EntityPickerManager,
202
+ GrapherManager
203
+ {
204
+ analytics = new GrapherAnalytics()
205
+ grapherState: GrapherState
206
+ isOnArchivalPage: boolean
207
+ inputTableTransformer = (table: OwidTable) => table
208
+
209
+ constructor(props: ExplorerProps) {
210
+ super(props)
211
+
212
+ makeObservable<
213
+ Explorer,
214
+ | "isNarrow"
215
+ | "grapherContainerRef"
216
+ | "grapherRef"
217
+ | "showMobileControlsPopup"
218
+ >(this, {
219
+ appendOnlyAvailableEntityNames: observable,
220
+ grapher: observable.ref,
221
+ isNarrow: observable,
222
+ grapherContainerRef: observable,
223
+ grapherRef: observable.ref,
224
+ showMobileControlsPopup: observable,
225
+ entityPickerMetric: observable,
226
+ entityPickerSort: observable,
227
+ entityPickerTable: observable.ref,
228
+ entityPickerTableIsLoading: observable.ref,
229
+ })
230
+
231
+ this.explorerProgram = ExplorerProgram.fromJson(
232
+ props
233
+ ).initDecisionMatrix(this.initialQueryParams)
234
+ const { archiveContext } = props
235
+ const isOnArchivalPage = archiveContext?.type === "archive-page"
236
+ const assetMaps = isOnArchivalPage ? archiveContext?.assets : undefined
237
+ this.isOnArchivalPage = isOnArchivalPage
238
+ this.grapherState = new GrapherState({
239
+ staticBounds: props.staticBounds,
240
+ bounds: props.bounds,
241
+ enableKeyboardShortcuts: this.props.isInStandalonePage,
242
+ manager: this,
243
+ isEmbeddedInAnOwidPage: this.props.isEmbeddedInAnOwidPage,
244
+ adminBaseUrl: this.adminBaseUrl,
245
+ canHideExternalControlsInEmbed: true,
246
+ archiveContext: props.archiveContext,
247
+ additionalDataLoaderFn: (
248
+ varId: number,
249
+ loadMetadataOnly?: boolean
250
+ ) =>
251
+ loadVariableDataAndMetadata(varId, this.dataApiUrl, {
252
+ assetMap: assetMaps?.runtime,
253
+ noCache: props.isPreview,
254
+ loadMetadataOnly,
255
+ }),
256
+ })
257
+
258
+ if (props.setupGrapher !== false)
259
+ this.grapher = new Grapher({ grapherState: this.grapherState })
260
+ }
261
+ // caution: do a ctrl+f to find untyped usages
262
+ static renderSingleExplorerOnExplorerPage(
263
+ program: ExplorerProps,
264
+ grapherConfigs: GrapherInterface[],
265
+ partialGrapherConfigs: GrapherInterface[],
266
+ explorerConstants: Record<string, string>,
267
+ urlMigrationSpec?: ExplorerPageUrlMigrationSpec,
268
+ archiveContext?: ArchiveContext
269
+ ) {
270
+ const props: ExplorerProps = {
271
+ ...program,
272
+ ...explorerConstants,
273
+ grapherConfigs,
274
+ partialGrapherConfigs,
275
+ isEmbeddedInAnOwidPage: false,
276
+ isInStandalonePage: true,
277
+ archiveContext,
278
+ }
279
+
280
+ if (window.location.href.includes(EXPLORERS_PREVIEW_ROUTE)) {
281
+ renderLivePreviewVersion(props)
282
+ return
283
+ }
284
+
285
+ let url = Url.fromURL(window.location.href)
286
+
287
+ // Handle redirect spec that's baked on the page.
288
+ // e.g. the old COVID Grapher to Explorer redirects are implemented this way.
289
+ if (urlMigrationSpec) {
290
+ const { explorerUrlMigrationId, baseQueryStr } = urlMigrationSpec
291
+ const migration = explorerUrlMigrationsById[explorerUrlMigrationId]
292
+ if (migration) {
293
+ url = migration.migrateUrl(url, baseQueryStr)
294
+ } else {
295
+ console.error(
296
+ `No explorer URL migration with id ${explorerUrlMigrationId}`
297
+ )
298
+ }
299
+ }
300
+
301
+ // Handle explorer-specific migrations.
302
+ // This is how we migrate the old CO2 explorer to the new CO2 explorer.
303
+ // Because they are on the same path, we can't handle it like we handle
304
+ // the COVID explorer redirects above.
305
+ url = migrateExplorerUrl(url)
306
+
307
+ // Update the window URL
308
+ setWindowUrl(url)
309
+
310
+ const elem = document.getElementById(ExplorerContainerId)
311
+ if (!elem) throw new Error("ExplorerContainerId not found in DOM")
312
+
313
+ const root = createRoot(elem)
314
+ root.render(<Explorer {...props} queryStr={url.queryStr} />)
315
+ }
316
+
317
+ private initialQueryParams = Url.fromQueryStr(this.props.queryStr ?? "")
318
+ .queryParams as ExplorerFullQueryParams
319
+
320
+ explorerProgram = ExplorerProgram.fromJson(this.props).initDecisionMatrix(
321
+ this.initialQueryParams
322
+ )
323
+
324
+ bakedBaseUrl = this.props.bakedBaseUrl
325
+ dataApiUrl = this.props.dataApiUrl
326
+ adminBaseUrl = this.props.adminBaseUrl
327
+ bakedGrapherUrl = this.props.bakedGrapherUrl
328
+
329
+ selection = this.props.selection?.hasSelection
330
+ ? this.props.selection
331
+ : new SelectionArray(this.explorerProgram.selection)
332
+
333
+ entityType = this.explorerProgram.entityType ?? DEFAULT_GRAPHER_ENTITY_TYPE
334
+
335
+ // We want to ensure that unavailable entity are shown in the explorer entity
336
+ // that's why we employ an append-only version of `grapher.availableEntityNames`
337
+ appendOnlyAvailableEntityNames: Set<EntityName> = new Set()
338
+
339
+ grapher: Grapher | undefined = undefined
340
+
341
+ @action.bound setGrapher(grapher: Grapher) {
342
+ this.grapher = grapher
343
+ }
344
+
345
+ // Note: Can't use @computed here because this.props isn't observable in mobx-react
346
+ get grapherConfigs() {
347
+ const arr = this.props.grapherConfigs || []
348
+ return new Map(arr.map((config) => [config.id!, config]))
349
+ }
350
+
351
+ // Note: Can't use @computed here because this.props isn't observable in mobx-react
352
+ get partialGrapherConfigsByVariableId() {
353
+ const arr = this.props.partialGrapherConfigs || []
354
+ return new Map(arr.map((config) => [config.id!, config]))
355
+ }
356
+
357
+ @computed get partialGrapherConfigsBySlug() {
358
+ const configsBySlug = new Map<string, GrapherInterface>()
359
+ for (const columnDef of this.explorerProgram
360
+ .columnDefsWithoutTableSlug) {
361
+ if (columnDef.transform?.startsWith("duplicate")) {
362
+ const indicatorId = +columnDef.transform
363
+ .replace("duplicate", "")
364
+ .trim()
365
+ const partialGrapherConfig =
366
+ this.partialGrapherConfigsByVariableId.get(indicatorId)
367
+ if (partialGrapherConfig)
368
+ configsBySlug.set(columnDef.slug, partialGrapherConfig)
369
+ }
370
+ }
371
+ return configsBySlug
372
+ }
373
+
374
+ private setUpIntersectionObserver(): void {
375
+ if (typeof window !== "undefined" && "IntersectionObserver" in window) {
376
+ const observer = new IntersectionObserver((entries) => {
377
+ entries.forEach((entry) => {
378
+ if (entry.isIntersecting) {
379
+ this.analytics.logExplorerView(
380
+ this.explorerProgram.slug,
381
+ this.explorerProgram.decisionMatrix.currentParams
382
+ )
383
+ observer.disconnect()
384
+ }
385
+ })
386
+ })
387
+ observer.observe(this.grapherContainerRef.current!)
388
+ this.disposers.push(() => observer.disconnect())
389
+ }
390
+ }
391
+
392
+ disposers: (() => void)[] = []
393
+ override async componentDidMount() {
394
+ this.setGrapher(this.grapherRef!.current!)
395
+
396
+ let url = Url.fromQueryParams({
397
+ ...this.initialQueryParams,
398
+ ...this.currentChoiceParams, // Needed for Grapher's embedArchivedUrl.
399
+ })
400
+
401
+ if (this.props.selection?.hasSelection) {
402
+ url = setSelectedEntityNamesParam(
403
+ url,
404
+ this.props.selection.selectedEntityNames
405
+ )
406
+ }
407
+ // Run the initializiation for the grapherState but don't await it. The update code
408
+ // will reset and initialize the grapherState and then the data loading will be awaited inside the function.
409
+ // We don't want to wait for the data loading to finish as we only care about the non-data loading
410
+ // part to run (the data loading can finish whenever)
411
+ void this.updateGrapherFromExplorer()
412
+
413
+ // Do the rest of the initialization
414
+ this.grapherState.populateFromQueryParams(url.queryParams)
415
+ exposeInstanceOnWindow(this, "explorer")
416
+ this.setUpIntersectionObserver()
417
+ this.attachEventListeners()
418
+ this.updateEntityPickerTable() // call for the first time to initialize EntityPicker
419
+ }
420
+
421
+ private attachEventListeners() {
422
+ if (typeof window !== "undefined" && "ResizeObserver" in window) {
423
+ const onResizeThrottled = _.debounce(this.onResize, 200, {
424
+ leading: true,
425
+ })
426
+ const resizeObserver = new ResizeObserver(onResizeThrottled)
427
+ resizeObserver.observe(this.grapherContainerRef.current!)
428
+ this.disposers.push(() => {
429
+ resizeObserver.disconnect()
430
+ })
431
+ } else if (typeof window === "object" && typeof document === "object") {
432
+ // only show the warning when we're in something that roughly resembles a browser
433
+ console.warn(
434
+ "ResizeObserver not available; the explorer will not be responsive to window resizes"
435
+ )
436
+
437
+ this.onResize() // fire once to initialize, at least
438
+ }
439
+
440
+ // We always prefer the entity picker metric to be sourced from the currently displayed table.
441
+ // To do this properly, we need to also react to the table changing.
442
+ this.disposers.push(
443
+ reaction(
444
+ () => [
445
+ this.entityPickerMetric,
446
+ this.explorerProgram.explorerGrapherConfig.tableSlug,
447
+ ],
448
+ () => this.updateEntityPickerTable()
449
+ )
450
+ )
451
+
452
+ this.disposers.push(
453
+ reaction(
454
+ () => this.grapherState.availableEntityNames,
455
+ (availableEntityNames) => {
456
+ availableEntityNames?.forEach((entity) =>
457
+ this.appendOnlyAvailableEntityNames.add(entity)
458
+ )
459
+ }
460
+ )
461
+ )
462
+
463
+ if (this.props.isInStandalonePage) this.bindToWindow()
464
+ }
465
+
466
+ override componentWillUnmount() {
467
+ this.disposers.forEach((dispose) => dispose())
468
+ }
469
+
470
+ private initSlideshow() {
471
+ if (this.grapherState.slideShow) return
472
+
473
+ this.grapherState.slideShow = new SlideShowController(
474
+ this.explorerProgram.decisionMatrix.allDecisionsAsQueryParams(),
475
+ 0,
476
+ this
477
+ )
478
+ }
479
+
480
+ private persistedGrapherQueryParamsBySelectedRow: Map<
481
+ number,
482
+ Partial<GrapherQueryParams>
483
+ > = new Map()
484
+
485
+ // todo: break this method up and unit test more. this is pretty ugly right now.
486
+ @action.bound async reactToUserChangingSelection(oldSelectedRow: number) {
487
+ if (!this.explorerProgram.currentlySelectedGrapherRow) return
488
+ this.initSlideshow()
489
+
490
+ const oldGrapherParams = this.grapherState.changedParams
491
+ this.persistedGrapherQueryParamsBySelectedRow.set(
492
+ oldSelectedRow,
493
+ oldGrapherParams
494
+ )
495
+
496
+ const newGrapherParams = {
497
+ ...this.currentChoiceParams, // Needed for Grapher's embedArchivedUrl.
498
+ ...this.persistedGrapherQueryParamsBySelectedRow.get(
499
+ this.explorerProgram.currentlySelectedGrapherRow
500
+ ),
501
+ country: oldGrapherParams.country,
502
+ region: oldGrapherParams.region,
503
+ time: this.grapherState.timeParam,
504
+ }
505
+
506
+ const previousTab = this.grapherState.activeTab
507
+
508
+ await this.updateGrapherFromExplorer()
509
+
510
+ newGrapherParams.tab =
511
+ this.grapherState.mapGrapherTabToQueryParam(previousTab)
512
+
513
+ // reset map state if switching to a chart
514
+ if (newGrapherParams.tab !== GRAPHER_TAB_QUERY_PARAMS.map) {
515
+ newGrapherParams.globe = "0"
516
+ newGrapherParams.mapSelect = ""
517
+ }
518
+
519
+ this.grapherState.populateFromQueryParams(newGrapherParams)
520
+
521
+ // When switching between explorer views, we usually preserve the tab.
522
+ // However, if the new chart doesn't support the previously selected tab,
523
+ // Grapher automatically switches to a supported one. In such cases,
524
+ // we call onChartSwitching to make adjustments that ensure the new view
525
+ // is sensible (e.g. updating the time selection when switching from a
526
+ // single-time chart like a discrete bar chart to a multi-time chart like
527
+ // a line chart).
528
+ const currentTab = this.grapherState.activeTab
529
+ if (previousTab !== currentTab)
530
+ this.grapherState.onChartSwitching(previousTab, currentTab)
531
+
532
+ this.analytics.logExplorerView(
533
+ this.explorerProgram.slug,
534
+ this.explorerProgram.decisionMatrix.currentParams
535
+ )
536
+ }
537
+
538
+ @action.bound private setGrapherTable(table: OwidTable) {
539
+ this.grapherState.inputTable = this.inputTableTransformer(table)
540
+ }
541
+
542
+ @computed get availableEntityNames(): EntityName[] {
543
+ const entityNameSet = this.appendOnlyAvailableEntityNames
544
+ return Array.from(entityNameSet)
545
+ }
546
+
547
+ @computed private get enableMapSelection(): boolean {
548
+ return !this.isNarrow
549
+ }
550
+
551
+ private futureGrapherTable = new PromiseSwitcher<OwidTable>({
552
+ onResolve: (table) => this.setGrapherTable(table),
553
+ onReject: (error) => this.grapher?.setError(error),
554
+ })
555
+
556
+ tableLoader = new PromiseCache((slug: TableSlug | undefined) =>
557
+ this.explorerProgram.constructTable(slug)
558
+ )
559
+
560
+ @action.bound async updateGrapherFromExplorer() {
561
+ switch (this.explorerProgram.chartCreationMode) {
562
+ case ExplorerChartCreationMode.FromGrapherId:
563
+ return this.updateGrapherFromExplorerUsingGrapherId()
564
+ case ExplorerChartCreationMode.FromVariableIds:
565
+ return this.updateGrapherFromExplorerUsingVariableIds()
566
+ case ExplorerChartCreationMode.FromExplorerTableColumnSlugs:
567
+ return this.updateGrapherFromExplorerUsingColumnSlugs()
568
+ }
569
+ }
570
+
571
+ @computed private get columnDefsWithoutTableSlugByIdOrSlug(): Record<
572
+ number | string,
573
+ OwidColumnDef
574
+ > {
575
+ const { columnDefsWithoutTableSlug } = this.explorerProgram
576
+ return _.keyBy(
577
+ columnDefsWithoutTableSlug,
578
+ (def: OwidColumnDef) => def.owidVariableId ?? def.slug
579
+ )
580
+ }
581
+
582
+ // gets the slugs of all base and intermediate columns that a
583
+ // transformed column depends on; for example, if a column's transform
584
+ // is 'divideBy 170775 other_slug' and 'other_slug' is also a transformed
585
+ // column defined by 'multiplyBy 539022 2', then this function
586
+ // returns ['539022', '170775', 'other_slug']
587
+ private getBaseColumnsForColumnWithTransform(slug: string): string[] {
588
+ const def = this.columnDefsWithoutTableSlugByIdOrSlug[slug]
589
+ if (!def?.transform) return []
590
+ const dataSlugs =
591
+ extractPotentialDataSlugsFromTransform(def.transform) ?? []
592
+ return dataSlugs.flatMap((dataSlug) => [
593
+ ...this.getBaseColumnsForColumnWithTransform(dataSlug),
594
+ dataSlug,
595
+ ])
596
+ }
597
+
598
+ // gets the IDs of all variables that a transformed column depends on;
599
+ // for example, if a there are two columns, 'slug' and 'other_slug', that
600
+ // are defined by the transforms 'divideBy 170775 other_slug' and 'multiplyBy 539022 2',
601
+ // respectively, then getBaseVariableIdsForColumnWithTransform('slug')
602
+ // returns ['539022', '170775'] as these are the IDs of the two variables
603
+ // that the 'slug' column depends on
604
+ private getBaseVariableIdsForColumnWithTransform(slug: string): string[] {
605
+ const { columnDefsWithoutTableSlug } = this.explorerProgram
606
+ const baseVariableIdsAndColumnSlugs =
607
+ this.getBaseColumnsForColumnWithTransform(slug)
608
+ const slugsInColumnBlock: string[] = columnDefsWithoutTableSlug
609
+ .filter((def) => !def.owidVariableId)
610
+ .map((def) => def.slug)
611
+ return baseVariableIdsAndColumnSlugs.filter(
612
+ (variableIdOrColumnSlug) =>
613
+ !slugsInColumnBlock.includes(variableIdOrColumnSlug)
614
+ )
615
+ }
616
+
617
+ @action.bound async updateGrapherFromExplorerUsingGrapherId() {
618
+ const grapherState = this.grapherState
619
+
620
+ const { grapherId } = this.explorerProgram.explorerGrapherConfig
621
+ const grapherConfig = this.grapherConfigs.get(grapherId!)
622
+ if (!grapherConfig) {
623
+ if (this.props.throwOnMissingGrapher) {
624
+ throw new Error(`Grapher config not found for ID: ${grapherId}`)
625
+ }
626
+ }
627
+ const finalGrapherConfig = grapherConfig ?? {}
628
+
629
+ const config: GrapherProgrammaticInterface = {
630
+ ...mergeGrapherConfigs(
631
+ finalGrapherConfig,
632
+ this.explorerProgram.grapherConfig
633
+ ),
634
+ baseUrl: this.baseUrl,
635
+ bakedGrapherURL: this.bakedBaseUrl,
636
+ adminBaseUrl: this.adminBaseUrl,
637
+ hideEntityControls: this.showExplorerControls,
638
+ enableMapSelection: this.enableMapSelection,
639
+ }
640
+
641
+ // if not empty, respect the explorer's selection
642
+ if (this.selection.hasSelection) {
643
+ config.selectedEntityNames = this.selection.selectedEntityNames
644
+ }
645
+
646
+ grapherState.setAuthoredVersion(config)
647
+ grapherState.reset()
648
+ grapherState.inputTable = BlankOwidTable()
649
+ grapherState.updateFromObject(config)
650
+ if (!config.table) {
651
+ const loadFn =
652
+ this.props.loadInputTableForConfig ?? fetchInputTableForConfig
653
+ const inputTable = loadFn({
654
+ dimensions: config.dimensions,
655
+ selectedEntityColors: config.selectedEntityColors,
656
+ dataApiUrl: this.props.dataApiUrl,
657
+ archiveContext: this.props.archiveContext,
658
+ noCache: this.props.isPreview,
659
+ loadMetadataOnly: this.props.loadMetadataOnly,
660
+ }).then((owidTable) => (owidTable ? owidTable : BlankOwidTable()))
661
+ // We use the PromiseSwitcher here to make sure that only the last
662
+ // of several user triggered load operations in quick succession
663
+ // will actually set the table.
664
+ await this.futureGrapherTable.set(inputTable)
665
+ } else {
666
+ grapherState.inputTable = this.inputTableTransformer(config.table)
667
+ }
668
+ }
669
+
670
+ @action.bound async updateGrapherFromExplorerUsingVariableIds() {
671
+ const grapherState = this.grapherState
672
+ const {
673
+ yVariableIds = "",
674
+ xVariableId,
675
+ colorVariableId,
676
+ sizeVariableId,
677
+ ySlugs = "",
678
+ xSlug,
679
+ colorSlug,
680
+ sizeSlug,
681
+ } = this.explorerProgram.explorerGrapherConfig
682
+
683
+ const yVariableIdsList = yVariableIds
684
+ .split(" ")
685
+ .map(parseIntOrUndefined)
686
+ .filter((item) => item !== undefined)
687
+ const ySlugList = ySlugs.split(" ")
688
+
689
+ const partialGrapherConfig =
690
+ this.partialGrapherConfigsByVariableId.get(yVariableIdsList[0]) ??
691
+ // if ySlug references a column that duplicates an indicator via the
692
+ // `duplicate` transform, make sure the partial grapher config for
693
+ // that indicator is pulled in
694
+ this.partialGrapherConfigsBySlug.get(ySlugList[0]) ??
695
+ {}
696
+
697
+ const config: GrapherProgrammaticInterface = {
698
+ ...mergeGrapherConfigs(
699
+ partialGrapherConfig,
700
+ this.explorerProgram.grapherConfig
701
+ ),
702
+ bakedGrapherURL: this.bakedBaseUrl,
703
+ adminBaseUrl: this.adminBaseUrl,
704
+ hideEntityControls: this.showExplorerControls,
705
+ enableMapSelection: this.enableMapSelection,
706
+ }
707
+
708
+ // if not empty, respect the explorer's selection
709
+ if (this.selection.hasSelection) {
710
+ config.selectedEntityNames = this.selection.selectedEntityNames
711
+ }
712
+
713
+ // set given variable IDs as dimensions to make Grapher
714
+ // download the data and metadata for these variables
715
+ const dimensions = config.dimensions?.slice() ?? []
716
+ yVariableIdsList.forEach((yVariableId) => {
717
+ dimensions.push({
718
+ variableId: yVariableId,
719
+ property: DimensionProperty.y,
720
+ })
721
+ })
722
+
723
+ if (xVariableId) {
724
+ const maybeXVariableId = parseIntOrUndefined(xVariableId)
725
+ if (maybeXVariableId !== undefined)
726
+ dimensions.push({
727
+ variableId: maybeXVariableId,
728
+ property: DimensionProperty.x,
729
+ })
730
+ }
731
+ if (colorVariableId) {
732
+ const maybeColorVariableId = parseIntOrUndefined(colorVariableId)
733
+ if (maybeColorVariableId !== undefined)
734
+ dimensions.push({
735
+ variableId: maybeColorVariableId,
736
+ property: DimensionProperty.color,
737
+ })
738
+ }
739
+ if (sizeVariableId) {
740
+ const maybeSizeVariableId = parseIntOrUndefined(sizeVariableId)
741
+ if (maybeSizeVariableId !== undefined)
742
+ dimensions.push({
743
+ variableId: maybeSizeVariableId,
744
+ property: DimensionProperty.size,
745
+ })
746
+ }
747
+
748
+ // Slugs that are used to create a chart refer to columns derived from variables
749
+ // by a transform string (e.g. 'multiplyBy 539022 2'). To render such a chart, we
750
+ // need to download the data for all variables the transformed columns depend on
751
+ // and construct an appropriate Grapher table. This is done in three steps:
752
+ // 1. find all variables that the transformed columns depend on and add them to
753
+ // the config's dimensions array
754
+ // 2. download data and metadata of the variables
755
+ // 3. append the transformed columns to the Grapher table (note that this includes
756
+ // intermediate columns that are defined for multi-step transforms but are not
757
+ // referred to in any Grapher row)
758
+
759
+ // all slugs specified by the author in the explorer config
760
+ const uniqueSlugsInGrapherRow = _.uniq(
761
+ [...ySlugs.split(" "), xSlug, colorSlug, sizeSlug].filter(
762
+ _.identity
763
+ )
764
+ ) as string[]
765
+
766
+ // find all variables that the transformed columns depend on and add them to the dimensions array
767
+ if (uniqueSlugsInGrapherRow.length) {
768
+ const baseVariableIds = _.uniq(
769
+ uniqueSlugsInGrapherRow.flatMap((slug) =>
770
+ this.getBaseVariableIdsForColumnWithTransform(slug)
771
+ )
772
+ )
773
+ .map((id) => parseInt(id, 10))
774
+ .filter((id) => !isNaN(id))
775
+ baseVariableIds.forEach((variableId) => {
776
+ const hasDimension = dimensions.some(
777
+ (d) => d.variableId === variableId
778
+ )
779
+ if (!hasDimension) {
780
+ dimensions.push({
781
+ variableId: variableId,
782
+ property: DimensionProperty.table, // no specific dimension
783
+ })
784
+ }
785
+ })
786
+ }
787
+
788
+ config.dimensions = dimensions
789
+ if (ySlugs && yVariableIds) config.ySlugs = ySlugs + " " + yVariableIds
790
+
791
+ this.inputTableTransformer = (table: OwidTable) => {
792
+ // add transformed (and intermediate) columns to the grapher table
793
+ if (uniqueSlugsInGrapherRow.length) {
794
+ const allColumnSlugs = _.uniq(
795
+ uniqueSlugsInGrapherRow.flatMap((slug) => [
796
+ ...this.getBaseColumnsForColumnWithTransform(slug),
797
+ slug,
798
+ ])
799
+ )
800
+ const existingColumnSlugs = table.columnSlugs
801
+ const outstandingColumnSlugs = allColumnSlugs.filter(
802
+ (slug) => !existingColumnSlugs.includes(slug)
803
+ )
804
+ const requiredColumnDefs = outstandingColumnSlugs
805
+ .map(
806
+ (slug) =>
807
+ this.columnDefsWithoutTableSlugByIdOrSlug[slug]
808
+ )
809
+ .filter(_.identity)
810
+ table = table.appendColumns(requiredColumnDefs)
811
+ }
812
+
813
+ // update column definitions with manually provided properties
814
+ table = table.updateDefs((def: OwidColumnDef) => {
815
+ const manuallyProvidedDef =
816
+ this.columnDefsWithoutTableSlugByIdOrSlug[def.slug] ?? {}
817
+ const mergedDef = { ...def, ...manuallyProvidedDef }
818
+
819
+ // update display properties
820
+ mergedDef.display ??= {}
821
+ if (manuallyProvidedDef.name)
822
+ mergedDef.display.name = manuallyProvidedDef.name
823
+ if (manuallyProvidedDef.unit)
824
+ mergedDef.display.unit = manuallyProvidedDef.unit
825
+ if (manuallyProvidedDef.shortUnit)
826
+ mergedDef.display.shortUnit = manuallyProvidedDef.shortUnit
827
+
828
+ return mergedDef
829
+ })
830
+ return table
831
+ }
832
+
833
+ grapherState.setAuthoredVersion(config)
834
+ grapherState.reset()
835
+ grapherState.updateFromObject(config)
836
+ if (dimensions.length === 0) {
837
+ // If dimensions are empty, explicitly set the table to an empty table
838
+ // so we don't end up confusingly showing stale data from a previous chart
839
+ grapherState.inputTable = BlankOwidTable()
840
+ } else {
841
+ const loadFn =
842
+ this.props.loadInputTableForConfig ?? fetchInputTableForConfig
843
+ const inputTable = loadFn({
844
+ dimensions: config.dimensions,
845
+ selectedEntityColors: config.selectedEntityColors,
846
+ archiveContext: this.props.archiveContext,
847
+ dataApiUrl: this.props.dataApiUrl,
848
+ noCache: this.props.isPreview,
849
+ loadMetadataOnly: this.props.loadMetadataOnly,
850
+ }).then((owidTable) => (owidTable ? owidTable : BlankOwidTable()))
851
+ // We use the PromiseSwitcher here to make sure that only the last
852
+ // of several user triggered load operations in quick succession
853
+ // will actually set the table.
854
+ await this.futureGrapherTable.set(inputTable)
855
+ }
856
+ }
857
+
858
+ @action.bound async updateGrapherFromExplorerUsingColumnSlugs() {
859
+ const grapherState = this.grapherState
860
+ const { tableSlug } = this.explorerProgram.explorerGrapherConfig
861
+
862
+ const config: GrapherProgrammaticInterface = {
863
+ ...this.explorerProgram.grapherConfig,
864
+ bakedGrapherURL: this.bakedBaseUrl,
865
+ adminBaseUrl: this.adminBaseUrl,
866
+ hideEntityControls: this.showExplorerControls,
867
+ enableMapSelection: this.enableMapSelection,
868
+ }
869
+
870
+ // if not empty, respect the explorer's selection
871
+ if (this.selection.hasSelection) {
872
+ config.selectedEntityNames = this.selection.selectedEntityNames
873
+ }
874
+
875
+ grapherState.setAuthoredVersion(config)
876
+ grapherState.reset()
877
+ grapherState.updateFromObject(config)
878
+
879
+ // Clear any error messages, they are likely to be related to dataset loading.
880
+ this.grapher?.clearErrors()
881
+ // Set a table immediately. A BlankTable shows a loading animation.
882
+ this.setGrapherTable(
883
+ BlankOwidTable(tableSlug, `Loading table '${tableSlug}'`)
884
+ )
885
+ // We use the PromiseSwitcher here to make sure that only the last
886
+ // of several user triggered load operations in quick succession
887
+ // will actually set the table.
888
+ await this.futureGrapherTable.set(this.tableLoader.get(tableSlug))
889
+ }
890
+
891
+ @action.bound setSlide(choiceParams: ExplorerFullQueryParams) {
892
+ this.explorerProgram.decisionMatrix.setValuesFromChoiceParams(
893
+ choiceParams
894
+ )
895
+ }
896
+
897
+ @computed private get currentChoiceParams(): ExplorerChoiceParams {
898
+ const { decisionMatrix } = this.explorerProgram
899
+ return decisionMatrix.currentParams
900
+ }
901
+
902
+ @computed get queryParams(): ExplorerFullQueryParams {
903
+ if (
904
+ typeof window !== "undefined" &&
905
+ window.location.href.includes(EXPLORERS_PREVIEW_ROUTE)
906
+ )
907
+ localStorage.setItem(
908
+ UNSAVED_EXPLORER_PREVIEW_QUERYPARAMS +
909
+ this.explorerProgram.slug,
910
+ JSON.stringify(this.currentChoiceParams)
911
+ )
912
+
913
+ let url = Url.fromQueryParams(
914
+ omitUndefinedValues({
915
+ ...this.grapherState.changedParams,
916
+ pickerSort: this.entityPickerSort,
917
+ pickerMetric: this.entityPickerMetric,
918
+ hideControls: this.initialQueryParams.hideControls || undefined,
919
+ ...this.currentChoiceParams,
920
+ })
921
+ )
922
+
923
+ url = setSelectedEntityNamesParam(
924
+ url,
925
+ this.selection.hasSelection
926
+ ? this.selection.selectedEntityNames
927
+ : undefined
928
+ )
929
+
930
+ return url.queryParams as ExplorerFullQueryParams
931
+ }
932
+
933
+ @computed get currentUrl(): Url {
934
+ if (this.props.isPreview) return Url.fromQueryParams(this.queryParams)
935
+ return Url.fromURL(window.location.href).setQueryParams(
936
+ this.queryParams
937
+ )
938
+ }
939
+
940
+ private bindToWindow() {
941
+ // There is a surprisingly considerable performance overhead to updating the url
942
+ // while animating, so we debounce to allow e.g. smoother timelines
943
+ const pushParams = () =>
944
+ setWindowUrl(Url.fromQueryParams(this.queryParams))
945
+ const debouncedPushParams = _.debounce(pushParams, 100)
946
+
947
+ this.disposers.push(
948
+ reaction(
949
+ () => this.queryParams,
950
+ () =>
951
+ this.grapher?.debounceMode
952
+ ? debouncedPushParams()
953
+ : pushParams()
954
+ )
955
+ )
956
+ }
957
+
958
+ private get panels() {
959
+ return this.explorerProgram.decisionMatrix.choicesWithAvailability.map(
960
+ (choice) => (
961
+ <ExplorerControlPanel
962
+ key={choice.title}
963
+ explorerSlug={this.explorerProgram.slug}
964
+ choice={choice}
965
+ onChange={this.onChangeChoice(choice.title)}
966
+ isMobile={this.isNarrow}
967
+ />
968
+ )
969
+ )
970
+ }
971
+
972
+ onChangeChoice = (choiceTitle: string) => async (value: string) => {
973
+ const { currentlySelectedGrapherRow } = this.explorerProgram
974
+ this.explorerProgram.decisionMatrix.setValueCommand(choiceTitle, value)
975
+ if (currentlySelectedGrapherRow)
976
+ await this.reactToUserChangingSelection(currentlySelectedGrapherRow)
977
+ }
978
+
979
+ private renderHeaderElement() {
980
+ return (
981
+ <div className="ExplorerHeaderBox">
982
+ <div className="ExplorerTitle">
983
+ {this.explorerProgram.explorerTitle} Data Explorer
984
+ </div>
985
+ <div className="ExplorerSubtitle">
986
+ <MarkdownTextWrap
987
+ fontSize={12}
988
+ text={this.explorerProgram.explorerSubtitle || ""}
989
+ />
990
+ </div>
991
+ {this.explorerProgram.downloadDataLink && (
992
+ <a
993
+ href={this.explorerProgram.downloadDataLink}
994
+ className="ExplorerDownloadLink"
995
+ >
996
+ Download this dataset
997
+ </a>
998
+ )}
999
+ </div>
1000
+ )
1001
+ }
1002
+
1003
+ private isNarrow = this.props.forceWideMode ? false : isNarrow()
1004
+
1005
+ @computed private get isInIFrame() {
1006
+ return isInIFrame()
1007
+ }
1008
+
1009
+ @computed private get showExplorerControls() {
1010
+ if (!this.props.isEmbeddedInAnOwidPage && !this.isInIFrame) return true
1011
+ // Only allow hiding controls on embedded pages
1012
+ return !(
1013
+ this.explorerProgram.hideControls ||
1014
+ this.initialQueryParams.hideControls === "true"
1015
+ )
1016
+ }
1017
+
1018
+ @computed private get downloadDataLink(): string | undefined {
1019
+ return this.explorerProgram.downloadDataLink
1020
+ }
1021
+
1022
+ private grapherContainerRef = React.createRef<HTMLDivElement>()
1023
+
1024
+ private grapherRef = React.createRef<Grapher>()
1025
+
1026
+ private renderControlBar() {
1027
+ return (
1028
+ <ExplorerControlBar
1029
+ isMobile={this.isNarrow}
1030
+ showControls={this.showMobileControlsPopup}
1031
+ closeControls={this.closeControls}
1032
+ >
1033
+ {this.panels}
1034
+ </ExplorerControlBar>
1035
+ )
1036
+ }
1037
+
1038
+ private renderEntityPicker() {
1039
+ const selection =
1040
+ this.grapherState.isOnMapTab && this.enableMapSelection
1041
+ ? this.grapherState.mapConfig.selection
1042
+ : this.selection
1043
+
1044
+ return (
1045
+ <EntityPicker
1046
+ key="entityPicker"
1047
+ manager={this}
1048
+ selection={selection}
1049
+ onSelectEntity={this.grapherState.onSelectEntity}
1050
+ onDeselectEntity={this.grapherState.onDeselectEntity}
1051
+ onClearEntities={this.grapherState.onClearEntities}
1052
+ isDropdownMenu={this.isNarrow}
1053
+ />
1054
+ )
1055
+ }
1056
+
1057
+ @action.bound private toggleMobileControls() {
1058
+ this.showMobileControlsPopup = !this.showMobileControlsPopup
1059
+ }
1060
+
1061
+ @action.bound private onResize() {
1062
+ // Don't bother rendering if the container is hidden
1063
+ // see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
1064
+ if (this.grapherContainerRef.current?.offsetParent === null) return
1065
+
1066
+ const oldIsNarrow = this.isNarrow
1067
+ this.isNarrow = this.props.forceWideMode ? false : isNarrow()
1068
+ this.updateGrapherBounds()
1069
+
1070
+ // If we changed between narrow and wide mode, we need to wait for CSS changes to kick in
1071
+ // to properly calculate the new grapher bounds
1072
+ if (this.isNarrow !== oldIsNarrow)
1073
+ window.setTimeout(() => this.updateGrapherBounds(), 0)
1074
+ }
1075
+
1076
+ // Todo: add better logic to maximize the size of the Grapher
1077
+ private updateGrapherBounds() {
1078
+ const grapherContainer = this.grapherContainerRef.current
1079
+ if (grapherContainer)
1080
+ this.grapherState.externalBounds = new Bounds(
1081
+ 0,
1082
+ 0,
1083
+ grapherContainer.clientWidth,
1084
+ grapherContainer.clientHeight
1085
+ )
1086
+ }
1087
+
1088
+ private showMobileControlsPopup = false
1089
+ private get mobileCustomizeButton() {
1090
+ return (
1091
+ <a
1092
+ className="btn btn-primary mobile-button"
1093
+ onClick={this.toggleMobileControls}
1094
+ data-track-note="explorer_customize_chart_mobile"
1095
+ >
1096
+ <FontAwesomeIcon icon={faChartLine} /> Customize chart
1097
+ </a>
1098
+ )
1099
+ }
1100
+
1101
+ @action.bound private closeControls() {
1102
+ this.showMobileControlsPopup = false
1103
+ }
1104
+
1105
+ // todo: add tests for this and better tests for this class in general
1106
+ @computed private get showHeaderElement() {
1107
+ return (
1108
+ this.showExplorerControls &&
1109
+ this.explorerProgram.explorerTitle &&
1110
+ this.panels.length > 0
1111
+ )
1112
+ }
1113
+
1114
+ override render() {
1115
+ const { showExplorerControls, showHeaderElement } = this
1116
+
1117
+ return (
1118
+ <div
1119
+ className={classNames({
1120
+ Explorer: true,
1121
+ "mobile-explorer": this.isNarrow,
1122
+ HideControls: !showExplorerControls,
1123
+ "is-embed": this.props.isEmbeddedInAnOwidPage,
1124
+ })}
1125
+ >
1126
+ {showHeaderElement && this.renderHeaderElement()}
1127
+ {showHeaderElement && this.renderControlBar()}
1128
+ {showExplorerControls && this.renderEntityPicker()}
1129
+ {showExplorerControls &&
1130
+ this.isNarrow &&
1131
+ this.mobileCustomizeButton}
1132
+ <div className="ExplorerFigure" ref={this.grapherContainerRef}>
1133
+ <Grapher
1134
+ ref={this.grapherRef}
1135
+ grapherState={this.grapherState}
1136
+ />
1137
+ </div>
1138
+ </div>
1139
+ )
1140
+ }
1141
+
1142
+ @computed get editUrl() {
1143
+ return `${EXPLORERS_ROUTE_FOLDER}/${this.props.slug}`
1144
+ }
1145
+
1146
+ @computed get baseUrl() {
1147
+ let archiveUrl = undefined
1148
+ if (this.isOnArchivalPage) {
1149
+ archiveUrl = this.props.archiveContext?.archiveUrl
1150
+ }
1151
+ return (
1152
+ archiveUrl ??
1153
+ `${this.bakedBaseUrl}/${EXPLORERS_ROUTE_FOLDER}/${this.props.slug}`
1154
+ )
1155
+ }
1156
+
1157
+ @computed get canonicalUrl() {
1158
+ return (
1159
+ this.props.canonicalUrl ??
1160
+ Url.fromURL(this.baseUrl).setQueryParams(this.queryParams).fullUrl
1161
+ )
1162
+ }
1163
+
1164
+ @computed get grapherTable() {
1165
+ return this.grapherState?.tableAfterAuthorTimelineAndEntityFilter
1166
+ }
1167
+
1168
+ entityPickerMetric: string | undefined =
1169
+ this.initialQueryParams.pickerMetric
1170
+ entityPickerSort: SortOrder | undefined = this.initialQueryParams.pickerSort
1171
+
1172
+ entityPickerTable: OwidTable | undefined = undefined
1173
+ entityPickerTableIsLoading: boolean = false
1174
+
1175
+ private futureEntityPickerTable = new PromiseSwitcher<OwidTable>({
1176
+ onResolve: (table) => {
1177
+ this.entityPickerTable = table
1178
+ this.entityPickerTableIsLoading = false
1179
+ },
1180
+ onReject: () => {
1181
+ this.entityPickerTableIsLoading = false
1182
+ },
1183
+ })
1184
+
1185
+ private updateEntityPickerTable(): void {
1186
+ // If we don't currently have a entity picker metric, then set pickerTable to the currently-used table anyways,
1187
+ // so that when we start sorting by entity name we can infer that the column is a string column immediately.
1188
+ const tableSlugToLoad = this.entityPickerMetric
1189
+ ? this.getTableSlugOfColumnSlug(this.entityPickerMetric)
1190
+ : this.explorerProgram.explorerGrapherConfig.tableSlug
1191
+
1192
+ this.entityPickerTableIsLoading = true
1193
+ void this.futureEntityPickerTable.set(
1194
+ this.tableLoader.get(tableSlugToLoad)
1195
+ )
1196
+ }
1197
+
1198
+ setEntityPicker({
1199
+ metric,
1200
+ sort,
1201
+ }: {
1202
+ metric: string | undefined
1203
+ sort?: SortOrder
1204
+ }) {
1205
+ this.entityPickerMetric = metric
1206
+ if (sort) this.entityPickerSort = sort
1207
+ }
1208
+
1209
+ private tableSlugHasColumnSlug(
1210
+ tableSlug: TableSlug | undefined,
1211
+ columnSlug: ColumnSlug,
1212
+ columnDefsByTableSlug: Map<TableSlug | undefined, CoreColumnDef[]>
1213
+ ) {
1214
+ return !!columnDefsByTableSlug
1215
+ .get(tableSlug)
1216
+ ?.find((def) => def.slug === columnSlug)
1217
+ }
1218
+
1219
+ private getTableSlugOfColumnSlug(
1220
+ columnSlug: ColumnSlug
1221
+ ): TableSlug | undefined {
1222
+ const columnDefsByTableSlug = this.explorerProgram.columnDefsByTableSlug
1223
+
1224
+ // In most cases, column slugs will be duplicated in the tables, e.g. entityName.
1225
+ // Prefer the current Grapher table if it contains the column slug.
1226
+ const grapherTableSlug =
1227
+ this.explorerProgram.explorerGrapherConfig.tableSlug
1228
+ if (
1229
+ this.tableSlugHasColumnSlug(
1230
+ grapherTableSlug,
1231
+ columnSlug,
1232
+ columnDefsByTableSlug
1233
+ )
1234
+ ) {
1235
+ return grapherTableSlug
1236
+ }
1237
+ // ...otherwise, search all tables for the column slug
1238
+ return this.explorerProgram.tableSlugs.find((tableSlug) =>
1239
+ this.tableSlugHasColumnSlug(
1240
+ tableSlug,
1241
+ columnSlug,
1242
+ columnDefsByTableSlug
1243
+ )
1244
+ )
1245
+ }
1246
+
1247
+ @computed get entityPickerColumnDefs(): CoreColumnDef[] {
1248
+ const allColumnDefs = _.uniqBy(
1249
+ Array.from(
1250
+ this.explorerProgram.columnDefsByTableSlug.values()
1251
+ ).flat(),
1252
+ (def) => def.slug
1253
+ )
1254
+
1255
+ if (this.explorerProgram.pickerColumnSlugs) {
1256
+ const columnDefsBySlug = keyMap(allColumnDefs, (def) => def.slug)
1257
+ // Preserve the order of columns in the Explorer `pickerColumnSlugs`
1258
+ return excludeUndefined(
1259
+ this.explorerProgram.pickerColumnSlugs.map((slug) =>
1260
+ columnDefsBySlug.get(slug)
1261
+ )
1262
+ )
1263
+ } else {
1264
+ const discardColumnTypes = new Set([
1265
+ ColumnTypeNames.Year,
1266
+ ColumnTypeNames.Date,
1267
+ ColumnTypeNames.Day,
1268
+ ColumnTypeNames.EntityId,
1269
+ ColumnTypeNames.EntityCode,
1270
+ ])
1271
+ return allColumnDefs.filter(
1272
+ (def) =>
1273
+ (def.type === undefined ||
1274
+ !discardColumnTypes.has(def.type)) &&
1275
+ def.slug !== undefined
1276
+ )
1277
+ }
1278
+ }
1279
+
1280
+ @computed get requiredColumnSlugs() {
1281
+ return this.grapherState?.newSlugs ?? []
1282
+ }
1283
+ }