@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,1332 @@
1
+ import * as _ from "lodash-es"
2
+ import * as React from "react"
3
+ import { computed, observable, action, makeObservable } from "mobx"
4
+ import { observer } from "mobx-react"
5
+ import classnames from "classnames"
6
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
7
+ import {
8
+ faArrowDownLong,
9
+ faArrowUpLong,
10
+ faInfoCircle,
11
+ } from "@fortawesome/free-solid-svg-icons"
12
+ import { scaleLinear } from "d3-scale"
13
+ import { extent } from "d3-array"
14
+ import { line } from "d3-shape"
15
+ import {
16
+ SortOrder,
17
+ Time,
18
+ EntityName,
19
+ OwidTableSlugs,
20
+ OwidVariableRoundingMode,
21
+ OwidVariableRow,
22
+ } from "../../types/index.js"
23
+ import { OwidTable, CoreColumn } from "../../core-table/index.js"
24
+ import {
25
+ valuesByEntityAtTimes,
26
+ es6mapValues,
27
+ exposeInstanceOnWindow,
28
+ DataValue,
29
+ Bounds,
30
+ TickFormattingOptions,
31
+ Tippy,
32
+ excludeUndefined,
33
+ joinTitleFragments,
34
+ FuzzySearch,
35
+ } from "../../utils/index.js"
36
+ import { SelectionArray } from "../selection/SelectionArray"
37
+ import {
38
+ DEFAULT_GRAPHER_BOUNDS,
39
+ DEFAULT_GRAPHER_ENTITY_TYPE,
40
+ SVG_STYLE_PROPS,
41
+ } from "../core/GrapherConstants"
42
+ import * as R from "remeda"
43
+ import { makeSelectionArray } from "../chart/ChartUtils"
44
+ import { isEntityRegionType } from "../core/EntitiesByRegionType"
45
+ import { NoDataModal } from "../noDataModal/NoDataModal"
46
+ import {
47
+ DataTableColumnKey,
48
+ DisplayDataTableDimension,
49
+ DataTableRow,
50
+ DataTableDimension,
51
+ DataTableColumnDefinition,
52
+ DataTableValuesForEntity,
53
+ RangeValuesForEntity,
54
+ RangeColumnKey,
55
+ PointValuesForEntity,
56
+ PointColumnKey,
57
+ SparklineHighlight,
58
+ TargetTimeMode,
59
+ MinimalOwidRow,
60
+ DataTableConfig,
61
+ DataTableSortState,
62
+ DataTableState,
63
+ DimensionSortIndex,
64
+ DataTableManager,
65
+ CommonDataTableFilter,
66
+ COMMON_DATA_TABLE_FILTERS,
67
+ DataTableFilter,
68
+ SparklineKey,
69
+ } from "./DataTableConstants"
70
+ import { GRAY_30 } from "../color/ColorConstants"
71
+
72
+ const ENTITY_SORT_INDEX = -1
73
+
74
+ const DEFAULT_SORT_STATE: DataTableSortState = {
75
+ dimIndex: ENTITY_SORT_INDEX,
76
+ columnKey: undefined,
77
+ order: SortOrder.asc,
78
+ }
79
+
80
+ const columnNameByType: Record<DataTableColumnKey, string> = {
81
+ single: "Value",
82
+ start: "Start",
83
+ end: "End",
84
+ delta: "Absolute Change",
85
+ deltaRatio: "Relative Change",
86
+ sparkline: "Sparkline",
87
+ }
88
+
89
+ const inverseSortOrder = (order: SortOrder): SortOrder =>
90
+ order === SortOrder.asc ? SortOrder.desc : SortOrder.asc
91
+
92
+ interface DataTableProps {
93
+ manager: DataTableManager
94
+ bounds?: Bounds
95
+ }
96
+
97
+ @observer
98
+ export class DataTable extends React.Component<DataTableProps> {
99
+ private storedState: DataTableState = {
100
+ sort: DEFAULT_SORT_STATE,
101
+ }
102
+
103
+ constructor(props: DataTableProps) {
104
+ super(props)
105
+ makeObservable<DataTable, "storedState">(this, {
106
+ storedState: observable,
107
+ })
108
+ }
109
+
110
+ @computed get manager(): DataTableManager {
111
+ return this.props.manager
112
+ }
113
+
114
+ @computed private get selectionArray(): SelectionArray {
115
+ return makeSelectionArray(this.manager.dataTableSelection)
116
+ }
117
+
118
+ @computed private get tableConfig(): DataTableConfig {
119
+ return this.manager.dataTableConfig
120
+ }
121
+
122
+ @computed private get timelineMinTime(): Time | undefined {
123
+ return this.manager.closestTimelineMinTime
124
+ }
125
+
126
+ @computed private get timelineMaxTime(): Time | undefined {
127
+ return this.manager.closestTimelineMaxTime
128
+ }
129
+
130
+ @computed get table(): OwidTable {
131
+ let table = this.manager.filteredTableForDisplay
132
+
133
+ // make sure the given table doesn't contain any rows outside of the time range
134
+ table = table.filterByTimeRange(
135
+ this.manager.closestTimelineMinTime ?? -Infinity,
136
+ this.manager.closestTimelineMaxTime ?? Infinity
137
+ )
138
+
139
+ return table
140
+ }
141
+
142
+ @computed private get tableState(): DataTableState {
143
+ return { sort: this.sortState }
144
+ }
145
+
146
+ @computed private get sortState(): DataTableSortState {
147
+ let { dimIndex, columnKey, order } = {
148
+ ...DEFAULT_SORT_STATE,
149
+ ...this.storedState.sort,
150
+ }
151
+
152
+ // If not sorted by entity, then make sure the index of the chosen column exists
153
+ dimIndex = Math.min(dimIndex, this.table.numColumns - 1)
154
+ if (dimIndex !== ENTITY_SORT_INDEX) {
155
+ const availableColumns = this.dataTableDimensionsWithValues[
156
+ dimIndex
157
+ ].columnDefinitions.map((colDef) => colDef.key)
158
+ if (
159
+ columnKey === undefined ||
160
+ !availableColumns.includes(columnKey)
161
+ )
162
+ columnKey = availableColumns[0]
163
+ }
164
+
165
+ return {
166
+ dimIndex,
167
+ columnKey,
168
+ order,
169
+ }
170
+ }
171
+
172
+ @computed private get entityType(): string {
173
+ return this.manager.entityType ?? DEFAULT_GRAPHER_ENTITY_TYPE
174
+ }
175
+
176
+ @computed private get sortValueMapper(): (
177
+ row: DataTableRow
178
+ ) => number | string {
179
+ const { dimIndex: sortIndex, columnKey, order } = this.tableState.sort
180
+ if (sortIndex === ENTITY_SORT_INDEX)
181
+ return (row): string => row.entityName
182
+
183
+ return (row): string | number => {
184
+ const dv = row.values[sortIndex] as DataTableValuesForEntity
185
+
186
+ let value: number | string | undefined
187
+ if (dv) {
188
+ if (isSingleValue(dv)) {
189
+ value = dv.single?.value
190
+ } else if (
191
+ isRangeValue(dv) &&
192
+ columnKey !== undefined &&
193
+ isRangeColumnKey(columnKey)
194
+ ) {
195
+ value = dv[columnKey]?.value
196
+ }
197
+ }
198
+
199
+ // We always want undefined values to be last
200
+ if (
201
+ value === undefined ||
202
+ (typeof value === "number" &&
203
+ (!isFinite(value) || isNaN(value)))
204
+ )
205
+ return order === SortOrder.asc ? Infinity : -Infinity
206
+
207
+ return value
208
+ }
209
+ }
210
+
211
+ @computed private get hasSubheaders(): boolean {
212
+ return (
213
+ !this.hasDimensionHeaders ||
214
+ this.dataTableDimensionsWithValues.some(
215
+ (header) => header.columnDefinitions.length > 1
216
+ )
217
+ )
218
+ }
219
+
220
+ @action.bound private updateSort(
221
+ dimIndex: DimensionSortIndex,
222
+ columnKey?: DataTableColumnKey
223
+ ): void {
224
+ const { sort } = this.tableState
225
+ const order =
226
+ sort.dimIndex === dimIndex && sort.columnKey === columnKey
227
+ ? inverseSortOrder(sort.order)
228
+ : dimIndex === ENTITY_SORT_INDEX
229
+ ? SortOrder.asc
230
+ : SortOrder.desc
231
+
232
+ this.storedState.sort.dimIndex = dimIndex
233
+ this.storedState.sort.columnKey = columnKey
234
+ this.storedState.sort.order = order
235
+ }
236
+
237
+ private get entityHeaderText(): string {
238
+ return _.capitalize(this.entityType)
239
+ }
240
+
241
+ private get entityHeader(): React.ReactElement {
242
+ const { sort } = this.tableState
243
+ return (
244
+ <ColumnHeader
245
+ key="entity"
246
+ sortable={this.entityCount > 1}
247
+ sortedCol={sort.dimIndex === ENTITY_SORT_INDEX}
248
+ sortOrder={sort.order}
249
+ onClick={(): void => this.updateSort(ENTITY_SORT_INDEX)}
250
+ headerText={this.entityHeaderText}
251
+ colType="entity"
252
+ />
253
+ )
254
+ }
255
+
256
+ @computed private get hasDimensionHeaders(): boolean {
257
+ return this.dataTableDimensionsWithValues.length > 1
258
+ }
259
+
260
+ // If the table has a single data column, we move the data column
261
+ // closer to the entity column to make it easier to read the table
262
+ @computed private get singleDataColumnStyle():
263
+ | { minWidth: number; contentMaxWidth: number }
264
+ | undefined {
265
+ // no need to do this on mobile
266
+ if (this.manager.isNarrow) return
267
+
268
+ const hasSingleDataColumn =
269
+ this.displayDimensions.length === 1 &&
270
+ this.displayDimensions[0].columnDefinitions.length === 1
271
+
272
+ if (!hasSingleDataColumn) return
273
+
274
+ // header text
275
+ const dimension = this.displayDimensions[0]
276
+ const column = this.displayDimensions[0].columnDefinitions[0]
277
+ const headerText = this.subheaderText(column, dimension)
278
+
279
+ // display values
280
+ const values = excludeUndefined(
281
+ this.displayRows.map(
282
+ (row) => (row?.values[0] as PointValuesForEntity).single
283
+ )
284
+ )
285
+
286
+ const accessor = (row: MinimalOwidRow): number | undefined =>
287
+ typeof row.value === "string" ? row.value.length : row.value
288
+ const maxValue = _.maxBy(values, accessor)
289
+ const minValue = _.minBy(values, accessor)
290
+
291
+ const measureWidth = (text: string): number =>
292
+ Bounds.forText(text, { fontSize: 14 }).width
293
+
294
+ // in theory, we should be measuring the length of all values
295
+ // but we might have a lot of values, so we just measure the length
296
+ // of the min and max values as a proxy
297
+ const contentMaxWidth = Math.ceil(
298
+ Math.max(
299
+ measureWidth(maxValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon
300
+ measureWidth(minValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon
301
+ measureWidth(headerText) + 26 // 26px accounts for the sort icon
302
+ )
303
+ )
304
+
305
+ // minimum width of the column
306
+ const minWidth = 0.66 * this.bounds.width
307
+
308
+ // only do this if there is an actual need
309
+ if (minWidth - contentMaxWidth < 320) return
310
+
311
+ return { minWidth, contentMaxWidth }
312
+ }
313
+
314
+ private get dimensionHeaders(): React.ReactElement[] | null {
315
+ const { sort } = this.tableState
316
+ return this.displayDimensions.map((dim, dimIndex) => {
317
+ const { coreTableColumn, display } = dim
318
+ const singleColumn = dim.columnDefinitions.find(
319
+ (column) => column.key === PointColumnKey.single
320
+ )
321
+ const targetTime = singleColumn?.targetTime
322
+
323
+ const title = _.upperFirst(display.columnName.title)
324
+ const titleFragments = joinTitleFragments(
325
+ display.columnName.attributionShort,
326
+ display.columnName.titleVariant
327
+ )
328
+
329
+ const dimensionHeaderText = (
330
+ <React.Fragment>
331
+ <div
332
+ className="name"
333
+ title={
334
+ titleFragments
335
+ ? `${title} – ${titleFragments}`
336
+ : title
337
+ }
338
+ >
339
+ <span className="title-text">{title} </span>
340
+ <span className="title-fragments">
341
+ {titleFragments}
342
+ </span>
343
+ </div>
344
+ <div className="description">
345
+ <span className="unit">{display.unit}</span>
346
+ <span>
347
+ {display.unit && targetTime !== undefined && ","}
348
+ </span>{" "}
349
+ <span className="time">
350
+ {targetTime !== undefined &&
351
+ coreTableColumn.formatTime(targetTime)}
352
+ </span>
353
+ </div>
354
+ </React.Fragment>
355
+ )
356
+
357
+ const onClick = dim.sortable
358
+ ? (): void => this.updateSort(dimIndex, PointColumnKey.single)
359
+ : undefined
360
+
361
+ const props = {
362
+ sortable: dim.sortable,
363
+ sortedCol: dim.sortable && sort.dimIndex === dimIndex,
364
+ sortOrder: sort.order,
365
+ onClick,
366
+ colSpan: dim.columnDefinitions.length,
367
+ headerText: dimensionHeaderText,
368
+ colType: "dimension" as const,
369
+ }
370
+
371
+ return <ColumnHeader key={coreTableColumn.slug} {...props} />
372
+ })
373
+ }
374
+
375
+ private subheaderText(
376
+ column: DataTableColumnDefinition,
377
+ dimension: DisplayDataTableDimension
378
+ ): string {
379
+ const col = dimension.coreTableColumn
380
+
381
+ if (column.key === SparklineKey.sparkline) {
382
+ const minTime = col.formatTime(this.timelineMinTime!)
383
+ const maxTime = col.formatTime(this.timelineMaxTime!)
384
+ return `${minTime}–${maxTime}`
385
+ }
386
+
387
+ return isDeltaColumn(column.key)
388
+ ? columnNameByType[column.key]
389
+ : col.formatTime(column.targetTime!)
390
+ }
391
+
392
+ private get dimensionSubheaders(): React.ReactElement[][] {
393
+ const { sort } = this.tableState
394
+ return this.displayDimensions.map((dim, dimIndex) =>
395
+ dim.columnDefinitions.map((column, colIndex) => {
396
+ const headerText = this.subheaderText(column, dim)
397
+ const onClick = column.sortable
398
+ ? (): void => this.updateSort(dimIndex, column.key)
399
+ : undefined
400
+ return (
401
+ <ColumnHeader
402
+ key={column.key}
403
+ sortable={column.sortable}
404
+ sortedCol={
405
+ sort.dimIndex === dimIndex &&
406
+ sort.columnKey === column.key
407
+ }
408
+ sortOrder={sort.order}
409
+ onClick={onClick}
410
+ headerText={headerText}
411
+ colType="subdimension"
412
+ classNames={classnames({
413
+ "subdimension-first": colIndex === 0,
414
+ })}
415
+ minWidth={this.singleDataColumnStyle?.minWidth}
416
+ contentMaxWidth={
417
+ this.singleDataColumnStyle?.contentMaxWidth
418
+ }
419
+ />
420
+ )
421
+ })
422
+ )
423
+ }
424
+
425
+ private get headerRow(): React.ReactElement {
426
+ const { hasDimensionHeaders, hasSubheaders } = this
427
+ return hasDimensionHeaders && hasSubheaders ? (
428
+ <>
429
+ <tr>
430
+ <th className="above-entity" />
431
+ {this.dimensionHeaders}
432
+ </tr>
433
+ <tr>
434
+ {this.entityHeader}
435
+ {this.dimensionSubheaders}
436
+ </tr>
437
+ </>
438
+ ) : (
439
+ <tr>
440
+ {this.entityHeader}
441
+ {hasSubheaders
442
+ ? this.dimensionSubheaders
443
+ : this.dimensionHeaders}
444
+ </tr>
445
+ )
446
+ }
447
+
448
+ private renderValueCellContent({
449
+ columnDefinition,
450
+ valuesForEntity,
451
+ formatTime,
452
+ }: {
453
+ columnDefinition: DataTableColumnDefinition
454
+ valuesForEntity?: DataTableValuesForEntity
455
+ formatTime: (time: Time) => string
456
+ }): React.ReactElement | null {
457
+ if (!valuesForEntity) return null
458
+ if (!(columnDefinition.key in valuesForEntity)) return null
459
+
460
+ const value = getValueForEntityByKey(
461
+ valuesForEntity,
462
+ columnDefinition.key
463
+ )
464
+ if (!value) return null
465
+
466
+ return (
467
+ <>
468
+ <ClosestTimeNotice
469
+ value={value}
470
+ columnDefinition={columnDefinition}
471
+ formatTime={formatTime}
472
+ />
473
+ <span>{value.displayValue}</span>
474
+ </>
475
+ )
476
+ }
477
+
478
+ private renderSparklineCellContent({
479
+ columnDefinition,
480
+ valuesForEntity,
481
+ isProjection,
482
+ }: {
483
+ columnDefinition: DataTableColumnDefinition
484
+ valuesForEntity?: DataTableValuesForEntity
485
+ isProjection?: boolean
486
+ }): React.ReactElement | null {
487
+ if (
488
+ columnDefinition.key !== SparklineKey.sparkline ||
489
+ !valuesForEntity?.sparkline
490
+ )
491
+ return null
492
+
493
+ const highlights: SparklineHighlight[] = []
494
+
495
+ const start = isRangeValue(valuesForEntity)
496
+ ? valuesForEntity.start
497
+ : valuesForEntity.single
498
+ const startTime = start?.time ?? this.targetTimes?.[0]
499
+
500
+ const end = isRangeValue(valuesForEntity)
501
+ ? valuesForEntity.end
502
+ : valuesForEntity.single
503
+ const endTime = end?.time ?? this.targetTimes?.[1]
504
+
505
+ // Add a highlight for the start time
506
+ if (startTime !== undefined) {
507
+ const value =
508
+ typeof start?.value === "string" ? undefined : start?.value
509
+
510
+ const showMarker =
511
+ this.manager.timelineDragTarget === "start" ||
512
+ this.manager.timelineDragTarget === "both"
513
+
514
+ highlights.push({ time: startTime, value, showMarker })
515
+ }
516
+
517
+ // Add a highlight for the end time
518
+ if (endTime !== undefined && endTime !== startTime) {
519
+ const value =
520
+ typeof end?.value === "string" ? undefined : end?.value
521
+
522
+ const showMarker =
523
+ this.manager.timelineDragTarget === "end" ||
524
+ this.manager.timelineDragTarget === "both"
525
+
526
+ highlights.push({ time: endTime, value, showMarker })
527
+ }
528
+
529
+ return (
530
+ <Sparkline
531
+ owidRows={valuesForEntity.sparkline}
532
+ minTime={this.timelineMinTime!}
533
+ maxTime={this.timelineMaxTime!}
534
+ highlights={highlights}
535
+ strokeStyle={isProjection ? "dotted" : "solid"}
536
+ />
537
+ )
538
+ }
539
+
540
+ private renderEntityRow(
541
+ row: DataTableRow,
542
+ dimensions: DisplayDataTableDimension[]
543
+ ): React.ReactElement {
544
+ return (
545
+ <tr key={row.entityName}>
546
+ <td key="entity" className={classnames({ entity: true })}>
547
+ {row.entityName}
548
+ </td>
549
+
550
+ {row.values.map((valuesForEntity, dimIndex) => {
551
+ const dimension = dimensions[dimIndex]
552
+ const { isProjection } = dimension.coreTableColumn
553
+
554
+ return dimension.columnDefinitions.map(
555
+ (columnDefinition, colIndex) => {
556
+ const key = `${dimIndex}-${colIndex}`
557
+ const formatTime = (time: Time): string =>
558
+ dimension.coreTableColumn.formatTime(time)
559
+
560
+ if (columnDefinition.key === SparklineKey.sparkline)
561
+ return (
562
+ <ValueCell
563
+ key={key}
564
+ columnKey={columnDefinition.key}
565
+ isFirstColumn={colIndex === 0}
566
+ >
567
+ {this.renderSparklineCellContent({
568
+ columnDefinition,
569
+ valuesForEntity,
570
+ isProjection,
571
+ })}
572
+ </ValueCell>
573
+ )
574
+
575
+ return (
576
+ <ValueCell
577
+ key={key}
578
+ columnKey={columnDefinition.key}
579
+ isFirstColumn={colIndex === 0}
580
+ maxWidth={
581
+ this.singleDataColumnStyle
582
+ ?.contentMaxWidth
583
+ }
584
+ >
585
+ {this.renderValueCellContent({
586
+ columnDefinition,
587
+ valuesForEntity,
588
+ formatTime,
589
+ })}
590
+ </ValueCell>
591
+ )
592
+ }
593
+ )
594
+ })}
595
+ </tr>
596
+ )
597
+ }
598
+
599
+ @computed get bounds(): Bounds {
600
+ return this.props.bounds ?? DEFAULT_GRAPHER_BOUNDS
601
+ }
602
+
603
+ @computed private get tableCaption(): React.ReactElement | null {
604
+ if (this.hasDimensionHeaders) return null
605
+ if (this.displayDimensions.length === 0) return null
606
+
607
+ const singleDimension = this.displayDimensions[0]
608
+ const titleFragments =
609
+ singleDimension.display.columnName.attributionShort ||
610
+ singleDimension.display.columnName.titleVariant
611
+ ? joinTitleFragments(
612
+ singleDimension.display.columnName.attributionShort,
613
+ singleDimension.display.columnName.titleVariant
614
+ )
615
+ : undefined
616
+
617
+ return singleDimension ? (
618
+ <div className="caption">
619
+ {singleDimension.display.columnName.title}{" "}
620
+ <span className="title-fragments">
621
+ {titleFragments}
622
+ {singleDimension.display.unit &&
623
+ ` (${singleDimension.display.unit})`}
624
+ </span>
625
+ </div>
626
+ ) : null
627
+ }
628
+
629
+ private renderNoDataModal(): React.ReactElement {
630
+ return (
631
+ <svg
632
+ width={this.bounds.width}
633
+ height={this.bounds.height}
634
+ style={SVG_STYLE_PROPS}
635
+ >
636
+ <NoDataModal
637
+ manager={this.manager}
638
+ bounds={this.bounds}
639
+ message={`No ${this.entityType} matches this query`}
640
+ helpText="Try checking for typos or searching for something else"
641
+ hideTextOutline
642
+ />
643
+ </svg>
644
+ )
645
+ }
646
+
647
+ override render(): React.ReactElement | null {
648
+ if (this.sortedDisplayRows.length === 0) return this.renderNoDataModal()
649
+
650
+ return (
651
+ <div className="DataTable">
652
+ {this.tableCaption}
653
+ <div className="table-wrapper">
654
+ <table>
655
+ <thead>{this.headerRow}</thead>
656
+ <tbody>
657
+ {this.sortedDisplayRows.map((row) =>
658
+ this.renderEntityRow(
659
+ row,
660
+ this.displayDimensions
661
+ )
662
+ )}
663
+ </tbody>
664
+ </table>
665
+ </div>
666
+ </div>
667
+ )
668
+ }
669
+
670
+ @computed private get loadedWithData(): boolean {
671
+ return this.columnsToShow.length > 0
672
+ }
673
+
674
+ private readonly AUTO_SELECTION_THRESHOLD_PERCENTAGE = 0.5
675
+
676
+ /**
677
+ * If the user or the editor hasn't specified a start, auto-select a start time
678
+ * where AUTO_SELECTION_THRESHOLD_PERCENTAGE of the entities have values.
679
+ */
680
+ @computed get autoSelectedStartTime(): number | undefined {
681
+ let autoSelectedStartTime: number | undefined = undefined
682
+
683
+ if (!this.loadedWithData) return undefined
684
+
685
+ const numEntitiesInTable = this.entityNames.length
686
+
687
+ this.columnsToShow.forEach((column): boolean => {
688
+ const numberOfEntitiesWithDataSortedByTime = _.sortBy(
689
+ Object.entries(R.countBy(column.uniqTimesAsc, R.identity())),
690
+ ([time, _count]) => parseInt(time)
691
+ )
692
+
693
+ const firstTimeWithSufficientData =
694
+ numberOfEntitiesWithDataSortedByTime.find((time) => {
695
+ const numEntitiesWithData = time[1]
696
+ const percentEntitiesWithData =
697
+ numEntitiesWithData / numEntitiesInTable
698
+ return (
699
+ percentEntitiesWithData >=
700
+ this.AUTO_SELECTION_THRESHOLD_PERCENTAGE
701
+ )
702
+ })?.[0]
703
+
704
+ if (firstTimeWithSufficientData) {
705
+ autoSelectedStartTime = parseInt(firstTimeWithSufficientData)
706
+ return false
707
+ }
708
+ return true
709
+ })
710
+
711
+ return autoSelectedStartTime
712
+ }
713
+
714
+ @computed private get columnsToShow(): CoreColumn[] {
715
+ const slugs = this.manager.dataTableSlugs ?? []
716
+ if (slugs.length)
717
+ return slugs
718
+ .map((slug: string) => {
719
+ const col = this.table.get(slug)
720
+ if (!col)
721
+ console.warn(`Warning: column '${slug}' not found`)
722
+ return col
723
+ })
724
+ .filter((col) => col)
725
+
726
+ const skips = new Set(Object.keys(OwidTableSlugs))
727
+ return this.table.columnsAsArray.filter(
728
+ (column) =>
729
+ !skips.has(column.slug) &&
730
+ // dim.property !== "color" &&
731
+ (column.display?.includeInTable ?? true)
732
+ )
733
+ }
734
+
735
+ @computed private get availableEntityNames(): EntityName[] {
736
+ return _.union(
737
+ ...this.columnsToShow.map(
738
+ (col) => this.table.get(col.slug).uniqEntityNames
739
+ )
740
+ )
741
+ }
742
+
743
+ @computed get fuzzy(): FuzzySearch<string> {
744
+ return FuzzySearch.withKey(
745
+ this.availableEntityNames,
746
+ (entityName) => entityName
747
+ )
748
+ }
749
+
750
+ @computed private get entityNames(): EntityName[] {
751
+ if (!this.tableConfig.search) return this.availableEntityNames
752
+ return this.fuzzy.search(this.tableConfig.search)
753
+ }
754
+
755
+ @computed private get entityCount(): number {
756
+ return this.entityNames.length
757
+ }
758
+
759
+ @computed private get isSortable(): boolean {
760
+ return this.entityCount > 1
761
+ }
762
+
763
+ override componentDidMount(): void {
764
+ exposeInstanceOnWindow(this, "dataTable")
765
+ }
766
+
767
+ private formatValue(
768
+ column: CoreColumn,
769
+ value: number | string | undefined,
770
+ formattingOverrides?: TickFormattingOptions
771
+ ): string | undefined {
772
+ if (value === undefined) return undefined
773
+ return column.formatValueShort(value, {
774
+ roundingMode: OwidVariableRoundingMode.decimalPlaces,
775
+ numberAbbreviation: false,
776
+ trailingZeroes: true,
777
+ useNoBreakSpace: true,
778
+ ...formattingOverrides,
779
+ })
780
+ }
781
+
782
+ @computed get targetTimes(): [number] | [number, number] | undefined {
783
+ const { startTime, endTime } = this.manager
784
+ if (startTime === undefined || endTime === undefined) return undefined
785
+
786
+ if (startTime !== endTime) return [startTime, endTime]
787
+ return [endTime]
788
+ }
789
+
790
+ private getTargetTimesForColumn(
791
+ coreTableColumn: CoreColumn
792
+ ): [number] | [number, number] {
793
+ // Respect the column's target time if it's set
794
+ if (coreTableColumn.def.targetTime !== undefined)
795
+ return [coreTableColumn.def.targetTime]
796
+
797
+ // Otherwise, use the table's target times
798
+ if (this.targetTimes !== undefined) return this.targetTimes
799
+
800
+ return [coreTableColumn.maxTime]
801
+ }
802
+
803
+ @computed get dataTableDimensionsWithValues(): DataTableDimension[] {
804
+ return this.columnsToShow.map((coreTableColumn) => {
805
+ const targetTimes = this.getTargetTimesForColumn(coreTableColumn)
806
+ const targetTimeMode =
807
+ targetTimes.length < 2
808
+ ? TargetTimeMode.point
809
+ : TargetTimeMode.range
810
+
811
+ // Get values for the given target times and apply tolerance
812
+ const targetValuesByEntity = this.interpolateTargetValues({
813
+ coreTableColumn,
814
+ targetTimes,
815
+ })
816
+
817
+ // Add absolute and relative change columns if necessary
818
+ const valuesByEntityName =
819
+ this.calculateDataValuesForTargetTimeMode({
820
+ targetValuesByEntity,
821
+ coreTableColumn,
822
+ targetTimeMode,
823
+ })
824
+
825
+ // Add data for sparklines
826
+ if (this.columnHasSparkline(coreTableColumn)) {
827
+ for (const [entityName, values] of valuesByEntityName) {
828
+ values.sparkline =
829
+ coreTableColumn.owidRowsByEntityName.get(entityName)
830
+ }
831
+ }
832
+
833
+ // Construct column definitions for the given target time mode
834
+ const columnDefinitions = this.constructColumnDefinitions({
835
+ coreTableColumn,
836
+ targetTimes,
837
+ targetTimeMode,
838
+ })
839
+
840
+ return { columnDefinitions, valuesByEntityName, coreTableColumn }
841
+ })
842
+ }
843
+
844
+ private columnHasSparkline(coreTableColumn: CoreColumn): boolean {
845
+ return (
846
+ this.timelineMinTime !== undefined &&
847
+ this.timelineMaxTime !== undefined &&
848
+ this.timelineMinTime !== this.timelineMaxTime &&
849
+ coreTableColumn.hasNumberFormatting &&
850
+ // For columns with a target time, the data table is fixed at that time.
851
+ // It thus doesn't make sense to show a sparkline
852
+ coreTableColumn.def.targetTime === undefined
853
+ )
854
+ }
855
+
856
+ private constructColumnDefinitions({
857
+ coreTableColumn,
858
+ targetTimes,
859
+ targetTimeMode,
860
+ }: {
861
+ coreTableColumn: CoreColumn
862
+ targetTimes: number[]
863
+ targetTimeMode: TargetTimeMode
864
+ }): DataTableColumnDefinition[] {
865
+ // Inject delta columns if the data is numerical and we have start & end
866
+ // values to compare in the table. One column for absolute difference,
867
+ // another for % difference.
868
+ const deltaColumns: DataTableColumnDefinition[] = []
869
+ if (coreTableColumn.hasNumberFormatting) {
870
+ if (targetTimeMode === TargetTimeMode.range) {
871
+ const { tableDisplay = {} } = coreTableColumn.display ?? {}
872
+ if (!tableDisplay.hideAbsoluteChange)
873
+ deltaColumns.push({
874
+ key: RangeColumnKey.delta,
875
+ sortable: this.isSortable,
876
+ })
877
+ if (!tableDisplay.hideRelativeChange)
878
+ deltaColumns.push({
879
+ key: RangeColumnKey.deltaRatio,
880
+ sortable: this.isSortable,
881
+ })
882
+ }
883
+ }
884
+
885
+ const valueColumns: DataTableColumnDefinition[] = targetTimes.map(
886
+ (targetTime, index) => ({
887
+ key:
888
+ targetTimeMode === TargetTimeMode.range
889
+ ? index === 0
890
+ ? RangeColumnKey.start
891
+ : RangeColumnKey.end
892
+ : PointColumnKey.single,
893
+ targetTime,
894
+ sortable: this.isSortable,
895
+ })
896
+ )
897
+
898
+ // Show a column with sparklines if appropriate
899
+ const sparklineColumn = this.columnHasSparkline(coreTableColumn)
900
+ ? { key: SparklineKey.sparkline, sortable: false }
901
+ : undefined
902
+
903
+ return excludeUndefined([
904
+ ...valueColumns,
905
+ sparklineColumn,
906
+ ...deltaColumns,
907
+ ])
908
+ }
909
+
910
+ private interpolateTargetValues({
911
+ coreTableColumn,
912
+ targetTimes,
913
+ }: {
914
+ coreTableColumn: CoreColumn
915
+ targetTimes: number[]
916
+ }): Map<string, (DataValue | undefined)[]> {
917
+ return valuesByEntityAtTimes(
918
+ coreTableColumn.valueByEntityNameAndOriginalTime,
919
+ targetTimes,
920
+ coreTableColumn.tolerance
921
+ )
922
+ }
923
+
924
+ private calculateDataValuesForTargetTimeMode({
925
+ targetValuesByEntity,
926
+ targetTimeMode,
927
+ coreTableColumn,
928
+ }: {
929
+ targetValuesByEntity: Map<string, (DataValue | undefined)[]>
930
+ targetTimeMode: TargetTimeMode
931
+ coreTableColumn: CoreColumn
932
+ }): Map<string, DataTableValuesForEntity> {
933
+ return es6mapValues(targetValuesByEntity, (dvs) => {
934
+ // There is always a column, but not always a data value (in the delta column the
935
+ // value needs to be calculated)
936
+ if (targetTimeMode === TargetTimeMode.range) {
937
+ const [start, end]: (MinimalOwidRow | undefined)[] = dvs
938
+ const result: RangeValuesForEntity = {
939
+ start: {
940
+ ...start,
941
+ displayValue: this.formatValue(
942
+ coreTableColumn,
943
+ start?.value
944
+ ),
945
+ },
946
+ end: {
947
+ ...end,
948
+ displayValue: this.formatValue(
949
+ coreTableColumn,
950
+ end?.value
951
+ ),
952
+ },
953
+ delta: undefined,
954
+ deltaRatio: undefined,
955
+ }
956
+
957
+ if (
958
+ start !== undefined &&
959
+ end !== undefined &&
960
+ typeof start.value === "number" &&
961
+ typeof end.value === "number" &&
962
+ // sanity check: start time should always be <= end time
963
+ start.time !== undefined &&
964
+ end.time !== undefined &&
965
+ start.time <= end.time
966
+ ) {
967
+ const deltaValue = end.value - start.value
968
+ const deltaRatioValue = deltaValue / Math.abs(start.value)
969
+
970
+ result.delta = {
971
+ value: deltaValue,
972
+ displayValue: this.formatValue(
973
+ coreTableColumn,
974
+ deltaValue,
975
+ {
976
+ showPlus: true,
977
+ unit:
978
+ coreTableColumn.shortUnit === "%"
979
+ ? "pp"
980
+ : coreTableColumn.shortUnit,
981
+ }
982
+ ),
983
+ }
984
+
985
+ result.deltaRatio = {
986
+ value: deltaRatioValue,
987
+ displayValue:
988
+ isFinite(deltaRatioValue) && !isNaN(deltaRatioValue)
989
+ ? this.formatValue(
990
+ coreTableColumn,
991
+ deltaRatioValue * 100,
992
+ {
993
+ unit: "%",
994
+ numDecimalPlaces: 0,
995
+ showPlus: true,
996
+ }
997
+ )
998
+ : undefined,
999
+ }
1000
+ }
1001
+ return result
1002
+ } else {
1003
+ // if single time
1004
+ const dv = dvs[0]
1005
+ const result: PointValuesForEntity = {
1006
+ single: { ...dv },
1007
+ }
1008
+ if (dv !== undefined)
1009
+ result.single!.displayValue = this.formatValue(
1010
+ coreTableColumn,
1011
+ dv.value
1012
+ )
1013
+ return result
1014
+ }
1015
+ })
1016
+ }
1017
+
1018
+ @computed get displayDimensions(): DisplayDataTableDimension[] {
1019
+ return this.dataTableDimensionsWithValues.map((d) => {
1020
+ const coreTableColumn = d.coreTableColumn
1021
+ const columnName = coreTableColumn.titlePublicOrDisplayName
1022
+ const unit =
1023
+ coreTableColumn.unit === "%" ? "percent" : coreTableColumn.unit
1024
+
1025
+ return {
1026
+ coreTableColumn,
1027
+ columnDefinitions: d.columnDefinitions,
1028
+ display: { columnName, unit },
1029
+ sortable: !this.hasSubheaders,
1030
+ }
1031
+ })
1032
+ }
1033
+
1034
+ @computed private get displayRows(): DataTableRow[] {
1035
+ return this.entityNames.map((entityName) => {
1036
+ return {
1037
+ entityName,
1038
+ values: this.dataTableDimensionsWithValues.map((d) =>
1039
+ d.valuesByEntityName.get(entityName)
1040
+ ),
1041
+ }
1042
+ })
1043
+ }
1044
+
1045
+ @computed private get sortedDisplayRows(): DataTableRow[] {
1046
+ const { order } = this.tableState.sort
1047
+ return _.orderBy(this.displayRows, this.sortValueMapper, order)
1048
+ }
1049
+ }
1050
+
1051
+ function ColumnHeader(props: {
1052
+ classNames?: string
1053
+ sortable: boolean
1054
+ sortedCol: boolean
1055
+ sortOrder: SortOrder
1056
+ onClick?: () => void
1057
+ rowSpan?: number
1058
+ colSpan?: number
1059
+ headerText: React.ReactNode
1060
+ colType: "entity" | "dimension" | "subdimension"
1061
+ minWidth?: number
1062
+ contentMaxWidth?: number
1063
+ }): React.ReactElement {
1064
+ const { sortable, sortedCol, colType } = props
1065
+ const isEntityColumn = colType === "entity"
1066
+ const sortIcon = sortable && (
1067
+ <SortIcon
1068
+ isActiveIcon={sortedCol}
1069
+ order={
1070
+ sortedCol
1071
+ ? props.sortOrder
1072
+ : isEntityColumn
1073
+ ? SortOrder.asc
1074
+ : SortOrder.desc
1075
+ }
1076
+ />
1077
+ )
1078
+
1079
+ return (
1080
+ <th
1081
+ className={classnames(props.classNames, colType, { sortable })}
1082
+ rowSpan={props.rowSpan ?? 1}
1083
+ colSpan={props.colSpan ?? 1}
1084
+ onClick={props.onClick}
1085
+ style={{ minWidth: props.minWidth }}
1086
+ >
1087
+ <CellContent maxWidth={props.contentMaxWidth}>
1088
+ <div className="content">
1089
+ {!isEntityColumn && sortIcon}
1090
+ <span>{props.headerText}</span>
1091
+ {isEntityColumn && sortIcon}
1092
+ </div>
1093
+ </CellContent>
1094
+ </th>
1095
+ )
1096
+ }
1097
+
1098
+ function CellContent(props: {
1099
+ maxWidth?: number
1100
+ children?: React.ReactNode
1101
+ }): React.ReactElement {
1102
+ if (!props.maxWidth) return <>{props.children}</>
1103
+ return <div style={{ maxWidth: props.maxWidth }}>{props.children}</div>
1104
+ }
1105
+
1106
+ function ValueCell(props: {
1107
+ columnKey: DataTableColumnKey
1108
+ isFirstColumn?: boolean
1109
+ maxWidth?: number
1110
+ children?: React.ReactNode
1111
+ }): React.ReactElement {
1112
+ const className = classnames([
1113
+ "cell",
1114
+ `cell-${props.columnKey}`,
1115
+ { "cell-first": props.isFirstColumn },
1116
+ ])
1117
+ return (
1118
+ <td className={className}>
1119
+ <CellContent maxWidth={props.maxWidth}>
1120
+ {props.children}
1121
+ </CellContent>
1122
+ </td>
1123
+ )
1124
+ }
1125
+
1126
+ function SortIcon(props: {
1127
+ isActiveIcon?: boolean
1128
+ order: SortOrder
1129
+ }): React.ReactElement {
1130
+ const isActiveIcon = props.isActiveIcon ?? false
1131
+ const activeIcon =
1132
+ props.order === SortOrder.desc ? faArrowUpLong : faArrowDownLong
1133
+
1134
+ return (
1135
+ <span
1136
+ className={classnames({ "sort-icon": true, active: isActiveIcon })}
1137
+ >
1138
+ {isActiveIcon ? (
1139
+ <FontAwesomeIcon icon={activeIcon} />
1140
+ ) : (
1141
+ <span style={{ display: "inline-block", width: "max-content" }}>
1142
+ <FontAwesomeIcon icon={faArrowUpLong} />
1143
+ <FontAwesomeIcon
1144
+ icon={faArrowDownLong}
1145
+ style={{ marginLeft: "-2px" }}
1146
+ />
1147
+ </span>
1148
+ )}
1149
+ </span>
1150
+ )
1151
+ }
1152
+
1153
+ function ClosestTimeNotice({
1154
+ value,
1155
+ columnDefinition,
1156
+ formatTime,
1157
+ }: {
1158
+ value: MinimalOwidRow
1159
+ columnDefinition: DataTableColumnDefinition
1160
+ formatTime: (time: Time) => string
1161
+ }): React.ReactElement | null {
1162
+ const shouldShowClosestTimeNotice =
1163
+ !isDeltaColumn(columnDefinition.key) &&
1164
+ columnDefinition.targetTime !== value.time
1165
+
1166
+ if (
1167
+ value.time === undefined ||
1168
+ columnDefinition.targetTime === undefined ||
1169
+ !shouldShowClosestTimeNotice
1170
+ )
1171
+ return null
1172
+
1173
+ const targetTime = formatTime(columnDefinition.targetTime)
1174
+ const closestTime = formatTime(value.time)
1175
+
1176
+ return (
1177
+ <Tippy
1178
+ content={
1179
+ <div className="closest-time-notice-tippy">
1180
+ <strong>Data not available for {targetTime}</strong>
1181
+ <br />
1182
+ Showing closest available data point ({closestTime})
1183
+ </div>
1184
+ }
1185
+ arrow={false}
1186
+ >
1187
+ <span className="closest-time-notice-icon">
1188
+ <span className="icon">
1189
+ <FontAwesomeIcon icon={faInfoCircle} />
1190
+ </span>
1191
+ </span>
1192
+ </Tippy>
1193
+ )
1194
+ }
1195
+
1196
+ function Sparkline({
1197
+ width = 75,
1198
+ height = 18,
1199
+ owidRows,
1200
+ minTime,
1201
+ maxTime,
1202
+ highlights = [],
1203
+ dotSize = 3.5,
1204
+ color = "#4C6A9C",
1205
+ strokeStyle = "solid",
1206
+ }: {
1207
+ width?: number
1208
+ height?: number
1209
+ owidRows: OwidVariableRow<number>[]
1210
+ minTime: number
1211
+ maxTime: number
1212
+ highlights?: SparklineHighlight[]
1213
+ dotSize?: number
1214
+ color?: string
1215
+ strokeStyle?: "solid" | "dotted"
1216
+ }): React.ReactElement | null {
1217
+ if (owidRows.length <= 1) return null
1218
+
1219
+ // add a little padding so the dots don't overflow
1220
+ const bounds = new Bounds(0, 0, width, height).padWidth(dotSize)
1221
+
1222
+ // calculate x-scale
1223
+ const xDomain = [minTime, maxTime]
1224
+ const xScale = scaleLinear()
1225
+ .domain(xDomain)
1226
+ .range([bounds.left, bounds.right])
1227
+
1228
+ // calculate y-scale
1229
+ const yDomain = extent(owidRows.map((row) => row.value)) as [number, number]
1230
+ const yScale = scaleLinear()
1231
+ .domain(yDomain)
1232
+ .range([bounds.bottom, bounds.top])
1233
+
1234
+ const makePath = line<OwidVariableRow<number>>()
1235
+ .x((row) => xScale(row.originalTime))
1236
+ .y((row) => yScale(row.value))
1237
+
1238
+ const path = makePath(owidRows)
1239
+ if (!path) return null
1240
+
1241
+ const strokeDasharray = strokeStyle === "dotted" ? "2,3" : undefined
1242
+
1243
+ return (
1244
+ <svg
1245
+ width={width}
1246
+ height={height}
1247
+ viewBox={`0 0 ${width} ${height}`}
1248
+ style={{ overflow: "visible" }}
1249
+ >
1250
+ {/* marker lines of highlights */}
1251
+ {highlights
1252
+ .filter((highlight) => highlight.showMarker)
1253
+ .map((highlight) => (
1254
+ <line
1255
+ key={highlight.time}
1256
+ x1={xScale(highlight.time)}
1257
+ x2={xScale(highlight.time)}
1258
+ y1={0}
1259
+ y2={height}
1260
+ stroke={GRAY_30}
1261
+ />
1262
+ ))}
1263
+
1264
+ {/* sparkline */}
1265
+ <path
1266
+ d={path}
1267
+ stroke={color}
1268
+ fill="none"
1269
+ strokeWidth={1.5}
1270
+ strokeDasharray={strokeDasharray}
1271
+ />
1272
+
1273
+ {/* highlighted data points */}
1274
+ {highlights
1275
+ .filter((highlight) => highlight.value !== undefined)
1276
+ .map((highlight) => (
1277
+ <circle
1278
+ key={highlight.time}
1279
+ cx={xScale(highlight.time)}
1280
+ cy={yScale(highlight.value!)}
1281
+ r={dotSize}
1282
+ fill={color}
1283
+ stroke="#fff"
1284
+ />
1285
+ ))}
1286
+ </svg>
1287
+ )
1288
+ }
1289
+
1290
+ function getValueForEntityByKey(
1291
+ dimensionValue: DataTableValuesForEntity,
1292
+ columnKey: DataTableColumnKey
1293
+ ): MinimalOwidRow | undefined {
1294
+ if (isSingleValue(dimensionValue)) {
1295
+ return dimensionValue[columnKey as PointColumnKey] as MinimalOwidRow
1296
+ } else if (isRangeValue(dimensionValue)) {
1297
+ return dimensionValue[columnKey as RangeColumnKey] as MinimalOwidRow
1298
+ }
1299
+ return undefined
1300
+ }
1301
+
1302
+ function isRangeColumnKey(key: string): key is RangeColumnKey {
1303
+ return Object.values(RangeColumnKey).includes(key as any)
1304
+ }
1305
+
1306
+ function isRangeValue(
1307
+ value: DataTableValuesForEntity
1308
+ ): value is RangeValuesForEntity {
1309
+ return "start" in value
1310
+ }
1311
+
1312
+ function isSingleValue(
1313
+ value: DataTableValuesForEntity
1314
+ ): value is PointValuesForEntity {
1315
+ return "single" in value
1316
+ }
1317
+
1318
+ function isDeltaColumn(columnKey?: DataTableColumnKey): boolean {
1319
+ return columnKey === "delta" || columnKey === "deltaRatio"
1320
+ }
1321
+
1322
+ function isCommonDataTableFilter(
1323
+ candidate: string
1324
+ ): candidate is CommonDataTableFilter {
1325
+ return COMMON_DATA_TABLE_FILTERS.includes(candidate as any)
1326
+ }
1327
+
1328
+ export function isValidDataTableFilter(
1329
+ candidate: string
1330
+ ): candidate is DataTableFilter {
1331
+ return isCommonDataTableFilter(candidate) || isEntityRegionType(candidate)
1332
+ }