@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,1355 @@
1
+ import * as _ from "lodash-es"
2
+ import {
3
+ csvEscape,
4
+ intersection,
5
+ isPresent,
6
+ ColumnSlug,
7
+ PrimitiveType,
8
+ imemo,
9
+ } from "../utils/index.js"
10
+ import {
11
+ CoreColumn,
12
+ ColumnTypeMap,
13
+ MissingColumn,
14
+ TimeColumn,
15
+ } from "./CoreTableColumns.js"
16
+ import {
17
+ CoreColumnStore,
18
+ CoreRow,
19
+ CoreTableInputOption,
20
+ Time,
21
+ TransformType,
22
+ ValueRange,
23
+ CoreQuery,
24
+ CoreValueType,
25
+ InputType,
26
+ CoreMatrix,
27
+ TableSlug,
28
+ ColumnTypeNames,
29
+ CoreColumnDef,
30
+ JsTypes,
31
+ OwidTableSlugs,
32
+ OwidColumnDef,
33
+ } from "../types/index.js"
34
+ import {
35
+ makeAutoTypeFn,
36
+ columnStoreToRows,
37
+ makeKeyFn,
38
+ makeRowFromColumnStore,
39
+ standardizeSlugs,
40
+ concatColumnStores,
41
+ rowsToColumnStore,
42
+ autodetectColumnDefs,
43
+ renameColumnStore,
44
+ replaceRandomCellsInColumnStore,
45
+ parseDelimited,
46
+ rowsFromMatrix,
47
+ sortColumnStore,
48
+ emptyColumnsInFirstRowInDelimited,
49
+ truncate,
50
+ } from "./CoreTableUtils.js"
51
+ import {
52
+ ErrorValueTypes,
53
+ isNotErrorValue,
54
+ DroppedForTesting,
55
+ } from "./ErrorValues.js"
56
+ import { applyTransforms, extractTransformNameAndParams } from "./Transforms.js"
57
+
58
+ interface AdvancedOptions {
59
+ tableDescription?: string
60
+ transformCategory?: TransformType
61
+ parent?: CoreTable
62
+ filterMask?: FilterMask
63
+ tableSlug?: TableSlug
64
+ forceReuseColumnStore?: boolean
65
+ }
66
+
67
+ // The complex generic with default here just enables you to optionally specify a more
68
+ // narrow interface for the input rows. This is helpful for OwidTable.
69
+ export class CoreTable<
70
+ ROW_TYPE extends CoreRow = CoreRow,
71
+ COL_DEF_TYPE extends CoreColumnDef = CoreColumnDef,
72
+ > {
73
+ private _columns: Map<ColumnSlug, CoreColumn> = new Map()
74
+ protected parent?: this
75
+ tableDescription: string
76
+ private timeToLoad = 0
77
+ private initTime = Date.now()
78
+
79
+ private originalInput: CoreTableInputOption
80
+ private advancedOptions: AdvancedOptions
81
+
82
+ private inputColumnDefs: COL_DEF_TYPE[]
83
+ constructor(
84
+ input: CoreTableInputOption = [],
85
+ inputColumnDefs: COL_DEF_TYPE[] | string = [],
86
+ advancedOptions: AdvancedOptions = {}
87
+ ) {
88
+ const start = Date.now() // Perf aid
89
+ const { parent, tableDescription = "" } = advancedOptions
90
+
91
+ this.originalInput = input
92
+ this.tableDescription = tableDescription
93
+ this.parent = parent as this
94
+ this.inputColumnDefs =
95
+ typeof inputColumnDefs === "string"
96
+ ? columnDefinitionsFromInput<COL_DEF_TYPE>(inputColumnDefs)
97
+ : inputColumnDefs
98
+
99
+ // Column definitions with a "duplicate" transform are merged with the column definition of the specified source column
100
+ this.inputColumnDefs = this.inputColumnDefs.map((def) => {
101
+ if (!def.transform) return def
102
+ const transform = extractTransformNameAndParams(def.transform)
103
+ if (transform?.transformName !== "duplicate") return def
104
+
105
+ const sourceSlug = transform.params[0]
106
+ const sourceDef = this.inputColumnDefs.find(
107
+ (def) => def.slug === sourceSlug
108
+ )
109
+ return { ...sourceDef, ...def }
110
+ })
111
+
112
+ // If any values were passed in, copy those to column store now and then remove them from column definitions.
113
+ // todo: remove values property entirely? may be an anti-pattern.
114
+ this.inputColumnDefs = this.inputColumnDefs.map((def) => {
115
+ if (!def.values) return def
116
+ this.valuesFromColumnDefs[def.slug] = def.values
117
+ const copy = {
118
+ ...def,
119
+ }
120
+ delete copy.values
121
+ return copy
122
+ })
123
+
124
+ this.inputColumnDefs.forEach((def) => this.setColumn(def))
125
+
126
+ this.advancedOptions = advancedOptions
127
+
128
+ // If this has a parent table, than we expect all defs. This makes "deletes" and "renames" fast.
129
+ // If this is the first input table, then we do a simple check to generate any missing column defs.
130
+ if (!parent)
131
+ autodetectColumnDefs(this.inputColumnStore, this._columns).forEach(
132
+ (def) => this.setColumn(def as COL_DEF_TYPE)
133
+ )
134
+
135
+ this.timeToLoad = Date.now() - start // Perf aid
136
+ }
137
+
138
+ private valuesFromColumnDefs: CoreColumnStore = {}
139
+
140
+ // A method currently used just in debugging but may be useful in the author backend.
141
+ // If your charts look funny, a good thing to check is if the autodetected columns are wrong.
142
+ get autodetectedColumnDefs(): CoreTable {
143
+ const providedSlugs = new Set(
144
+ this.inputColumnDefs.map((def) => def.slug)
145
+ )
146
+ return new CoreTable(
147
+ this.defs.filter((def) => !providedSlugs.has(def.slug))
148
+ )
149
+ }
150
+
151
+ @imemo get transformCategory(): TransformType {
152
+ const { advancedOptions, inputType } = this
153
+ if (advancedOptions.transformCategory)
154
+ return advancedOptions.transformCategory
155
+
156
+ if (inputType === InputType.Delimited)
157
+ return TransformType.LoadFromDelimited
158
+ if (inputType === InputType.Matrix) return TransformType.LoadFromMatrix
159
+ if (inputType === InputType.RowStore)
160
+ return TransformType.LoadFromRowStore
161
+ return TransformType.LoadFromColumnStore
162
+ }
163
+
164
+ // If the input is a column store, returns that. If it is DSV, parses that and turns it into a column store.
165
+ // If it is a Rows[], turns it into a column store.
166
+ @imemo private get inputColumnStore(): CoreColumnStore {
167
+ const { originalInput, inputType } = this
168
+
169
+ if (inputType === InputType.Delimited)
170
+ return this.delimitedAsColumnStore
171
+ else if (inputType === InputType.Matrix)
172
+ return rowsToColumnStore(
173
+ rowsFromMatrix(originalInput as CoreMatrix)
174
+ )
175
+ else if (inputType === InputType.RowStore)
176
+ return rowsToColumnStore(originalInput as CoreRow[])
177
+ return originalInput as CoreColumnStore
178
+ }
179
+
180
+ // This can, in theory, be used to detect whether we can reuse the input column store, which can save us time parsing.
181
+ // However, I'm not entirely sure whether the conditions included here are complete, so we don't currently use it.
182
+ // We do use `advancedOptions.forceReuseColumnStore`, however.
183
+ @imemo get canReuseInputColumnStore(): boolean {
184
+ const { inputColumnDefs, valuesFromColumnDefs, columnSlugs } = this
185
+
186
+ const storeHasAllColumns = columnSlugs.every(
187
+ (slug) => slug in this.inputColumnStore
188
+ )
189
+
190
+ const columnsFromTransforms = inputColumnDefs.filter(
191
+ (def) => def.transform && !def.transformHasRun
192
+ )
193
+
194
+ return (
195
+ !this.colsToParse.length &&
196
+ storeHasAllColumns &&
197
+ !Object.keys(valuesFromColumnDefs).length &&
198
+ !columnsFromTransforms.length
199
+ )
200
+ }
201
+
202
+ @imemo get columnStore(): CoreColumnStore {
203
+ const { inputColumnStore, advancedOptions } = this
204
+
205
+ if (advancedOptions.forceReuseColumnStore) {
206
+ if (advancedOptions.filterMask)
207
+ return advancedOptions.filterMask.apply(inputColumnStore)
208
+ else return inputColumnStore
209
+ }
210
+
211
+ const {
212
+ valuesFromColumnDefs,
213
+ inputColumnsToParsedColumnStore,
214
+ inputColumnDefs,
215
+ } = this
216
+
217
+ // Set blank columns
218
+ let columnStore = Object.assign(
219
+ {},
220
+ this.blankColumnStore,
221
+ inputColumnStore,
222
+ valuesFromColumnDefs
223
+ )
224
+
225
+ // Overwrite any non-parsed columns with parsed values
226
+ if (Object.keys(inputColumnsToParsedColumnStore).length)
227
+ columnStore = Object.assign(
228
+ columnStore,
229
+ inputColumnsToParsedColumnStore
230
+ )
231
+
232
+ // If we ever pass Mobx observable arrays, we need to convert them to regular arrays.
233
+ // Otherwise, operations like `.concat()` will break in unexpected ways.
234
+ // See https://github.com/mobxjs/mobx/blob/mobx4and5/docs/best/pitfalls.md
235
+ // Also, see https://github.com/owid/owid-grapher/issues/2948 for an issue caused by this problem.
236
+ type CoreValueArrayThatMayBeMobxProxy = CoreValueType[] & {
237
+ toJS?: () => CoreValueType[]
238
+ }
239
+
240
+ for (const [slug, values] of Object.entries(columnStore)) {
241
+ const valuesThatMayBeMobxProxy =
242
+ values as CoreValueArrayThatMayBeMobxProxy
243
+ if (typeof valuesThatMayBeMobxProxy.toJS === "function") {
244
+ columnStore[slug] = valuesThatMayBeMobxProxy.toJS()
245
+ }
246
+ }
247
+
248
+ const columnsFromTransforms = inputColumnDefs.filter(
249
+ (def) => def.transform && !def.transformHasRun
250
+ ) // todo: sort by graph dependency order
251
+ if (columnsFromTransforms.length) {
252
+ columnStore = applyTransforms(columnStore, columnsFromTransforms)
253
+ }
254
+
255
+ return advancedOptions.filterMask
256
+ ? advancedOptions.filterMask.apply(columnStore)
257
+ : columnStore
258
+ }
259
+
260
+ private get blankColumnStore(): CoreColumnStore {
261
+ const columnsObject: CoreColumnStore = {}
262
+ this.columnSlugs.forEach((slug) => {
263
+ columnsObject[slug] = []
264
+ })
265
+ return columnsObject
266
+ }
267
+
268
+ @imemo private get delimitedAsColumnStore(): CoreColumnStore {
269
+ const { originalInput, _numericColumnSlugs } = this
270
+ const parsed = parseDelimited(
271
+ originalInput as string,
272
+ undefined,
273
+ makeAutoTypeFn(_numericColumnSlugs)
274
+ ) as any
275
+ // Remove the columns object from the parsed object
276
+ delete parsed.columns
277
+
278
+ const renamedRows = standardizeSlugs(parsed) // todo: pass renamed defs back in.
279
+ return rowsToColumnStore(renamedRows ? renamedRows.rows : parsed)
280
+ }
281
+
282
+ get tableSlug(): string | undefined {
283
+ return this.advancedOptions.tableSlug
284
+ }
285
+
286
+ private get inputColumnsToParsedColumnStore(): CoreColumnStore {
287
+ const { inputColumnStore, colsToParse } = this
288
+ const columnsObject: CoreColumnStore = {}
289
+ if (!colsToParse.length) return columnsObject
290
+ const missingCols: CoreColumn[] = []
291
+ let len = 0
292
+ colsToParse.forEach((col) => {
293
+ const { slug } = col
294
+ const unparsedVals = inputColumnStore[slug]
295
+ if (!unparsedVals) {
296
+ missingCols.push(col)
297
+ return
298
+ }
299
+ columnsObject[slug] = unparsedVals.map((val) => col.parse(val))
300
+ len = columnsObject[slug].length
301
+ })
302
+
303
+ // If column defs were provided but there were no values provided for those columns, create blank columns the same size
304
+ // as the filled columns.
305
+ missingCols.forEach(
306
+ (col) =>
307
+ (columnsObject[col.slug] = _.range(0, len).map(() =>
308
+ col.parse(undefined)
309
+ ))
310
+ )
311
+ return columnsObject
312
+ }
313
+
314
+ private get colsToParse(): CoreColumn[] {
315
+ const { inputType, columnsAsArray, inputColumnStore } = this
316
+ const columnsToMaybeParse = columnsAsArray.filter(
317
+ (col) => !col.def.skipParsing
318
+ )
319
+ const firstInputRow = makeRowFromColumnStore(0, inputColumnStore)
320
+ if (inputType === InputType.Delimited) {
321
+ const missingTypes = new Set(
322
+ this.getColumns(
323
+ emptyColumnsInFirstRowInDelimited(
324
+ this.originalInput as string
325
+ )
326
+ )
327
+ ) // Our autotyping is poor if the first value in a column is empty
328
+ return columnsToMaybeParse.filter(
329
+ (col) =>
330
+ col.needsParsing(firstInputRow[col.slug]) ||
331
+ missingTypes.has(col)
332
+ )
333
+ }
334
+
335
+ if (this.parent || !firstInputRow) return []
336
+
337
+ // The default behavior is to assume some missing or bad data in user data, so we always parse the full input the first time we load
338
+ // user data, with the exception of columns that have values passed directly.
339
+ // Todo: measure the perf hit and add a parameter to opt out of this this if you know the data is complete?
340
+ const alreadyTypedSlugs = new Set(
341
+ Object.keys(this.valuesFromColumnDefs)
342
+ )
343
+ if (this.isRoot) {
344
+ return columnsToMaybeParse.filter(
345
+ (col) => !alreadyTypedSlugs.has(col.slug)
346
+ )
347
+ }
348
+
349
+ return columnsToMaybeParse.filter(
350
+ (col) =>
351
+ !alreadyTypedSlugs.has(col.slug) ||
352
+ col.needsParsing(firstInputRow[col.slug])
353
+ )
354
+ }
355
+
356
+ private setColumn(def: COL_DEF_TYPE): void {
357
+ const { type, slug } = def
358
+ const ColumnType = (type && ColumnTypeMap[type]) || ColumnTypeMap.String
359
+ this._columns.set(slug, new ColumnType(this, def))
360
+ }
361
+
362
+ protected transform(
363
+ rowsOrColumnStore: ROW_TYPE[] | CoreColumnStore | CoreMatrix,
364
+ defs: COL_DEF_TYPE[],
365
+ tableDescription: string,
366
+ transformCategory: TransformType,
367
+ filterMask?: FilterMask
368
+ ): this {
369
+ // The combo of the "this" return type and then casting this to any allows subclasses to create transforms of the
370
+ // same type. The "any" typing is very brief (the returned type will have the same type as the instance being transformed).
371
+ return new (this.constructor as any)(rowsOrColumnStore, defs, {
372
+ parent: this,
373
+ tableDescription,
374
+ transformCategory,
375
+ filterMask,
376
+ } as AdvancedOptions)
377
+ }
378
+
379
+ protected noopTransform(tableDescription: string): this {
380
+ return new (this.constructor as any)(this.columnStore, this.defs, {
381
+ parent: this,
382
+ tableDescription,
383
+ transformCategory: TransformType.Noop,
384
+ forceReuseColumnStore: true,
385
+ } as AdvancedOptions)
386
+ }
387
+
388
+ // Time between when the parent table finished loading and this table started constructing.
389
+ // A large time may just be due to a transform only happening after a user action, or it
390
+ // could be do to other sync code executing between transforms.
391
+ private get betweenTime(): number {
392
+ return this.parent
393
+ ? this.initTime - (this.parent.initTime + this.parent.timeToLoad)
394
+ : 0
395
+ }
396
+
397
+ @imemo get rows(): ROW_TYPE[] {
398
+ return columnStoreToRows(this.columnStore) as ROW_TYPE[]
399
+ }
400
+
401
+ @imemo get indices(): number[] {
402
+ return _.range(0, this.numRows)
403
+ }
404
+
405
+ *[Symbol.iterator](): Generator<CoreRow, void, unknown> {
406
+ const { columnStore, numRows } = this
407
+ for (let index = 0; index < numRows; index++) {
408
+ yield makeRowFromColumnStore(index, columnStore)
409
+ }
410
+ }
411
+
412
+ getTimesAtIndices(indices: number[]): number[] {
413
+ if (!indices.length) return []
414
+ return this.getValuesAtIndices(this.timeColumn!.slug, indices) as Time[]
415
+ }
416
+
417
+ getValuesAtIndices(
418
+ columnSlug: ColumnSlug,
419
+ indices: number[]
420
+ ): CoreValueType[] {
421
+ const values = this.get(columnSlug).valuesIncludingErrorValues
422
+ return indices.map((index) => values[index])
423
+ }
424
+
425
+ @imemo get firstRow(): ROW_TYPE {
426
+ return makeRowFromColumnStore(0, this.columnStore) as ROW_TYPE
427
+ }
428
+
429
+ @imemo get lastRow(): ROW_TYPE {
430
+ return makeRowFromColumnStore(
431
+ this.numRows - 1,
432
+ this.columnStore
433
+ ) as ROW_TYPE
434
+ }
435
+
436
+ @imemo get numRows(): number {
437
+ const firstColValues = Object.values(this.columnStore)[0]
438
+ return firstColValues ? firstColValues.length : 0
439
+ }
440
+
441
+ @imemo get numColumns(): number {
442
+ return this.columnSlugs.length
443
+ }
444
+
445
+ get(columnSlug: ColumnSlug | undefined): CoreColumn {
446
+ if (columnSlug === undefined)
447
+ return new MissingColumn(this, {
448
+ slug: `undefined_slug`,
449
+ })
450
+ return (
451
+ this._columns.get(columnSlug) ??
452
+ new MissingColumn(this, {
453
+ slug: columnSlug,
454
+ })
455
+ )
456
+ }
457
+
458
+ has(columnSlug: ColumnSlug | undefined): boolean {
459
+ if (columnSlug === undefined) return false
460
+ return this._columns.has(columnSlug)
461
+ }
462
+
463
+ getFirstColumnWithType(
464
+ columnTypeName: ColumnTypeNames
465
+ ): CoreColumn | undefined {
466
+ return this.columnsAsArray.find(
467
+ (col) => col.def.type === columnTypeName
468
+ )
469
+ }
470
+
471
+ // todo: move this. time methods should not be in CoreTable, in OwidTable instead (which is really TimeSeriesTable).
472
+ // TODO: remove this. Currently we use this to get the right day/year time formatting. For now a chart is either a "day chart" or a "year chart".
473
+ // But we can have charts with multiple time columns. Ideally each place that needs access to the timeColumn, would get the specific column
474
+ // and not the first time column from the table.
475
+ @imemo get timeColumn(): TimeColumn | MissingColumn {
476
+ // "time" is the canonical time column slug.
477
+ // See LegacyToOwidTable where this column is injected for all Graphers.
478
+ const maybeTimeColumn = this.get(OwidTableSlugs.time)
479
+ if (maybeTimeColumn instanceof ColumnTypeMap.Time)
480
+ return maybeTimeColumn
481
+ // If a valid "time" column doesn't exist, find _some_ time column to use.
482
+ // This is somewhat unreliable and currently only used to infer the time
483
+ // column on explorers.
484
+ return (this.columnsAsArray.find(
485
+ (col) => col instanceof ColumnTypeMap.Day
486
+ ) ??
487
+ this.columnsAsArray.find(
488
+ (col) => col instanceof ColumnTypeMap.Date
489
+ ) ??
490
+ this.columnsAsArray.find(
491
+ (col) => col instanceof ColumnTypeMap.Year
492
+ ) ??
493
+ this.columnsAsArray.find(
494
+ (col) => col instanceof ColumnTypeMap.Quarter
495
+ ) ??
496
+ maybeTimeColumn) as TimeColumn | MissingColumn
497
+ }
498
+
499
+ // todo: should be on owidtable
500
+ @imemo get entityNameColumn(): CoreColumn {
501
+ return (
502
+ this.getFirstColumnWithType(ColumnTypeNames.EntityName) ??
503
+ this.get(OwidTableSlugs.entityName)
504
+ )
505
+ }
506
+
507
+ // todo: should be on owidtable
508
+ @imemo get entityNameSlug(): string {
509
+ return this.entityNameColumn.slug
510
+ }
511
+
512
+ @imemo private get columnsWithParseErrors(): CoreColumn[] {
513
+ return this.columnsAsArray.filter((col) => col.numErrorValues)
514
+ }
515
+
516
+ @imemo get numColumnsWithErrorValues(): number {
517
+ return this.columnsWithParseErrors.length
518
+ }
519
+
520
+ @imemo get numErrorValues(): number {
521
+ return _.sum(this.columnsAsArray.map((col) => col.numErrorValues))
522
+ }
523
+
524
+ @imemo get numValidCells(): number {
525
+ return _.sum(this.columnsAsArray.map((col) => col.numValues))
526
+ }
527
+
528
+ @imemo get colStoreIsEqualToParent(): boolean {
529
+ return this.parent
530
+ ? this.columnStore === this.parent.columnStore
531
+ : false
532
+ }
533
+
534
+ get rootTable(): this {
535
+ return this.parent ? this.parent.rootTable : this
536
+ }
537
+
538
+ /**
539
+ * Returns a string map (aka index) where the keys are the combined string values of columnSlug[], and the values
540
+ * are the indices for the rows that match.
541
+ *
542
+ * {country: "USA", population: 100}
543
+ *
544
+ * So `table.rowIndex(["country", "population"]).get("USA 100")` would return [0].
545
+ *
546
+ */
547
+ rowIndex(columnSlugs: ColumnSlug[]): Map<string, number[]> {
548
+ const index = new Map<string, number[]>()
549
+ const keyFn = makeKeyFn(this.columnStore, columnSlugs)
550
+ for (let i = 0; i < this.numRows; i++) {
551
+ const key = keyFn(i)
552
+ if (index.has(key)) index.get(key)!.push(i)
553
+ else index.set(key, [i])
554
+ }
555
+ return index
556
+ }
557
+
558
+ /**
559
+ * Returns a map (aka index) where the keys are the values of the indexColumnSlug, and the values
560
+ * are the values of the valueColumnSlug.
561
+ *
562
+ * {country: "USA", population: 100}
563
+ *
564
+ * So `table.valueIndex("country", "population").get("USA")` would return 100.
565
+ *
566
+ */
567
+ protected valueIndex(
568
+ indexColumnSlug: ColumnSlug,
569
+ valueColumnSlug: ColumnSlug
570
+ ): Map<PrimitiveType, PrimitiveType> {
571
+ const indexCol = this.get(indexColumnSlug)
572
+ const valueCol = this.get(valueColumnSlug)
573
+ const indexValues = indexCol.valuesIncludingErrorValues
574
+ const valueValues = valueCol.valuesIncludingErrorValues
575
+ const valueIndices = new Set(valueCol.validRowIndices)
576
+ const intersection = indexCol.validRowIndices.filter((index) =>
577
+ valueIndices.has(index)
578
+ )
579
+
580
+ const map = new Map<PrimitiveType, PrimitiveType>()
581
+ intersection.forEach((index) => {
582
+ map.set(
583
+ indexValues[index] as PrimitiveType,
584
+ valueValues[index] as PrimitiveType
585
+ )
586
+ })
587
+ return map
588
+ }
589
+
590
+ grep(searchStringOrRegex: string | RegExp): this {
591
+ return this.rowFilter((row) => {
592
+ const line = Object.values(row).join(" ")
593
+ return typeof searchStringOrRegex === "string"
594
+ ? line.includes(searchStringOrRegex)
595
+ : searchStringOrRegex.test(line)
596
+ }, `Kept rows that matched '${searchStringOrRegex.toString()}'`)
597
+ }
598
+
599
+ rowFilter(
600
+ predicate: (row: ROW_TYPE, index: number) => boolean,
601
+ opName: string
602
+ ): this {
603
+ const mask = new FilterMask(this.numRows, this.rows.map(predicate)) // Warning: this will be slow
604
+ if (mask.isNoop()) return this.noopTransform(opName)
605
+
606
+ return this.transform(
607
+ this.columnStore,
608
+ this.defs,
609
+ opName,
610
+ TransformType.FilterRows,
611
+ mask
612
+ )
613
+ }
614
+
615
+ columnFilter(
616
+ columnSlug: ColumnSlug,
617
+ predicate: (value: CoreValueType, index: number) => boolean,
618
+ opName: string
619
+ ): this {
620
+ const mask = new FilterMask(
621
+ this.numRows,
622
+ this.get(columnSlug).valuesIncludingErrorValues.map(predicate)
623
+ )
624
+ if (mask.isNoop()) return this.noopTransform(opName)
625
+
626
+ return this.transform(
627
+ this.columnStore,
628
+ this.defs,
629
+ opName,
630
+ TransformType.FilterRows,
631
+ mask
632
+ )
633
+ }
634
+
635
+ sortBy(slugs: ColumnSlug[]): this {
636
+ const description = `Sort by ${slugs.join(",")}`
637
+ const sorted = sortColumnStore(this.columnStore, slugs)
638
+
639
+ if (sorted === this.columnStore) return this.noopTransform(description)
640
+ else
641
+ return this.transform(
642
+ sorted,
643
+ this.defs,
644
+ description,
645
+ TransformType.SortRows
646
+ )
647
+ }
648
+
649
+ // Assumes table is sorted by columnSlug. Returns an array representing the starting index of each new group.
650
+ protected groupBoundaries(columnSlug: ColumnSlug): number[] {
651
+ const values = this.get(columnSlug).valuesIncludingErrorValues
652
+ const arr: number[] = []
653
+ let last: CoreValueType | undefined = undefined
654
+ for (let i = 0; i < values.length; i++) {
655
+ const val = values[i]
656
+ if (val !== last) {
657
+ arr.push(i)
658
+ last = val
659
+ }
660
+ }
661
+ // Include the end of the last group, which doesn't result in a change in value above.
662
+ if (values && values.length) {
663
+ arr.push(values.length)
664
+ }
665
+ return arr
666
+ }
667
+
668
+ @imemo get defs(): COL_DEF_TYPE[] {
669
+ return this.columnsAsArray.map((col) => col.def) as COL_DEF_TYPE[]
670
+ }
671
+
672
+ @imemo get columnNames(): string[] {
673
+ return this.columnsAsArray.map((col) => col.name)
674
+ }
675
+
676
+ @imemo get columnTypes(): (ColumnTypeNames | undefined)[] {
677
+ return this.columnsAsArray.map((col) => col.def.type)
678
+ }
679
+
680
+ @imemo get columnJsTypes(): JsTypes[] {
681
+ return this.columnsAsArray.map((col) => col.jsType)
682
+ }
683
+
684
+ @imemo get columnSlugs(): string[] {
685
+ return Array.from(this._columns.keys())
686
+ }
687
+
688
+ @imemo get numericColumnSlugs(): string[] {
689
+ return this._numericColumnSlugs
690
+ }
691
+
692
+ private get _numericColumnSlugs(): string[] {
693
+ return this._columnsAsArray
694
+ .filter((col) => col instanceof ColumnTypeMap.Numeric)
695
+ .map((col) => col.slug)
696
+ }
697
+
698
+ private get _columnsAsArray(): CoreColumn[] {
699
+ return Array.from(this._columns.values())
700
+ }
701
+
702
+ @imemo get columnsAsArray(): CoreColumn[] {
703
+ return this._columnsAsArray
704
+ }
705
+
706
+ getColumns(slugs: ColumnSlug[]): CoreColumn[] {
707
+ return slugs.map((slug) => this.get(slug))
708
+ }
709
+
710
+ // Get the min and max for multiple columns at once
711
+ domainFor(slugs: ColumnSlug[]): ValueRange {
712
+ const cols = this.getColumns(slugs)
713
+ const mins = cols.map((col) => col.minValue)
714
+ const maxes = cols.map((col) => col.maxValue)
715
+ return [_.min(mins), _.max(maxes)]
716
+ }
717
+
718
+ private get isRoot(): boolean {
719
+ return !this.parent
720
+ }
721
+
722
+ dump(rowLimit = 30): void {
723
+ this.dumpPipeline()
724
+ this.dumpColumns()
725
+ this.dumpRows(rowLimit)
726
+ }
727
+
728
+ dumpPipeline(): void {
729
+ // eslint-disable-next-line no-console
730
+ console.table(this.ancestors.map((tb) => tb.explanation))
731
+ }
732
+
733
+ dumpColumns(): void {
734
+ // eslint-disable-next-line no-console
735
+ console.table(this.explainColumns)
736
+ }
737
+
738
+ rowsFrom(start: number, end: number): any {
739
+ if (start >= this.numRows) return []
740
+ if (end > this.numRows) end = this.numRows
741
+ return _.range(start, end).map((index) =>
742
+ makeRowFromColumnStore(index, this.columnStore)
743
+ )
744
+ }
745
+
746
+ dumpRows(rowLimit = 30): void {
747
+ // eslint-disable-next-line no-console
748
+ console.table(this.rowsFrom(0, rowLimit), this.columnSlugs)
749
+ }
750
+
751
+ dumpInputTable(): void {
752
+ // eslint-disable-next-line no-console
753
+ console.table(this.inputAsTable)
754
+ }
755
+
756
+ @imemo private get inputType(): InputType {
757
+ const { originalInput } = this
758
+ if (typeof originalInput === "string") return InputType.Delimited
759
+ if (Array.isArray(originalInput))
760
+ return Array.isArray(originalInput[0])
761
+ ? InputType.Matrix
762
+ : InputType.RowStore
763
+ return InputType.ColumnStore
764
+ }
765
+
766
+ @imemo private get inputColumnStoreToRows(): Record<
767
+ string,
768
+ CoreValueType
769
+ >[] {
770
+ return columnStoreToRows(this.inputColumnStore)
771
+ }
772
+
773
+ @imemo private get inputAsTable():
774
+ | Record<string, CoreValueType>[]
775
+ | CoreTableInputOption {
776
+ const { inputType } = this
777
+ return inputType === InputType.ColumnStore
778
+ ? this.inputColumnStoreToRows
779
+ : inputType === InputType.Matrix
780
+ ? rowsFromMatrix(this.originalInput as CoreMatrix)
781
+ : this.originalInput
782
+ }
783
+
784
+ @imemo private get explainColumns(): Record<string, unknown>[] {
785
+ return this.columnsAsArray.map((col) => {
786
+ const {
787
+ slug,
788
+ jsType,
789
+ name,
790
+ numValues,
791
+ numErrorValues,
792
+ displayName,
793
+ def,
794
+ } = col
795
+ return {
796
+ slug,
797
+ type: def.type,
798
+ jsType,
799
+ name,
800
+ numValues,
801
+ numErrorValues,
802
+ displayName,
803
+ color: def.color,
804
+ }
805
+ })
806
+ }
807
+
808
+ get ancestors(): this[] {
809
+ return this.parent ? [...this.parent.ancestors, this] : [this]
810
+ }
811
+
812
+ @imemo private get numColsToParse(): number {
813
+ return this.colsToParse.length
814
+ }
815
+
816
+ private static guids = 0
817
+ private guid = ++CoreTable.guids
818
+
819
+ private get explanation(): Record<string, unknown> {
820
+ // todo: is there a better way to do this in JS?
821
+ const {
822
+ tableDescription,
823
+ transformCategory,
824
+ guid,
825
+ numColumns,
826
+ numRows,
827
+ betweenTime,
828
+ timeToLoad,
829
+ numColsToParse,
830
+ numValidCells,
831
+ numErrorValues,
832
+ numColumnsWithErrorValues,
833
+ colStoreIsEqualToParent,
834
+ } = this
835
+ return {
836
+ tableDescription: truncate(tableDescription, 40),
837
+ transformCategory,
838
+ guid,
839
+ numColumns,
840
+ numRows,
841
+ betweenTime,
842
+ timeToLoad,
843
+ numColsToParse,
844
+ numValidCells,
845
+ numErrorValues,
846
+ numColumnsWithErrorValues,
847
+ colStoreIsEqualToParent,
848
+ }
849
+ }
850
+
851
+ toCsvWithColumnNames(useShortNames: boolean = false): string {
852
+ const delimiter = ","
853
+ const header =
854
+ this.columnsAsArray
855
+ .map((col) =>
856
+ csvEscape(
857
+ useShortNames && (col.def as OwidColumnDef).shortName
858
+ ? (col.def as OwidColumnDef).shortName
859
+ : col.name
860
+ )
861
+ )
862
+ .join(delimiter) + "\n"
863
+ const body = this.rows
864
+ .map((row) =>
865
+ this.columnsAsArray.map(
866
+ (col) => col.formatForCsv(row[col.slug]) ?? ""
867
+ )
868
+ )
869
+ .map((row) => row.join(delimiter))
870
+ .join("\n")
871
+ return header + body
872
+ }
873
+
874
+ rowsAt(indices: number[]): ROW_TYPE[] {
875
+ const { columnStore } = this
876
+ return indices.map(
877
+ (index) => makeRowFromColumnStore(index, columnStore) as ROW_TYPE
878
+ )
879
+ }
880
+
881
+ findRows(query: CoreQuery): ROW_TYPE[] {
882
+ return this.rowsAt(this.findRowsIndices(query))
883
+ }
884
+
885
+ findRowsIndices(query: CoreQuery): any {
886
+ const slugs = Object.keys(query)
887
+ if (!slugs.length) return this.indices
888
+ const arrs = this.getColumns(slugs).map((col) =>
889
+ col.indicesWhere(query[col.slug])
890
+ )
891
+ return intersection(...arrs)
892
+ }
893
+
894
+ indexOf(row: ROW_TYPE): any {
895
+ return this.findRowsIndices(row)[0] ?? -1
896
+ }
897
+
898
+ where(query: CoreQuery): this {
899
+ const rows = this.findRows(query)
900
+ const queryDescription = Object.entries(query)
901
+ .map(([col, value]) => `${col}=${value}`)
902
+ .join("&")
903
+
904
+ return this.transform(
905
+ rows,
906
+ this.defs,
907
+ `Selecting ${rows.length} rows where ${queryDescription}`,
908
+ TransformType.FilterRows
909
+ )
910
+ }
911
+
912
+ appendRows(rows: ROW_TYPE[], opDescription: string): this {
913
+ return this.concat(
914
+ [
915
+ new (this.constructor as typeof CoreTable)(rows, this.defs, {
916
+ parent: this,
917
+ }),
918
+ ],
919
+ opDescription
920
+ )
921
+ }
922
+
923
+ updateDefs(fn: (def: COL_DEF_TYPE) => COL_DEF_TYPE): this {
924
+ return this.transform(
925
+ this.columnStore,
926
+ this.defs.map(fn),
927
+ `Updated column defs`,
928
+ TransformType.UpdateColumnDefs
929
+ )
930
+ }
931
+
932
+ select(slugs: ColumnSlug[]): this {
933
+ const columnsToKeep = new Set(slugs)
934
+ const newStore: CoreColumnStore = {}
935
+ const defs = this.columnsAsArray
936
+ .filter((col) => columnsToKeep.has(col.slug))
937
+ .map((col) => col.def) as COL_DEF_TYPE[]
938
+
939
+ Object.keys(this.columnStore)
940
+ .filter((slug) => columnsToKeep.has(slug))
941
+ .forEach((slug) => {
942
+ newStore[slug] = this.columnStore[slug]
943
+ })
944
+
945
+ return this.transform(
946
+ newStore,
947
+ defs,
948
+ `Kept columns '${slugs}'`,
949
+ TransformType.FilterColumns
950
+ )
951
+ }
952
+
953
+ dropColumns(slugs: ColumnSlug[], message?: string): this {
954
+ const columnsToDrop = new Set(slugs)
955
+ const newStore = {
956
+ ...this.columnStore,
957
+ }
958
+ const defs = this.columnsAsArray
959
+ .filter((col) => !columnsToDrop.has(col.slug))
960
+ .map((col) => col.def) as COL_DEF_TYPE[]
961
+ slugs.forEach((slug) => {
962
+ delete newStore[slug]
963
+ })
964
+ return this.transform(
965
+ newStore,
966
+ defs,
967
+ message ?? `Dropped columns '${slugs}'`,
968
+ TransformType.FilterColumns
969
+ )
970
+ }
971
+
972
+ isRowEmpty(index: number): boolean {
973
+ const { columnStore } = this
974
+ return (
975
+ this.columnSlugs
976
+ .map((slug) => columnStore[slug][index])
977
+ .filter((value) => isNotErrorValue(value) && value !== "")
978
+ .length === 0
979
+ )
980
+ }
981
+
982
+ dropEmptyRows(): this {
983
+ return this.dropRowsAt(
984
+ this.indices
985
+ .map((index) => (this.isRowEmpty(index) ? index : null))
986
+ .filter(isPresent)
987
+ )
988
+ }
989
+
990
+ renameColumn(oldSlug: ColumnSlug, newSlug: ColumnSlug): this {
991
+ return this.renameColumns({ [oldSlug]: newSlug })
992
+ }
993
+
994
+ // Todo: improve typings. After renaming a column the row interface should change. Applies to some other methods as well.
995
+ renameColumns(columnRenameMap: { [columnSlug: string]: ColumnSlug }): this {
996
+ const oldSlugs = Object.keys(columnRenameMap)
997
+ const newSlugs = Object.values(columnRenameMap)
998
+
999
+ const message =
1000
+ `Renamed ` +
1001
+ oldSlugs
1002
+ .map((name, index) => `'${name}' to '${newSlugs[index]}'`)
1003
+ .join(" and ")
1004
+
1005
+ return this.transform(
1006
+ renameColumnStore(this.columnStore, columnRenameMap),
1007
+ this.defs.map((def) =>
1008
+ oldSlugs.indexOf(def.slug) > -1
1009
+ ? {
1010
+ ...def,
1011
+ slug: newSlugs[oldSlugs.indexOf(def.slug)],
1012
+ }
1013
+ : def
1014
+ ),
1015
+ message,
1016
+ TransformType.RenameColumns
1017
+ )
1018
+ }
1019
+
1020
+ dropRowsAt(indices: number[], message?: string): this {
1021
+ const mask = new FilterMask(this.numRows, indices, false)
1022
+ if (mask.isNoop())
1023
+ return this.noopTransform(message ?? `Dropping 0 rows`)
1024
+
1025
+ return this.transform(
1026
+ this.columnStore,
1027
+ this.defs,
1028
+ message ?? `Dropping ${indices.length} rows`,
1029
+ TransformType.FilterRows,
1030
+ mask
1031
+ )
1032
+ }
1033
+
1034
+ replaceCells(
1035
+ columnSlugs: ColumnSlug[],
1036
+ replaceFn: (val: CoreValueType) => CoreValueType
1037
+ ): this {
1038
+ const newStore: CoreColumnStore = { ...this.columnStore }
1039
+ columnSlugs.forEach((slug) => {
1040
+ newStore[slug] = newStore[slug].map(replaceFn)
1041
+ })
1042
+ return this.transform(
1043
+ newStore,
1044
+ this.defs,
1045
+ `Replaced all cells across columns ${columnSlugs.join(" and ")}`,
1046
+ TransformType.UpdateRows
1047
+ )
1048
+ }
1049
+
1050
+ combineColumns(
1051
+ columnSlugs: ColumnSlug[],
1052
+ def: COL_DEF_TYPE,
1053
+ combineFn: (
1054
+ row: Record<ColumnSlug, { value: CoreValueType; time: Time }>,
1055
+ time: Time
1056
+ ) => CoreValueType
1057
+ ): this {
1058
+ if (columnSlugs.length === 0) return this
1059
+ const newStore: CoreColumnStore = { ...this.columnStore }
1060
+ newStore[def.slug] = this.indices.map((index) => {
1061
+ const time = this.timeColumn.valuesIncludingErrorValues[index]
1062
+
1063
+ const row: Record<
1064
+ ColumnSlug,
1065
+ { value: CoreValueType; time: Time }
1066
+ > = {}
1067
+ columnSlugs.forEach((slug) => {
1068
+ row[slug] = {
1069
+ value: this.get(slug).valuesIncludingErrorValues[index],
1070
+ time: this.get(slug).originalTimeColumn
1071
+ .valuesIncludingErrorValues[index] as Time,
1072
+ }
1073
+ })
1074
+
1075
+ return combineFn(row, time as Time)
1076
+ })
1077
+ return this.transform(
1078
+ newStore,
1079
+ [...this.defs, def],
1080
+ `Combined columns '${columnSlugs.join(", ")}' into '${def.slug}'`,
1081
+ TransformType.CombineColumns
1082
+ )
1083
+ }
1084
+
1085
+ replaceNonPositiveCellsForLogScale(columnSlugs: ColumnSlug[] = []): this {
1086
+ return this.replaceCells(columnSlugs, (val) =>
1087
+ typeof val !== "number" || val <= 0
1088
+ ? ErrorValueTypes.InvalidOnALogScale
1089
+ : val
1090
+ )
1091
+ }
1092
+
1093
+ replaceNegativeCellsWithErrorValues(columnSlugs: ColumnSlug[] = []): this {
1094
+ return this.replaceCells(columnSlugs, (val) =>
1095
+ typeof val !== "number" || val < 0
1096
+ ? ErrorValueTypes.InvalidNegativeValue
1097
+ : val
1098
+ )
1099
+ }
1100
+
1101
+ replaceNonNumericCellsWithErrorValues(columnSlugs: ColumnSlug[]): this {
1102
+ return this.replaceCells(columnSlugs, (val) =>
1103
+ !_.isNumber(val) ? ErrorValueTypes.NaNButShouldBeNumber : val
1104
+ )
1105
+ }
1106
+
1107
+ replaceRandomCells(
1108
+ howMany = 1,
1109
+ columnSlugs: ColumnSlug[] = [],
1110
+ seed = Date.now(),
1111
+ replacementGenerator: () => any = (): DroppedForTesting =>
1112
+ ErrorValueTypes.DroppedForTesting
1113
+ ): this {
1114
+ return this.transform(
1115
+ replaceRandomCellsInColumnStore(
1116
+ this.columnStore,
1117
+ howMany,
1118
+ columnSlugs,
1119
+ seed,
1120
+ replacementGenerator
1121
+ ),
1122
+ this.defs,
1123
+ `Replaced a random ${howMany} cells in ${columnSlugs.join(
1124
+ " and "
1125
+ )}`,
1126
+ TransformType.UpdateRows
1127
+ )
1128
+ }
1129
+
1130
+ appendColumns(defs: COL_DEF_TYPE[]): this {
1131
+ return this.transform(
1132
+ this.columnStore,
1133
+ this.defs.concat(defs),
1134
+ `Appended columns ${defs
1135
+ .map((def) => `'${def.slug}'`)
1136
+ .join(" and ")}`,
1137
+ TransformType.AppendColumns
1138
+ )
1139
+ }
1140
+
1141
+ duplicateColumn(slug: ColumnSlug, overrides: COL_DEF_TYPE): this {
1142
+ return this.transform(
1143
+ {
1144
+ ...this.columnStore,
1145
+ [overrides.slug]: this.columnStore[slug],
1146
+ },
1147
+ this.defs.concat([
1148
+ {
1149
+ ...this.get(slug).def,
1150
+ ...overrides,
1151
+ },
1152
+ ]),
1153
+ `Duplicated column '${slug}' to column '${overrides.slug}'`,
1154
+ TransformType.AppendColumns
1155
+ )
1156
+ }
1157
+
1158
+ appendColumnsIfNew(defs: COL_DEF_TYPE[]): this {
1159
+ return this.appendColumns(defs.filter((def) => !this.has(def.slug)))
1160
+ }
1161
+
1162
+ toMatrix(): any[][] {
1163
+ const slugs = this.columnSlugs
1164
+ const rows = this.rows.map((row) =>
1165
+ slugs.map((slug) =>
1166
+ isNotErrorValue(row[slug]) ? row[slug] : undefined
1167
+ )
1168
+ )
1169
+ return [this.columnSlugs, ...rows]
1170
+ }
1171
+
1172
+ // Same as toMatrix, but preserves error types
1173
+ toTypedMatrix(): any[][] {
1174
+ const slugs = this.columnSlugs
1175
+ const rows = this.rows.map((row) => slugs.map((slug) => row[slug]))
1176
+ return [this.columnSlugs, ...rows]
1177
+ }
1178
+
1179
+ concat(tables: CoreTable[], message: string = `Combined tables`): this {
1180
+ const all = [this, ...tables] as CoreTable[]
1181
+ const defs = all.flatMap((table) => table.defs) as COL_DEF_TYPE[]
1182
+ const uniqDefs = _.uniqBy(defs, (def) => def.slug)
1183
+ return this.transform(
1184
+ concatColumnStores(
1185
+ all.map((table) => table.columnStore),
1186
+ uniqDefs.map((def) => def.slug)
1187
+ ),
1188
+ uniqDefs,
1189
+ message,
1190
+ TransformType.Concat
1191
+ )
1192
+ }
1193
+
1194
+ /**
1195
+ * Ensure a row exists for all values in columnSlug1 × columnSlug2 × ...
1196
+ *
1197
+ * For example, if we have a table:
1198
+ *
1199
+ * ```
1200
+ * entityName, year, …
1201
+ * UK, 2000, …
1202
+ * UK, 2005, …
1203
+ * USA, 2003, …
1204
+ * ```
1205
+ *
1206
+ * After `complete(["entityName", "year"])`, we'd get:
1207
+ *
1208
+ * ```
1209
+ * entityName, year, …
1210
+ * UK, 2000, …
1211
+ * UK, 2003, …
1212
+ * UK, 2005, …
1213
+ * USA, 2000, …
1214
+ * USA, 2003, …
1215
+ * USA, 2005, …
1216
+ * ```
1217
+ *
1218
+ */
1219
+ complete(columnSlugs: [ColumnSlug, ColumnSlug]): this {
1220
+ if (columnSlugs.length !== 2)
1221
+ throw new Error("Can only run complete() for exactly 2 columns")
1222
+
1223
+ const [slug1, slug2] = columnSlugs
1224
+ const col1 = this.get(slug1)
1225
+ const col2 = this.get(slug2)
1226
+
1227
+ // The output table will have exactly this many rows, since we assume that [col1, col2] are primary keys
1228
+ // (i.e. there are no two rows with the same key), and every combination that doesn't exist yet we will add.
1229
+ const cartesianProductSize = col1.numUniqs * col2.numUniqs
1230
+ if (this.numRows >= cartesianProductSize) {
1231
+ if (this.numRows > cartesianProductSize)
1232
+ throw new Error("Table has more rows than expected")
1233
+
1234
+ // Table is already complete
1235
+ return this
1236
+ }
1237
+
1238
+ // Map that points from a value in col1 to a set of values in col2.
1239
+ // It's filled with all the values that already exist in the table, so we
1240
+ // can later take the difference.
1241
+ const existingRowValues = new Map<CoreValueType, Set<CoreValueType>>()
1242
+ for (const index of this.indices) {
1243
+ const val1 = col1.values[index]
1244
+ const val2 = col2.values[index]
1245
+ if (!existingRowValues.has(val1))
1246
+ existingRowValues.set(val1, new Set([val2]))
1247
+ else existingRowValues.get(val1)!.add(val2)
1248
+ }
1249
+
1250
+ // The below code should be as performant as possible, since it's often iterating over hundreds of thousands of rows.
1251
+ // The below implementation has been benchmarked against a few alternatives (using flatMap, map, and Array.from), and
1252
+ // is the fastest.
1253
+ // See https://jsperf.app/zudoye.
1254
+ const rowsToAddCol1: CoreValueType[] = []
1255
+ const rowsToAddCol2: CoreValueType[] = []
1256
+ const col2UniqValuesCount = col2.uniqValuesAsSet.size
1257
+ // Add rows for all combinations of values that are not contained in `existingRowValues`.
1258
+ for (const val1 of col1.uniqValuesAsSet) {
1259
+ const existingVals2 = existingRowValues.get(val1)
1260
+
1261
+ // perf: if all values in col2 are already present for this value in col1, skip
1262
+ // this iteration. This is a relatively common case, so we can save some time.
1263
+ if (existingVals2?.size === col2UniqValuesCount) continue
1264
+
1265
+ // Find the values in col2 that need to be inserted for this value in col1: col2.uniqValuesAsSet - existingVals2
1266
+ const diff = new Set(col2.uniqValuesAsSet)
1267
+ for (const val2 of existingVals2 || []) diff.delete(val2)
1268
+
1269
+ for (const val2 of diff) {
1270
+ rowsToAddCol1.push(val1)
1271
+ rowsToAddCol2.push(val2)
1272
+ }
1273
+ }
1274
+ const appendColumnStore: CoreColumnStore = {
1275
+ [slug1]: rowsToAddCol1,
1276
+ [slug2]: rowsToAddCol2,
1277
+ }
1278
+ const appendTable = new (this.constructor as typeof CoreTable)(
1279
+ appendColumnStore,
1280
+ this.defs,
1281
+ { parent: this }
1282
+ )
1283
+
1284
+ return this.concat(
1285
+ [appendTable],
1286
+ `Append missing combos of ${columnSlugs}`
1287
+ )
1288
+ }
1289
+
1290
+ static getPreposition(col: TimeColumn | CoreColumn): string {
1291
+ return col instanceof TimeColumn ? col.preposition : "in"
1292
+ }
1293
+ }
1294
+
1295
+ class FilterMask {
1296
+ private mask: boolean[]
1297
+ private numRows: number
1298
+ constructor(
1299
+ numRows: number,
1300
+ input: boolean[] | number[],
1301
+ keepThese = true
1302
+ ) {
1303
+ this.numRows = numRows
1304
+ if (typeof input[0] === "boolean") this.mask = input as boolean[]
1305
+ else {
1306
+ const set = new Set(input as number[])
1307
+ this.mask = _.range(0, numRows).map((index) =>
1308
+ set.has(index) ? keepThese : !keepThese
1309
+ )
1310
+ }
1311
+ }
1312
+
1313
+ isNoop(): boolean {
1314
+ return this.mask.every((value) => value)
1315
+ }
1316
+
1317
+ apply(columnStore: CoreColumnStore): CoreColumnStore {
1318
+ const columnsObject: CoreColumnStore = {}
1319
+ const keepIndexes: number[] = []
1320
+ for (let i = 0; i < this.numRows; i++) {
1321
+ if (this.mask[i]) keepIndexes.push(i)
1322
+ }
1323
+
1324
+ // Optimization: early return if we're keeping all rows
1325
+ if (keepIndexes.length === this.numRows) {
1326
+ return columnStore
1327
+ }
1328
+
1329
+ Object.keys(columnStore).forEach((slug) => {
1330
+ const originalColumn = columnStore[slug]
1331
+ const newColumn: CoreValueType[] = new Array(keepIndexes.length)
1332
+ for (let i = 0; i < keepIndexes.length; i++) {
1333
+ newColumn[i] = originalColumn[keepIndexes[i]]
1334
+ }
1335
+
1336
+ columnsObject[slug] = newColumn
1337
+ })
1338
+ return columnsObject
1339
+ }
1340
+ }
1341
+
1342
+ /**
1343
+ * Allows you to store your column definitions in CSV/TSV like:
1344
+ * slug,name,type etc.
1345
+ *
1346
+ * todo: define all column def property types
1347
+ */
1348
+ export const columnDefinitionsFromInput = <T extends CoreRow>(
1349
+ input: CoreTableInputOption
1350
+ ): T[] =>
1351
+ new CoreTable<T>(input).columnFilter(
1352
+ "slug",
1353
+ (value) => !!value,
1354
+ "Keep only column defs with a slug"
1355
+ ).rows