@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,1173 @@
1
+ import * as _ from "lodash-es"
2
+ import { CSSProperties } from "react"
3
+ import * as React from "react"
4
+ import { computed, makeObservable } from "mobx"
5
+ import {
6
+ excludeUndefined,
7
+ imemo,
8
+ Bounds,
9
+ FontFamily,
10
+ } from "../../utils/index.js"
11
+ import { DetailsMarker } from "../../types/index.js"
12
+ import { TextWrap } from "../TextWrap/TextWrap.js"
13
+ import { fromMarkdown } from "mdast-util-from-markdown"
14
+ import type { Content, Root } from "mdast"
15
+ import { match } from "ts-pattern"
16
+ import { urlRegex } from "../markdown/remarkPlainLinks.js"
17
+ import * as R from "remeda"
18
+
19
+ const SUPERSCRIPT_NUMERALS = {
20
+ "0": "\u2070",
21
+ "1": "\u00b9",
22
+ "2": "\u00b2",
23
+ "3": "\u00b3",
24
+ "4": "\u2074",
25
+ "5": "\u2075",
26
+ "6": "\u2076",
27
+ "7": "\u2077",
28
+ "8": "\u2078",
29
+ "9": "\u2079",
30
+ }
31
+
32
+ export interface IRFontParams {
33
+ fontSize?: number
34
+ fontWeight?: number
35
+ fontFamily?: FontFamily
36
+ isItalic?: boolean
37
+ }
38
+
39
+ export interface IRBreakpoint {
40
+ tokenIndex: number
41
+ tokenStartOffset: number
42
+ breakOffset: number
43
+ }
44
+
45
+ export interface IRToken {
46
+ width: number
47
+ getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined
48
+ toHTML(key?: React.Key): React.ReactElement | undefined
49
+ toSVG(key?: React.Key): React.ReactElement | undefined
50
+ toPlaintext(): string | undefined
51
+ }
52
+
53
+ export class IRText implements IRToken {
54
+ constructor(
55
+ public text: string,
56
+ public fontParams?: IRFontParams
57
+ ) {}
58
+ @imemo get width(): number {
59
+ return Bounds.forText(this.text, this.fontParams).width
60
+ }
61
+ @imemo get height(): number {
62
+ return this.fontParams?.fontSize || 13
63
+ }
64
+ getBreakpointBefore(): undefined {
65
+ return undefined
66
+ }
67
+ toHTML(key?: React.Key): React.ReactElement {
68
+ return <span key={key}>{this.text}</span>
69
+ }
70
+ toSVG(key?: React.Key): React.ReactElement {
71
+ return <React.Fragment key={key}>{this.text}</React.Fragment>
72
+ }
73
+ toPlaintext(): string {
74
+ return this.text
75
+ }
76
+ }
77
+
78
+ export class IRWhitespace implements IRToken {
79
+ constructor(public fontParams?: IRFontParams) {}
80
+ @imemo get width(): number {
81
+ return Bounds.forText(" ", this.fontParams).width
82
+ }
83
+ getBreakpointBefore(): IRBreakpoint {
84
+ // Have to give it some `breakOffset` because we designate locations
85
+ // to split based on it, and `0` leads to being exactly in between tokens.
86
+ return { tokenIndex: 0, tokenStartOffset: 0, breakOffset: 0.0001 }
87
+ }
88
+ toHTML(key?: React.Key): React.ReactElement {
89
+ return <span key={key}> </span>
90
+ }
91
+ toSVG(key?: React.Key): React.ReactElement {
92
+ return <React.Fragment key={key}> </React.Fragment>
93
+ }
94
+ toPlaintext(): string {
95
+ return " "
96
+ }
97
+ }
98
+
99
+ export class IRLineBreak implements IRToken {
100
+ get width(): number {
101
+ return 0
102
+ }
103
+ getBreakpointBefore(): undefined {
104
+ return undefined
105
+ }
106
+ toHTML(key?: React.Key): React.ReactElement {
107
+ return <br key={key} />
108
+ }
109
+ toSVG(): undefined {
110
+ // We have to deal with this special case in
111
+ // whatever procedure does text reflow.
112
+ return undefined
113
+ }
114
+ toPlaintext(): string {
115
+ return "\n"
116
+ }
117
+ }
118
+
119
+ export abstract class IRElement implements IRToken {
120
+ constructor(
121
+ public children: IRToken[],
122
+ public fontParams?: IRFontParams
123
+ ) {}
124
+
125
+ @imemo get width(): number {
126
+ return getLineWidth(this.children)
127
+ }
128
+
129
+ getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined {
130
+ return getBreakpointBefore(this.children, targetWidth)
131
+ }
132
+
133
+ splitBefore(maxWidth: number): {
134
+ before: IRToken | undefined
135
+ after: IRToken | undefined
136
+ } {
137
+ const { before, after } = splitLineAtBreakpoint(this.children, maxWidth)
138
+ return {
139
+ // do not create tokens without children
140
+ before: before.length ? this.getClone(before) : undefined,
141
+ after: after.length ? this.getClone(after) : undefined,
142
+ }
143
+ }
144
+
145
+ splitOnLineBreaks(): IRToken[][] {
146
+ const lines = splitAllOnNewline(this.children)
147
+ if (lines.length > 1) {
148
+ return lines.map((tokens) =>
149
+ // Do not create a clone without children.
150
+ // There aren't any children in a line when the first or last
151
+ // token is a newline.
152
+ tokens.length ? [this.getClone(tokens)] : []
153
+ )
154
+ }
155
+ // Do not create copies of element
156
+ // if there are no newlines inside.
157
+ return [[this]]
158
+ }
159
+
160
+ abstract getClone(children: IRToken[]): IRElement
161
+ abstract toHTML(key?: React.Key): React.ReactElement
162
+ abstract toSVG(key?: React.Key): React.ReactElement
163
+
164
+ toPlaintext(): string {
165
+ return lineToPlaintext(this.children)
166
+ }
167
+ }
168
+
169
+ export class IRBold extends IRElement {
170
+ getClone(children: IRToken[]): IRBold {
171
+ return new IRBold(children, this.fontParams)
172
+ }
173
+ toHTML(key?: React.Key): React.ReactElement {
174
+ return (
175
+ <strong key={key}>
176
+ {this.children.map((child, i) => child.toHTML(i))}
177
+ </strong>
178
+ )
179
+ }
180
+ toSVG(key?: React.Key): React.ReactElement {
181
+ return (
182
+ <tspan key={key} style={{ fontWeight: 700 }}>
183
+ {this.children.map((child, i) => child.toSVG(i))}
184
+ </tspan>
185
+ )
186
+ }
187
+ }
188
+
189
+ export class IRSpan extends IRElement {
190
+ getClone(children: IRToken[]): IRSpan {
191
+ return new IRSpan(children, this.fontParams)
192
+ }
193
+ toHTML(key?: React.Key): React.ReactElement {
194
+ return (
195
+ <span key={key}>
196
+ {this.children.map((child, i) => child.toHTML(i))}
197
+ </span>
198
+ )
199
+ }
200
+ toSVG(key?: React.Key): React.ReactElement {
201
+ return (
202
+ <tspan key={key}>
203
+ {this.children.map((child, i) => child.toSVG(i))}
204
+ </tspan>
205
+ )
206
+ }
207
+ }
208
+
209
+ export class IRSuperscript implements IRToken {
210
+ constructor(
211
+ public text: string,
212
+ public fontParams?: IRFontParams
213
+ ) {}
214
+ @imemo get width(): number {
215
+ return Bounds.forText(this.text, { fontSize: this.height / 2 }).width
216
+ }
217
+ @imemo get height(): number {
218
+ return this.fontParams?.fontSize || 16
219
+ }
220
+ getBreakpointBefore(): undefined {
221
+ return undefined
222
+ }
223
+ toHTML(key?: React.Key): React.ReactElement {
224
+ return <sup key={key}>{this.text}</sup>
225
+ }
226
+ toSVG(key?: React.Key): React.ReactElement {
227
+ // replace numerals with literals, for everything else let the font-feature handle it
228
+ const style = { fontFeatureSettings: '"sups"' }
229
+ const text = this.text.replace(/./g, (c) =>
230
+ _.get(SUPERSCRIPT_NUMERALS, c, c)
231
+ )
232
+ return (
233
+ <React.Fragment key={key}>
234
+ <tspan style={style}>{text}</tspan>
235
+ </React.Fragment>
236
+ )
237
+ }
238
+ toPlaintext(): string {
239
+ return this.text
240
+ }
241
+ }
242
+
243
+ export class IRItalic extends IRElement {
244
+ getClone(children: IRToken[]): IRItalic {
245
+ return new IRItalic(children, this.fontParams)
246
+ }
247
+ toHTML(key?: React.Key): React.ReactElement {
248
+ return (
249
+ <em key={key}>
250
+ {this.children.map((child, i) => child.toHTML(i))}
251
+ </em>
252
+ )
253
+ }
254
+ toSVG(key?: React.Key): React.ReactElement {
255
+ return (
256
+ <tspan key={key} style={{ fontStyle: "italic" }}>
257
+ {this.children.map((child, i) => child.toSVG(i))}
258
+ </tspan>
259
+ )
260
+ }
261
+ }
262
+
263
+ export class IRLink extends IRElement {
264
+ constructor(
265
+ public href: string,
266
+ children: IRToken[],
267
+ fontParams?: IRFontParams
268
+ ) {
269
+ super(children, fontParams)
270
+ }
271
+ getClone(children: IRToken[]): IRLink {
272
+ return new IRLink(this.href, children, this.fontParams)
273
+ }
274
+ toHTML(key?: React.Key): React.ReactElement {
275
+ return (
276
+ <a key={key} href={this.href}>
277
+ {this.children.map((child, i) => child.toHTML(i))}
278
+ </a>
279
+ )
280
+ }
281
+ toSVG(key?: React.Key): React.ReactElement {
282
+ return (
283
+ <a
284
+ key={key}
285
+ href={this.href}
286
+ style={{ textDecoration: "underline" }}
287
+ >
288
+ {this.children.map((child, i) => child.toSVG(i))}
289
+ </a>
290
+ )
291
+ }
292
+ }
293
+
294
+ export class IRDetailOnDemand extends IRElement {
295
+ constructor(
296
+ public term: string,
297
+ children: IRToken[],
298
+ fontParams?: IRFontParams
299
+ ) {
300
+ super(children, fontParams)
301
+ }
302
+ getClone(children: IRToken[]): IRDetailOnDemand {
303
+ return new IRDetailOnDemand(this.term, children, this.fontParams)
304
+ }
305
+ toHTML(key?: React.Key): React.ReactElement {
306
+ return (
307
+ <span
308
+ key={key}
309
+ className="dod-span"
310
+ data-id={this.term}
311
+ tabIndex={0}
312
+ >
313
+ {this.children.map((child, i) => child.toHTML(i))}
314
+ </span>
315
+ )
316
+ }
317
+ toSVG(key?: React.Key): React.ReactElement {
318
+ return (
319
+ <tspan key={key} className="dod-span" data-id={this.term}>
320
+ {this.children.map((child, i) => child.toSVG(i))}
321
+ </tspan>
322
+ )
323
+ }
324
+ }
325
+
326
+ function splitAllOnNewline(tokens: IRToken[]): IRToken[][] {
327
+ if (!tokens.length) return []
328
+ let currentLine: IRToken[] = []
329
+ const lines: IRToken[][] = [currentLine]
330
+ const unproccessed: IRToken[] = [...tokens]
331
+ while (unproccessed.length > 0) {
332
+ const token = unproccessed.shift()!
333
+ if (token instanceof IRElement) {
334
+ const [firstLine, ...otherLines] = token.splitOnLineBreaks()
335
+ if (firstLine) currentLine.push(...firstLine)
336
+ if (otherLines.length) {
337
+ lines.push(...otherLines)
338
+ currentLine = R.last(lines)!
339
+ }
340
+ } else if (token instanceof IRLineBreak) {
341
+ currentLine = []
342
+ lines.push(currentLine)
343
+ } else {
344
+ currentLine.push(token)
345
+ }
346
+ }
347
+ return lines
348
+ }
349
+
350
+ export function splitLineAtBreakpoint(
351
+ tokens: IRToken[],
352
+ breakWidth: number
353
+ ): { before: IRToken[]; after: IRToken[] } {
354
+ let i = 0
355
+ let offset = 0
356
+ // finding the token where the split should be
357
+ // NOTE: the token may not be splittable, which is why we need an exact
358
+ // `breakWidth` provided, not the line width.
359
+ while (i < tokens.length - 1 && offset + tokens[i].width < breakWidth) {
360
+ offset += tokens[i].width
361
+ i++
362
+ }
363
+ const token = tokens[i]
364
+ if (token instanceof IRElement) {
365
+ const { before, after } = token.splitBefore(breakWidth - offset)
366
+ return {
367
+ before: excludeUndefined([...tokens.slice(0, i), before]),
368
+ after: excludeUndefined([after, ...tokens.slice(i + 1)]),
369
+ }
370
+ } else {
371
+ return { before: tokens.slice(0, i), after: trimLeft(tokens.slice(i)) }
372
+ }
373
+ }
374
+
375
+ function trimLeft(tokens: IRToken[]): IRToken[] {
376
+ let i = 0
377
+ while (i < tokens.length && tokens[i] instanceof IRWhitespace) {
378
+ i++
379
+ }
380
+ return tokens.slice(i)
381
+ }
382
+
383
+ // Even though it says "before", it may return a breakpoint after, because
384
+ // there is no earlier breakpoint in the line.
385
+ export function getBreakpointBefore(
386
+ tokens: IRToken[],
387
+ maxWidth: number
388
+ ): IRBreakpoint | undefined {
389
+ let tokenStartOffset = 0
390
+ let prevBreakpoint: IRBreakpoint | undefined = undefined
391
+ for (let index = 0; index < tokens.length; index++) {
392
+ const token = tokens[index]
393
+ const candidate = token.getBreakpointBefore(maxWidth - tokenStartOffset)
394
+ if (candidate !== undefined) {
395
+ if (
396
+ prevBreakpoint &&
397
+ candidate.breakOffset + tokenStartOffset > maxWidth
398
+ ) {
399
+ break
400
+ }
401
+ prevBreakpoint = {
402
+ tokenStartOffset,
403
+ tokenIndex: index,
404
+ breakOffset: candidate.breakOffset + tokenStartOffset,
405
+ }
406
+ }
407
+ tokenStartOffset += token.width
408
+ }
409
+ return prevBreakpoint
410
+ }
411
+
412
+ export function getLineWidth(tokens: IRToken[]): number {
413
+ return _.sum(tokens.map((token) => token.width))
414
+ }
415
+
416
+ // useful for debugging
417
+ export function lineToPlaintext(tokens: IRToken[]): string {
418
+ return tokens.map((t) => t.toPlaintext()).join("")
419
+ }
420
+
421
+ export const isTextToken = (token: IRToken): token is IRText | IRWhitespace =>
422
+ token instanceof IRText || token instanceof IRWhitespace
423
+
424
+ /**
425
+ * Merges adjacent text tokens, because the way we render text in React is otherwise
426
+ * not very compatible with Google Translate, breaking our site in weird ways when
427
+ * translated.
428
+ * This is to be run _just before_ rendering to HTML, because it loses some
429
+ * information and is not easily reversible.
430
+ * See also https://github.com/owid/owid-grapher/issues/1785
431
+ */
432
+ export const recursiveMergeTextTokens = (
433
+ tokens: IRToken[],
434
+ fontParams?: IRFontParams
435
+ ): IRToken[] => {
436
+ if (tokens.length === 0) return []
437
+
438
+ // merge adjacent text tokens into one
439
+ const mergedTextTokens: IRToken[] = tokens.reduce((acc, token) => {
440
+ if (isTextToken(token)) {
441
+ const l = R.last(acc)
442
+ if (l && isTextToken(l)) {
443
+ // replace last value in acc with merged text token
444
+ acc.pop()
445
+ return [
446
+ ...acc,
447
+ new IRText(
448
+ l.toPlaintext() + token.toPlaintext(),
449
+ fontParams
450
+ ),
451
+ ]
452
+ }
453
+ }
454
+ return [...acc, token]
455
+ }, [] as IRToken[])
456
+
457
+ // recursively enter non-text tokens, and merge their children
458
+ return mergedTextTokens.map((token) => {
459
+ if (token instanceof IRElement) {
460
+ return token.getClone(
461
+ recursiveMergeTextTokens(token.children, fontParams)
462
+ )
463
+ }
464
+ return token
465
+ })
466
+ }
467
+
468
+ export function splitIntoLines(
469
+ tokens: IRToken[],
470
+ maxWidth: number
471
+ ): IRToken[][] {
472
+ const processedLines: IRToken[][] = []
473
+ const unprocessedLines: IRToken[][] = splitAllOnNewline(tokens)
474
+ while (unprocessedLines.length) {
475
+ const currentLine = unprocessedLines.shift()!
476
+ if (getLineWidth(currentLine) <= maxWidth) {
477
+ processedLines.push(currentLine)
478
+ } else {
479
+ const breakpoint = getBreakpointBefore(currentLine, maxWidth)
480
+ if (!breakpoint) {
481
+ processedLines.push(currentLine)
482
+ } else {
483
+ const { before, after } = splitLineAtBreakpoint(
484
+ currentLine,
485
+ breakpoint.breakOffset
486
+ )
487
+ processedLines.push(before)
488
+ unprocessedLines.unshift(after)
489
+ }
490
+ }
491
+ }
492
+ return processedLines
493
+ }
494
+
495
+ export const sumTextWrapHeights = (
496
+ elements: MarkdownTextWrap[] | TextWrap[],
497
+ spacer: number = 0
498
+ ): number =>
499
+ _.sum(elements.map((element) => element.height)) +
500
+ (elements.length - 1) * spacer
501
+
502
+ type MarkdownTextWrapOptions = {
503
+ maxWidth?: number
504
+ fontFamily?: FontFamily
505
+ fontSize: number
506
+ fontWeight?: number
507
+ lineHeight?: number
508
+ style?: CSSProperties
509
+ detailsOrderedByReference?: string[]
510
+ }
511
+
512
+ type MarkdownTextWrapProps = { text: string } & MarkdownTextWrapOptions
513
+
514
+ type TextFragment = { text: string; bold?: boolean }
515
+
516
+ export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
517
+ constructor(props: MarkdownTextWrapProps) {
518
+ super(props)
519
+ makeObservable(this)
520
+ }
521
+
522
+ static fromFragments({
523
+ main,
524
+ secondary,
525
+ newLine = "continue-line",
526
+ textWrapProps,
527
+ }: {
528
+ main: TextFragment
529
+ secondary: TextFragment
530
+ newLine?: "continue-line" | "always" | "avoid-wrap"
531
+ textWrapProps: Omit<MarkdownTextWrapOptions, "fontWeight">
532
+ }) {
533
+ const mainMarkdownText = maybeBoldMarkdownText(main)
534
+ const secondaryMarkdownText = maybeBoldMarkdownText(secondary)
535
+
536
+ const combinedTextContinued = [
537
+ mainMarkdownText,
538
+ secondaryMarkdownText,
539
+ ].join(" ")
540
+ const combinedTextNewLine = [
541
+ mainMarkdownText,
542
+ secondaryMarkdownText,
543
+ ].join("\n")
544
+
545
+ if (newLine === "always") {
546
+ return new MarkdownTextWrap({
547
+ text: combinedTextNewLine,
548
+ ...textWrapProps,
549
+ })
550
+ }
551
+
552
+ if (newLine === "continue-line") {
553
+ return new MarkdownTextWrap({
554
+ text: combinedTextContinued,
555
+ ...textWrapProps,
556
+ })
557
+ }
558
+
559
+ // if newLine is set to 'avoid-wrap', we first try to fit the secondary text
560
+ // on the same line as the main text. If it doesn't fit, we place it on a new line.
561
+
562
+ const mainTextWrap = new MarkdownTextWrap({ ...main, ...textWrapProps })
563
+ const secondaryTextWrap = new MarkdownTextWrap({
564
+ text: secondaryMarkdownText,
565
+ ...textWrapProps,
566
+ maxWidth:
567
+ mainTextWrap.maxWidth -
568
+ mainTextWrap.lastLineWidth -
569
+ Bounds.forText(" ", textWrapProps).width -
570
+ 10, // arbitrary wiggle room
571
+ })
572
+
573
+ const secondaryTextFitsOnSameLine =
574
+ secondaryTextWrap.svgLines.length === 1
575
+ if (secondaryTextFitsOnSameLine) {
576
+ return new MarkdownTextWrap({
577
+ text: combinedTextContinued,
578
+ ...textWrapProps,
579
+ })
580
+ } else {
581
+ return new MarkdownTextWrap({
582
+ text: combinedTextNewLine,
583
+ ...textWrapProps,
584
+ })
585
+ }
586
+ }
587
+
588
+ @computed get maxWidth(): number {
589
+ return this.props.maxWidth ?? Infinity
590
+ }
591
+ @computed get lineHeight(): number {
592
+ return this.props.lineHeight ?? 1.1
593
+ }
594
+ @computed get fontSize(): number {
595
+ return this.props.fontSize
596
+ }
597
+ @computed get fontParams(): IRFontParams {
598
+ return {
599
+ fontFamily: this.props.fontFamily,
600
+ fontSize: this.props.fontSize,
601
+ fontWeight: this.props.fontWeight,
602
+ }
603
+ }
604
+ @computed get text(): string {
605
+ // NOTE: ❗Here we deviate from the normal markdown spec. We replace \n with <SPACE><SPACE>\n to make sure that single \n are treated as
606
+ // actual line breaks but only if none of the other markdown line break rules apply.
607
+ // This is a bit different to how markdown usually works but we have a substantial
608
+ // amount of legacy charts that use newlines in this way and it seems that it is
609
+ // better to support this simple case than to do a data migration of many chart subtitles.
610
+ const baseText = this.props.text
611
+ // This replace is a bit funky - we want to make sure that single \n are treated as
612
+ // actual line breaks but only if none of the other markdown line break rules apply.
613
+ // These are:
614
+ // - \n\n is always a new paragraph
615
+ // - Two spaces before \n is a line break (this rule is not entirely checked as we only check for a single space)
616
+ // - A backslash before \n is a line break
617
+ // The code below normalizes all cases to <SPACE><SPACE>\n which will lead to them surviving the markdown parsing
618
+ let text = baseText.trim()
619
+ text = text.replaceAll("\n\n", "@@LINEBREAK@@")
620
+ text = text.replaceAll("\\\n", "@@LINEBREAK@@")
621
+ text = text.replaceAll(" \n", "@@LINEBREAK@@")
622
+ text = text.replaceAll("\n", " \n")
623
+ text = text.replaceAll("@@LINEBREAK@@", " \n")
624
+ return text
625
+ }
626
+ @computed get detailsOrderedByReference(): string[] {
627
+ return this.props.detailsOrderedByReference || []
628
+ }
629
+
630
+ @computed get plaintext(): string {
631
+ return this.htmlLines.map(lineToPlaintext).join("\n")
632
+ }
633
+
634
+ @computed get tokensFromMarkdown(): IRToken[] {
635
+ const tokens = convertMarkdownToIRTokens(this.text, this.fontParams)
636
+ return tokens
637
+ }
638
+
639
+ @computed get htmlLines(): IRToken[][] {
640
+ const tokens = this.tokensFromMarkdown
641
+ const lines = splitIntoLines(tokens, this.maxWidth)
642
+ return lines.map((line) =>
643
+ recursiveMergeTextTokens(line, this.fontParams)
644
+ )
645
+ }
646
+
647
+ @computed get svgLines(): IRToken[][] {
648
+ const tokens = this.tokensFromMarkdown
649
+ const lines = splitIntoLines(tokens, this.maxWidth)
650
+ return lines
651
+ }
652
+
653
+ @computed get svgLinesWithDodReferenceNumbers(): IRToken[][] {
654
+ const references = this.detailsOrderedByReference
655
+ const tokens = this.tokensFromMarkdown
656
+ const tokensWithReferenceNumbers = appendReferenceNumbers(
657
+ tokens,
658
+ references
659
+ )
660
+ return splitIntoLines(tokensWithReferenceNumbers, this.maxWidth)
661
+ }
662
+
663
+ @computed get width(): number {
664
+ const { htmlLines } = this
665
+ const lineLengths = htmlLines.map((tokens) =>
666
+ _.sumBy(tokens, (token) => token.width)
667
+ )
668
+ return _.max(lineLengths) ?? 0
669
+ }
670
+
671
+ @computed get singleLineHeight(): number {
672
+ return this.fontSize * this.lineHeight
673
+ }
674
+
675
+ @computed get lastLineWidth(): number {
676
+ return _.sumBy(R.last(this.htmlLines), (token) => token.width) ?? 0
677
+ }
678
+
679
+ @computed get height(): number {
680
+ const { htmlLines } = this
681
+ if (htmlLines.length === 0) return 0
682
+ return htmlLines.length * this.singleLineHeight
683
+ }
684
+
685
+ @computed get style(): any {
686
+ return {
687
+ ...this.fontParams,
688
+ ...this.props.style,
689
+ lineHeight: this.lineHeight,
690
+ }
691
+ }
692
+
693
+ renderHTML() {
694
+ const { htmlLines } = this
695
+ if (htmlLines.length === 0) return null
696
+ return (
697
+ <span style={this.style} className="markdown-text-wrap">
698
+ {htmlLines.map((line, i) => {
699
+ const plaintextLine = line
700
+ .map((token) => token.toPlaintext())
701
+ .join("")
702
+ return (
703
+ <MarkdownTextWrapLine
704
+ key={`${plaintextLine}-${i}`}
705
+ line={line}
706
+ />
707
+ )
708
+ })}
709
+ </span>
710
+ )
711
+ }
712
+
713
+ renderSVG(
714
+ x: number,
715
+ y: number,
716
+ {
717
+ textProps,
718
+ detailsMarker = "superscript",
719
+ id,
720
+ }: {
721
+ textProps?: React.SVGProps<SVGTextElement>
722
+ detailsMarker?: DetailsMarker
723
+ id?: string
724
+ } = {}
725
+ ) {
726
+ const { fontSize, lineHeight } = this
727
+ const lines =
728
+ detailsMarker === "superscript"
729
+ ? this.svgLinesWithDodReferenceNumbers
730
+ : this.svgLines
731
+ if (lines.length === 0) return <></>
732
+
733
+ // Magic number set through experimentation.
734
+ // The HTML and SVG renderers need to position lines identically.
735
+ // This number was tweaked until the overlaid HTML and SVG outputs
736
+ // overlap.
737
+ const HEIGHT_CORRECTION_FACTOR = 0.74
738
+
739
+ const textHeight = fontSize * HEIGHT_CORRECTION_FACTOR
740
+ const containerHeight = lineHeight * fontSize
741
+ const yOffset =
742
+ y + (containerHeight - (containerHeight - textHeight) / 2)
743
+
744
+ const getLineY = (lineIndex: number) =>
745
+ yOffset + lineHeight * fontSize * lineIndex
746
+
747
+ return (
748
+ <g id={id} className="markdown-text-wrap">
749
+ <text
750
+ x={x.toFixed(1)}
751
+ y={yOffset.toFixed(1)}
752
+ style={this.style}
753
+ {...textProps}
754
+ >
755
+ {lines.map((line, lineIndex) => (
756
+ <tspan
757
+ key={lineIndex}
758
+ x={x}
759
+ y={getLineY(lineIndex).toFixed(1)}
760
+ >
761
+ {line.map((token, tokenIndex) =>
762
+ token.toSVG(tokenIndex)
763
+ )}
764
+ </tspan>
765
+ ))}
766
+ </text>
767
+ {/* SVG doesn't support dotted underlines, so we draw them manually */}
768
+ {detailsMarker === "underline" &&
769
+ lines.map((line, lineIndex) => {
770
+ const y = (getLineY(lineIndex) + 2).toFixed(1)
771
+ let currWidth = 0
772
+ return line.map((token) => {
773
+ const underline =
774
+ token instanceof IRDetailOnDemand ? (
775
+ <line
776
+ className="dod-underline"
777
+ x1={x + currWidth}
778
+ y1={y}
779
+ x2={x + currWidth + token.width}
780
+ y2={y}
781
+ stroke="currentColor"
782
+ strokeWidth={1}
783
+ strokeDasharray={1}
784
+ // important for rotated text
785
+ transform={textProps?.transform}
786
+ />
787
+ ) : null
788
+ currWidth += token.width
789
+ return underline
790
+ })
791
+ })}
792
+ </g>
793
+ )
794
+ }
795
+
796
+ // An alias method that allows MarkdownTextWrap to be
797
+ // instantiated via JSX for HTML rendering
798
+ // <MarkdownTextWrap ... />
799
+ override render(): React.ReactElement | null {
800
+ return this.renderHTML()
801
+ }
802
+ }
803
+
804
+ // The rule doesn't support class components in the same file.
805
+ // eslint-disable-next-line react-refresh/only-export-components
806
+ function MarkdownTextWrapLine({
807
+ line,
808
+ }: {
809
+ line: IRToken[]
810
+ }): React.ReactElement {
811
+ return (
812
+ <span className="markdown-text-wrap__line">
813
+ {line.length ? line.map((token, i) => token.toHTML(i)) : <br />}
814
+ </span>
815
+ )
816
+ }
817
+
818
+ export function convertMarkdownToIRTokens(
819
+ markdown: string,
820
+ fontParams?: IRFontParams
821
+ ): IRToken[] {
822
+ const ast: Root = fromMarkdown(markdown)
823
+ const children = ast.children.flatMap((item) =>
824
+ convertMarkdownNodeToIRTokens(item, fontParams)
825
+ )
826
+ // ensure that there are no leading or trailing line breaks
827
+ return R.dropLastWhile(
828
+ R.dropWhile(children, (token) => token instanceof IRLineBreak),
829
+ (token) => token instanceof IRLineBreak
830
+ )
831
+ }
832
+
833
+ // When using mdast types version 4 this should be typed as:
834
+ // node: RootContentMap[keyof RootContentMap]
835
+ function convertMarkdownNodeToIRTokens(
836
+ node: Content,
837
+ fontParams: IRFontParams = {}
838
+ ): IRToken[] {
839
+ const converted = match(node)
840
+ .with(
841
+ {
842
+ type: "blockquote",
843
+ },
844
+ (item) => {
845
+ return item.children.flatMap((child) =>
846
+ convertMarkdownNodeToIRTokens(child, fontParams)
847
+ )
848
+ }
849
+ )
850
+ .with(
851
+ {
852
+ type: "break",
853
+ },
854
+ (_) => {
855
+ return [new IRLineBreak()]
856
+ }
857
+ )
858
+ .with(
859
+ {
860
+ type: "code",
861
+ },
862
+ (item) => {
863
+ return [new IRText(item.value, fontParams)]
864
+ }
865
+ )
866
+ .with(
867
+ {
868
+ type: "emphasis",
869
+ },
870
+ (item) => {
871
+ return [
872
+ new IRItalic(
873
+ item.children.flatMap((child) =>
874
+ convertMarkdownNodeToIRTokens(child, {
875
+ ...fontParams,
876
+ isItalic: true,
877
+ })
878
+ )
879
+ ),
880
+ ]
881
+ }
882
+ )
883
+ .with(
884
+ {
885
+ type: "heading",
886
+ },
887
+ (item) => {
888
+ return item.children.flatMap((child) =>
889
+ convertMarkdownNodeToIRTokens(child, fontParams)
890
+ )
891
+ }
892
+ )
893
+ .with(
894
+ {
895
+ type: "html",
896
+ },
897
+ (item) => {
898
+ return [new IRText(item.value, fontParams)]
899
+ }
900
+ )
901
+ .with(
902
+ {
903
+ type: "image",
904
+ },
905
+ (item) => {
906
+ return [new IRText(item.alt ?? "", fontParams)]
907
+ }
908
+ )
909
+ .with(
910
+ {
911
+ type: "inlineCode",
912
+ },
913
+ (item) => {
914
+ return [new IRText(item.value, fontParams)]
915
+ }
916
+ )
917
+ .with(
918
+ {
919
+ type: "link",
920
+ },
921
+ (item) => {
922
+ if (item.url.startsWith("#dod:")) {
923
+ const term = item.url.replace("#dod:", "")
924
+ return [
925
+ new IRDetailOnDemand(
926
+ term,
927
+ item.children.flatMap((child) =>
928
+ convertMarkdownNodeToIRTokens(child, fontParams)
929
+ ),
930
+ fontParams
931
+ ),
932
+ ]
933
+ } else
934
+ return [
935
+ new IRLink(
936
+ item.url,
937
+ item.children.flatMap((child) =>
938
+ convertMarkdownNodeToIRTokens(child, fontParams)
939
+ )
940
+ ),
941
+ ]
942
+ }
943
+ )
944
+ .with(
945
+ {
946
+ type: "list",
947
+ },
948
+ (item) => {
949
+ if (item.ordered)
950
+ return item.children.flatMap((child, index) => [
951
+ new IRLineBreak(),
952
+ new IRText(`${index + 1}) `, fontParams),
953
+ ...convertMarkdownNodeToIRTokens(child, fontParams),
954
+ ])
955
+ else
956
+ return item.children.flatMap((child) => [
957
+ new IRLineBreak(),
958
+ new IRText(`• `, fontParams),
959
+ ...convertMarkdownNodeToIRTokens(child, fontParams),
960
+ ])
961
+ }
962
+ )
963
+ .with(
964
+ {
965
+ type: "listItem",
966
+ },
967
+ (item) => {
968
+ return item.children.flatMap((child) =>
969
+ convertMarkdownNodeToIRTokens(child, fontParams)
970
+ )
971
+ }
972
+ )
973
+ .with(
974
+ {
975
+ type: "paragraph",
976
+ },
977
+ (item) => {
978
+ return item.children.flatMap((child) =>
979
+ convertMarkdownNodeToIRTokens(child, fontParams)
980
+ )
981
+ }
982
+ )
983
+ .with(
984
+ {
985
+ type: "strong",
986
+ },
987
+ (item) => {
988
+ return [
989
+ new IRBold(
990
+ item.children.flatMap((child) =>
991
+ convertMarkdownNodeToIRTokens(child, {
992
+ ...fontParams,
993
+ fontWeight: 700,
994
+ })
995
+ )
996
+ ),
997
+ ]
998
+ }
999
+ )
1000
+ .with(
1001
+ {
1002
+ type: "text",
1003
+ },
1004
+ (item) => {
1005
+ const splitted = item.value.split(/\s+/)
1006
+ const tokens = splitted.flatMap((text, i) => {
1007
+ const textNode = new IRText(text, fontParams)
1008
+ const node = text.match(urlRegex)
1009
+ ? new IRLink(text, [textNode], fontParams)
1010
+ : textNode
1011
+ if (i < splitted.length - 1) {
1012
+ return [node, new IRWhitespace(fontParams)]
1013
+ } else return [node]
1014
+ })
1015
+ return tokens
1016
+ }
1017
+ )
1018
+ .with(
1019
+ {
1020
+ type: "thematicBreak",
1021
+ },
1022
+ (_) => {
1023
+ return [new IRText("---", fontParams)]
1024
+ }
1025
+ )
1026
+ .with(
1027
+ {
1028
+ type: "delete",
1029
+ },
1030
+ (item) => {
1031
+ return item.children.flatMap((child) =>
1032
+ convertMarkdownNodeToIRTokens(child, fontParams)
1033
+ )
1034
+ }
1035
+ )
1036
+ // Now lets finish this with blocks for FootnoteDefinition, Definition, ImageReference, LinkReference, FootnoteReference, and Table
1037
+ .with(
1038
+ {
1039
+ type: "footnoteDefinition",
1040
+ },
1041
+ (item) => {
1042
+ return item.children.flatMap((child) =>
1043
+ convertMarkdownNodeToIRTokens(child, fontParams)
1044
+ )
1045
+ }
1046
+ )
1047
+ .with(
1048
+ {
1049
+ type: "definition",
1050
+ },
1051
+ (item) => {
1052
+ return [
1053
+ new IRText(`${item.identifier}: ${item.label}`, fontParams),
1054
+ ]
1055
+ }
1056
+ )
1057
+ .with(
1058
+ {
1059
+ type: "imageReference",
1060
+ },
1061
+ (item) => {
1062
+ return [
1063
+ new IRText(`${item.identifier}: ${item.label}`, fontParams),
1064
+ ]
1065
+ }
1066
+ )
1067
+ .with(
1068
+ {
1069
+ type: "linkReference",
1070
+ },
1071
+ (item) => {
1072
+ return [
1073
+ new IRText(`${item.identifier}: ${item.label}`, fontParams),
1074
+ ]
1075
+ }
1076
+ )
1077
+ .with(
1078
+ {
1079
+ type: "footnoteReference",
1080
+ },
1081
+ (item) => {
1082
+ return [
1083
+ new IRText(`${item.identifier}: ${item.label}`, fontParams),
1084
+ ]
1085
+ }
1086
+ )
1087
+ .with(
1088
+ {
1089
+ type: "table",
1090
+ },
1091
+ (item) => {
1092
+ return item.children.flatMap((child) =>
1093
+ convertMarkdownNodeToIRTokens(child, fontParams)
1094
+ )
1095
+ }
1096
+ )
1097
+ .with(
1098
+ {
1099
+ type: "tableCell",
1100
+ },
1101
+ (item) => {
1102
+ return item.children.flatMap((child) =>
1103
+ convertMarkdownNodeToIRTokens(child, fontParams)
1104
+ )
1105
+ }
1106
+ )
1107
+ // and now TableRow and Yaml
1108
+ .with(
1109
+ {
1110
+ type: "tableRow",
1111
+ },
1112
+ (item) => {
1113
+ return item.children.flatMap((child) =>
1114
+ convertMarkdownNodeToIRTokens(child, fontParams)
1115
+ )
1116
+ }
1117
+ )
1118
+ .with(
1119
+ {
1120
+ type: "yaml",
1121
+ },
1122
+ (item) => {
1123
+ return [new IRText(item.value, fontParams)]
1124
+ }
1125
+ )
1126
+ .exhaustive()
1127
+ return converted
1128
+ }
1129
+
1130
+ function appendReferenceNumbers(
1131
+ tokens: IRToken[],
1132
+ references: string[]
1133
+ ): IRToken[] {
1134
+ function traverse(token: IRToken, callback: (token: IRToken) => any): any {
1135
+ if (token instanceof IRElement) {
1136
+ token.children.flatMap((child) => traverse(child, callback))
1137
+ }
1138
+ return callback(token)
1139
+ }
1140
+
1141
+ const appendedTokens: IRToken[] = _.cloneDeep(tokens).flatMap((token) =>
1142
+ traverse(token, (token: IRToken) => {
1143
+ if (token instanceof IRDetailOnDemand) {
1144
+ const referenceIndex =
1145
+ references.findIndex((term) => term === token.term) + 1
1146
+ if (referenceIndex === 0) return token
1147
+ token.children.push(
1148
+ new IRSuperscript(String(referenceIndex), token.fontParams)
1149
+ )
1150
+ }
1151
+ return token
1152
+ })
1153
+ )
1154
+
1155
+ return appendedTokens
1156
+ }
1157
+
1158
+ function maybeBoldMarkdownText({
1159
+ text,
1160
+ bold,
1161
+ }: {
1162
+ text: string
1163
+ bold?: boolean
1164
+ }): string {
1165
+ return bold ? `**${text}**` : text
1166
+ }
1167
+
1168
+ export function toPlaintext(markdown: string): string {
1169
+ return new MarkdownTextWrap({
1170
+ text: markdown,
1171
+ fontSize: 10, // doesn't matter, but is a mandatory field
1172
+ }).plaintext
1173
+ }