@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,1152 @@
1
+ import * as _ from "lodash-es"
2
+ import * as R from "remeda"
3
+ import React from "react"
4
+ import {
5
+ Bounds,
6
+ exposeInstanceOnWindow,
7
+ PointVector,
8
+ makeIdForHumanConsumption,
9
+ guid,
10
+ excludeUndefined,
11
+ getRelativeMouse,
12
+ dyFromAlign,
13
+ isTouchDevice,
14
+ domainExtent,
15
+ calculateTrendDirection,
16
+ } from "../../utils/index.js"
17
+ import { observable, computed, action, makeObservable } from "mobx"
18
+ import { observer } from "mobx-react"
19
+ import { NoDataModal } from "../noDataModal/NoDataModal"
20
+ import {
21
+ BASE_FONT_SIZE,
22
+ DEFAULT_GRAPHER_BOUNDS,
23
+ GRAPHER_FONT_SCALE_11,
24
+ GRAPHER_FONT_SCALE_12,
25
+ GRAPHER_TEXT_OUTLINE_FACTOR,
26
+ } from "../core/GrapherConstants"
27
+ import {
28
+ SeriesName,
29
+ MissingDataStrategy,
30
+ Time,
31
+ SeriesStrategy,
32
+ VerticalAlign,
33
+ HorizontalAlign,
34
+ } from "../../types/index.js"
35
+ import { ChartInterface } from "../chart/ChartInterface"
36
+ import { scaleLinear, ScaleLinear } from "d3-scale"
37
+ import { select, type BaseType, type Selection } from "d3-selection"
38
+ import {
39
+ PlacedSlopeChartSeries,
40
+ RawSlopeChartSeries,
41
+ RenderSlopeChartSeries,
42
+ SlopeChartSeries,
43
+ SlopeChartManager,
44
+ } from "./SlopeChartConstants"
45
+ import { CoreColumn } from "../../core-table/index.js"
46
+ import { getHoverStateForSeries } from "../chart/ChartUtils"
47
+ import { VerticalAxis } from "../axis/Axis"
48
+ import { VerticalAxisZeroLine } from "../axis/AxisViews"
49
+ import { NoDataSection } from "../scatterCharts/NoDataSection"
50
+
51
+ import { LineLegend, LineLegendProps } from "../lineLegend/LineLegend"
52
+ import {
53
+ formatTooltipRangeValues,
54
+ makeTooltipRoundingNotice,
55
+ makeTooltipToleranceNotice,
56
+ Tooltip,
57
+ TooltipState,
58
+ TooltipValueRange,
59
+ } from "../tooltip/Tooltip"
60
+ import { TooltipFooterIcon } from "../tooltip/TooltipProps"
61
+
62
+ import { Halo } from "../../components/index.js"
63
+ import { HorizontalColorLegendManager } from "../legend/HorizontalColorLegends"
64
+ import { CategoricalBin } from "../color/ColorScaleBin"
65
+ import {
66
+ GRAPHER_BACKGROUND_DEFAULT,
67
+ GRAPHER_DARK_TEXT,
68
+ } from "../color/ColorConstants"
69
+ import { FocusArray } from "../focus/FocusArray"
70
+ import { LineLabelSeries } from "../lineLegend/LineLegendTypes"
71
+ import { SlopeChartState } from "./SlopeChartState"
72
+ import { AxisConfig, AxisManager } from "../axis/AxisConfig"
73
+ import { ChartComponentProps } from "../chart/ChartTypeMap.js"
74
+ import { InteractionState } from "../interaction/InteractionState"
75
+ import {
76
+ getYAxisConfigDefaults,
77
+ toPlacedSlopeChartSeries,
78
+ toRenderSlopeChartSeries,
79
+ } from "./SlopeChartHelpers"
80
+ import { Slope } from "./Slope"
81
+ import { MarkX } from "./MarkX"
82
+ import { CATEGORICAL_LEGEND_STYLE } from "../lineCharts/LineChartConstants"
83
+
84
+ type SVGMouseOrTouchEvent =
85
+ | React.MouseEvent<SVGGElement>
86
+ | React.TouchEvent<SVGGElement>
87
+
88
+ const LINE_LEGEND_PADDING = 4
89
+
90
+ export type SlopeChartProps = ChartComponentProps<SlopeChartState>
91
+
92
+ @observer
93
+ export class SlopeChart
94
+ extends React.Component<SlopeChartProps>
95
+ implements ChartInterface, AxisManager
96
+ {
97
+ constructor(props: SlopeChartProps) {
98
+ super(props)
99
+
100
+ makeObservable<SlopeChart, "hoveredSeriesName" | "tooltipState">(this, {
101
+ hoveredSeriesName: observable,
102
+ tooltipState: observable,
103
+ })
104
+ }
105
+
106
+ private slopeAreaRef = React.createRef<SVGGElement>()
107
+
108
+ private sidebarMargin = 10
109
+
110
+ private hoveredSeriesName: string | undefined = undefined
111
+ private tooltipState = new TooltipState<{
112
+ series: SlopeChartSeries
113
+ }>({ fade: "immediate" })
114
+
115
+ @computed get chartState(): SlopeChartState {
116
+ return this.props.chartState
117
+ }
118
+
119
+ @computed private get manager(): SlopeChartManager {
120
+ return this.chartState.manager
121
+ }
122
+
123
+ @computed private get bounds(): Bounds {
124
+ return this.props.bounds ?? DEFAULT_GRAPHER_BOUNDS
125
+ }
126
+
127
+ @computed private get innerBounds(): Bounds {
128
+ return this.bounds
129
+ .padTop(6) // Leave room for overflowing dots
130
+ .padBottom(this.bottomPadding)
131
+ .padRight(this.sidebarWidth + this.sidebarMargin)
132
+ }
133
+
134
+ @computed get fontSize(): number {
135
+ return this.manager.fontSize ?? BASE_FONT_SIZE
136
+ }
137
+
138
+ @computed private get missingDataStrategy(): MissingDataStrategy {
139
+ return this.chartState.missingDataStrategy
140
+ }
141
+
142
+ @computed private get focusArray(): FocusArray {
143
+ return this.chartState.focusArray
144
+ }
145
+
146
+ @computed private get formatColumn(): CoreColumn {
147
+ return this.chartState.formatColumn
148
+ }
149
+
150
+ @computed private get lineStrokeWidth(): number {
151
+ return this.manager.isStaticAndSmall ? 3 : 1.5
152
+ }
153
+
154
+ @computed private get backgroundColor(): string {
155
+ return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT
156
+ }
157
+
158
+ @computed private get isHoverModeActive(): boolean {
159
+ return (
160
+ this.hoveredSeriesNames.length > 0 ||
161
+ // if the external legend is hovered, we want to mute
162
+ // all non-hovered series even if the chart doesn't plot
163
+ // the currently hovered series
164
+ !!this.manager.externalLegendHoverBin
165
+ )
166
+ }
167
+
168
+ @computed private get canToggleFocusMode(): boolean {
169
+ return !isTouchDevice() && this.series.length > 1
170
+ }
171
+
172
+ @computed private get startTime(): Time {
173
+ return this.chartState.startTime
174
+ }
175
+
176
+ @computed private get endTime(): Time {
177
+ return this.chartState.endTime
178
+ }
179
+
180
+ @computed private get startX(): number {
181
+ return this.xScale(this.startTime)
182
+ }
183
+
184
+ @computed private get endX(): number {
185
+ return this.xScale(this.endTime)
186
+ }
187
+
188
+ @computed private get seriesStrategy(): SeriesStrategy {
189
+ return this.chartState.seriesStrategy
190
+ }
191
+
192
+ @computed get series(): SlopeChartSeries[] {
193
+ return this.chartState.series
194
+ }
195
+
196
+ @computed private get placedSeries(): PlacedSlopeChartSeries[] {
197
+ return toPlacedSlopeChartSeries(this.series, {
198
+ yAxis: this.yAxis,
199
+ startX: this.startX,
200
+ endX: this.endX,
201
+ })
202
+ }
203
+
204
+ private hoverStateForSeries(series: SlopeChartSeries): InteractionState {
205
+ return getHoverStateForSeries(series, {
206
+ isHoverModeActive: this.isHoverModeActive,
207
+ hoveredSeriesNames: this.hoveredSeriesNames,
208
+ })
209
+ }
210
+
211
+ @computed private get renderSeries(): RenderSlopeChartSeries[] {
212
+ return toRenderSlopeChartSeries(this.placedSeries, {
213
+ isFocusModeActive: this.chartState.isFocusModeActive,
214
+ isHoverModeActive: this.isHoverModeActive,
215
+ hoveredSeriesNames: this.hoveredSeriesNames,
216
+ })
217
+ }
218
+
219
+ @computed get noDataSeries(): RawSlopeChartSeries[] {
220
+ return this.chartState.rawSeries.filter(
221
+ (series) => !this.chartState.isSeriesValid(series)
222
+ )
223
+ }
224
+
225
+ @computed private get showNoDataSection(): boolean {
226
+ if (this.manager.hideNoDataSection) return false
227
+
228
+ // nothing to show if there are no series with missing data
229
+ if (this.noDataSeries.length === 0) return false
230
+
231
+ // the No Data section is HTML and won't show up in the SVG export
232
+ if (this.manager.isStatic) return false
233
+
234
+ // we usually don't show the no data section if columns are plotted
235
+ // (since columns don't appear in the entity selector there is no need
236
+ // to explain that a column is missing – it just adds noise). but if
237
+ // the missing data strategy is set to hide, then we do want to give
238
+ // feedback as to why a slope is currently not rendered
239
+ return (
240
+ this.seriesStrategy === SeriesStrategy.entity ||
241
+ this.missingDataStrategy === MissingDataStrategy.hide
242
+ )
243
+ }
244
+
245
+ @computed private get bottomPadding(): number {
246
+ return 1.5 * GRAPHER_FONT_SCALE_12 * this.fontSize
247
+ }
248
+
249
+ @computed private get xLabelPadding(): number {
250
+ return this.useCompactLayout ? 4 : 8
251
+ }
252
+
253
+ @computed get yAxisConfig(): AxisConfig {
254
+ const { yAxisConfig } = this.manager
255
+ const defaults = getYAxisConfigDefaults(yAxisConfig)
256
+ const custom = { hideAxis: true }
257
+ return new AxisConfig({ ...defaults, ...yAxisConfig, ...custom }, this)
258
+ }
259
+
260
+ @computed get yDomainDefault(): [number, number] {
261
+ const defaultDomain: [number, number] = [Infinity, -Infinity]
262
+ return (
263
+ domainExtent(
264
+ this.chartState.allYValues,
265
+ this.chartState.yScaleType
266
+ ) ?? defaultDomain
267
+ )
268
+ }
269
+
270
+ @computed get yDomain(): [number, number] {
271
+ const domain = this.yAxisConfig.domain || [Infinity, -Infinity]
272
+ const domainDefault = this.yDomainDefault
273
+ return [
274
+ Math.min(domain[0], domainDefault[0]),
275
+ Math.max(domain[1], domainDefault[1]),
276
+ ]
277
+ }
278
+
279
+ @computed private get yRange(): [number, number] {
280
+ return this.innerBounds.yRange()
281
+ }
282
+
283
+ @computed get yAxis(): VerticalAxis {
284
+ return this.chartState.toVerticalAxis(this.yAxisConfig, {
285
+ yDomain: this.yDomain,
286
+ yRange: this.yRange,
287
+ })
288
+ }
289
+
290
+ @computed private get yAxisWidth(): number {
291
+ return this.yAxis.width
292
+ }
293
+
294
+ @computed private get xScale(): ScaleLinear<number, number> {
295
+ const { xRange } = this
296
+ return scaleLinear().domain(this.chartState.xDomain).range(xRange)
297
+ }
298
+
299
+ @computed private get sidebarWidth(): number {
300
+ return this.showNoDataSection
301
+ ? R.clamp(this.bounds.width * 0.125, { min: 60, max: 140 })
302
+ : 0
303
+ }
304
+
305
+ @computed private get formattedStartTime(): string {
306
+ return this.formatColumn.formatTime(this.chartState.xDomain[0])
307
+ }
308
+
309
+ @computed private get formattedEndTime(): string {
310
+ return this.formatColumn.formatTime(this.chartState.xDomain[1])
311
+ }
312
+
313
+ @computed private get xMarkFontSize(): number {
314
+ return this.yAxis.tickFontSize
315
+ }
316
+
317
+ @computed private get xStartMarkWidth(): number {
318
+ return Bounds.forText(this.formattedStartTime, {
319
+ fontSize: this.xMarkFontSize,
320
+ }).width
321
+ }
322
+
323
+ @computed private get xEndMarkWidth(): number {
324
+ return Bounds.forText(this.formattedEndTime, {
325
+ fontSize: this.xMarkFontSize,
326
+ }).width
327
+ }
328
+
329
+ @computed get externalLegend(): HorizontalColorLegendManager | undefined {
330
+ if (!this.manager.showLegend) {
331
+ const categoricalLegendData = this.series.map(
332
+ (series, index) =>
333
+ new CategoricalBin({
334
+ index,
335
+ value: series.seriesName,
336
+ label: series.displayName,
337
+ color: series.color,
338
+ })
339
+ )
340
+ return {
341
+ categoricalLegendData,
342
+ categoricalLegendStyleConfig: CATEGORICAL_LEGEND_STYLE,
343
+ }
344
+ }
345
+ return undefined
346
+ }
347
+
348
+ @computed private get maxLineLegendWidth(): number {
349
+ return 0.25 * this.innerBounds.width
350
+ }
351
+
352
+ @computed private get lineLegendFontSize(): number {
353
+ return LineLegend.fontSize({ fontSize: this.fontSize })
354
+ }
355
+
356
+ @computed private get lineLegendYRange(): [number, number] {
357
+ const top = this.bounds.top
358
+
359
+ const bottom =
360
+ this.bounds.bottom -
361
+ // leave space for the x-axis labels
362
+ this.bottomPadding +
363
+ // but allow for a little extra space
364
+ this.lineLegendFontSize / 2
365
+
366
+ return [top, bottom]
367
+ }
368
+
369
+ @computed private get lineLegendPropsCommon(): Partial<LineLegendProps> {
370
+ return {
371
+ yAxis: this.yAxis,
372
+ maxWidth: this.maxLineLegendWidth,
373
+ fontSize: this.fontSize,
374
+ isStatic: this.manager.isStatic,
375
+ yRange: this.lineLegendYRange,
376
+ verticalAlign: VerticalAlign.top,
377
+ showTextOutlines: true,
378
+ textOutlineColor: this.backgroundColor,
379
+ onMouseOver: this.onLineLegendMouseOver,
380
+ onMouseLeave: this.onLineLegendMouseLeave,
381
+ onClick: this.canToggleFocusMode
382
+ ? this.onLineLegendClick
383
+ : undefined,
384
+ }
385
+ }
386
+
387
+ @computed private get lineLegendPropsRight(): Partial<LineLegendProps> {
388
+ return { xAnchor: "start" }
389
+ }
390
+
391
+ @computed private get lineLegendPropsLeft(): Partial<LineLegendProps> {
392
+ return {
393
+ xAnchor: "end",
394
+ seriesNamesSortedByImportance:
395
+ this.seriesSortedByImportanceForLineLegendLeft,
396
+ }
397
+ }
398
+
399
+ private formatValue(value: number): string {
400
+ return this.formatColumn.formatValueShortWithAbbreviations(value)
401
+ }
402
+
403
+ @computed private get lineLegendMaxLevelLeft(): number {
404
+ if (!this.manager.showLegend) return 0
405
+
406
+ // can't use `lineLegendSeriesLeft` due to a circular dependency
407
+ const series = this.series.map((series) =>
408
+ this.constructSingleLineLegendSeries(
409
+ series,
410
+ (series) => series.start.value,
411
+ { showSeriesName: false }
412
+ )
413
+ )
414
+
415
+ return LineLegend.maxLevel({
416
+ series,
417
+ ...this.lineLegendPropsCommon,
418
+ seriesNamesSortedByImportance:
419
+ this.seriesSortedByImportanceForLineLegendLeft,
420
+ // not including `lineLegendPropsLeft` due to a circular dependency
421
+ })
422
+ }
423
+
424
+ @computed private get lineLegendWidthLeft(): number {
425
+ const props: LineLegendProps = {
426
+ series: this.lineLegendSeriesLeft,
427
+ ...this.lineLegendPropsCommon,
428
+ ...this.lineLegendPropsLeft,
429
+ }
430
+
431
+ // We usually use the "stable" width of the line legend, which might be
432
+ // a bit too wide because the connector line width is always added, even
433
+ // it no connector lines are drawn. Using the stable width prevents
434
+ // layout shifts when the connector lines are toggled on and off.
435
+ // However, if the chart area is very narrow (like when it's faceted),
436
+ // the stable width of the line legend takes too much space, so we use the
437
+ // actual width instead.
438
+ return this.isNarrow
439
+ ? LineLegend.width(props)
440
+ : LineLegend.stableWidth(props)
441
+ }
442
+
443
+ @computed private get lineLegendRight(): LineLegend {
444
+ return new LineLegend({
445
+ series: this.lineLegendSeriesRight,
446
+ ...this.lineLegendPropsCommon,
447
+ ...this.lineLegendPropsRight,
448
+ })
449
+ }
450
+
451
+ @computed private get lineLegendWidthRight(): number {
452
+ // We usually use the "stable" width of the line legend, which might be
453
+ // a bit too wide because the connector line width is always added, even
454
+ // it no connector lines are drawn. Using the stable width prevents
455
+ // layout shifts when the connector lines are toggled on and off.
456
+ // However, if the chart area is very narrow (like when it's faceted),
457
+ // the stable width of the line legend takes too much space, so we use the
458
+ // actual width instead.
459
+ return this.isNarrow
460
+ ? this.lineLegendRight.width
461
+ : this.lineLegendRight.stableWidth
462
+ }
463
+
464
+ @computed private get visibleLineLegendLabelsRight(): Set<SeriesName> {
465
+ return new Set(this.lineLegendRight?.visibleSeriesNames ?? [])
466
+ }
467
+
468
+ @computed
469
+ private get seriesSortedByImportanceForLineLegendLeft(): SeriesName[] {
470
+ return this.series
471
+ .map((s) => s.seriesName)
472
+ .sort((s1: SeriesName, s2: SeriesName): number => {
473
+ const PREFER_S1 = -1
474
+ const PREFER_S2 = 1
475
+
476
+ const s1_isLabelled = this.visibleLineLegendLabelsRight.has(s1)
477
+ const s2_isLabelled = this.visibleLineLegendLabelsRight.has(s2)
478
+
479
+ // prefer to show value labels for series that are already labelled
480
+ if (s1_isLabelled && !s2_isLabelled) return PREFER_S1
481
+ if (s2_isLabelled && !s1_isLabelled) return PREFER_S2
482
+
483
+ return 0
484
+ })
485
+ }
486
+
487
+ @computed private get xRange(): [number, number] {
488
+ const lineLegendWidthLeft =
489
+ this.lineLegendWidthLeft + LINE_LEGEND_PADDING
490
+ const lineLegendWidthRight =
491
+ this.lineLegendWidthRight + LINE_LEGEND_PADDING
492
+ const chartAreaWidth = this.innerBounds.width
493
+
494
+ // start and end value when the slopes are as wide as possible
495
+ const minStartX =
496
+ this.innerBounds.x + this.yAxisWidth + lineLegendWidthLeft
497
+ const maxEndX = this.innerBounds.right - lineLegendWidthRight
498
+
499
+ // use all available space if the chart is narrow
500
+ if (this.manager.isNarrow || this.isNarrow) {
501
+ return [minStartX, maxEndX]
502
+ }
503
+
504
+ const offset = 0.25
505
+ let startX = this.innerBounds.x + offset * chartAreaWidth
506
+ let endX = this.innerBounds.right - offset * chartAreaWidth
507
+
508
+ // make sure the start and end values are within the bounds
509
+ startX = Math.max(startX, minStartX)
510
+ endX = Math.min(endX, maxEndX)
511
+
512
+ // pick a reasonable max width based on an ideal aspect ratio
513
+ const idealAspectRatio = 0.9
514
+ const availableWidth =
515
+ chartAreaWidth -
516
+ this.yAxisWidth -
517
+ lineLegendWidthLeft -
518
+ lineLegendWidthRight
519
+ const idealWidth = idealAspectRatio * this.bounds.height
520
+ const maxSlopeWidth = Math.min(idealWidth, availableWidth)
521
+
522
+ const currentSlopeWidth = endX - startX
523
+ if (currentSlopeWidth > maxSlopeWidth) {
524
+ const padding = currentSlopeWidth - maxSlopeWidth
525
+ startX += padding / 2
526
+ endX -= padding / 2
527
+ }
528
+
529
+ return [startX, endX]
530
+ }
531
+
532
+ @computed private get isNarrow(): boolean {
533
+ return this.bounds.width < 320
534
+ }
535
+
536
+ @computed private get useCompactLayout(): boolean {
537
+ return !!this.manager.isSemiNarrow || this.isNarrow
538
+ }
539
+
540
+ @computed private get hoveredSeriesNames(): SeriesName[] {
541
+ const hoveredSeriesNames: SeriesName[] = []
542
+
543
+ // hovered series name (either by hovering over a slope or a line legend label)
544
+ if (this.hoveredSeriesName)
545
+ hoveredSeriesNames.push(this.hoveredSeriesName)
546
+
547
+ // hovered legend item in the external facet legend
548
+ if (this.manager.externalLegendHoverBin) {
549
+ hoveredSeriesNames.push(
550
+ ...this.series
551
+ .map((s) => s.seriesName)
552
+ .filter((name) =>
553
+ this.manager.externalLegendHoverBin?.contains(name)
554
+ )
555
+ )
556
+ }
557
+
558
+ return hoveredSeriesNames
559
+ }
560
+
561
+ private constructSingleLineLegendSeries(
562
+ series: SlopeChartSeries,
563
+ getValue: (series: SlopeChartSeries) => number,
564
+ {
565
+ showSeriesName,
566
+ showAnnotation,
567
+ }: {
568
+ showSeriesName?: boolean
569
+ showAnnotation?: boolean
570
+ }
571
+ ): LineLabelSeries {
572
+ const { seriesName, displayName, color, annotation } = series
573
+ const value = getValue(series)
574
+ const formattedValue = this.formatValue(value)
575
+ return {
576
+ color,
577
+ seriesName,
578
+ annotation: showAnnotation ? annotation : undefined,
579
+ label: showSeriesName ? displayName : formattedValue,
580
+ formattedValue: showSeriesName ? formattedValue : undefined,
581
+ placeFormattedValueInNewLine: this.useCompactLayout,
582
+ yValue: value,
583
+ focus: series.focus,
584
+ hover: this.hoverStateForSeries(series),
585
+ }
586
+ }
587
+
588
+ @computed private get lineLegendSeriesLeft(): LineLabelSeries[] {
589
+ const { showSeriesNamesInLineLegendLeft: showSeriesName } = this
590
+ return this.series.map((series) =>
591
+ this.constructSingleLineLegendSeries(
592
+ series,
593
+ (series) => series.start.value,
594
+ { showSeriesName }
595
+ )
596
+ )
597
+ }
598
+
599
+ @computed private get lineLegendSeriesRight(): LineLabelSeries[] {
600
+ return this.series.map((series) =>
601
+ this.constructSingleLineLegendSeries(
602
+ series,
603
+ (series) => series.end.value,
604
+ {
605
+ showSeriesName: this.manager.showLegend,
606
+ showAnnotation: !this.useCompactLayout,
607
+ }
608
+ )
609
+ )
610
+ }
611
+
612
+ private animSelection?: Selection<
613
+ BaseType,
614
+ unknown,
615
+ SVGGElement | null,
616
+ unknown
617
+ >
618
+ private playIntroAnimation() {
619
+ // Nice little intro animation
620
+ this.animSelection = select(this.slopeAreaRef.current)
621
+ .selectAll(".slope")
622
+ .attr("stroke-dasharray", "100%")
623
+ .attr("stroke-dashoffset", "100%")
624
+
625
+ this.animSelection
626
+ .transition()
627
+ .duration(600)
628
+ .attr("stroke-dashoffset", "0%")
629
+ }
630
+
631
+ override componentDidMount() {
632
+ exposeInstanceOnWindow(this)
633
+
634
+ if (!this.manager.disableIntroAnimation) {
635
+ this.playIntroAnimation()
636
+ }
637
+ }
638
+
639
+ override componentWillUnmount(): void {
640
+ if (this.animSelection) this.animSelection.interrupt()
641
+ }
642
+
643
+ @computed private get showSeriesNamesInLineLegendLeft(): boolean {
644
+ return this.lineLegendMaxLevelLeft >= 4 && !!this.manager.showLegend
645
+ }
646
+
647
+ private updateTooltipPosition(event: SVGMouseOrTouchEvent): void {
648
+ const ref = this.manager.base?.current
649
+ if (ref) this.tooltipState.position = getRelativeMouse(ref, event)
650
+ }
651
+
652
+ private detectHoveredSlope(event: SVGMouseOrTouchEvent): void {
653
+ const ref = this.slopeAreaRef.current
654
+ if (!ref) return
655
+
656
+ const mouse = getRelativeMouse(ref, event)
657
+ this.mouseFrame = requestAnimationFrame(() => {
658
+ if (this.placedSeries.length === 0) return
659
+
660
+ const distanceMap = new Map<PlacedSlopeChartSeries, number>()
661
+ for (const series of this.placedSeries) {
662
+ distanceMap.set(
663
+ series,
664
+ PointVector.distanceFromPointToLineSegmentSq(
665
+ mouse,
666
+ series.startPoint,
667
+ series.endPoint
668
+ )
669
+ )
670
+ }
671
+
672
+ const closestSlope = _.minBy(this.placedSeries, (s) =>
673
+ distanceMap.get(s)
674
+ )!
675
+ const distanceSq = distanceMap.get(closestSlope)!
676
+ const tolerance = 10
677
+ const toleranceSq = tolerance * tolerance
678
+
679
+ if (closestSlope && distanceSq < toleranceSq) {
680
+ this.onSlopeMouseOver(closestSlope)
681
+ } else {
682
+ this.onSlopeMouseLeave()
683
+ }
684
+ })
685
+ }
686
+
687
+ private hoverTimer?: number
688
+ @action.bound onLineLegendMouseOver(seriesName: SeriesName): void {
689
+ clearTimeout(this.hoverTimer)
690
+ this.hoveredSeriesName = seriesName
691
+ }
692
+
693
+ @action.bound private clearHoveredSeries(): void {
694
+ this.hoveredSeriesName = undefined
695
+ }
696
+
697
+ @action.bound onLineLegendMouseLeave(): void {
698
+ clearTimeout(this.hoverTimer)
699
+
700
+ // Wait before clearing selection in case the mouse is moving
701
+ // quickly over neighboring labels
702
+ this.hoverTimer = window.setTimeout(() => {
703
+ this.clearHoveredSeries()
704
+ }, 200)
705
+ }
706
+
707
+ @action.bound onLineLegendClick(seriesName: SeriesName): void {
708
+ this.focusArray.toggle(seriesName)
709
+ }
710
+
711
+ @action.bound onSlopeMouseOver(series: SlopeChartSeries): void {
712
+ this.hoveredSeriesName = series.seriesName
713
+ this.tooltipState.target = { series }
714
+ }
715
+
716
+ @action.bound onSlopeMouseLeave(): void {
717
+ this.clearHoveredSeries()
718
+ this.tooltipState.target = null
719
+ }
720
+
721
+ mouseFrame?: number
722
+ @action.bound onMouseMove(event: SVGMouseOrTouchEvent): void {
723
+ this.updateTooltipPosition(event)
724
+ this.detectHoveredSlope(event)
725
+ }
726
+
727
+ @action.bound onMouseLeave(): void {
728
+ if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame)
729
+
730
+ this.onSlopeMouseLeave()
731
+ }
732
+
733
+ @computed private get renderUid(): number {
734
+ return guid()
735
+ }
736
+
737
+ @computed private get tooltip(): React.ReactElement | undefined {
738
+ const {
739
+ manager: { isRelativeMode },
740
+ tooltipState: { target, position, fading },
741
+ formatColumn,
742
+ startTime,
743
+ endTime,
744
+ } = this
745
+
746
+ const { series } = target || {}
747
+ if (!series) return
748
+
749
+ const formatTime = (time: Time) => formatColumn.formatTime(time)
750
+
751
+ const title = series.displayName
752
+ const titleAnnotation = series.annotation
753
+
754
+ const actualStartTime = series.start.originalTime
755
+ const actualEndTime = series.end.originalTime
756
+ const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}`
757
+ const timeLabel = isRelativeMode
758
+ ? `% change between ${formatColumn.formatTime(actualStartTime)} and ${formatColumn.formatTime(actualEndTime)}`
759
+ : timeRange
760
+
761
+ const constructTargetYearForToleranceNotice = () => {
762
+ const isStartValueOriginal = series.start.originalTime === startTime
763
+ const isEndValueOriginal = series.end.originalTime === endTime
764
+
765
+ if (!isStartValueOriginal && !isEndValueOriginal) {
766
+ return `${formatTime(startTime)} and ${formatTime(endTime)}`
767
+ } else if (!isStartValueOriginal) {
768
+ return formatTime(startTime)
769
+ } else if (!isEndValueOriginal) {
770
+ return formatTime(endTime)
771
+ } else {
772
+ return undefined
773
+ }
774
+ }
775
+
776
+ const targetYear = constructTargetYearForToleranceNotice()
777
+ const toleranceNotice = targetYear
778
+ ? {
779
+ icon: TooltipFooterIcon.Notice,
780
+ text: makeTooltipToleranceNotice(targetYear),
781
+ }
782
+ : undefined
783
+ const roundingNotice = series.column.roundsToSignificantFigures
784
+ ? {
785
+ icon: TooltipFooterIcon.None,
786
+ text: makeTooltipRoundingNotice(
787
+ [series.column.numSignificantFigures],
788
+ { plural: !isRelativeMode }
789
+ ),
790
+ }
791
+ : undefined
792
+ const footer = excludeUndefined([toleranceNotice, roundingNotice])
793
+
794
+ const values = isRelativeMode
795
+ ? [series.end.value]
796
+ : [series.start.value, series.end.value]
797
+
798
+ return (
799
+ <Tooltip
800
+ id={this.renderUid}
801
+ tooltipManager={this.props.chartState.manager}
802
+ x={position.x}
803
+ y={position.y}
804
+ offsetX={20}
805
+ offsetY={-16}
806
+ style={{ maxWidth: "250px" }}
807
+ title={title}
808
+ titleAnnotation={titleAnnotation}
809
+ subtitle={timeLabel}
810
+ subtitleFormat={targetYear ? "notice" : undefined}
811
+ dissolve={fading}
812
+ footer={footer}
813
+ dismiss={() => (this.tooltipState.target = null)}
814
+ >
815
+ <TooltipValueRange
816
+ label={series.column.displayName}
817
+ unit={series.column.displayUnit}
818
+ values={formatTooltipRangeValues(values, series.column)}
819
+ trend={calculateTrendDirection(...values)}
820
+ isRoundedToSignificantFigures={
821
+ series.column.roundsToSignificantFigures
822
+ }
823
+ labelVariant="unit-only"
824
+ />
825
+ </Tooltip>
826
+ )
827
+ }
828
+
829
+ private makeMissingDataLabel(series: RawSlopeChartSeries): string {
830
+ const { displayName, start, end } = series
831
+
832
+ const startTime = this.formatColumn.formatTime(this.startTime)
833
+ const endTime = this.formatColumn.formatTime(this.endTime)
834
+
835
+ // mention the start or end value if they're missing
836
+ if (start?.value === undefined && end?.value === undefined) {
837
+ return `${displayName} (${startTime} & ${endTime})`
838
+ } else if (start?.value === undefined) {
839
+ return `${displayName} (${startTime})`
840
+ } else if (end?.value === undefined) {
841
+ return `${displayName} (${endTime})`
842
+ }
843
+
844
+ // if both values are given but the series shows up in the No Data
845
+ // section, then tolerance has been applied to one of the values
846
+ // in such a way that we decided not to render the slope after all
847
+ // (e.g. when the original times are too close to each other)
848
+ const isToleranceAppliedToStartValue =
849
+ start.originalTime !== this.startTime
850
+ const isToleranceAppliedToEndValue = end.originalTime !== this.endTime
851
+ if (isToleranceAppliedToStartValue && isToleranceAppliedToEndValue) {
852
+ return `${displayName} (${startTime} & ${endTime})`
853
+ } else if (isToleranceAppliedToStartValue) {
854
+ return `${displayName} (${startTime})`
855
+ } else if (isToleranceAppliedToEndValue) {
856
+ return `${displayName} (${endTime})`
857
+ }
858
+
859
+ return displayName
860
+ }
861
+
862
+ private renderNoDataSection(): React.ReactElement | undefined {
863
+ if (!this.showNoDataSection) return
864
+
865
+ const bounds = new Bounds(
866
+ this.innerBounds.right + this.sidebarMargin,
867
+ this.bounds.top,
868
+ this.sidebarWidth,
869
+ this.bounds.height
870
+ )
871
+ const seriesNames = this.noDataSeries.map((series) =>
872
+ this.makeMissingDataLabel(series)
873
+ )
874
+
875
+ return (
876
+ <NoDataSection
877
+ seriesNames={seriesNames}
878
+ bounds={bounds}
879
+ align={HorizontalAlign.right}
880
+ baseFontSize={this.fontSize}
881
+ />
882
+ )
883
+ }
884
+
885
+ private renderSlopes() {
886
+ return (
887
+ <g id={makeIdForHumanConsumption("slopes")}>
888
+ {this.renderSeries.map((series) => (
889
+ <Slope
890
+ key={series.seriesName}
891
+ series={series}
892
+ strokeWidth={this.lineStrokeWidth}
893
+ outlineWidth={0.5}
894
+ outlineStroke={this.backgroundColor}
895
+ />
896
+ ))}
897
+ </g>
898
+ )
899
+ }
900
+
901
+ private renderInteractiveSlopes(): React.ReactElement {
902
+ return (
903
+ <g
904
+ ref={this.slopeAreaRef}
905
+ onMouseMove={this.onMouseMove}
906
+ onTouchMove={this.onMouseMove}
907
+ onTouchStart={this.onMouseMove}
908
+ onMouseLeave={this.onMouseLeave}
909
+ >
910
+ <rect
911
+ x={this.startX}
912
+ y={this.bounds.y}
913
+ width={this.endX - this.startX}
914
+ height={this.bounds.height}
915
+ fillOpacity={0}
916
+ />
917
+ {this.renderSlopes()}
918
+ </g>
919
+ )
920
+ }
921
+
922
+ private renderZeroLine(): React.ReactElement | null {
923
+ // Don't draw a zero line if all start values are zero,
924
+ // which is trivially true in relative mode
925
+ if (this.chartState.isRelativeMode) return null
926
+
927
+ // Don't draw a zero line if all start values are zero
928
+ const areAllStartValuesZero = !this.chartState.series.some(
929
+ (series) => series.start.value !== 0
930
+ )
931
+ if (areAllStartValuesZero) return null
932
+
933
+ // Don't show a zero line if it's not in the domain
934
+ const isZeroInDomain =
935
+ this.yAxis.domain[0] <= 0 && this.yAxis.domain[1] >= 0
936
+ if (!isZeroInDomain) return null
937
+
938
+ const fontSize = GRAPHER_FONT_SCALE_12 * this.fontSize
939
+ const tickLabelOffset = 5
940
+
941
+ const tickLabel = this.yAxis.formatTick(0)
942
+ const tickLabelLength = Bounds.forText(tickLabel, { fontSize }).width
943
+ const bounds = this.innerBounds.padLeft(
944
+ tickLabelLength + tickLabelOffset
945
+ )
946
+
947
+ return (
948
+ <>
949
+ {!this.yAxis.hideGridlines && (
950
+ <VerticalAxisZeroLine
951
+ bounds={bounds}
952
+ verticalAxis={this.yAxis}
953
+ stroke="#ddd"
954
+ strokeDasharray="3,2"
955
+ />
956
+ )}
957
+ <text
958
+ x={this.innerBounds.left}
959
+ y={this.yAxis.place(0).toFixed(2)}
960
+ dy={dyFromAlign(VerticalAlign.middle)}
961
+ fontSize={fontSize}
962
+ fill={GRAPHER_DARK_TEXT}
963
+ >
964
+ {tickLabel}
965
+ </text>
966
+ </>
967
+ )
968
+ }
969
+
970
+ private renderLogNotice(): React.ReactElement | null {
971
+ if (!this.chartState.isLogScale) return null
972
+
973
+ const fontSize = GRAPHER_FONT_SCALE_11 * this.fontSize
974
+
975
+ const longText = "plotted on a logarithmic axis"
976
+ const shortText = "log axis"
977
+
978
+ const longTextWidth = Bounds.forText(longText, { fontSize }).width
979
+ const shortTextWidth = Bounds.forText(shortText, { fontSize }).width
980
+
981
+ // Determine how much space is available for the log notice
982
+ const xDist = this.endX - this.startX
983
+ const rightPadding = 0.5 * this.xEndMarkWidth
984
+ const leftPadding = 0.5 * this.xStartMarkWidth
985
+ const maxWidth = xDist - rightPadding - leftPadding - 24
986
+
987
+ // Prefer the long text if it fits. If both texts are too long,
988
+ // don't display a notice
989
+ const renderedText =
990
+ longTextWidth <= maxWidth
991
+ ? longText
992
+ : shortTextWidth <= maxWidth
993
+ ? shortText
994
+ : null
995
+
996
+ if (!renderedText) return null
997
+
998
+ // Placed in between the start and end x marks
999
+ const midX = (this.startX + this.endX) / 2
1000
+ const y = this.innerBounds.bottom + this.xLabelPadding
1001
+
1002
+ return (
1003
+ <text
1004
+ x={midX}
1005
+ y={y}
1006
+ fontSize={fontSize}
1007
+ textAnchor="middle"
1008
+ dy={dyFromAlign(VerticalAlign.bottom)}
1009
+ fill={GRAPHER_DARK_TEXT}
1010
+ fontStyle="italic"
1011
+ >
1012
+ {renderedText}
1013
+ </text>
1014
+ )
1015
+ }
1016
+
1017
+ private renderYAxis(): React.ReactElement | null {
1018
+ return (
1019
+ <>
1020
+ {this.renderZeroLine()}
1021
+ {this.renderLogNotice()}
1022
+ </>
1023
+ )
1024
+ }
1025
+
1026
+ private renderXAxis() {
1027
+ const { startX, endX } = this
1028
+
1029
+ return (
1030
+ <g id={makeIdForHumanConsumption("horizontal-axis")}>
1031
+ <MarkX
1032
+ label={this.formattedStartTime}
1033
+ x={startX}
1034
+ top={this.innerBounds.top}
1035
+ bottom={this.innerBounds.bottom}
1036
+ labelPadding={this.xLabelPadding}
1037
+ fontSize={this.xMarkFontSize}
1038
+ />
1039
+ <MarkX
1040
+ label={this.formattedEndTime}
1041
+ x={endX}
1042
+ top={this.innerBounds.top}
1043
+ bottom={this.innerBounds.bottom}
1044
+ labelPadding={this.xLabelPadding}
1045
+ fontSize={this.xMarkFontSize}
1046
+ />
1047
+ </g>
1048
+ )
1049
+ }
1050
+
1051
+ private renderLineLegendRight(): React.ReactElement {
1052
+ return (
1053
+ <LineLegend
1054
+ series={this.lineLegendSeriesRight}
1055
+ x={this.xRange[1] + LINE_LEGEND_PADDING}
1056
+ {...this.lineLegendPropsCommon}
1057
+ {...this.lineLegendPropsRight}
1058
+ />
1059
+ )
1060
+ }
1061
+
1062
+ private renderLineLegendLeft(): React.ReactElement | null {
1063
+ // don't show labels for the start values in relative mode since they're all trivially zero
1064
+ if (this.manager.isRelativeMode) return null
1065
+
1066
+ const uniqYValues = _.uniq(
1067
+ this.lineLegendSeriesLeft.map((series) => series.yValue)
1068
+ )
1069
+ const allSlopesStartFromZero =
1070
+ uniqYValues.length === 1 && uniqYValues[0] === 0
1071
+
1072
+ // if all values have a start value of 0, show the 0-label only once
1073
+ if (allSlopesStartFromZero)
1074
+ return (
1075
+ <Halo
1076
+ id="x-axis-zero-label"
1077
+ outlineWidth={
1078
+ GRAPHER_TEXT_OUTLINE_FACTOR * this.lineLegendFontSize
1079
+ }
1080
+ outlineColor={this.backgroundColor}
1081
+ >
1082
+ <text
1083
+ x={this.startX}
1084
+ y={this.yAxis.place(0)}
1085
+ textAnchor="end"
1086
+ dx={-LINE_LEGEND_PADDING - 4}
1087
+ dy={dyFromAlign(VerticalAlign.middle)}
1088
+ fontSize={this.lineLegendFontSize}
1089
+ >
1090
+ {this.formatValue(0)}
1091
+ </text>
1092
+ </Halo>
1093
+ )
1094
+
1095
+ return (
1096
+ <LineLegend
1097
+ series={this.lineLegendSeriesLeft}
1098
+ x={this.xRange[0] - LINE_LEGEND_PADDING}
1099
+ {...this.lineLegendPropsCommon}
1100
+ {...this.lineLegendPropsLeft}
1101
+ />
1102
+ )
1103
+ }
1104
+
1105
+ private renderLineLegends(): React.ReactElement | undefined {
1106
+ return (
1107
+ <>
1108
+ {this.renderLineLegendLeft()}
1109
+ {this.renderLineLegendRight()}
1110
+ </>
1111
+ )
1112
+ }
1113
+
1114
+ private renderInteractive(): React.ReactElement {
1115
+ return (
1116
+ <>
1117
+ {this.renderYAxis()}
1118
+ {this.renderXAxis()}
1119
+ {this.renderInteractiveSlopes()}
1120
+ {this.renderLineLegends()}
1121
+ {this.renderNoDataSection()}
1122
+ {this.tooltip}
1123
+ </>
1124
+ )
1125
+ }
1126
+
1127
+ private renderStatic(): React.ReactElement {
1128
+ return (
1129
+ <>
1130
+ {this.renderYAxis()}
1131
+ {this.renderXAxis()}
1132
+ {this.renderSlopes()}
1133
+ {this.renderLineLegends()}
1134
+ </>
1135
+ )
1136
+ }
1137
+
1138
+ override render() {
1139
+ if (this.chartState.errorInfo.reason)
1140
+ return (
1141
+ <NoDataModal
1142
+ manager={this.manager}
1143
+ bounds={this.props.bounds}
1144
+ message={this.chartState.errorInfo.reason}
1145
+ />
1146
+ )
1147
+
1148
+ return this.manager.isStatic
1149
+ ? this.renderStatic()
1150
+ : this.renderInteractive()
1151
+ }
1152
+ }