@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,1226 @@
1
+ import * as _ from "lodash-es"
2
+ import { useCallback, useMemo, useState } from "react"
3
+ import * as React from "react"
4
+ import { observable, computed, action, makeObservable } from "mobx"
5
+ import { observer } from "mobx-react"
6
+ import cx from "classnames"
7
+ import {
8
+ Bounds,
9
+ canWriteToClipboard,
10
+ fetchWithTimeout,
11
+ formatValue,
12
+ getOriginAttributionFragments,
13
+ getPhraseForProcessingLevel,
14
+ triggerDownloadFromBlob,
15
+ triggerDownloadFromUrl,
16
+ } from "../../utils/index.js"
17
+ import {
18
+ Checkbox,
19
+ CodeSnippet,
20
+ OverlayHeader,
21
+ RadioButton,
22
+ LoadingIndicator,
23
+ } from "../../components/index.js"
24
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
25
+ import {
26
+ faCircleExclamation,
27
+ faCopy,
28
+ faDownload,
29
+ faInfoCircle,
30
+ faSpinner,
31
+ } from "@fortawesome/free-solid-svg-icons"
32
+ import {
33
+ OwidColumnDef,
34
+ OwidOrigin,
35
+ QueryParams,
36
+ type GrapherImageDownloadEvent,
37
+ } from "../../types/index.js"
38
+ import {
39
+ BlankOwidTable,
40
+ OwidTable,
41
+ CoreColumn,
42
+ } from "../../core-table/index.js"
43
+ import { Modal } from "./Modal"
44
+ import { GrapherRasterizeFn } from "../captionedChart/StaticChartRasterizer.js"
45
+ import { TabItem, Tabs } from "../tabs/Tabs.js"
46
+ import {
47
+ DownloadIconFullDataset,
48
+ DownloadIconSelected,
49
+ } from "./DownloadIcons.js"
50
+ import { match } from "ts-pattern"
51
+ import * as R from "remeda"
52
+ import {
53
+ DEFAULT_GRAPHER_BOUNDS,
54
+ DEFAULT_GRAPHER_BOUNDS_SQUARE,
55
+ GrapherModal,
56
+ } from "../core/GrapherConstants"
57
+
58
+ export interface DownloadModalManager {
59
+ displaySlug: string
60
+ rasterize: GrapherRasterizeFn
61
+ staticBounds?: Bounds
62
+ staticBoundsWithDetails?: Bounds
63
+ baseUrl?: string
64
+ queryStr?: string
65
+ externalQueryParams?: QueryParams
66
+ inputTable?: OwidTable
67
+ transformedTable?: OwidTable
68
+ filteredTableForDisplay?: OwidTable
69
+ yColumnsFromDimensionsOrSlugsOrAuto?: CoreColumn[]
70
+ detailsOrderedByReference?: string[]
71
+ activeModal?: GrapherModal
72
+ frameBounds?: Bounds
73
+ captionedChartBounds?: Bounds
74
+ isOnChartOrMapTab?: boolean
75
+ isOnTableTab?: boolean
76
+ isOnArchivalPage?: boolean
77
+ hasArchivedPage?: boolean
78
+ showAdminControls?: boolean
79
+ isSocialMediaExport?: boolean
80
+ isWikimediaExport?: boolean
81
+ isPublished?: boolean
82
+ activeColumnSlugs?: string[]
83
+ isServerSideDownloadAvailable?: boolean
84
+ logImageDownloadEvent?: (action: GrapherImageDownloadEvent) => void
85
+ activeDownloadModalTab: DownloadModalTabName
86
+ }
87
+
88
+ interface DownloadModalProps {
89
+ manager: DownloadModalManager
90
+ }
91
+
92
+ export enum DownloadModalTabName {
93
+ "Vis" = "Vis",
94
+ "Data" = "Data",
95
+ }
96
+
97
+ @observer
98
+ export class DownloadModal extends React.Component<DownloadModalProps> {
99
+ constructor(props: DownloadModalProps) {
100
+ super(props)
101
+ makeObservable(this)
102
+ }
103
+
104
+ private get tabItems(): TabItem<DownloadModalTabName>[] {
105
+ return [
106
+ {
107
+ key: DownloadModalTabName.Vis,
108
+ element: <>Visualization</>,
109
+ buttonProps: {
110
+ dataTrackNote: "chart_download_modal_tab_visualization",
111
+ },
112
+ },
113
+ {
114
+ key: DownloadModalTabName.Data,
115
+ element: <>Data</>,
116
+ buttonProps: {
117
+ dataTrackNote: "chart_download_modal_tab_data",
118
+ },
119
+ },
120
+ ]
121
+ }
122
+
123
+ @computed private get frameBounds() {
124
+ return this.props.manager.frameBounds ?? DEFAULT_GRAPHER_BOUNDS
125
+ }
126
+
127
+ @computed private get modalBounds() {
128
+ const maxWidth = 640
129
+ const padWidth = Math.max(16, (this.frameBounds.width - maxWidth) / 2)
130
+ return this.frameBounds.padHeight(16).padWidth(padWidth)
131
+ }
132
+
133
+ @computed private get activeTab(): DownloadModalTabName {
134
+ return this.props.manager.activeDownloadModalTab
135
+ }
136
+
137
+ @computed private get isVisTabActive() {
138
+ return this.activeTab === DownloadModalTabName.Vis
139
+ }
140
+
141
+ @computed private get isDataTabActive() {
142
+ return this.activeTab === DownloadModalTabName.Data
143
+ }
144
+
145
+ @action.bound private onTabChange(key: DownloadModalTabName) {
146
+ this.props.manager.activeDownloadModalTab = key
147
+ }
148
+
149
+ @action.bound private onDismiss() {
150
+ this.props.manager.activeModal = undefined
151
+ }
152
+
153
+ override render(): React.ReactElement {
154
+ return (
155
+ <Modal
156
+ bounds={this.modalBounds}
157
+ onDismiss={this.onDismiss}
158
+ alignVertical="top"
159
+ >
160
+ <div
161
+ className="download-modal-content"
162
+ style={{ maxHeight: this.modalBounds.height }}
163
+ >
164
+ <OverlayHeader
165
+ title="Download"
166
+ onDismiss={this.onDismiss}
167
+ />
168
+ <div className="download-modal__tab-list">
169
+ <Tabs
170
+ variant="slim"
171
+ items={this.tabItems}
172
+ selectedKey={this.activeTab}
173
+ onChange={this.onTabChange}
174
+ />
175
+ </div>
176
+
177
+ {/* Tabs */}
178
+ {/**
179
+ * We only hide the inactive tab with display: none and don't unmount it,
180
+ * so that the tab state (selected radio buttons, etc) is preserved
181
+ * when switching between tabs.
182
+ */}
183
+ <div className="download-modal__tab-panel" role="tabpanel">
184
+ <div
185
+ className="download-modal__tab-content"
186
+ style={{
187
+ display: this.isVisTabActive
188
+ ? undefined
189
+ : "none",
190
+ }}
191
+ role="tab"
192
+ aria-hidden={!this.isVisTabActive}
193
+ >
194
+ <DownloadModalVisTab {...this.props} />
195
+ </div>
196
+ <div
197
+ className="download-modal__tab-content"
198
+ style={{
199
+ display: this.isDataTabActive
200
+ ? undefined
201
+ : "none",
202
+ }}
203
+ role="tab"
204
+ aria-hidden={!this.isDataTabActive}
205
+ >
206
+ <DownloadModalDataTab {...this.props} />
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </Modal>
211
+ )
212
+ }
213
+ }
214
+
215
+ @observer
216
+ export class DownloadModalVisTab extends React.Component<DownloadModalProps> {
217
+ constructor(props: DownloadModalProps) {
218
+ super(props)
219
+
220
+ makeObservable<
221
+ DownloadModalVisTab,
222
+ | "svgBlob"
223
+ | "svgPreviewUrl"
224
+ | "pngBlob"
225
+ | "pngPreviewUrl"
226
+ | "canWriteToClipboard"
227
+ | "isReady"
228
+ | "shouldIncludeDetails"
229
+ >(this, {
230
+ svgBlob: observable,
231
+ svgPreviewUrl: observable,
232
+ pngBlob: observable,
233
+ pngPreviewUrl: observable,
234
+ canWriteToClipboard: observable,
235
+ isReady: observable,
236
+ shouldIncludeDetails: observable,
237
+ })
238
+ }
239
+
240
+ @computed private get staticBounds(): Bounds {
241
+ return this.manager.staticBounds ?? DEFAULT_GRAPHER_BOUNDS
242
+ }
243
+
244
+ @computed private get captionedChartBounds(): Bounds {
245
+ return this.manager.captionedChartBounds ?? DEFAULT_GRAPHER_BOUNDS
246
+ }
247
+
248
+ @computed private get isExportingSquare(): boolean {
249
+ return (
250
+ this.manager.staticBounds?.width ===
251
+ this.manager.staticBounds?.height
252
+ )
253
+ }
254
+
255
+ @computed private get isSocialMediaExport(): boolean {
256
+ return this.manager.isSocialMediaExport ?? false
257
+ }
258
+
259
+ @computed private get isWikimediaExport(): boolean {
260
+ return this.manager.isWikimediaExport ?? false
261
+ }
262
+
263
+ @computed private get targetBounds(): Bounds {
264
+ if (this.shouldIncludeDetails)
265
+ return this.manager.staticBoundsWithDetails ?? this.staticBounds
266
+ else return this.staticBounds
267
+ }
268
+
269
+ @computed private get targetWidth(): number {
270
+ return this.targetBounds.width
271
+ }
272
+
273
+ @computed private get targetHeight(): number {
274
+ return this.targetBounds.height
275
+ }
276
+
277
+ @computed private get manager(): DownloadModalManager {
278
+ return this.props.manager
279
+ }
280
+
281
+ private svgBlob: Blob | undefined = undefined
282
+ private svgPreviewUrl: string | undefined = undefined
283
+
284
+ private pngBlob: Blob | undefined = undefined
285
+ private pngPreviewUrl: string | undefined = undefined
286
+ private canWriteToClipboard: boolean = false
287
+
288
+ private isReady: boolean = false
289
+
290
+ private shouldIncludeDetails = true
291
+
292
+ @action.bound private export(): void {
293
+ // render the graphic then cache data-urls for display & blobs for downloads
294
+ this.manager
295
+ .rasterize({ includeDetails: this.shouldIncludeDetails })
296
+ .then(({ url, blob, svgUrl, svgBlob }) => {
297
+ this.pngPreviewUrl = url
298
+ this.pngBlob = blob
299
+ this.svgPreviewUrl = svgUrl
300
+ this.svgBlob = svgBlob
301
+ this.markAsReady()
302
+ })
303
+ .catch((err) => {
304
+ console.error(JSON.stringify(err))
305
+ this.markAsReady()
306
+ })
307
+ }
308
+
309
+ @action.bound private markAsReady(): void {
310
+ this.isReady = true
311
+ }
312
+
313
+ @action.bound private reset(): void {
314
+ this.isReady = false
315
+ }
316
+
317
+ @computed private get fallbackPngUrl(): string {
318
+ return `${this.manager.baseUrl || ""}.png${this.manager.queryStr || ""}`
319
+ }
320
+ @computed private get baseFilename(): string {
321
+ return this.manager.displaySlug
322
+ }
323
+
324
+ @action.bound private onPngDownload(): void {
325
+ const filename = this.baseFilename + ".png"
326
+ if (this.pngBlob) {
327
+ triggerDownloadFromBlob(filename, this.pngBlob)
328
+ } else {
329
+ triggerDownloadFromUrl(filename, this.fallbackPngUrl)
330
+ }
331
+ this.manager.logImageDownloadEvent?.("chart_download_png")
332
+ }
333
+
334
+ @action.bound private onSvgDownload(): void {
335
+ const filename = this.baseFilename + ".svg"
336
+ if (this.svgBlob) {
337
+ triggerDownloadFromBlob(filename, this.svgBlob)
338
+ }
339
+ this.manager.logImageDownloadEvent?.("chart_download_svg")
340
+ }
341
+
342
+ @action.bound private toggleExportFormat(): void {
343
+ this.manager.staticBounds = this.isExportingSquare
344
+ ? DEFAULT_GRAPHER_BOUNDS
345
+ : DEFAULT_GRAPHER_BOUNDS_SQUARE
346
+ }
347
+
348
+ @action.bound private toggleExportForUseInSocialMedia(): void {
349
+ this.manager.isSocialMediaExport = !this.isSocialMediaExport
350
+ }
351
+
352
+ @action.bound private toggleExportForUseOnWikimedia(): void {
353
+ this.manager.isWikimediaExport = !this.isWikimediaExport
354
+ }
355
+
356
+ @action.bound private toggleIncludeDetails(): void {
357
+ this.shouldIncludeDetails = !this.shouldIncludeDetails
358
+ }
359
+
360
+ @computed private get hasDetails(): boolean {
361
+ return !_.isEmpty(this.manager.detailsOrderedByReference)
362
+ }
363
+
364
+ @computed private get showExportControls(): boolean {
365
+ return this.hasDetails || !!this.manager.showAdminControls
366
+ }
367
+
368
+ @computed private get showInteractiveEmbedTip(): boolean {
369
+ return !!(this.manager.isOnArchivalPage || this.manager.hasArchivedPage)
370
+ }
371
+
372
+ @action.bound openEmbedDialog(): void {
373
+ this.manager.activeModal = GrapherModal.Embed
374
+ }
375
+
376
+ @computed get showCopyPngButton(): boolean {
377
+ return !!this.manager.showAdminControls && this.canWriteToClipboard
378
+ }
379
+
380
+ @action.bound async onCopyPng(): Promise<void> {
381
+ try {
382
+ if (!this.pngBlob) return
383
+ await navigator.clipboard.write([
384
+ new ClipboardItem({ "image/png": this.pngBlob }),
385
+ ])
386
+ } catch (err) {
387
+ console.error("couldn't copy PNG to clipboard", err)
388
+ }
389
+ }
390
+
391
+ override componentDidMount(): void {
392
+ queueMicrotask(() => this.export())
393
+
394
+ void canWriteToClipboard().then(
395
+ (canWriteToClipboard) =>
396
+ (this.canWriteToClipboard = canWriteToClipboard)
397
+ )
398
+ }
399
+
400
+ override render(): React.ReactElement {
401
+ if (!this.isReady) return <LoadingIndicator color="#000" />
402
+
403
+ const {
404
+ manager,
405
+ svgPreviewUrl,
406
+ captionedChartBounds,
407
+ targetWidth,
408
+ targetHeight,
409
+ showInteractiveEmbedTip,
410
+ } = this
411
+ const pngPreviewUrl = this.pngPreviewUrl || this.fallbackPngUrl
412
+
413
+ let previewWidth: number
414
+ let previewHeight: number
415
+ const boundScalar = 0.17
416
+ if (
417
+ captionedChartBounds.width / captionedChartBounds.height >
418
+ targetWidth / targetHeight
419
+ ) {
420
+ previewHeight = Math.min(
421
+ 72,
422
+ captionedChartBounds.height * boundScalar
423
+ )
424
+ previewWidth = (targetWidth / targetHeight) * previewHeight
425
+ } else {
426
+ previewWidth = Math.min(
427
+ 102,
428
+ captionedChartBounds.width * boundScalar
429
+ )
430
+ previewHeight = (targetHeight / targetWidth) * previewWidth
431
+ }
432
+
433
+ const imageStyle = {
434
+ minWidth: previewWidth,
435
+ minHeight: previewHeight,
436
+ maxWidth: previewWidth,
437
+ maxHeight: previewHeight,
438
+ opacity: this.isReady ? 1 : 0,
439
+ }
440
+
441
+ return (
442
+ <div>
443
+ {manager.isOnChartOrMapTab ? (
444
+ <div className="download-modal__vis-section">
445
+ {showInteractiveEmbedTip && (
446
+ <Callout
447
+ title="Did you know?"
448
+ icon={<FontAwesomeIcon icon={faInfoCircle} />}
449
+ >
450
+ Instead of downloading a static image of this
451
+ chart, you can also{" "}
452
+ <a
453
+ onClick={this.openEmbedDialog}
454
+ data-track-note="chart_download_click_interactive_embed"
455
+ >
456
+ embed an interactive version
457
+ </a>
458
+ .{" "}
459
+ {this.manager.isOnArchivalPage ? (
460
+ <>
461
+ The interactive version will stay fixed
462
+ over time and will always show the same
463
+ chart and data you are seeing now.
464
+ </>
465
+ ) : (
466
+ <>
467
+ You can choose between a live embed that
468
+ always reflects our latest data updates,
469
+ or a snapshot embed that stays fixed at
470
+ the time you created it.
471
+ </>
472
+ )}
473
+ </Callout>
474
+ )}
475
+ <div>
476
+ {this.showCopyPngButton && (
477
+ <button
478
+ className="download-modal__download-button download-modal__download-button--variant-copy"
479
+ onClick={this.onCopyPng}
480
+ >
481
+ <FontAwesomeIcon icon={faCopy} />
482
+ Copy PNG
483
+ </button>
484
+ )}
485
+ <DownloadButton
486
+ title="Image (PNG)"
487
+ description="Suitable for most uses, widely compatible."
488
+ previewImageUrl={pngPreviewUrl}
489
+ onClick={this.onPngDownload}
490
+ imageStyle={imageStyle}
491
+ />
492
+ <DownloadButton
493
+ title="Vector graphic (SVG)"
494
+ description="For high quality prints, or further editing the chart in graphics software."
495
+ previewImageUrl={svgPreviewUrl}
496
+ onClick={this.onSvgDownload}
497
+ imageStyle={imageStyle}
498
+ />
499
+ </div>
500
+ {this.showExportControls && (
501
+ <>
502
+ {this.hasDetails && (
503
+ <Checkbox
504
+ checked={this.shouldIncludeDetails}
505
+ label="Include terminology definitions at bottom of chart"
506
+ onChange={(): void => {
507
+ this.reset()
508
+ this.toggleIncludeDetails()
509
+ this.export()
510
+ }}
511
+ />
512
+ )}
513
+ {this.manager.showAdminControls && (
514
+ <Checkbox
515
+ checked={this.isExportingSquare}
516
+ label="Square format"
517
+ onChange={action((): void => {
518
+ this.reset()
519
+ this.toggleExportFormat()
520
+
521
+ if (!this.isExportingSquare) {
522
+ this.manager.isSocialMediaExport = false
523
+ }
524
+
525
+ this.export()
526
+ })}
527
+ />
528
+ )}
529
+ {this.manager.showAdminControls && (
530
+ <Checkbox
531
+ checked={this.isSocialMediaExport}
532
+ label="For use in social media (internal)"
533
+ onChange={action((): void => {
534
+ this.reset()
535
+ this.toggleExportForUseInSocialMedia()
536
+
537
+ // set reasonable defaults for social media exports
538
+ if (this.isSocialMediaExport) {
539
+ this.manager.staticBounds =
540
+ DEFAULT_GRAPHER_BOUNDS_SQUARE
541
+ this.shouldIncludeDetails = false
542
+ }
543
+
544
+ this.export()
545
+ })}
546
+ />
547
+ )}
548
+ {this.manager.showAdminControls && (
549
+ <Checkbox
550
+ checked={this.isWikimediaExport}
551
+ label="Optimize SVG for Wikipedia upload"
552
+ onChange={action((): void => {
553
+ this.reset()
554
+ this.toggleExportForUseOnWikimedia()
555
+
556
+ this.export()
557
+ })}
558
+ />
559
+ )}
560
+ </>
561
+ )}
562
+ </div>
563
+ ) : (
564
+ <Callout
565
+ title="Chart can't currently be exported to image"
566
+ icon={<FontAwesomeIcon icon={faCircleExclamation} />}
567
+ >
568
+ Try switching to the "Chart" or "Map" tab to download a
569
+ static image of this chart.
570
+ <br />
571
+ You can also download the data used in this chart by
572
+ navigating to the "Data" tab.
573
+ </Callout>
574
+ )}
575
+ </div>
576
+ )
577
+ }
578
+ }
579
+
580
+ enum CsvDownloadType {
581
+ Full = "full",
582
+ CurrentSelection = "current_selection",
583
+ }
584
+
585
+ interface DataDownloadContextBase {
586
+ slug: string
587
+ searchParams: URLSearchParams
588
+ externalSearchParams: URLSearchParams
589
+ baseUrl: string
590
+ }
591
+
592
+ interface DataDownloadContextServerSide extends DataDownloadContextBase {
593
+ // Configurable options
594
+ csvDownloadType: CsvDownloadType
595
+ shortColNames: boolean
596
+ }
597
+
598
+ interface DataDownloadContextClientSide extends DataDownloadContextBase {
599
+ // Configurable options
600
+ csvDownloadType: CsvDownloadType
601
+ shortColNames: boolean
602
+
603
+ // Only needed for local CSV generation
604
+ fullTable: OwidTable
605
+ filteredTable: OwidTable
606
+ activeColumnSlugs: string[] | undefined
607
+ }
608
+
609
+ const createCsvBlobLocally = async (ctx: DataDownloadContextClientSide) => {
610
+ const downloadTable =
611
+ ctx.csvDownloadType === CsvDownloadType.Full
612
+ ? ctx.fullTable
613
+ : ctx.filteredTable
614
+ const csv = downloadTable.toPrettyCsv(
615
+ ctx.shortColNames,
616
+ ctx.activeColumnSlugs
617
+ )
618
+
619
+ return new Blob([csv], { type: "text/csv;charset=utf-8" })
620
+ }
621
+
622
+ const getDownloadSearchParams = (ctx: DataDownloadContextServerSide) => {
623
+ const searchParams = new URLSearchParams()
624
+ searchParams.set("v", "1") // API versioning
625
+ searchParams.set(
626
+ "csvType",
627
+ match(ctx.csvDownloadType)
628
+ .with(CsvDownloadType.CurrentSelection, () => "filtered")
629
+ .with(CsvDownloadType.Full, () => "full")
630
+ .exhaustive()
631
+ )
632
+ searchParams.set("useColumnShortNames", ctx.shortColNames.toString())
633
+ const otherParams =
634
+ ctx.csvDownloadType === CsvDownloadType.CurrentSelection
635
+ ? // Append all the current grapher settings, e.g.
636
+ // ?time=2020&selection=~USA + mdim dimensions.
637
+ ctx.searchParams
638
+ : // Use the base grapher settings + mdim dimensions.
639
+ ctx.externalSearchParams
640
+ for (const [key, value] of otherParams.entries()) {
641
+ searchParams.set(key, value)
642
+ }
643
+ return searchParams
644
+ }
645
+
646
+ const getDownloadUrl = (
647
+ extension: "csv" | "metadata.json" | "zip",
648
+ ctx: DataDownloadContextServerSide
649
+ ) => {
650
+ const searchParams = getDownloadSearchParams(ctx)
651
+ const searchStr = searchParams.toString().replaceAll("%7E", "~")
652
+ return `${ctx.baseUrl}.${extension}` + (searchStr ? `?${searchStr}` : "")
653
+ }
654
+
655
+ export const getNonRedistributableInfo = (
656
+ table: OwidTable | undefined
657
+ ): { cols: CoreColumn[] | undefined; sourceLinks: string[] | undefined } => {
658
+ if (!table) return { cols: undefined, sourceLinks: undefined }
659
+
660
+ const nonRedistributableCols = table.columnsAsArray.filter(
661
+ (col) => (col.def as OwidColumnDef).nonRedistributable
662
+ )
663
+
664
+ if (!nonRedistributableCols.length)
665
+ return { cols: undefined, sourceLinks: undefined }
666
+
667
+ const sourceLinks = nonRedistributableCols
668
+ .map((col) => {
669
+ const def = col.def as OwidColumnDef
670
+ return def.sourceLink ?? def.origins?.[0]?.urlMain
671
+ })
672
+ .filter((link): link is string => !!link)
673
+
674
+ return { cols: nonRedistributableCols, sourceLinks: _.uniq(sourceLinks) }
675
+ }
676
+
677
+ const CodeExamplesBlock = (props: { csvUrl: string; metadataUrl: string }) => {
678
+ const code = {
679
+ "Excel / Google Sheets": `=IMPORTDATA("${props.csvUrl}")`,
680
+ "Python with Pandas": `import pandas as pd
681
+ import requests
682
+
683
+ # Fetch the data.
684
+ df = pd.read_csv("${props.csvUrl}", storage_options = {'User-Agent': 'Our World In Data data fetch/1.0'})
685
+
686
+ # Fetch the metadata
687
+ metadata = requests.get("${props.metadataUrl}").json()`,
688
+ R: `library(jsonlite)
689
+
690
+ # Fetch the data
691
+ df <- read.csv("${props.csvUrl}")
692
+
693
+ # Fetch the metadata
694
+ metadata <- fromJSON("${props.metadataUrl}")`,
695
+ Stata: `import delimited "${props.csvUrl}", encoding("utf-8") clear`,
696
+ }
697
+
698
+ return (
699
+ <div className="download-modal__data-section">
700
+ <div className="download-modal__heading-with-caption">
701
+ <h3 className="grapher_h3-semibold">Code examples</h3>
702
+ <p className="grapher_label-2-regular">
703
+ Examples of how to load this data into different data
704
+ analysis tools.
705
+ </p>
706
+ </div>
707
+ <div className="download-modal__code-blocks">
708
+ {Object.entries(code).map(([name, snippet]) => (
709
+ <div key={name}>
710
+ <h4 className="grapher_body-2-medium">{name}</h4>
711
+ <CodeSnippet code={snippet} />
712
+ </div>
713
+ ))}
714
+ </div>
715
+ </div>
716
+ )
717
+ }
718
+
719
+ const SourceAndCitationSection = ({ table }: { table?: OwidTable }) => {
720
+ // Sources can come either from origins (new format) or from the source field of the column (old format)
721
+ const origins =
722
+ table?.defs
723
+ .flatMap((def) => def.origins ?? [])
724
+ ?.filter((o) => o !== undefined) ?? []
725
+
726
+ const otherSources =
727
+ table?.columnsAsArray
728
+ .map((col) => col.source)
729
+ .filter((s) => s !== undefined && s.dataPublishedBy !== undefined)
730
+ .map(
731
+ (s): OwidOrigin => ({
732
+ producer: s.dataPublishedBy,
733
+ urlMain: s.link,
734
+ })
735
+ ) ?? []
736
+
737
+ const originsUniq = _.uniqBy(
738
+ [...origins, ...otherSources],
739
+ (o) => o.urlMain ?? o.datePublished
740
+ )
741
+
742
+ const attributions = getOriginAttributionFragments(originsUniq)
743
+
744
+ const sourceLinks = R.zip(attributions, originsUniq).map(
745
+ ([attribution, origin]) => {
746
+ const link = origin?.urlMain
747
+
748
+ if (link)
749
+ return (
750
+ <li key={link}>
751
+ <a href={link}>{attribution}</a>
752
+ </li>
753
+ )
754
+ else return <li key={attribution}>{attribution}</li>
755
+ }
756
+ )
757
+
758
+ // Find the highest processing level of all columns
759
+ const owidProcessingLevel = table?.columnsAsArray
760
+ .map((col) => (col.def as OwidColumnDef).owidProcessingLevel)
761
+ .reduce((prev, curr) => {
762
+ if (prev === "major" || curr === "major") return "major" as const
763
+ if (prev === "minor" || curr === "minor") return "minor" as const
764
+ return undefined
765
+ }, undefined)
766
+
767
+ const sourceIsOwid =
768
+ attributions.length === 1 &&
769
+ attributions[0].toLowerCase() === "our world in data"
770
+ const processingLevelPhrase = !sourceIsOwid
771
+ ? getPhraseForProcessingLevel(owidProcessingLevel)
772
+ : undefined
773
+ const fullProcessingPhrase = processingLevelPhrase ? (
774
+ <>
775
+ {" "}
776
+ – <i>{processingLevelPhrase} by Our World in Data</i>
777
+ </>
778
+ ) : undefined
779
+
780
+ return (
781
+ <div className="download-modal__data-section download-modal__sources">
782
+ <h3 className="grapher_h3-semibold">Source and citation</h3>
783
+ {sourceLinks.length > 0 && (
784
+ <div className="download-modal__data-sources">
785
+ <strong>Data sources:</strong>{" "}
786
+ <ul className="download-modal__data-sources-list">
787
+ {sourceLinks}
788
+ </ul>
789
+ {fullProcessingPhrase}
790
+ </div>
791
+ )}
792
+ <div>
793
+ <strong>Citation guidance:</strong> Please credit all sources
794
+ listed above. Data provided by third-party sources through Our
795
+ World in Data remains subject to the original{" "}
796
+ {sourceLinks.length === 1 ? "provider's" : "providers'"} license
797
+ terms.
798
+ </div>
799
+ </div>
800
+ )
801
+ }
802
+
803
+ const ApiAndCodeExamplesSection = (props: {
804
+ downloadCtxBase: DataDownloadContextBase
805
+ firstYColDef?: OwidColumnDef
806
+ }) => {
807
+ const [onlyVisible, setOnlyVisible] = useState(false)
808
+ const [shortColNames, setShortColNames] = useState(true)
809
+
810
+ const exLongName = props.firstYColDef?.name
811
+ const exShortName = props.firstYColDef?.shortName
812
+
813
+ // Some charts, like pre-ETL ones or csv-based explorers, don't have short names available for their variables
814
+ const shortNamesAvailable = !!exShortName
815
+
816
+ const downloadCtx: DataDownloadContextServerSide = useMemo(
817
+ () => ({
818
+ ...props.downloadCtxBase,
819
+ csvDownloadType: onlyVisible
820
+ ? CsvDownloadType.CurrentSelection
821
+ : CsvDownloadType.Full,
822
+ shortColNames,
823
+ }),
824
+ [props.downloadCtxBase, onlyVisible, shortColNames]
825
+ )
826
+
827
+ const csvUrl = useMemo(
828
+ () => getDownloadUrl("csv", downloadCtx),
829
+ [downloadCtx]
830
+ )
831
+ const metadataUrl = useMemo(
832
+ () => getDownloadUrl("metadata.json", downloadCtx),
833
+ [downloadCtx]
834
+ )
835
+
836
+ return (
837
+ <>
838
+ <div className="download-modal__data-section">
839
+ <div className="download-modal__heading-with-caption">
840
+ <h3 className="grapher_h3-semibold">Data API</h3>
841
+ <p className="grapher_label-2-regular">
842
+ Use these URLs to programmatically access this chart's
843
+ data and configure your requests with the options below.{" "}
844
+ <a
845
+ href="https://docs.owid.io/projects/etl/api/"
846
+ data-track-note="chart_download_modal_api_docs"
847
+ >
848
+ Our documentation provides more information
849
+ </a>{" "}
850
+ on how to use the API, and you can find a few code
851
+ examples below.
852
+ </p>
853
+ </div>
854
+
855
+ <section className="download-modal__api-urls">
856
+ <div>
857
+ <h4 className="grapher_body-2-medium">
858
+ Data URL (CSV format)
859
+ </h4>
860
+ <CodeSnippet code={csvUrl} />
861
+ </div>
862
+ <div>
863
+ <h4 className="grapher_body-2-medium">
864
+ Metadata URL (JSON format)
865
+ </h4>
866
+ <CodeSnippet code={metadataUrl} />
867
+ </div>
868
+ </section>
869
+ <section className="download-modal__config-list">
870
+ <RadioButton
871
+ label="Download full data, including all entities and time points"
872
+ group="onlyVisible"
873
+ checked={!onlyVisible}
874
+ onChange={() => setOnlyVisible(false)}
875
+ />
876
+ <RadioButton
877
+ label="Download only the currently selected data visible in the chart"
878
+ group="onlyVisible"
879
+ checked={onlyVisible}
880
+ onChange={() => setOnlyVisible(true)}
881
+ />
882
+ </section>
883
+ {shortNamesAvailable && (
884
+ <section className="download-modal__config-list">
885
+ <div>
886
+ <RadioButton
887
+ label="Long column names"
888
+ group="shortColNames"
889
+ checked={!shortColNames}
890
+ onChange={() => setShortColNames(false)}
891
+ />
892
+ <p>
893
+ e.g. <code>{exLongName}</code>
894
+ </p>
895
+ </div>
896
+ <div>
897
+ <RadioButton
898
+ label="Shortened column names"
899
+ group="shortColNames"
900
+ checked={shortColNames}
901
+ onChange={() => setShortColNames(true)}
902
+ />
903
+ <p>
904
+ e.g.{" "}
905
+ <code style={{ wordBreak: "break-all" }}>
906
+ {exShortName}
907
+ </code>
908
+ </p>
909
+ </div>
910
+ </section>
911
+ )}
912
+ </div>
913
+
914
+ <CodeExamplesBlock csvUrl={csvUrl} metadataUrl={metadataUrl} />
915
+ </>
916
+ )
917
+ }
918
+
919
+ export const DownloadModalDataTab = (props: DownloadModalProps) => {
920
+ const { yColumnsFromDimensionsOrSlugsOrAuto: yColumns } = props.manager
921
+
922
+ const { cols: nonRedistributableCols, sourceLinks } =
923
+ getNonRedistributableInfo(props.manager.inputTable)
924
+
925
+ // Server-side download is not necessarily available for all types of charts
926
+ const serverSideDownloadAvailable =
927
+ props.manager.isServerSideDownloadAvailable
928
+
929
+ const downloadCtx: Omit<
930
+ DataDownloadContextClientSide,
931
+ "csvDownloadType" | "shortColNames"
932
+ > = useMemo(() => {
933
+ const externalSearchParams = new URLSearchParams()
934
+ for (const [key, value] of Object.entries(
935
+ props.manager.externalQueryParams ?? {}
936
+ )) {
937
+ if (value) {
938
+ externalSearchParams.set(key, value)
939
+ }
940
+ }
941
+ return {
942
+ slug: props.manager.displaySlug,
943
+ searchParams: new URLSearchParams(props.manager.queryStr),
944
+ externalSearchParams,
945
+ baseUrl:
946
+ props.manager.baseUrl ??
947
+ `/grapher/${props.manager.displaySlug}`,
948
+
949
+ fullTable: props.manager.inputTable ?? BlankOwidTable(),
950
+ filteredTable:
951
+ (props.manager.isOnTableTab
952
+ ? props.manager.filteredTableForDisplay
953
+ : props.manager.transformedTable) ?? BlankOwidTable(),
954
+ activeColumnSlugs: props.manager.activeColumnSlugs,
955
+ }
956
+ }, [
957
+ props.manager.baseUrl,
958
+ props.manager.displaySlug,
959
+ props.manager.queryStr,
960
+ props.manager.externalQueryParams,
961
+ props.manager.isOnTableTab,
962
+ props.manager.inputTable,
963
+ props.manager.transformedTable,
964
+ props.manager.filteredTableForDisplay,
965
+ props.manager.activeColumnSlugs,
966
+ ])
967
+
968
+ const onDownloadClick = useCallback(
969
+ async (csvDownloadType: CsvDownloadType) => {
970
+ const ctx = {
971
+ ...downloadCtx,
972
+ csvDownloadType,
973
+ // Hard code shortColNames here, since it's not obvious that the
974
+ // radio button to change shortColNames would influence what
975
+ // this button does. We use long names, since they are always
976
+ // available and more useful than short names in e.g. Excel,
977
+ // which is the more likely tool of choice for a casual user,
978
+ // who doesn't use the API.
979
+ shortColNames: false,
980
+ }
981
+ if (serverSideDownloadAvailable) {
982
+ try {
983
+ const url = getDownloadUrl("zip", ctx)
984
+ const response = await fetchWithTimeout(url, 5000, {
985
+ method: "GET",
986
+ headers: { Accept: "application/zip" },
987
+ })
988
+
989
+ if (!response.ok) {
990
+ throw new Error(
991
+ `Server download failed: ${response.status}`
992
+ )
993
+ }
994
+
995
+ const blob = await response.blob()
996
+ const fullOrFiltered =
997
+ csvDownloadType === CsvDownloadType.Full
998
+ ? ""
999
+ : ".filtered"
1000
+ triggerDownloadFromBlob(
1001
+ ctx.slug + fullOrFiltered + ".zip",
1002
+ blob
1003
+ )
1004
+ } catch (error) {
1005
+ // Fallback to client-side CSV download
1006
+ console.warn(
1007
+ "Server-side download failed, falling back to client-side",
1008
+ error
1009
+ )
1010
+ const blob = await createCsvBlobLocally(ctx)
1011
+ triggerDownloadFromBlob(ctx.slug + ".csv", blob)
1012
+ }
1013
+ } else {
1014
+ // Direct client-side download
1015
+ void createCsvBlobLocally(ctx).then((blob) => {
1016
+ triggerDownloadFromBlob(ctx.slug + ".csv", blob)
1017
+ })
1018
+ }
1019
+ },
1020
+ [downloadCtx, serverSideDownloadAvailable]
1021
+ )
1022
+
1023
+ if (nonRedistributableCols?.length) {
1024
+ return (
1025
+ <div>
1026
+ <Callout
1027
+ title="The data in this chart is not available to download"
1028
+ icon={<FontAwesomeIcon icon={faInfoCircle} />}
1029
+ >
1030
+ The data is published under a license that doesn't allow us
1031
+ to redistribute it.
1032
+ {sourceLinks?.length && (
1033
+ <>
1034
+ {" "}
1035
+ Please visit the
1036
+ {sourceLinks.length > 1
1037
+ ? " data publishers' websites "
1038
+ : " data publisher's website "}
1039
+ for more details:
1040
+ <ul>
1041
+ {sourceLinks.map((link, i) => (
1042
+ <li key={i}>
1043
+ <a href={link}>{link}</a>
1044
+ </li>
1045
+ ))}
1046
+ </ul>
1047
+ </>
1048
+ )}
1049
+ </Callout>
1050
+ </div>
1051
+ )
1052
+ }
1053
+
1054
+ const downloadHelpText = serverSideDownloadAvailable ? (
1055
+ <p className="grapher_label-2-regular">
1056
+ Download the data shown in this chart as a ZIP file containing a CSV
1057
+ file, metadata in JSON format, and a README. The CSV file can be
1058
+ opened in Excel, Google Sheets, and other data analysis tools.
1059
+ </p>
1060
+ ) : (
1061
+ <p className="grapher_label-2-regular">
1062
+ Download the data used to create this chart. The data is provided in
1063
+ CSV format, which can be opened in Excel, Google Sheets, and other
1064
+ data analysis tools.
1065
+ </p>
1066
+ )
1067
+
1068
+ const firstYColDef = yColumns?.[0]?.def as OwidColumnDef | undefined
1069
+
1070
+ const fullDataDescription = `Includes all entities and time points`
1071
+ const filteredDataDescription = `Includes only the entities and time points currently visible in the chart`
1072
+
1073
+ const fullTableRowCountSnippet = makeNumberOfRowsSnippet(
1074
+ downloadCtx.fullTable.numRows
1075
+ )
1076
+ const filteredTableRowCountSnippet = makeNumberOfRowsSnippet(
1077
+ downloadCtx.filteredTable.numRows
1078
+ )
1079
+
1080
+ return (
1081
+ <>
1082
+ <SourceAndCitationSection table={props.manager.inputTable} />
1083
+ <div className="download-modal__data-section">
1084
+ <div className="download-modal__heading-with-caption">
1085
+ <h3 className="grapher_h3-semibold">Quick download</h3>
1086
+ {downloadHelpText}
1087
+ </div>
1088
+ <div>
1089
+ <DownloadButton
1090
+ title="Download full data"
1091
+ description={
1092
+ fullDataDescription + fullTableRowCountSnippet
1093
+ }
1094
+ icon={<DownloadIconFullDataset />}
1095
+ onClick={() => onDownloadClick(CsvDownloadType.Full)}
1096
+ tracking={
1097
+ "chart_download_full_data--" +
1098
+ (serverSideDownloadAvailable ? "server" : "client")
1099
+ }
1100
+ />
1101
+ <DownloadButton
1102
+ title="Download displayed data"
1103
+ description={
1104
+ filteredDataDescription +
1105
+ filteredTableRowCountSnippet
1106
+ }
1107
+ icon={<DownloadIconSelected />}
1108
+ onClick={() =>
1109
+ onDownloadClick(CsvDownloadType.CurrentSelection)
1110
+ }
1111
+ tracking={
1112
+ "chart_download_filtered_data--" +
1113
+ (serverSideDownloadAvailable ? "server" : "client")
1114
+ }
1115
+ />
1116
+ </div>
1117
+ </div>
1118
+ {serverSideDownloadAvailable && (
1119
+ <ApiAndCodeExamplesSection
1120
+ downloadCtxBase={downloadCtx}
1121
+ firstYColDef={firstYColDef}
1122
+ />
1123
+ )}
1124
+ </>
1125
+ )
1126
+ }
1127
+
1128
+ interface DownloadButtonProps {
1129
+ title: string
1130
+ description: string
1131
+ onClick: () => void
1132
+ icon?: React.ReactElement
1133
+ previewImageUrl?: string
1134
+ imageStyle?: React.CSSProperties
1135
+ tracking?: string
1136
+ }
1137
+
1138
+ function DownloadButton(props: DownloadButtonProps): React.ReactElement {
1139
+ const { onClick } = props
1140
+
1141
+ const [isDownloading, setIsDownloading] = useState(false)
1142
+ const [showLoadingUI, setShowLoadingUI] = useState(false)
1143
+
1144
+ const handleClick = useCallback(async () => {
1145
+ setIsDownloading(true)
1146
+
1147
+ // Delay showing the loading UI to prevent flashing for quick downloads
1148
+ const loadingTimeout = setTimeout(() => setShowLoadingUI(true), 300)
1149
+
1150
+ try {
1151
+ await onClick()
1152
+ } finally {
1153
+ clearTimeout(loadingTimeout)
1154
+ setIsDownloading(false)
1155
+ setShowLoadingUI(false)
1156
+ }
1157
+ }, [onClick])
1158
+
1159
+ return (
1160
+ <button
1161
+ className={cx("download-modal__download-button", {
1162
+ "download-modal__download-button--loading": showLoadingUI,
1163
+ })}
1164
+ onClick={handleClick}
1165
+ data-track-note={props.tracking}
1166
+ disabled={isDownloading}
1167
+ >
1168
+ {props.icon && (
1169
+ <div className="download-modal__option-icon">{props.icon}</div>
1170
+ )}
1171
+ {props.previewImageUrl && (
1172
+ <div className="download-modal__download-preview-img">
1173
+ <img src={props.previewImageUrl} style={props.imageStyle} />
1174
+ </div>
1175
+ )}
1176
+ <div className="download-modal__download-button-content">
1177
+ <h4 className="grapher_body-2-semibold">{props.title}</h4>
1178
+ <div className="download-modal__download-button-description-wrapper">
1179
+ <p className="grapher_label-1-regular download-modal__download-button-description">
1180
+ {props.description}
1181
+ </p>
1182
+ {showLoadingUI && (
1183
+ <p className="grapher_label-1-regular download-modal__download-button-loading-label">
1184
+ Downloading…
1185
+ </p>
1186
+ )}
1187
+ </div>
1188
+ </div>
1189
+ <div className="download-modal__download-icon">
1190
+ {showLoadingUI ? (
1191
+ <FontAwesomeIcon icon={faSpinner} spin />
1192
+ ) : (
1193
+ <FontAwesomeIcon icon={faDownload} />
1194
+ )}
1195
+ </div>
1196
+ </button>
1197
+ )
1198
+ }
1199
+
1200
+ interface CalloutProps {
1201
+ title: React.ReactNode
1202
+ icon?: React.ReactElement
1203
+ children: React.ReactNode
1204
+ }
1205
+
1206
+ function Callout(props: CalloutProps): React.ReactElement {
1207
+ return (
1208
+ <div className="download-modal__callout">
1209
+ {props.title && (
1210
+ <h4 className="title grapher_body-2-semibold">
1211
+ {props.icon}
1212
+ {props.title}
1213
+ </h4>
1214
+ )}
1215
+ <p className="grapher_label-2-regular grapher_light">
1216
+ {props.children}
1217
+ </p>
1218
+ </div>
1219
+ )
1220
+ }
1221
+
1222
+ function makeNumberOfRowsSnippet(numRows: number): string {
1223
+ if (numRows <= 0) return " (empty)"
1224
+ if (numRows === 1) return " (1 row)"
1225
+ return ` (${formatValue(numRows, { numDecimalPlaces: 0 })} rows)`
1226
+ }