@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,1175 @@
1
+ // @ts-nocheck
2
+ import * as _ from "lodash-es"
3
+ import {
4
+ intersectionOfSets,
5
+ findClosestTimeIndex,
6
+ sortNumeric,
7
+ getClosestTimePairs,
8
+ sortedFindClosest,
9
+ cagr,
10
+ makeAnnotationsSlug,
11
+ isPresent,
12
+ TimeBound,
13
+ ColumnSlug,
14
+ imemo,
15
+ ToleranceStrategy,
16
+ differenceOfSets,
17
+ sortedFindClosestIndex,
18
+ } from "../utils/index.js"
19
+ import {
20
+ Time,
21
+ TransformType,
22
+ CoreColumnStore,
23
+ Color,
24
+ CoreValueType,
25
+ ColumnTypeNames,
26
+ EntityName,
27
+ OwidColumnDef,
28
+ OwidRow,
29
+ OwidTableSlugs,
30
+ ErrorValue,
31
+ ToleranceOptions,
32
+ } from "../types/index.js"
33
+ import { CoreTable } from "./CoreTable.js"
34
+ import { ErrorValueTypes, isNotErrorValue } from "./ErrorValues.js"
35
+ import {
36
+ getOriginalTimeColumnSlug,
37
+ makeOriginalValueSlugFromColumnSlug,
38
+ makeOriginalTimeSlugFromColumnSlug,
39
+ makeOriginalStartTimeSlugFromColumnSlug,
40
+ timeColumnSlugFromColumnDef,
41
+ toPercentageColumnDef,
42
+ } from "./OwidTableUtil.js"
43
+ import {
44
+ linearInterpolation,
45
+ toleranceInterpolation,
46
+ replaceDef,
47
+ InterpolationProvider,
48
+ InterpolationContext,
49
+ } from "./CoreTableUtils.js"
50
+ import { CoreColumn, ColumnTypeMap } from "./CoreTableColumns.js"
51
+
52
+ // An OwidTable is a subset of Table. An OwidTable always has EntityName, EntityCode, EntityId, and Time columns,
53
+ // and value column(s). Whether or not we need in the long run is uncertain and it may just be a stepping stone
54
+ // to go from our Variables paradigm to the Table paradigm.
55
+ export class OwidTable extends CoreTable<OwidRow, OwidColumnDef> {
56
+ @imemo get availableEntityNames(): any[] {
57
+ return Array.from(this.availableEntityNameSet)
58
+ }
59
+
60
+ @imemo get availableEntityNameSet(): Set<string> {
61
+ return this.entityNameColumn.uniqValuesAsSet
62
+ }
63
+
64
+ @imemo override get entityNameColumn(): CoreColumn {
65
+ return (
66
+ this.getFirstColumnWithType(ColumnTypeNames.EntityName) ??
67
+ this.get(OwidTableSlugs.entityName)
68
+ )
69
+ }
70
+
71
+ @imemo get minTime(): Time {
72
+ return _.min(this.allTimes) as Time
73
+ }
74
+
75
+ @imemo get maxTime(): number | undefined {
76
+ return _.max(this.allTimes)
77
+ }
78
+
79
+ @imemo private get allTimes(): Time[] {
80
+ return this.get(this.timeColumn.slug).values
81
+ }
82
+
83
+ @imemo get rowIndicesByEntityName(): Map<string, number[]> {
84
+ return this.rowIndex([this.entityNameSlug])
85
+ }
86
+
87
+ getAnnotationColumnSlug(columnDef: OwidColumnDef): string | undefined {
88
+ return _.isEmpty(columnDef?.annotationsColumnSlug)
89
+ ? makeAnnotationsSlug(columnDef.slug)
90
+ : columnDef.annotationsColumnSlug
91
+ }
92
+
93
+ // todo: instead of this we should probably make annotations another property on charts—something like "annotationsColumnSlugs"
94
+ getAnnotationColumnForColumn(columnSlug: ColumnSlug): CoreColumn {
95
+ const def = this.get(columnSlug).def as OwidColumnDef
96
+ const slug = this.getAnnotationColumnSlug(def)
97
+ return this.get(slug)
98
+ }
99
+
100
+ getTimesUniqSortedAscForColumns(columnSlugs: ColumnSlug[]): number[] {
101
+ // todo: should be easy to speed up if necessary.
102
+ return sortNumeric(
103
+ _.uniq(
104
+ this.getColumns(columnSlugs)
105
+ .filter((col) => col)
106
+ .flatMap((col) => col.uniqTimesAsc)
107
+ )
108
+ )
109
+ }
110
+
111
+ timeDomainFor(slugs: ColumnSlug[]): [Time | undefined, Time | undefined] {
112
+ const cols = this.getColumns(slugs)
113
+ const mins = cols.map((col) => col.minTime)
114
+ const maxes = cols.map((col) => col.maxTime)
115
+ return [_.min(mins), _.max(maxes)]
116
+ }
117
+
118
+ originalTimeDomainFor(
119
+ slugs: ColumnSlug[]
120
+ ): [Time | undefined, Time | undefined] {
121
+ const cols = this.getColumns(slugs)
122
+ const mins = cols.map((col) => _.min(col.originalTimes))
123
+ const maxes = cols.map((col) => _.max(col.originalTimes))
124
+ return [_.min(mins), _.max(maxes)]
125
+ }
126
+
127
+ filterByEntityNames(names: EntityName[]): this {
128
+ const namesSet = new Set(names)
129
+ return this.columnFilter(
130
+ this.entityNameSlug,
131
+ (value) => namesSet.has(value as string),
132
+ `Filter out all entities except '${names}'`
133
+ )
134
+ }
135
+
136
+ filterByEntityNamesUsingIncludeExcludePattern({
137
+ excluded,
138
+ included,
139
+ }: {
140
+ excluded?: EntityName[]
141
+ included?: EntityName[]
142
+ }): this {
143
+ if (!included && !excluded) return this
144
+
145
+ const excludedSet = new Set(excluded)
146
+ const includedSet = new Set(included)
147
+
148
+ const excludeFilter = (entityName: EntityName): boolean =>
149
+ !excludedSet.has(entityName)
150
+ const includeFilter =
151
+ includedSet.size > 0
152
+ ? (entityName: EntityName): boolean =>
153
+ includedSet.has(entityName)
154
+ : (): boolean => true
155
+
156
+ const filterFn = (entityName: any): boolean =>
157
+ excludeFilter(entityName) && includeFilter(entityName)
158
+
159
+ const excludedList = excluded ? excluded.join(", ") : ""
160
+ const includedList = included ? included.join(", ") : ""
161
+
162
+ return this.columnFilter(
163
+ this.entityNameSlug,
164
+ filterFn,
165
+ `Excluded entities specified by author: ${excludedList} - Included entities specified by author: ${includedList}`
166
+ )
167
+ }
168
+
169
+ // Does a stable sort by time. You can refer to this table for fast time filtering.
170
+ @imemo private get sortedByTime(): this {
171
+ if (this.timeColumn.isMissing) return this
172
+ return this.sortBy([this.timeColumn.slug])
173
+ }
174
+
175
+ filterByTimeRange(start: TimeBound, end: TimeBound): this {
176
+ if (this.isBlank) return this
177
+
178
+ // We may want to do this time adjustment in Grapher instead of here.
179
+ const adjustedStart = start === Infinity ? this.maxTime! : start
180
+ const adjustedEnd = end === -Infinity ? this.minTime! : end
181
+ // todo: we should set a time column onload so we don't have to worry about it again.
182
+ const timeColumnSlug = this.timeColumn?.slug || OwidTableSlugs.time
183
+
184
+ const description = `Keep only rows with Time between ${adjustedStart} - ${adjustedEnd}`
185
+
186
+ // perf: if the time range is greater than the table's time range, we can skip the filter
187
+ if (adjustedStart <= this.minTime && adjustedEnd >= this.maxTime!)
188
+ return this.noopTransform(description).sortedByTime
189
+
190
+ return this.columnFilter(
191
+ timeColumnSlug,
192
+ (time) =>
193
+ (time as number) >= adjustedStart &&
194
+ (time as number) <= adjustedEnd,
195
+ description
196
+
197
+ // Sorting by time, because incidentally some parts of the code depended on this method
198
+ // returning sorted rows.
199
+ ).sortedByTime
200
+ }
201
+
202
+ filterByTargetTimes(targetTimes: Time[], tolerance = 0): this {
203
+ const timeColumn = this.timeColumn!
204
+ const timeValues = timeColumn.valuesIncludingErrorValues
205
+
206
+ // The common case here is that the tolerance is set to 0, in which case we can simply filter
207
+ // the time column for the target times.
208
+ if (tolerance === 0) {
209
+ const targetTimesSet = new Set(targetTimes)
210
+ return this.columnFilter(
211
+ timeColumn.slug,
212
+ (time) => targetTimesSet.has(time as number),
213
+ `Keep only rows with time equal to one of the target times: ${targetTimes.join(
214
+ ", "
215
+ )}`
216
+ )
217
+ }
218
+
219
+ // If tolerance isn't 0, then we need to find the closest time for each entity, while incorporating the tolerance.
220
+ const entityNameToIndices = this.rowIndicesByEntityName
221
+ const matchingIndices = new Set<number>()
222
+ this.availableEntityNames.forEach((entityName) => {
223
+ const indices = entityNameToIndices.get(entityName) || []
224
+ const allTimesAsc = indices
225
+ .map((index) => ({ time: timeValues[index] as number, index }))
226
+ .sort((a, b) => a.time - b.time)
227
+
228
+ targetTimes.forEach((targetTime) => {
229
+ const index = findClosestTimeIndex(
230
+ allTimesAsc.map((t) => t.time),
231
+ targetTime,
232
+ tolerance
233
+ )
234
+ const closest =
235
+ index === undefined ? undefined : allTimesAsc[index]
236
+ if (closest !== undefined) matchingIndices.add(closest.index)
237
+ })
238
+ })
239
+
240
+ return this.columnFilter(
241
+ this.entityNameSlug,
242
+ (row, index) => matchingIndices.has(index),
243
+ `Keep a row for each entity for each of the closest times ${targetTimes.join(
244
+ ", "
245
+ )} with tolerance ${tolerance}`
246
+ )
247
+ }
248
+
249
+ hasAnyColumnNoValidValue(slugs: ColumnSlug[]): boolean {
250
+ return slugs.some((slug) => this.get(slug).numValues === 0)
251
+ }
252
+
253
+ dropAllRows(): this {
254
+ return this.dropRowsAt(this.indices, "Drop all rows")
255
+ }
256
+
257
+ dropRowsWithErrorValuesForColumn(slug: ColumnSlug): this {
258
+ return this.columnFilter(
259
+ slug,
260
+ (value) => isNotErrorValue(value),
261
+ `Drop rows with empty or ErrorValues in ${slug} column`
262
+ )
263
+ }
264
+
265
+ // TODO rewrite with column ops
266
+ // TODO move to CoreTable
267
+ dropRowsWithErrorValuesForAnyColumn(slugs: ColumnSlug[]): this {
268
+ return this.rowFilter(
269
+ (row) => slugs.every((slug) => isNotErrorValue(row[slug])),
270
+ `Drop rows with empty or ErrorValues in any column: ${slugs.join(
271
+ ", "
272
+ )}`
273
+ )
274
+ }
275
+
276
+ // TODO rewrite with column ops
277
+ // TODO move to CoreTable
278
+ dropRowsWithErrorValuesForAllColumns(slugs: ColumnSlug[]): this {
279
+ return this.rowFilter(
280
+ (row) => slugs.some((slug) => isNotErrorValue(row[slug])),
281
+ `Drop rows with empty or ErrorValues in every column: ${slugs.join(
282
+ ", "
283
+ )}`
284
+ )
285
+ }
286
+
287
+ // Drop _all rows_ for an entity if there is any column that has no valid values for that entity.
288
+ dropEntitiesThatHaveNoDataInSomeColumn(columnSlugs: ColumnSlug[]): this {
289
+ const indexesByEntityName = this.rowIndicesByEntityName
290
+
291
+ // Iterate over all entities, and remove them as we go if they have no data in some column
292
+ const entityNamesToKeep = new Set(indexesByEntityName.keys())
293
+
294
+ for (const slug of columnSlugs) {
295
+ const col = this.get(slug)
296
+
297
+ // Optimization, if there are no error values in this column, we can skip this column
298
+ if (col.numErrorValues === 0) continue
299
+
300
+ for (const entityName of entityNamesToKeep) {
301
+ const indicesForEntityName = indexesByEntityName.get(entityName)
302
+ if (!indicesForEntityName)
303
+ throw new Error("Unexpected: entity not found in index map")
304
+
305
+ // Optimization: We don't care about the number of valid/error values, we just need
306
+ // to know if there is at least one valid value
307
+ const hasSomeValidValueForEntityInCol =
308
+ indicesForEntityName.some((index) =>
309
+ isNotErrorValue(col.valuesIncludingErrorValues[index])
310
+ )
311
+
312
+ // Optimization: If we find a column that this entity has no data in we can remove
313
+ // it immediately, no need to iterate over other columns also
314
+ if (!hasSomeValidValueForEntityInCol)
315
+ entityNamesToKeep.delete(entityName)
316
+ }
317
+ }
318
+
319
+ const entityNamesToDrop = differenceOfSets([
320
+ this.availableEntityNameSet,
321
+ entityNamesToKeep,
322
+ ])
323
+ const droppedEntitiesStr =
324
+ entityNamesToDrop.size > 0
325
+ ? [...entityNamesToDrop].join(", ")
326
+ : "(None)"
327
+
328
+ if (entityNamesToDrop.size === 0) {
329
+ return this.noopTransform(
330
+ `Drop entities that have no data in some column`
331
+ )
332
+ }
333
+
334
+ return this.columnFilter(
335
+ this.entityNameSlug,
336
+ (rowEntityName) => entityNamesToKeep.has(rowEntityName as string),
337
+ `Drop ${entityNamesToDrop.size} entities that have no data in some column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}`
338
+ )
339
+ }
340
+
341
+ // Drop _all rows_ for an entity if all columns have at least one invalid or missing value for that entity.
342
+ dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns(
343
+ columnSlugs: ColumnSlug[]
344
+ ): this {
345
+ const indexesByEntityName = this.rowIndicesByEntityName
346
+ const uniqTimes = new Set(this.allTimes)
347
+
348
+ // entity names to iterate over
349
+ const entityNamesToIterateOver = new Set(indexesByEntityName.keys())
350
+
351
+ // set of entities we want to keep
352
+ const entityNamesToKeep = new Set<string>()
353
+
354
+ // total number of entities
355
+ const entityCount = entityNamesToIterateOver.size
356
+
357
+ // helper function to generate operation name
358
+ const makeOpName = (entityNamesToKeep: Set<EntityName>): string => {
359
+ const entityNamesToDrop = differenceOfSets([
360
+ this.availableEntityNameSet,
361
+ entityNamesToKeep,
362
+ ])
363
+ const droppedEntitiesStr =
364
+ entityNamesToDrop.size > 0
365
+ ? [...entityNamesToDrop].join(", ")
366
+ : "(None)"
367
+ return `Drop entities that have some missing or error value in all column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}`
368
+ }
369
+
370
+ // Optimization: if there is a column that has a valid data entry for
371
+ // every entity and every time, we are done
372
+ for (const slug of columnSlugs) {
373
+ const col = this.get(slug)
374
+
375
+ if (
376
+ col.numValues === entityCount * uniqTimes.size &&
377
+ col.numErrorValues === 0
378
+ ) {
379
+ const entityNamesToKeep = new Set(indexesByEntityName.keys())
380
+
381
+ return this.columnFilter(
382
+ this.entityNameSlug,
383
+ (rowEntityName) =>
384
+ entityNamesToKeep.has(rowEntityName as string),
385
+ makeOpName(entityNamesToKeep)
386
+ )
387
+ }
388
+ }
389
+
390
+ for (const slug of columnSlugs) {
391
+ const col = this.get(slug)
392
+
393
+ for (const entityName of entityNamesToIterateOver) {
394
+ const indicesForEntityName = indexesByEntityName.get(entityName)
395
+ if (!indicesForEntityName)
396
+ throw new Error("Unexpected: entity not found in index map")
397
+
398
+ // Optimization: If the column is missing values for the entity,
399
+ // we know we can't make a decision yet, so we skip this entity
400
+ if (indicesForEntityName.length < uniqTimes.size) continue
401
+
402
+ // Optimization: We don't care about the number of valid/error
403
+ // values, we just need to know if there is at least one invalid value
404
+ const hasSomeInvalidValueForEntityInCol =
405
+ indicesForEntityName.some(
406
+ (index) =>
407
+ !isNotErrorValue(
408
+ col.valuesIncludingErrorValues[index]
409
+ )
410
+ )
411
+
412
+ // Optimization: If all values are valid, we know we want to keep this entity,
413
+ // so we remove it from the entities to iterate over
414
+ if (!hasSomeInvalidValueForEntityInCol) {
415
+ entityNamesToKeep.add(entityName)
416
+ entityNamesToIterateOver.delete(entityName)
417
+ }
418
+ }
419
+ }
420
+
421
+ return this.columnFilter(
422
+ this.entityNameSlug,
423
+ (rowEntityName) => entityNamesToKeep.has(rowEntityName as string),
424
+ makeOpName(entityNamesToKeep)
425
+ )
426
+ }
427
+
428
+ private sumsByTime(columnSlug: ColumnSlug): Map<number, number> {
429
+ const timeValues = this.timeColumn.values
430
+ const values = this.get(columnSlug).values as number[]
431
+ if (timeValues.length !== values.length)
432
+ // Throwing here may seem drastic but this will lead to an error in any case if the lengths are different
433
+ // (can happen when dropping rows with errors is done incorrectly).
434
+ // When it happens then throwing here is an easier to diagnose error
435
+ throw Error(
436
+ `Tried to run sumsByTime when timeValues (${timeValues.length}) and values (${values.length}) had different length`
437
+ )
438
+ const map = new Map<number, number>()
439
+ timeValues.forEach((time, index) =>
440
+ map.set(time, (map.get(time) ?? 0) + values[index])
441
+ )
442
+ return map
443
+ }
444
+
445
+ // todo: this needs tests (and/or drop in favor of someone else's package)
446
+ // Shows how much each entity contributed to the given column for each time period
447
+ toPercentageFromEachEntityForEachTime(columnSlug: ColumnSlug): this {
448
+ if (!this.has(columnSlug)) return this
449
+ const timeColumn = this.timeColumn!
450
+ const col = this.get(columnSlug)
451
+ const timeTotals = this.sumsByTime(columnSlug)
452
+ const timeValues = timeColumn.values
453
+ const newDefs = replaceDef(this.defs, [
454
+ toPercentageColumnDef(col.def, ColumnTypeNames.RelativePercentage),
455
+ ])
456
+ const newColumnStore: CoreColumnStore = {
457
+ ...this.columnStore,
458
+ [columnSlug]: this.columnStore[columnSlug].map((val, index) => {
459
+ const timeTotal = timeTotals.get(timeValues[index])
460
+ if (timeTotal === 0) return ErrorValueTypes.DivideByZeroError
461
+ return (100 * (val as number)) / timeTotal!
462
+ }),
463
+ }
464
+ return this.transform(
465
+ newColumnStore,
466
+ newDefs,
467
+ `Transformed ${columnSlug} column to be % contribution of each entity for that time`,
468
+ TransformType.UpdateColumnDefsAndApply
469
+ )
470
+ }
471
+
472
+ // If you want to see how much each column contributed to the entity for that year, use this.
473
+ // NB: Uses absolute value. So if one entity added 100, and another -100, they both would have contributed "50%" to that year.
474
+ // Otherwise we'd have NaN.
475
+ toPercentageFromEachColumnForEachEntityAndTime(
476
+ columnSlugs: ColumnSlug[]
477
+ ): this {
478
+ columnSlugs = columnSlugs.filter((slug) => this.has(slug))
479
+ if (!columnSlugs.length) return this
480
+
481
+ const newDefs = this.defs.map((def) => {
482
+ if (columnSlugs.includes(def.slug))
483
+ return toPercentageColumnDef(
484
+ def,
485
+ ColumnTypeNames.RelativePercentage
486
+ )
487
+ return def
488
+ })
489
+
490
+ const columnStore = this.columnStore
491
+ const columnStorePatch: CoreColumnStore = {}
492
+
493
+ const totals = new Array(this.numRows).fill(0).map((__, i) =>
494
+ _.sumBy(columnSlugs, (slug) => {
495
+ const value = columnStore[slug][i]
496
+ return _.isNumber(value) ? Math.abs(value) : 0
497
+ })
498
+ )
499
+
500
+ columnSlugs.forEach((slug) => {
501
+ columnStorePatch[slug] = columnStore[slug].map((value, i) => {
502
+ const total = totals[i]
503
+ if (!_.isNumber(value) || !_.isNumber(total)) return value
504
+ if (total === 0) return ErrorValueTypes.DivideByZeroError
505
+ return (100 * Math.abs(value)) / total
506
+ })
507
+ })
508
+
509
+ const newColumnStore = {
510
+ ...columnStore,
511
+ ...columnStorePatch,
512
+ }
513
+
514
+ return this.transform(
515
+ newColumnStore,
516
+ newDefs,
517
+ `Transformed columns from absolute numbers to % of abs sum of ${columnSlugs.join(
518
+ ","
519
+ )} `,
520
+ TransformType.UpdateColumnDefs
521
+ )
522
+ }
523
+
524
+ // todo: this needs tests (and/or drop in favor of someone else's package)
525
+ // If you wanted to build a table showing something like GDP growth relative to 1950, use this.
526
+ toTotalGrowthForEachColumnComparedToStartTime(
527
+ startTimeBound: TimeBound,
528
+ columnSlugs: ColumnSlug[]
529
+ ): this {
530
+ if (this.timeColumn.isMissing) return this
531
+ const timeColumnSlug = this.timeColumn.slug
532
+ const newDefs = this.defs.map((def) => {
533
+ if (columnSlugs.includes(def.slug))
534
+ return toPercentageColumnDef(
535
+ def,
536
+ ColumnTypeNames.PercentChangeOverTime
537
+ )
538
+ return def
539
+ })
540
+ const newRows = Object.values(
541
+ _.groupBy(this.sortedByTime.rows, (row) => row[this.entityNameSlug])
542
+ ).flatMap((rowsForSingleEntity) => {
543
+ columnSlugs.forEach((valueSlug) => {
544
+ let comparisonValue: number
545
+ rowsForSingleEntity = rowsForSingleEntity.map(
546
+ (row: Readonly<OwidRow>) => {
547
+ const newRow = {
548
+ ...row,
549
+ }
550
+
551
+ const value = row[valueSlug]
552
+
553
+ if (row[timeColumnSlug] < startTimeBound) {
554
+ newRow[valueSlug] =
555
+ ErrorValueTypes.MissingValuePlaceholder
556
+ } else if (!_.isNumber(value)) {
557
+ newRow[valueSlug] =
558
+ ErrorValueTypes.NaNButShouldBeNumber
559
+ } else if (comparisonValue !== undefined) {
560
+ // Note: comparisonValue can be negative!
561
+ // +value / -comparisonValue = negative growth, which is incorrect.
562
+ newRow[valueSlug] =
563
+ (100 * (value - comparisonValue)) /
564
+ Math.abs(comparisonValue)
565
+ } else if (value === 0) {
566
+ newRow[valueSlug] =
567
+ ErrorValueTypes.MissingValuePlaceholder
568
+ } else {
569
+ comparisonValue = value
570
+ newRow[valueSlug] = 0
571
+ }
572
+
573
+ return newRow
574
+ }
575
+ )
576
+ })
577
+ return rowsForSingleEntity
578
+ })
579
+
580
+ return this.transform(
581
+ newRows,
582
+ newDefs,
583
+ `Transformed columns from absolute values to % of time ${startTimeBound} for columns ${columnSlugs.join(
584
+ ","
585
+ )} `,
586
+ TransformType.UpdateColumnDefs
587
+ )
588
+ }
589
+
590
+ keepMinTimeAndMaxTimeForEachEntityOnly(): this {
591
+ const indexMap = this.rowIndicesByEntityName
592
+ const timeColumn = this.timeColumn
593
+ if (timeColumn.isMissing) return this
594
+ const timeValues = timeColumn.valuesIncludingErrorValues
595
+ const matchingIndices = new Set<number>()
596
+ indexMap.forEach((indices) =>
597
+ [_.minBy, _.maxBy]
598
+ .map((f) => f(indices, (index) => timeValues[index]))
599
+ .filter(isPresent)
600
+ .forEach((index) => matchingIndices.add(index))
601
+ )
602
+ return this.columnFilter(
603
+ timeColumn.slug,
604
+ (row, index) => matchingIndices.has(index),
605
+ `Keep minTime & maxTime rows only for each entity`
606
+ )
607
+ }
608
+
609
+ private getAverageAnnualChangeIndicesByEntity(
610
+ columnSlugs: ColumnSlug[]
611
+ ): Map<EntityName, [number, number]> {
612
+ const columns = columnSlugs.map((slug) => this.get(slug))
613
+ const indexMap = this.rowIndicesByEntityName
614
+ const timeValues = this.timeColumn.valuesIncludingErrorValues
615
+
616
+ // Find indices of min & max rows
617
+ const entityNameToIndices = new Map<EntityName, [number, number]>()
618
+ indexMap.forEach((indices, entityName) => {
619
+ // We are discarding every row which contains a 0 for any columnSlug.
620
+ // Technically, to be more correct, we should support distinct min/max indices for each
621
+ // columnSlug, but that only makes a tiny difference in a tiny subset of charts.
622
+ const nonZeroValueIndices = indices.filter((index) =>
623
+ columns.every((col) => {
624
+ const value = col.valuesIncludingErrorValues[index]
625
+ return _.isNumber(value) && value !== 0
626
+ })
627
+ )
628
+ const minIndex = _.minBy(
629
+ nonZeroValueIndices,
630
+ (index) => timeValues[index]
631
+ )
632
+ const maxIndex = _.maxBy(indices, (index) => timeValues[index])
633
+ if (minIndex === undefined || maxIndex === undefined) return
634
+
635
+ const allValuePairsHaveDistinctTime = columns.every((col) => {
636
+ const originalTimes =
637
+ this.columnStore[col.originalTimeColumnSlug]
638
+ return originalTimes[minIndex] !== originalTimes[maxIndex]
639
+ })
640
+ if (allValuePairsHaveDistinctTime)
641
+ entityNameToIndices.set(entityName, [minIndex, maxIndex])
642
+ })
643
+
644
+ return entityNameToIndices
645
+ }
646
+
647
+ toAverageAnnualChangeForEachEntity(columnSlugs: ColumnSlug[]): this {
648
+ columnSlugs = columnSlugs.filter((slug) => this.has(slug))
649
+ if (
650
+ this.timeColumn.isMissing ||
651
+ !(this.timeColumn instanceof ColumnTypeMap.Year) ||
652
+ columnSlugs.length === 0
653
+ )
654
+ return this
655
+
656
+ const columns = columnSlugs.map((slug) => this.get(slug))
657
+ const entityNameToIndices =
658
+ this.getAverageAnnualChangeIndicesByEntity(columnSlugs)
659
+
660
+ // Overwrite table rows
661
+ const rows: OwidRow[] = []
662
+ entityNameToIndices.forEach((indices) => {
663
+ const [startRow, endRow] = this.rowsAt(indices)
664
+
665
+ const newRow: OwidRow = { ...endRow }
666
+ columns.forEach((col) => {
667
+ const timeSlug = col.originalTimeColumnSlug
668
+
669
+ const startTime = startRow[timeSlug]
670
+ const endTime = endRow[timeSlug]
671
+ const yearsElapsed = endTime - startTime
672
+
673
+ const startValue = startRow[col.slug]
674
+ const endValue = endRow[col.slug]
675
+
676
+ // Update to average annual change
677
+ newRow[col.slug] = cagr(startValue, endValue, yearsElapsed)
678
+
679
+ // Add original start time column
680
+ const startTimeSlug = makeOriginalStartTimeSlugFromColumnSlug(
681
+ col.slug
682
+ )
683
+ newRow[startTimeSlug] = startTime
684
+ })
685
+
686
+ rows.push(newRow)
687
+ })
688
+
689
+ const newDefs = [
690
+ ...replaceDef(
691
+ this.defs,
692
+ columns.map((col) =>
693
+ toPercentageColumnDef(
694
+ col.def,
695
+ ColumnTypeNames.PercentChangeOverTime
696
+ )
697
+ )
698
+ ),
699
+ ...columns.map((col) => {
700
+ return {
701
+ ...this.timeColumn.def,
702
+ slug: makeOriginalStartTimeSlugFromColumnSlug(col.slug),
703
+ }
704
+ }),
705
+ ]
706
+
707
+ return this.transform(
708
+ rows,
709
+ newDefs,
710
+ `Average annual change for columns: ${columnSlugs.join(", ")}`,
711
+ TransformType.UpdateRows
712
+ )
713
+ }
714
+
715
+ // Give our users a clean CSV of each Grapher. Assumes an Owid Table with entityName.
716
+ toPrettyCsv(
717
+ useShortNames: boolean = false,
718
+ activeColumnSlugs: string[] | undefined = undefined
719
+ ): string {
720
+ let table
721
+ if (activeColumnSlugs?.length) {
722
+ const timeColumnToInclude = [
723
+ OwidTableSlugs.year,
724
+ OwidTableSlugs.day,
725
+ this.timeColumn.slug, // needed for explorers, where the time column may be called anything
726
+ ].find((colSlug) => this.has(colSlug))
727
+
728
+ if (!timeColumnToInclude)
729
+ throw new Error(
730
+ "Expected to find a time column to include in the CSV"
731
+ )
732
+
733
+ table = this.select([
734
+ timeColumnToInclude,
735
+ this.entityNameSlug,
736
+ ...activeColumnSlugs,
737
+ ])
738
+ } else {
739
+ table = this.dropColumns([
740
+ OwidTableSlugs.entityId,
741
+ OwidTableSlugs.time,
742
+ OwidTableSlugs.entityColor,
743
+ ])
744
+ }
745
+ return table
746
+ .sortBy([this.entityNameSlug])
747
+ .toCsvWithColumnNames(useShortNames)
748
+ }
749
+
750
+ @imemo get entityNameColorIndex(): Map<EntityName, Color> {
751
+ return this.valueIndex(
752
+ this.entityNameSlug,
753
+ OwidTableSlugs.entityColor
754
+ ) as Map<EntityName, Color>
755
+ }
756
+
757
+ getColorForEntityName(entityName: EntityName): Color | undefined {
758
+ return this.entityNameColorIndex.get(entityName)
759
+ }
760
+
761
+ @imemo get columnDisplayNameToColorMap(): Map<string, Color> {
762
+ return new Map(
763
+ this.columnsAsArray
764
+ .filter((col) => col.def.color)
765
+ .map((col) => [col.displayName, col.def.color!])
766
+ )
767
+ }
768
+
769
+ // This assumes the table is sorted where the times for entity names go in asc order.
770
+ // The whole table does not have to be sorted by time.
771
+ getLatestValueForEntity(
772
+ entityName: EntityName,
773
+ columnSlug: ColumnSlug
774
+ ): CoreValueType | undefined {
775
+ const indices = this.rowIndicesByEntityName.get(entityName)
776
+ if (!indices) return undefined
777
+ const values = this.get(columnSlug).valuesIncludingErrorValues
778
+ const descending = indices.toReversed()
779
+ const index = descending.find(
780
+ (index) => !(values[index] instanceof ErrorValue)
781
+ )
782
+ return index !== undefined ? values[index] : undefined
783
+ }
784
+
785
+ entitiesWith(columnSlugs: ColumnSlug[]): Set<EntityName> {
786
+ if (!columnSlugs.length) return new Set()
787
+ if (columnSlugs.length === 1)
788
+ return new Set(this.get(columnSlugs[0]).uniqEntityNames)
789
+
790
+ return intersectionOfSets<EntityName>(
791
+ columnSlugs.map((slug) => new Set(this.get(slug).uniqEntityNames))
792
+ )
793
+ }
794
+
795
+ // Retrieves the two columns `columnSlug` and `timeColumnSlug` from the table and
796
+ // passes their values to the respective interpolation method.
797
+ // `withAllRows` is expected to be completed and sorted.
798
+ private interpolate<K extends InterpolationContext>(
799
+ withAllRows: this,
800
+ columnSlug: ColumnSlug,
801
+ timeColumnSlug: ColumnSlug,
802
+ interpolation: InterpolationProvider<K>,
803
+ context: K
804
+ ): { values: CoreValueType[]; times: number[] } {
805
+ const groupBoundaries = withAllRows.groupBoundaries(this.entityNameSlug)
806
+ const col = withAllRows.get(columnSlug)
807
+ const validIndices = col.validRowIndices
808
+ const newValues = col.valuesIncludingErrorValues.slice()
809
+ const newTimes = withAllRows
810
+ .get(timeColumnSlug)
811
+ .valuesIncludingErrorValues.slice() as Time[]
812
+ groupBoundaries.forEach((_, index) => {
813
+ interpolation(
814
+ newValues,
815
+ newTimes,
816
+ validIndices,
817
+ context,
818
+ groupBoundaries[index],
819
+ groupBoundaries[index + 1]
820
+ )
821
+ })
822
+
823
+ return {
824
+ values: newValues,
825
+ times: newTimes,
826
+ }
827
+ }
828
+
829
+ // TODO generalize `interpolateColumnWithTolerance` and `interpolateColumnLinearly` more
830
+ // There are finicky details in both of them that complicate this
831
+ interpolateColumnWithTolerance(
832
+ columnSlug: ColumnSlug,
833
+ { toleranceStrategyOverride, toleranceOverride }: ToleranceOptions = {}
834
+ ): this {
835
+ // If the column doesn't exist, return the table unchanged.
836
+ if (!this.has(columnSlug)) return this
837
+
838
+ const column = this.get(columnSlug)
839
+ const columnDef = column.def as OwidColumnDef
840
+ const tolerance = toleranceOverride ?? column.tolerance ?? 0
841
+ const toleranceStrategy =
842
+ toleranceStrategyOverride ??
843
+ column.toleranceStrategy ??
844
+ ToleranceStrategy.closest
845
+
846
+ const timeColumnOfTable = !this.timeColumn.isMissing
847
+ ? this.timeColumn
848
+ : // CovidTable does not have a day or year column so we need to use time.
849
+ (this.get(OwidTableSlugs.time) as CoreColumn)
850
+
851
+ const maybeTimeColumnOfValue =
852
+ getOriginalTimeColumnSlug(this, columnSlug) ??
853
+ timeColumnSlugFromColumnDef(columnDef)
854
+ const timeColumnOfValue = this.get(maybeTimeColumnOfValue)
855
+ const originalTimeSlug = makeOriginalTimeSlugFromColumnSlug(columnSlug)
856
+
857
+ let columnStore: CoreColumnStore
858
+ if (tolerance) {
859
+ const withAllRows = this.complete([
860
+ this.entityNameSlug,
861
+ timeColumnOfTable.slug,
862
+ ]).sortBy([this.entityNameSlug, timeColumnOfTable.slug])
863
+
864
+ const interpolateInBothDirections =
865
+ !toleranceStrategy ||
866
+ toleranceStrategy === ToleranceStrategy.closest
867
+ const interpolateBackwards =
868
+ interpolateInBothDirections ||
869
+ toleranceStrategy === ToleranceStrategy.backwards
870
+ const interpolateForwards =
871
+ interpolateInBothDirections ||
872
+ toleranceStrategy === ToleranceStrategy.forwards
873
+
874
+ const interpolationResult = this.interpolate(
875
+ withAllRows,
876
+ columnSlug,
877
+ timeColumnOfValue.slug,
878
+ toleranceInterpolation,
879
+ {
880
+ timeToleranceBackwards: interpolateBackwards
881
+ ? tolerance
882
+ : 0,
883
+ timeToleranceForwards: interpolateForwards ? tolerance : 0,
884
+ }
885
+ )
886
+
887
+ columnStore = {
888
+ ...withAllRows.columnStore,
889
+ [columnSlug]: interpolationResult.values,
890
+ [originalTimeSlug]: interpolationResult.times,
891
+ }
892
+ } else {
893
+ // If there is no tolerance still append the tolerance column
894
+ columnStore = {
895
+ ...this.columnStore,
896
+ [originalTimeSlug]:
897
+ timeColumnOfValue.valuesIncludingErrorValues,
898
+ }
899
+ }
900
+
901
+ return this.transform(
902
+ columnStore,
903
+ [
904
+ ...this.defs,
905
+ {
906
+ ...timeColumnOfValue.def,
907
+ slug: originalTimeSlug,
908
+ display: {
909
+ includeInTable: false,
910
+ },
911
+ },
912
+ ],
913
+ `Interpolated values in column ${columnSlug} with tolerance ${tolerance} and appended column ${originalTimeSlug} with the original times`,
914
+ TransformType.UpdateColumnDefs
915
+ )
916
+ }
917
+
918
+ interpolateColumnLinearly(
919
+ columnSlug: ColumnSlug,
920
+ extrapolate: boolean = false
921
+ ): this {
922
+ // If the column doesn't exist, return the table unchanged.
923
+ if (!this.has(columnSlug)) return this
924
+
925
+ const column = this.get(columnSlug)
926
+ const columnDef = column?.def as OwidColumnDef
927
+
928
+ const maybeTimeColumnSlug =
929
+ getOriginalTimeColumnSlug(this, columnSlug) ??
930
+ timeColumnSlugFromColumnDef(columnDef)
931
+ const timeColumn =
932
+ this.get(maybeTimeColumnSlug) ??
933
+ (this.get(OwidTableSlugs.time) as CoreColumn) // CovidTable does not have a day or year column so we need to use time.
934
+
935
+ const originalColumnSlug =
936
+ makeOriginalValueSlugFromColumnSlug(columnSlug)
937
+ const originalColumnDef = {
938
+ ...columnDef,
939
+ slug: originalColumnSlug,
940
+ display: { includeInTable: false },
941
+ }
942
+
943
+ // todo: we can probably do this once early in the pipeline so we dont have to do it again since complete and sort can be expensive.
944
+ const withAllRows = this.complete([
945
+ this.entityNameSlug,
946
+ timeColumn.slug,
947
+ ]).sortBy([this.entityNameSlug, timeColumn.slug])
948
+
949
+ const interpolationResult = this.interpolate(
950
+ withAllRows,
951
+ columnSlug,
952
+ timeColumn.slug,
953
+ linearInterpolation,
954
+ { extrapolateAtStart: extrapolate, extrapolateAtEnd: extrapolate }
955
+ )
956
+
957
+ const columnStore = {
958
+ ...withAllRows.columnStore,
959
+ [originalColumnSlug]: withAllRows.columnStore[columnSlug],
960
+ [columnSlug]: interpolationResult.values,
961
+ }
962
+
963
+ return this.transform(
964
+ columnStore,
965
+ [
966
+ ...this.defs,
967
+ originalColumnDef,
968
+ {
969
+ ...timeColumn.def,
970
+ },
971
+ ],
972
+ `Interpolated values in column ${columnSlug} linearly and appended column ${originalColumnSlug} with the original values`,
973
+ TransformType.UpdateColumnDefs
974
+ )
975
+ }
976
+
977
+ interpolateColumnsByClosestTimeMatch(
978
+ columnSlugA: ColumnSlug,
979
+ columnSlugB: ColumnSlug
980
+ ): this {
981
+ if (!this.has(columnSlugA) || !this.has(columnSlugB)) return this
982
+
983
+ const columnA = this.get(columnSlugA)
984
+ const columnB = this.get(columnSlugB)
985
+
986
+ const toleranceA = columnA.tolerance ?? 0
987
+ const toleranceB = columnB.tolerance ?? 0
988
+
989
+ // If the columns are of mismatching time types, then we can't do any time matching.
990
+ // This can happen when we have a ScatterPlot with days in one column, and a column with
991
+ // xOverrideYear.
992
+ // We also don't need to do any time matching when the tolerance of both columns is 0.
993
+ if (
994
+ this.timeColumn.isMissing ||
995
+ this.timeColumn.slug !== columnA.originalTimeColumnSlug ||
996
+ this.timeColumn.slug !== columnB.originalTimeColumnSlug ||
997
+ (toleranceA === 0 && toleranceB === 0)
998
+ ) {
999
+ return this
1000
+ }
1001
+
1002
+ const maxDiff = Math.max(toleranceA, toleranceB)
1003
+
1004
+ const withAllRows = this.complete([
1005
+ this.entityNameSlug,
1006
+ this.timeColumn.slug,
1007
+ ]).sortBy([this.entityNameSlug, this.timeColumn.slug])
1008
+
1009
+ // Existing columns
1010
+ const valuesA = withAllRows.get(columnA.slug).valuesIncludingErrorValues
1011
+ const valuesB = withAllRows.get(columnB.slug).valuesIncludingErrorValues
1012
+ const times = withAllRows.timeColumn
1013
+ .valuesIncludingErrorValues as Time[]
1014
+
1015
+ // New columns
1016
+ const newValuesA = new Array(times.length).fill(
1017
+ ErrorValueTypes.NoValueWithinTolerance
1018
+ )
1019
+ const newValuesB = new Array(times.length).fill(
1020
+ ErrorValueTypes.NoValueWithinTolerance
1021
+ )
1022
+ const newTimesA = new Array(times.length).fill(
1023
+ ErrorValueTypes.NoValueWithinTolerance
1024
+ )
1025
+ const newTimesB = new Array(times.length).fill(
1026
+ ErrorValueTypes.NoValueWithinTolerance
1027
+ )
1028
+
1029
+ const groupBoundaries = withAllRows.groupBoundaries(this.entityNameSlug)
1030
+
1031
+ for (let i = 1; i < groupBoundaries.length; i++) {
1032
+ const startIndex = groupBoundaries[i - 1]
1033
+ const endIndex = groupBoundaries[i]
1034
+ const numRows = endIndex - startIndex
1035
+
1036
+ const availableTimesA = []
1037
+ const availableTimesB = []
1038
+
1039
+ for (let index = startIndex; index < endIndex; index++) {
1040
+ if (isNotErrorValue(valuesA[index]))
1041
+ availableTimesA.push(times[index])
1042
+ if (isNotErrorValue(valuesB[index]))
1043
+ availableTimesB.push(times[index])
1044
+ }
1045
+
1046
+ if (availableTimesA.length === 0 || availableTimesB.length === 0) {
1047
+ // If one of the columns has no values, we can't do any interpolation
1048
+ continue
1049
+ }
1050
+ if (
1051
+ availableTimesA.length === numRows &&
1052
+ availableTimesB.length === numRows
1053
+ ) {
1054
+ // If all values are available, we can skip the interpolation and just copy all values
1055
+ for (let index = startIndex; index < endIndex; index++) {
1056
+ newValuesA[index] = valuesA[index]
1057
+ newValuesB[index] = valuesB[index]
1058
+ newTimesA[index] = times[index]
1059
+ newTimesB[index] = times[index]
1060
+ }
1061
+ continue
1062
+ }
1063
+
1064
+ const timePairs = getClosestTimePairs(
1065
+ availableTimesA,
1066
+ availableTimesB,
1067
+ maxDiff
1068
+ )
1069
+ const timeAtoTimeB = new Map(timePairs)
1070
+ const pairedTimesInA = sortNumeric(
1071
+ Array.from(timeAtoTimeB.keys())
1072
+ ) as Time[]
1073
+
1074
+ for (let index = startIndex; index < endIndex; index++) {
1075
+ const currentTime = times[index]
1076
+
1077
+ const candidateTimeA = sortedFindClosest(
1078
+ pairedTimesInA,
1079
+ currentTime
1080
+ )
1081
+ if (candidateTimeA === undefined) continue
1082
+
1083
+ if (Math.abs(currentTime - candidateTimeA) > toleranceA)
1084
+ continue
1085
+
1086
+ const candidateTimeB = timeAtoTimeB.get(candidateTimeA)
1087
+
1088
+ if (
1089
+ candidateTimeB === undefined ||
1090
+ Math.abs(currentTime - candidateTimeB) > toleranceB
1091
+ ) {
1092
+ continue
1093
+ }
1094
+
1095
+ const candidateIndexA = sortedFindClosestIndex(
1096
+ times,
1097
+ candidateTimeA,
1098
+ startIndex,
1099
+ endIndex
1100
+ )
1101
+
1102
+ const candidateIndexB = sortedFindClosestIndex(
1103
+ times,
1104
+ candidateTimeB,
1105
+ startIndex,
1106
+ endIndex
1107
+ )
1108
+
1109
+ newValuesA[index] = valuesA[candidateIndexA]
1110
+ newValuesB[index] = valuesB[candidateIndexB]
1111
+ newTimesA[index] = times[candidateIndexA]
1112
+ newTimesB[index] = times[candidateIndexB]
1113
+ }
1114
+ }
1115
+
1116
+ const originalTimeColumnASlug = makeOriginalTimeSlugFromColumnSlug(
1117
+ columnA.slug
1118
+ )
1119
+ const originalTimeColumnBSlug = makeOriginalTimeSlugFromColumnSlug(
1120
+ columnB.slug
1121
+ )
1122
+
1123
+ const columnStore = {
1124
+ ...withAllRows.columnStore,
1125
+ [columnA.slug]: newValuesA,
1126
+ [columnB.slug]: newValuesB,
1127
+ [originalTimeColumnASlug]: newTimesA,
1128
+ [originalTimeColumnBSlug]: newTimesB,
1129
+ }
1130
+
1131
+ return withAllRows.transform(
1132
+ columnStore,
1133
+ [
1134
+ ...withAllRows.defs,
1135
+ {
1136
+ ...withAllRows.timeColumn.def,
1137
+ slug: originalTimeColumnASlug,
1138
+ },
1139
+ {
1140
+ ...withAllRows.timeColumn.def,
1141
+ slug: originalTimeColumnBSlug,
1142
+ },
1143
+ ],
1144
+ `Interpolated values`,
1145
+ TransformType.UpdateColumnDefs
1146
+ )
1147
+ }
1148
+
1149
+ sampleEntityName(howMany = 1): any[] {
1150
+ return this.availableEntityNames.slice(0, howMany)
1151
+ }
1152
+
1153
+ get isBlank(): boolean {
1154
+ return !this.numRows
1155
+ }
1156
+ }
1157
+
1158
+ const BLANK_TABLE_MESSAGE = `Table is empty.`
1159
+
1160
+ // This just assures that even an emtpty OwidTable will have an entityName column. Probably a cleaner way to do this pattern (add a defaultColumns prop??)
1161
+ export const BlankOwidTable = (
1162
+ tableSlug = `blankOwidTable`,
1163
+ extraTableDescription = ""
1164
+ ): OwidTable =>
1165
+ new OwidTable(
1166
+ undefined,
1167
+ [
1168
+ { slug: OwidTableSlugs.entityName },
1169
+ { slug: OwidTableSlugs.year, type: ColumnTypeNames.Year },
1170
+ ],
1171
+ {
1172
+ tableDescription: BLANK_TABLE_MESSAGE + extraTableDescription,
1173
+ tableSlug,
1174
+ }
1175
+ )