@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.
- package/LICENSE.md +8 -0
- package/README.md +113 -0
- package/package.json +137 -0
- package/src/components/BodyPortal/BodyPortal.tsx +40 -0
- package/src/components/Button/Button.scss +110 -0
- package/src/components/Button/Button.tsx +101 -0
- package/src/components/Checkbox.scss +93 -0
- package/src/components/Checkbox.tsx +47 -0
- package/src/components/ExpandableToggle/ExpandableToggle.scss +123 -0
- package/src/components/ExpandableToggle/ExpandableToggle.tsx +60 -0
- package/src/components/GrapherTabIcon.tsx +156 -0
- package/src/components/GrapherTrendArrow.scss +16 -0
- package/src/components/GrapherTrendArrow.tsx +30 -0
- package/src/components/Halo/Halo.tsx +44 -0
- package/src/components/LabeledSwitch/LabeledSwitch.scss +109 -0
- package/src/components/LabeledSwitch/LabeledSwitch.tsx +62 -0
- package/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx +1173 -0
- package/src/components/OverlayHeader.scss +18 -0
- package/src/components/OverlayHeader.tsx +29 -0
- package/src/components/RadioButton.scss +69 -0
- package/src/components/RadioButton.tsx +42 -0
- package/src/components/SimpleMarkdownText.tsx +89 -0
- package/src/components/TextInput.scss +17 -0
- package/src/components/TextInput.tsx +19 -0
- package/src/components/TextWrap/TextWrap.tsx +361 -0
- package/src/components/TextWrap/TextWrapUtils.ts +32 -0
- package/src/components/closeButton/CloseButton.scss +40 -0
- package/src/components/closeButton/CloseButton.tsx +27 -0
- package/src/components/index.ts +70 -0
- package/src/components/loadingIndicator/LoadingIndicator.scss +40 -0
- package/src/components/loadingIndicator/LoadingIndicator.tsx +28 -0
- package/src/components/markdown/remarkPlainLinks.ts +36 -0
- package/src/components/reactUtil.ts +20 -0
- package/src/components/stubs/CodeSnippet.tsx +19 -0
- package/src/components/stubs/DataCitation.tsx +16 -0
- package/src/components/stubs/IndicatorKeyData.tsx +45 -0
- package/src/components/stubs/IndicatorProcessing.tsx +15 -0
- package/src/components/stubs/IndicatorSources.tsx +15 -0
- package/src/components/styles/colors.scss +113 -0
- package/src/components/styles/mixins.scss +630 -0
- package/src/components/styles/typography.scss +579 -0
- package/src/components/styles/util.scss +89 -0
- package/src/components/styles/variables.scss +208 -0
- package/src/config/ChartsConfig.ts +163 -0
- package/src/config/ChartsProvider.tsx +157 -0
- package/src/config/index.ts +20 -0
- package/src/core-table/CoreTable.ts +1355 -0
- package/src/core-table/CoreTableColumns.ts +973 -0
- package/src/core-table/CoreTableUtils.ts +793 -0
- package/src/core-table/ErrorValues.ts +73 -0
- package/src/core-table/OwidTable.ts +1175 -0
- package/src/core-table/OwidTableSynthesizers.ts +272 -0
- package/src/core-table/OwidTableUtil.ts +76 -0
- package/src/core-table/Transforms.ts +484 -0
- package/src/core-table/index.ts +82 -0
- package/src/explorer/ColumnGrammar.ts +217 -0
- package/src/explorer/Explorer.sample.ts +212 -0
- package/src/explorer/Explorer.scss +148 -0
- package/src/explorer/Explorer.tsx +1283 -0
- package/src/explorer/ExplorerConstants.ts +85 -0
- package/src/explorer/ExplorerControls.scss +156 -0
- package/src/explorer/ExplorerControls.tsx +210 -0
- package/src/explorer/ExplorerDecisionMatrix.ts +471 -0
- package/src/explorer/ExplorerGrammar.ts +161 -0
- package/src/explorer/ExplorerProgram.ts +568 -0
- package/src/explorer/ExplorerUtils.ts +59 -0
- package/src/explorer/GrapherGrammar.ts +387 -0
- package/src/explorer/gridLang/GrammarUtils.ts +121 -0
- package/src/explorer/gridLang/GridCell.ts +298 -0
- package/src/explorer/gridLang/GridLangConstants.ts +255 -0
- package/src/explorer/gridLang/GridProgram.ts +311 -0
- package/src/explorer/gridLang/readme.md +17 -0
- package/src/explorer/index.ts +69 -0
- package/src/explorer/readme.md +19 -0
- package/src/explorer/urlMigrations/CO2UrlMigration.ts +46 -0
- package/src/explorer/urlMigrations/CovidUrlMigration.ts +37 -0
- package/src/explorer/urlMigrations/EnergyUrlMigration.ts +41 -0
- package/src/explorer/urlMigrations/ExplorerPageUrlMigrationSpec.ts +12 -0
- package/src/explorer/urlMigrations/ExplorerUrlMigrationUtils.ts +45 -0
- package/src/explorer/urlMigrations/ExplorerUrlMigrations.ts +33 -0
- package/src/explorer/urlMigrations/LegacyCovidUrlMigration.ts +144 -0
- package/src/explorer/urlMigrations/readme.md +39 -0
- package/src/grapher/axis/Axis.ts +973 -0
- package/src/grapher/axis/AxisConfig.ts +179 -0
- package/src/grapher/axis/AxisViews.tsx +597 -0
- package/src/grapher/barCharts/DiscreteBarChart.tsx +728 -0
- package/src/grapher/barCharts/DiscreteBarChartConstants.ts +60 -0
- package/src/grapher/barCharts/DiscreteBarChartHelpers.ts +338 -0
- package/src/grapher/barCharts/DiscreteBarChartState.ts +354 -0
- package/src/grapher/barCharts/DiscreteBarChartThumbnail.tsx +34 -0
- package/src/grapher/captionedChart/CaptionedChart.scss +61 -0
- package/src/grapher/captionedChart/CaptionedChart.tsx +523 -0
- package/src/grapher/captionedChart/Logos.tsx +141 -0
- package/src/grapher/captionedChart/LogosSVG.tsx +16 -0
- package/src/grapher/captionedChart/StaticChartRasterizer.tsx +178 -0
- package/src/grapher/captionedChart/assets/buildcanada-logo-square.svg +15 -0
- package/src/grapher/captionedChart/assets/buildcanada-logo.svg +15 -0
- package/src/grapher/captionedChart/assets/canadaspends.svg +7 -0
- package/src/grapher/captionedChart/readme.md +14 -0
- package/src/grapher/chart/Chart.tsx +62 -0
- package/src/grapher/chart/ChartAreaContent.tsx +172 -0
- package/src/grapher/chart/ChartDimension.ts +121 -0
- package/src/grapher/chart/ChartInterface.ts +83 -0
- package/src/grapher/chart/ChartManager.ts +113 -0
- package/src/grapher/chart/ChartTabs.ts +178 -0
- package/src/grapher/chart/ChartTypeMap.tsx +158 -0
- package/src/grapher/chart/ChartTypeSwitcher.tsx +26 -0
- package/src/grapher/chart/ChartUtils.tsx +364 -0
- package/src/grapher/chart/DimensionSlot.ts +45 -0
- package/src/grapher/chart/StaticChartWrapper.tsx +94 -0
- package/src/grapher/chart/guidedChartUtils.ts +82 -0
- package/src/grapher/color/BinningStrategies.ts +484 -0
- package/src/grapher/color/BinningStrategyEqualSizeBins.ts +132 -0
- package/src/grapher/color/BinningStrategyLogarithmic.ts +121 -0
- package/src/grapher/color/CategoricalColorAssigner.ts +97 -0
- package/src/grapher/color/ColorBrewerSchemes.ts +80 -0
- package/src/grapher/color/ColorConstants.ts +20 -0
- package/src/grapher/color/ColorScale.ts +339 -0
- package/src/grapher/color/ColorScaleBin.ts +147 -0
- package/src/grapher/color/ColorScaleConfig.ts +204 -0
- package/src/grapher/color/ColorScheme.ts +137 -0
- package/src/grapher/color/ColorSchemes.ts +149 -0
- package/src/grapher/color/ColorUtils.ts +86 -0
- package/src/grapher/color/CustomSchemes.ts +1772 -0
- package/src/grapher/color/readme.md +84 -0
- package/src/grapher/comparisonLine/ComparisonLine.tsx +31 -0
- package/src/grapher/comparisonLine/ComparisonLineConstants.ts +11 -0
- package/src/grapher/comparisonLine/ComparisonLineGenerator.ts +60 -0
- package/src/grapher/comparisonLine/ComparisonLineHelpers.ts +10 -0
- package/src/grapher/comparisonLine/CustomComparisonLine.tsx +159 -0
- package/src/grapher/comparisonLine/VerticalComparisonLine.tsx +208 -0
- package/src/grapher/controls/ActionButtons.scss +97 -0
- package/src/grapher/controls/ActionButtons.tsx +453 -0
- package/src/grapher/controls/CommandPalette.scss +50 -0
- package/src/grapher/controls/CommandPalette.tsx +74 -0
- package/src/grapher/controls/ContentSwitchers.scss +93 -0
- package/src/grapher/controls/ContentSwitchers.tsx +238 -0
- package/src/grapher/controls/Controls.scss +158 -0
- package/src/grapher/controls/DataTableFilterDropdown.scss +7 -0
- package/src/grapher/controls/DataTableFilterDropdown.tsx +168 -0
- package/src/grapher/controls/DataTableSearchField.scss +3 -0
- package/src/grapher/controls/DataTableSearchField.tsx +76 -0
- package/src/grapher/controls/Dropdown.scss +252 -0
- package/src/grapher/controls/Dropdown.tsx +235 -0
- package/src/grapher/controls/EntitySelectionToggle.tsx +135 -0
- package/src/grapher/controls/MapRegionDropdown.scss +3 -0
- package/src/grapher/controls/MapRegionDropdown.tsx +104 -0
- package/src/grapher/controls/MapResetButton.tsx +115 -0
- package/src/grapher/controls/MapZoomDropdown.scss +9 -0
- package/src/grapher/controls/MapZoomDropdown.tsx +270 -0
- package/src/grapher/controls/MapZoomToSelectionButton.tsx +87 -0
- package/src/grapher/controls/SearchField.scss +78 -0
- package/src/grapher/controls/SearchField.tsx +63 -0
- package/src/grapher/controls/SettingsMenu.scss +191 -0
- package/src/grapher/controls/SettingsMenu.tsx +399 -0
- package/src/grapher/controls/ShareMenu.scss +58 -0
- package/src/grapher/controls/ShareMenu.tsx +304 -0
- package/src/grapher/controls/SortIcon.tsx +39 -0
- package/src/grapher/controls/VerticalScrollContainer.tsx +263 -0
- package/src/grapher/controls/controlsRow/ControlsRow.tsx +168 -0
- package/src/grapher/controls/dropdown-icons.scss +4 -0
- package/src/grapher/controls/entityPicker/EntityPicker.scss +255 -0
- package/src/grapher/controls/entityPicker/EntityPicker.tsx +816 -0
- package/src/grapher/controls/entityPicker/EntityPickerConstants.ts +23 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.scss +129 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelector.tsx +463 -0
- package/src/grapher/controls/globalEntitySelector/GlobalEntitySelectorConstants.ts +3 -0
- package/src/grapher/controls/globalEntitySelector/readme.md +17 -0
- package/src/grapher/controls/settings/AbsRelToggle.tsx +64 -0
- package/src/grapher/controls/settings/AxisScaleToggle.tsx +53 -0
- package/src/grapher/controls/settings/FacetStrategySelector.tsx +110 -0
- package/src/grapher/controls/settings/FacetYDomainToggle.tsx +51 -0
- package/src/grapher/controls/settings/NoDataAreaToggle.tsx +38 -0
- package/src/grapher/controls/settings/ZoomToggle.tsx +36 -0
- package/src/grapher/core/EntitiesByRegionType.ts +174 -0
- package/src/grapher/core/EntityCodes.ts +19 -0
- package/src/grapher/core/EntityUrlBuilder.ts +200 -0
- package/src/grapher/core/FetchingGrapher.tsx +156 -0
- package/src/grapher/core/Grapher.tsx +760 -0
- package/src/grapher/core/GrapherAnalytics.ts +229 -0
- package/src/grapher/core/GrapherConstants.ts +173 -0
- package/src/grapher/core/GrapherState.tsx +3659 -0
- package/src/grapher/core/GrapherUrl.ts +184 -0
- package/src/grapher/core/GrapherUrlMigrations.ts +29 -0
- package/src/grapher/core/GrapherUseHelpers.tsx +147 -0
- package/src/grapher/core/LegacyToOwidTable.ts +841 -0
- package/src/grapher/core/grapher.entry.ts +5 -0
- package/src/grapher/core/grapher.scss +257 -0
- package/src/grapher/core/loadGrapherTableHelpers.ts +116 -0
- package/src/grapher/core/loadVariable.ts +104 -0
- package/src/grapher/core/relatedQuestion.ts +12 -0
- package/src/grapher/core/typography.scss +206 -0
- package/src/grapher/dataTable/DataTable.sample.ts +206 -0
- package/src/grapher/dataTable/DataTable.scss +249 -0
- package/src/grapher/dataTable/DataTable.tsx +1332 -0
- package/src/grapher/dataTable/DataTableConstants.ts +186 -0
- package/src/grapher/entitySelector/EntitySelector.scss +255 -0
- package/src/grapher/entitySelector/EntitySelector.tsx +1838 -0
- package/src/grapher/facet/FacetChart.tsx +943 -0
- package/src/grapher/facet/FacetChartConstants.ts +24 -0
- package/src/grapher/facet/FacetChartUtils.ts +51 -0
- package/src/grapher/facet/FacetMap.tsx +604 -0
- package/src/grapher/facet/FacetMapConstants.ts +23 -0
- package/src/grapher/facet/readme.md +13 -0
- package/src/grapher/focus/FocusArray.ts +79 -0
- package/src/grapher/footer/Footer.scss +63 -0
- package/src/grapher/footer/Footer.tsx +809 -0
- package/src/grapher/footer/FooterManager.ts +44 -0
- package/src/grapher/fullScreen/FullScreen.scss +11 -0
- package/src/grapher/fullScreen/FullScreen.tsx +61 -0
- package/src/grapher/header/Header.scss +35 -0
- package/src/grapher/header/Header.tsx +372 -0
- package/src/grapher/header/HeaderManager.ts +28 -0
- package/src/grapher/index.ts +157 -0
- package/src/grapher/interaction/InteractionState.ts +60 -0
- package/src/grapher/legend/HorizontalColorLegends.tsx +923 -0
- package/src/grapher/legend/LegendInteractionState.ts +40 -0
- package/src/grapher/legend/VerticalColorLegend.tsx +295 -0
- package/src/grapher/lineCharts/LineChart.tsx +968 -0
- package/src/grapher/lineCharts/LineChartConstants.ts +89 -0
- package/src/grapher/lineCharts/LineChartHelpers.ts +184 -0
- package/src/grapher/lineCharts/LineChartState.ts +394 -0
- package/src/grapher/lineCharts/LineChartThumbnail.tsx +437 -0
- package/src/grapher/lineCharts/Lines.tsx +258 -0
- package/src/grapher/lineLegend/LineLegend.tsx +723 -0
- package/src/grapher/lineLegend/LineLegendConstants.ts +9 -0
- package/src/grapher/lineLegend/LineLegendFilterAlgorithms.ts +143 -0
- package/src/grapher/lineLegend/LineLegendHelpers.ts +253 -0
- package/src/grapher/lineLegend/LineLegendTypes.ts +32 -0
- package/src/grapher/mapCharts/CanadaTopology.ts +17922 -0
- package/src/grapher/mapCharts/ChoroplethGlobe.tsx +949 -0
- package/src/grapher/mapCharts/ChoroplethMap.tsx +662 -0
- package/src/grapher/mapCharts/GeoFeatures.ts +184 -0
- package/src/grapher/mapCharts/GlobeController.ts +496 -0
- package/src/grapher/mapCharts/MapAnnotationPlacements.json +1040 -0
- package/src/grapher/mapCharts/MapAnnotationPlacements.ts +31 -0
- package/src/grapher/mapCharts/MapAnnotations.ts +723 -0
- package/src/grapher/mapCharts/MapChart.sample.ts +59 -0
- package/src/grapher/mapCharts/MapChart.scss +5 -0
- package/src/grapher/mapCharts/MapChart.tsx +720 -0
- package/src/grapher/mapCharts/MapChartConstants.ts +260 -0
- package/src/grapher/mapCharts/MapChartState.ts +416 -0
- package/src/grapher/mapCharts/MapChartThumbnail.tsx +25 -0
- package/src/grapher/mapCharts/MapComponents.tsx +338 -0
- package/src/grapher/mapCharts/MapConfig.ts +156 -0
- package/src/grapher/mapCharts/MapHelpers.ts +181 -0
- package/src/grapher/mapCharts/MapProjections.ts +49 -0
- package/src/grapher/mapCharts/MapSparkline.tsx +257 -0
- package/src/grapher/mapCharts/MapTooltip.scss +49 -0
- package/src/grapher/mapCharts/MapTooltip.tsx +409 -0
- package/src/grapher/mapCharts/MapTopology.ts +1766 -0
- package/src/grapher/mapCharts/d3-bboxCollide.js +204 -0
- package/src/grapher/mapCharts/d3-geo-projection.ts +198 -0
- package/src/grapher/modal/DownloadIcons.tsx +39 -0
- package/src/grapher/modal/DownloadModal.scss +300 -0
- package/src/grapher/modal/DownloadModal.tsx +1226 -0
- package/src/grapher/modal/EmbedModal.scss +40 -0
- package/src/grapher/modal/EmbedModal.tsx +160 -0
- package/src/grapher/modal/EntitySelectorModal.tsx +59 -0
- package/src/grapher/modal/Modal.scss +31 -0
- package/src/grapher/modal/Modal.tsx +90 -0
- package/src/grapher/modal/ModalHeader.scss +12 -0
- package/src/grapher/modal/ModalHeader.tsx +16 -0
- package/src/grapher/modal/SourcesDescriptions.scss +87 -0
- package/src/grapher/modal/SourcesDescriptions.tsx +89 -0
- package/src/grapher/modal/SourcesKeyDataTable.scss +49 -0
- package/src/grapher/modal/SourcesKeyDataTable.tsx +87 -0
- package/src/grapher/modal/SourcesModal.scss +301 -0
- package/src/grapher/modal/SourcesModal.tsx +568 -0
- package/src/grapher/noDataModal/NoDataModal.tsx +125 -0
- package/src/grapher/scatterCharts/ConnectedScatterLegend.tsx +143 -0
- package/src/grapher/scatterCharts/MultiColorPolyline.tsx +129 -0
- package/src/grapher/scatterCharts/NoDataSection.scss +14 -0
- package/src/grapher/scatterCharts/NoDataSection.tsx +56 -0
- package/src/grapher/scatterCharts/ScatterPlotChart.tsx +792 -0
- package/src/grapher/scatterCharts/ScatterPlotChartConstants.ts +157 -0
- package/src/grapher/scatterCharts/ScatterPlotChartState.ts +678 -0
- package/src/grapher/scatterCharts/ScatterPlotChartThumbnail.tsx +155 -0
- package/src/grapher/scatterCharts/ScatterPlotTooltip.tsx +560 -0
- package/src/grapher/scatterCharts/ScatterPoints.tsx +153 -0
- package/src/grapher/scatterCharts/ScatterPointsWithLabels.tsx +708 -0
- package/src/grapher/scatterCharts/ScatterSizeLegend.tsx +327 -0
- package/src/grapher/scatterCharts/ScatterUtils.ts +265 -0
- package/src/grapher/scatterCharts/Triangle.tsx +41 -0
- package/src/grapher/schema/README.md +33 -0
- package/src/grapher/schema/defaultGrapherConfig.ts +100 -0
- package/src/grapher/schema/grapher-schema.009.yaml +781 -0
- package/src/grapher/schema/migrations/helpers.ts +58 -0
- package/src/grapher/schema/migrations/migrate.ts +75 -0
- package/src/grapher/schema/migrations/migrations.ts +158 -0
- package/src/grapher/selection/MapSelectionArray.ts +99 -0
- package/src/grapher/selection/SelectionArray.ts +71 -0
- package/src/grapher/selection/readme.md +16 -0
- package/src/grapher/sidePanel/SidePanel.scss +10 -0
- package/src/grapher/sidePanel/SidePanel.tsx +23 -0
- package/src/grapher/slideInDrawer/SlideInDrawer.scss +57 -0
- package/src/grapher/slideInDrawer/SlideInDrawer.tsx +125 -0
- package/src/grapher/slideshowController/SlideShowController.tsx +43 -0
- package/src/grapher/slideshowController/readme.md +7 -0
- package/src/grapher/slopeCharts/MarkX.tsx +45 -0
- package/src/grapher/slopeCharts/Slope.tsx +102 -0
- package/src/grapher/slopeCharts/SlopeChart.tsx +1152 -0
- package/src/grapher/slopeCharts/SlopeChartConstants.ts +33 -0
- package/src/grapher/slopeCharts/SlopeChartHelpers.ts +73 -0
- package/src/grapher/slopeCharts/SlopeChartState.ts +392 -0
- package/src/grapher/slopeCharts/SlopeChartThumbnail.tsx +368 -0
- package/src/grapher/stackedCharts/AbstractStackedChartState.ts +370 -0
- package/src/grapher/stackedCharts/MarimekkoBars.tsx +190 -0
- package/src/grapher/stackedCharts/MarimekkoBarsForOneEntity.tsx +168 -0
- package/src/grapher/stackedCharts/MarimekkoChart.tsx +1144 -0
- package/src/grapher/stackedCharts/MarimekkoChartConstants.ts +112 -0
- package/src/grapher/stackedCharts/MarimekkoChartHelpers.ts +21 -0
- package/src/grapher/stackedCharts/MarimekkoChartState.ts +465 -0
- package/src/grapher/stackedCharts/MarimekkoChartThumbnail.tsx +168 -0
- package/src/grapher/stackedCharts/MarimekkoInternalLabels.tsx +124 -0
- package/src/grapher/stackedCharts/StackedAreaChart.tsx +678 -0
- package/src/grapher/stackedCharts/StackedAreaChartState.ts +34 -0
- package/src/grapher/stackedCharts/StackedAreaChartThumbnail.tsx +215 -0
- package/src/grapher/stackedCharts/StackedAreas.tsx +223 -0
- package/src/grapher/stackedCharts/StackedBarChart.tsx +619 -0
- package/src/grapher/stackedCharts/StackedBarChartState.ts +80 -0
- package/src/grapher/stackedCharts/StackedBarChartThumbnail.tsx +220 -0
- package/src/grapher/stackedCharts/StackedBarSegment.tsx +87 -0
- package/src/grapher/stackedCharts/StackedBars.tsx +102 -0
- package/src/grapher/stackedCharts/StackedConstants.ts +109 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChart.tsx +270 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChartState.ts +296 -0
- package/src/grapher/stackedCharts/StackedDiscreteBarChartThumbnail.tsx +27 -0
- package/src/grapher/stackedCharts/StackedDiscreteBars.tsx +648 -0
- package/src/grapher/stackedCharts/StackedUtils.ts +142 -0
- package/src/grapher/tabs/Tabs.scss +169 -0
- package/src/grapher/tabs/Tabs.tsx +54 -0
- package/src/grapher/tabs/TabsWithDropdown.scss +62 -0
- package/src/grapher/tabs/TabsWithDropdown.tsx +114 -0
- package/src/grapher/testData/OwidTestData.sample.ts +273 -0
- package/src/grapher/testData/OwidTestData.ts +64 -0
- package/src/grapher/timeline/TimelineComponent.scss +139 -0
- package/src/grapher/timeline/TimelineComponent.tsx +658 -0
- package/src/grapher/timeline/TimelineController.ts +368 -0
- package/src/grapher/timeline/readme.md +7 -0
- package/src/grapher/tooltip/Tooltip.scss +510 -0
- package/src/grapher/tooltip/Tooltip.tsx +294 -0
- package/src/grapher/tooltip/TooltipContents.tsx +383 -0
- package/src/grapher/tooltip/TooltipProps.ts +123 -0
- package/src/grapher/tooltip/TooltipState.ts +81 -0
- package/src/grapher/verticalLabels/VerticalLabels.tsx +31 -0
- package/src/grapher/verticalLabels/VerticalLabelsState.ts +154 -0
- package/src/index.ts +226 -0
- package/src/styles/charts.scss +15 -0
- package/src/types/NominalType.ts +30 -0
- package/src/types/OwidOrigin.ts +18 -0
- package/src/types/OwidSource.ts +9 -0
- package/src/types/OwidVariable.ts +133 -0
- package/src/types/OwidVariableDisplayConfigInterface.ts +49 -0
- package/src/types/analyticsTypes.ts +54 -0
- package/src/types/dbTypes/Tags.ts +11 -0
- package/src/types/domainTypes/Archive.ts +139 -0
- package/src/types/domainTypes/Author.ts +28 -0
- package/src/types/domainTypes/ContentGraph.ts +76 -0
- package/src/types/domainTypes/CoreTableTypes.ts +305 -0
- package/src/types/domainTypes/DeployStatus.ts +23 -0
- package/src/types/domainTypes/Layout.ts +34 -0
- package/src/types/domainTypes/Posts.ts +34 -0
- package/src/types/domainTypes/Search.ts +299 -0
- package/src/types/domainTypes/Site.ts +8 -0
- package/src/types/domainTypes/StaticViz.ts +64 -0
- package/src/types/domainTypes/Toc.ts +11 -0
- package/src/types/domainTypes/Tombstone.ts +19 -0
- package/src/types/domainTypes/Various.ts +79 -0
- package/src/types/gdocTypes/Gdoc.ts +280 -0
- package/src/types/grapherTypes/BinningStrategyTypes.ts +46 -0
- package/src/types/grapherTypes/GrapherConstants.ts +53 -0
- package/src/types/grapherTypes/GrapherTypes.ts +743 -0
- package/src/types/index.ts +316 -0
- package/src/types/wordpressTypes/WordpressTypes.ts +9 -0
- package/src/utils/Bounds.ts +439 -0
- package/src/utils/BrowserUtils.ts +12 -0
- package/src/utils/FuzzySearch.ts +74 -0
- package/src/utils/MultiDimDataPageConfig.ts +31 -0
- package/src/utils/OwidVariable.ts +82 -0
- package/src/utils/PointVector.ts +97 -0
- package/src/utils/PromiseCache.ts +36 -0
- package/src/utils/PromiseSwitcher.ts +52 -0
- package/src/utils/TimeBounds.ts +130 -0
- package/src/utils/Tippy.tsx +57 -0
- package/src/utils/Util.ts +2369 -0
- package/src/utils/archival/archivalDate.ts +48 -0
- package/src/utils/dayjs.ts +32 -0
- package/src/utils/formatValue.ts +242 -0
- package/src/utils/grapherConfigUtils.ts +81 -0
- package/src/utils/image.ts +225 -0
- package/src/utils/index.ts +318 -0
- package/src/utils/isPresent.ts +5 -0
- package/src/utils/metadataHelpers.ts +329 -0
- package/src/utils/persistable/Persistable.ts +82 -0
- package/src/utils/persistable/readme.md +50 -0
- package/src/utils/regions.json +5635 -0
- package/src/utils/regions.ts +463 -0
- package/src/utils/serializers.ts +16 -0
- package/src/utils/string.ts +42 -0
- package/src/utils/urls/Url.ts +195 -0
- package/src/utils/urls/UrlMigration.ts +10 -0
- package/src/utils/urls/UrlUtils.ts +54 -0
- package/src/utils/urls/readme.md +90 -0
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as _ from "lodash-es"
|
|
3
|
+
import { faChartLine } from "@fortawesome/free-solid-svg-icons"
|
|
4
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
5
|
+
import {
|
|
6
|
+
ArchiveContext,
|
|
7
|
+
ColumnTypeNames,
|
|
8
|
+
CoreColumnDef,
|
|
9
|
+
OwidColumnDef,
|
|
10
|
+
SortOrder,
|
|
11
|
+
TableSlug,
|
|
12
|
+
GrapherInterface,
|
|
13
|
+
GrapherQueryParams,
|
|
14
|
+
EntityName,
|
|
15
|
+
GRAPHER_TAB_QUERY_PARAMS,
|
|
16
|
+
} from "../types/index.js"
|
|
17
|
+
import {
|
|
18
|
+
OwidTable,
|
|
19
|
+
BlankOwidTable,
|
|
20
|
+
extractPotentialDataSlugsFromTransform,
|
|
21
|
+
} from "../core-table/index.js"
|
|
22
|
+
import {
|
|
23
|
+
EntityPicker,
|
|
24
|
+
EntityPickerManager,
|
|
25
|
+
Grapher,
|
|
26
|
+
GrapherManager,
|
|
27
|
+
GrapherProgrammaticInterface,
|
|
28
|
+
SelectionArray,
|
|
29
|
+
setSelectedEntityNamesParam,
|
|
30
|
+
SlideShowController,
|
|
31
|
+
SlideShowManager,
|
|
32
|
+
DEFAULT_GRAPHER_ENTITY_TYPE,
|
|
33
|
+
GrapherAnalytics,
|
|
34
|
+
GrapherState,
|
|
35
|
+
fetchInputTableForConfig,
|
|
36
|
+
loadVariableDataAndMetadata,
|
|
37
|
+
FetchInputTableForConfigFn,
|
|
38
|
+
} from "../grapher/index.js"
|
|
39
|
+
import {
|
|
40
|
+
Bounds,
|
|
41
|
+
ColumnSlug,
|
|
42
|
+
DimensionProperty,
|
|
43
|
+
excludeUndefined,
|
|
44
|
+
exposeInstanceOnWindow,
|
|
45
|
+
isInIFrame,
|
|
46
|
+
keyMap,
|
|
47
|
+
mergeGrapherConfigs,
|
|
48
|
+
omitUndefinedValues,
|
|
49
|
+
parseIntOrUndefined,
|
|
50
|
+
PromiseCache,
|
|
51
|
+
PromiseSwitcher,
|
|
52
|
+
SerializedGridProgram,
|
|
53
|
+
setWindowUrl,
|
|
54
|
+
Tippy,
|
|
55
|
+
Url,
|
|
56
|
+
} from "../utils/index.js"
|
|
57
|
+
import { MarkdownTextWrap } from "../components/index.js"
|
|
58
|
+
import classNames from "classnames"
|
|
59
|
+
import { action, computed, makeObservable, observable, reaction } from "mobx"
|
|
60
|
+
import { observer } from "mobx-react"
|
|
61
|
+
import React, { useCallback, useEffect, useState } from "react"
|
|
62
|
+
import { createRoot } from "react-dom/client"
|
|
63
|
+
import { ExplorerControlBar, ExplorerControlPanel } from "./ExplorerControls.js"
|
|
64
|
+
import { ExplorerProgram } from "./ExplorerProgram.js"
|
|
65
|
+
import {
|
|
66
|
+
ExplorerChartCreationMode,
|
|
67
|
+
ExplorerChoiceParams,
|
|
68
|
+
ExplorerContainerId,
|
|
69
|
+
ExplorerFullQueryParams,
|
|
70
|
+
EXPLORERS_PREVIEW_ROUTE,
|
|
71
|
+
EXPLORERS_ROUTE_FOLDER,
|
|
72
|
+
UNSAVED_EXPLORER_DRAFT,
|
|
73
|
+
UNSAVED_EXPLORER_PREVIEW_QUERYPARAMS,
|
|
74
|
+
} from "./ExplorerConstants.js"
|
|
75
|
+
import { ExplorerPageUrlMigrationSpec } from "./urlMigrations/ExplorerPageUrlMigrationSpec.js"
|
|
76
|
+
import {
|
|
77
|
+
explorerUrlMigrationsById,
|
|
78
|
+
migrateExplorerUrl,
|
|
79
|
+
} from "./urlMigrations/ExplorerUrlMigrations.js"
|
|
80
|
+
|
|
81
|
+
export interface ExplorerProps extends SerializedGridProgram {
|
|
82
|
+
grapherConfigs?: GrapherInterface[]
|
|
83
|
+
partialGrapherConfigs?: GrapherInterface[]
|
|
84
|
+
queryStr?: string
|
|
85
|
+
isEmbeddedInAnOwidPage?: boolean
|
|
86
|
+
isInStandalonePage?: boolean
|
|
87
|
+
isPreview?: boolean
|
|
88
|
+
canonicalUrl?: string
|
|
89
|
+
selection?: SelectionArray
|
|
90
|
+
adminBaseUrl: string
|
|
91
|
+
bakedBaseUrl: string
|
|
92
|
+
bakedGrapherUrl: string
|
|
93
|
+
dataApiUrl: string
|
|
94
|
+
bounds?: Bounds
|
|
95
|
+
staticBounds?: Bounds
|
|
96
|
+
loadMetadataOnly?: boolean
|
|
97
|
+
throwOnMissingGrapher?: boolean
|
|
98
|
+
setupGrapher?: boolean
|
|
99
|
+
archiveContext?: ArchiveContext
|
|
100
|
+
loadInputTableForConfig?: FetchInputTableForConfigFn
|
|
101
|
+
/** Force wide mode (sidebar layout) regardless of viewport width. Useful for Storybook testing. */
|
|
102
|
+
forceWideMode?: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const LivePreviewComponent = (props: ExplorerProps) => {
|
|
106
|
+
const [useLocalStorage, setUseLocalStorage] = useState(true)
|
|
107
|
+
const [renderedProgram, setRenderedProgram] = useState("")
|
|
108
|
+
const [hasLocalStorage, setHasLocalStorage] = useState(false)
|
|
109
|
+
|
|
110
|
+
const updateProgram = useCallback(() => {
|
|
111
|
+
const localStorageProgram = localStorage.getItem(
|
|
112
|
+
UNSAVED_EXPLORER_DRAFT + props.slug
|
|
113
|
+
)
|
|
114
|
+
let program: string
|
|
115
|
+
if (useLocalStorage) program = localStorageProgram ?? props.program
|
|
116
|
+
else program = props.program
|
|
117
|
+
|
|
118
|
+
setHasLocalStorage(!!localStorageProgram)
|
|
119
|
+
setRenderedProgram((previousProgram) => {
|
|
120
|
+
if (program === previousProgram) return previousProgram
|
|
121
|
+
return program
|
|
122
|
+
})
|
|
123
|
+
}, [props.program, props.slug, useLocalStorage])
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
updateProgram()
|
|
127
|
+
const interval = setInterval(updateProgram, 1000)
|
|
128
|
+
return () => clearInterval(interval)
|
|
129
|
+
}, [updateProgram])
|
|
130
|
+
|
|
131
|
+
const newProps = { ...props, program: renderedProgram }
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
{hasLocalStorage && (
|
|
136
|
+
<div className="admin-only-locally-edited-checkbox">
|
|
137
|
+
<Tippy
|
|
138
|
+
content={
|
|
139
|
+
<span>
|
|
140
|
+
<p>
|
|
141
|
+
<b>Checked</b>: Use the explorer version
|
|
142
|
+
with changes as present in the admin.
|
|
143
|
+
</p>
|
|
144
|
+
<p>
|
|
145
|
+
<b>Unchecked</b>: Use the currently-saved
|
|
146
|
+
version.
|
|
147
|
+
</p>
|
|
148
|
+
<hr />
|
|
149
|
+
<p>
|
|
150
|
+
Note that some features may only work
|
|
151
|
+
correctly when this checkbox is unchecked:
|
|
152
|
+
in particular, using <kbd>catalogPath</kbd>s
|
|
153
|
+
and <kbd>grapherId</kbd>s and variable IDs.
|
|
154
|
+
</p>
|
|
155
|
+
</span>
|
|
156
|
+
}
|
|
157
|
+
placement="bottom"
|
|
158
|
+
>
|
|
159
|
+
<label>
|
|
160
|
+
<input
|
|
161
|
+
type="checkbox"
|
|
162
|
+
id="useLocalStorage"
|
|
163
|
+
onChange={(e) =>
|
|
164
|
+
setUseLocalStorage(e.target.checked)
|
|
165
|
+
}
|
|
166
|
+
checked={useLocalStorage}
|
|
167
|
+
/>
|
|
168
|
+
Display locally edited explorer
|
|
169
|
+
</label>
|
|
170
|
+
</Tippy>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
<Explorer
|
|
174
|
+
{...newProps}
|
|
175
|
+
queryStr={window?.location?.search ?? ""}
|
|
176
|
+
key={Date.now()}
|
|
177
|
+
isPreview={true}
|
|
178
|
+
/>
|
|
179
|
+
</>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const renderLivePreviewVersion = (props: ExplorerProps) => {
|
|
184
|
+
const elem = document.getElementById(ExplorerContainerId)
|
|
185
|
+
if (!elem) throw new Error("ExplorerContainerId not found in DOM")
|
|
186
|
+
const root = createRoot(elem)
|
|
187
|
+
root.render(<LivePreviewComponent {...props} />)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const isNarrow = () =>
|
|
191
|
+
typeof window === "undefined"
|
|
192
|
+
? false
|
|
193
|
+
: window.screen.width < 450 ||
|
|
194
|
+
document.documentElement.clientWidth <= 800
|
|
195
|
+
|
|
196
|
+
@observer
|
|
197
|
+
export class Explorer
|
|
198
|
+
extends React.Component<ExplorerProps>
|
|
199
|
+
implements
|
|
200
|
+
SlideShowManager<ExplorerChoiceParams>,
|
|
201
|
+
EntityPickerManager,
|
|
202
|
+
GrapherManager
|
|
203
|
+
{
|
|
204
|
+
analytics = new GrapherAnalytics()
|
|
205
|
+
grapherState: GrapherState
|
|
206
|
+
isOnArchivalPage: boolean
|
|
207
|
+
inputTableTransformer = (table: OwidTable) => table
|
|
208
|
+
|
|
209
|
+
constructor(props: ExplorerProps) {
|
|
210
|
+
super(props)
|
|
211
|
+
|
|
212
|
+
makeObservable<
|
|
213
|
+
Explorer,
|
|
214
|
+
| "isNarrow"
|
|
215
|
+
| "grapherContainerRef"
|
|
216
|
+
| "grapherRef"
|
|
217
|
+
| "showMobileControlsPopup"
|
|
218
|
+
>(this, {
|
|
219
|
+
appendOnlyAvailableEntityNames: observable,
|
|
220
|
+
grapher: observable.ref,
|
|
221
|
+
isNarrow: observable,
|
|
222
|
+
grapherContainerRef: observable,
|
|
223
|
+
grapherRef: observable.ref,
|
|
224
|
+
showMobileControlsPopup: observable,
|
|
225
|
+
entityPickerMetric: observable,
|
|
226
|
+
entityPickerSort: observable,
|
|
227
|
+
entityPickerTable: observable.ref,
|
|
228
|
+
entityPickerTableIsLoading: observable.ref,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
this.explorerProgram = ExplorerProgram.fromJson(
|
|
232
|
+
props
|
|
233
|
+
).initDecisionMatrix(this.initialQueryParams)
|
|
234
|
+
const { archiveContext } = props
|
|
235
|
+
const isOnArchivalPage = archiveContext?.type === "archive-page"
|
|
236
|
+
const assetMaps = isOnArchivalPage ? archiveContext?.assets : undefined
|
|
237
|
+
this.isOnArchivalPage = isOnArchivalPage
|
|
238
|
+
this.grapherState = new GrapherState({
|
|
239
|
+
staticBounds: props.staticBounds,
|
|
240
|
+
bounds: props.bounds,
|
|
241
|
+
enableKeyboardShortcuts: this.props.isInStandalonePage,
|
|
242
|
+
manager: this,
|
|
243
|
+
isEmbeddedInAnOwidPage: this.props.isEmbeddedInAnOwidPage,
|
|
244
|
+
adminBaseUrl: this.adminBaseUrl,
|
|
245
|
+
canHideExternalControlsInEmbed: true,
|
|
246
|
+
archiveContext: props.archiveContext,
|
|
247
|
+
additionalDataLoaderFn: (
|
|
248
|
+
varId: number,
|
|
249
|
+
loadMetadataOnly?: boolean
|
|
250
|
+
) =>
|
|
251
|
+
loadVariableDataAndMetadata(varId, this.dataApiUrl, {
|
|
252
|
+
assetMap: assetMaps?.runtime,
|
|
253
|
+
noCache: props.isPreview,
|
|
254
|
+
loadMetadataOnly,
|
|
255
|
+
}),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (props.setupGrapher !== false)
|
|
259
|
+
this.grapher = new Grapher({ grapherState: this.grapherState })
|
|
260
|
+
}
|
|
261
|
+
// caution: do a ctrl+f to find untyped usages
|
|
262
|
+
static renderSingleExplorerOnExplorerPage(
|
|
263
|
+
program: ExplorerProps,
|
|
264
|
+
grapherConfigs: GrapherInterface[],
|
|
265
|
+
partialGrapherConfigs: GrapherInterface[],
|
|
266
|
+
explorerConstants: Record<string, string>,
|
|
267
|
+
urlMigrationSpec?: ExplorerPageUrlMigrationSpec,
|
|
268
|
+
archiveContext?: ArchiveContext
|
|
269
|
+
) {
|
|
270
|
+
const props: ExplorerProps = {
|
|
271
|
+
...program,
|
|
272
|
+
...explorerConstants,
|
|
273
|
+
grapherConfigs,
|
|
274
|
+
partialGrapherConfigs,
|
|
275
|
+
isEmbeddedInAnOwidPage: false,
|
|
276
|
+
isInStandalonePage: true,
|
|
277
|
+
archiveContext,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (window.location.href.includes(EXPLORERS_PREVIEW_ROUTE)) {
|
|
281
|
+
renderLivePreviewVersion(props)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let url = Url.fromURL(window.location.href)
|
|
286
|
+
|
|
287
|
+
// Handle redirect spec that's baked on the page.
|
|
288
|
+
// e.g. the old COVID Grapher to Explorer redirects are implemented this way.
|
|
289
|
+
if (urlMigrationSpec) {
|
|
290
|
+
const { explorerUrlMigrationId, baseQueryStr } = urlMigrationSpec
|
|
291
|
+
const migration = explorerUrlMigrationsById[explorerUrlMigrationId]
|
|
292
|
+
if (migration) {
|
|
293
|
+
url = migration.migrateUrl(url, baseQueryStr)
|
|
294
|
+
} else {
|
|
295
|
+
console.error(
|
|
296
|
+
`No explorer URL migration with id ${explorerUrlMigrationId}`
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Handle explorer-specific migrations.
|
|
302
|
+
// This is how we migrate the old CO2 explorer to the new CO2 explorer.
|
|
303
|
+
// Because they are on the same path, we can't handle it like we handle
|
|
304
|
+
// the COVID explorer redirects above.
|
|
305
|
+
url = migrateExplorerUrl(url)
|
|
306
|
+
|
|
307
|
+
// Update the window URL
|
|
308
|
+
setWindowUrl(url)
|
|
309
|
+
|
|
310
|
+
const elem = document.getElementById(ExplorerContainerId)
|
|
311
|
+
if (!elem) throw new Error("ExplorerContainerId not found in DOM")
|
|
312
|
+
|
|
313
|
+
const root = createRoot(elem)
|
|
314
|
+
root.render(<Explorer {...props} queryStr={url.queryStr} />)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private initialQueryParams = Url.fromQueryStr(this.props.queryStr ?? "")
|
|
318
|
+
.queryParams as ExplorerFullQueryParams
|
|
319
|
+
|
|
320
|
+
explorerProgram = ExplorerProgram.fromJson(this.props).initDecisionMatrix(
|
|
321
|
+
this.initialQueryParams
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
bakedBaseUrl = this.props.bakedBaseUrl
|
|
325
|
+
dataApiUrl = this.props.dataApiUrl
|
|
326
|
+
adminBaseUrl = this.props.adminBaseUrl
|
|
327
|
+
bakedGrapherUrl = this.props.bakedGrapherUrl
|
|
328
|
+
|
|
329
|
+
selection = this.props.selection?.hasSelection
|
|
330
|
+
? this.props.selection
|
|
331
|
+
: new SelectionArray(this.explorerProgram.selection)
|
|
332
|
+
|
|
333
|
+
entityType = this.explorerProgram.entityType ?? DEFAULT_GRAPHER_ENTITY_TYPE
|
|
334
|
+
|
|
335
|
+
// We want to ensure that unavailable entity are shown in the explorer entity
|
|
336
|
+
// that's why we employ an append-only version of `grapher.availableEntityNames`
|
|
337
|
+
appendOnlyAvailableEntityNames: Set<EntityName> = new Set()
|
|
338
|
+
|
|
339
|
+
grapher: Grapher | undefined = undefined
|
|
340
|
+
|
|
341
|
+
@action.bound setGrapher(grapher: Grapher) {
|
|
342
|
+
this.grapher = grapher
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Note: Can't use @computed here because this.props isn't observable in mobx-react
|
|
346
|
+
get grapherConfigs() {
|
|
347
|
+
const arr = this.props.grapherConfigs || []
|
|
348
|
+
return new Map(arr.map((config) => [config.id!, config]))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Note: Can't use @computed here because this.props isn't observable in mobx-react
|
|
352
|
+
get partialGrapherConfigsByVariableId() {
|
|
353
|
+
const arr = this.props.partialGrapherConfigs || []
|
|
354
|
+
return new Map(arr.map((config) => [config.id!, config]))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@computed get partialGrapherConfigsBySlug() {
|
|
358
|
+
const configsBySlug = new Map<string, GrapherInterface>()
|
|
359
|
+
for (const columnDef of this.explorerProgram
|
|
360
|
+
.columnDefsWithoutTableSlug) {
|
|
361
|
+
if (columnDef.transform?.startsWith("duplicate")) {
|
|
362
|
+
const indicatorId = +columnDef.transform
|
|
363
|
+
.replace("duplicate", "")
|
|
364
|
+
.trim()
|
|
365
|
+
const partialGrapherConfig =
|
|
366
|
+
this.partialGrapherConfigsByVariableId.get(indicatorId)
|
|
367
|
+
if (partialGrapherConfig)
|
|
368
|
+
configsBySlug.set(columnDef.slug, partialGrapherConfig)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return configsBySlug
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private setUpIntersectionObserver(): void {
|
|
375
|
+
if (typeof window !== "undefined" && "IntersectionObserver" in window) {
|
|
376
|
+
const observer = new IntersectionObserver((entries) => {
|
|
377
|
+
entries.forEach((entry) => {
|
|
378
|
+
if (entry.isIntersecting) {
|
|
379
|
+
this.analytics.logExplorerView(
|
|
380
|
+
this.explorerProgram.slug,
|
|
381
|
+
this.explorerProgram.decisionMatrix.currentParams
|
|
382
|
+
)
|
|
383
|
+
observer.disconnect()
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
observer.observe(this.grapherContainerRef.current!)
|
|
388
|
+
this.disposers.push(() => observer.disconnect())
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
disposers: (() => void)[] = []
|
|
393
|
+
override async componentDidMount() {
|
|
394
|
+
this.setGrapher(this.grapherRef!.current!)
|
|
395
|
+
|
|
396
|
+
let url = Url.fromQueryParams({
|
|
397
|
+
...this.initialQueryParams,
|
|
398
|
+
...this.currentChoiceParams, // Needed for Grapher's embedArchivedUrl.
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if (this.props.selection?.hasSelection) {
|
|
402
|
+
url = setSelectedEntityNamesParam(
|
|
403
|
+
url,
|
|
404
|
+
this.props.selection.selectedEntityNames
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
// Run the initializiation for the grapherState but don't await it. The update code
|
|
408
|
+
// will reset and initialize the grapherState and then the data loading will be awaited inside the function.
|
|
409
|
+
// We don't want to wait for the data loading to finish as we only care about the non-data loading
|
|
410
|
+
// part to run (the data loading can finish whenever)
|
|
411
|
+
void this.updateGrapherFromExplorer()
|
|
412
|
+
|
|
413
|
+
// Do the rest of the initialization
|
|
414
|
+
this.grapherState.populateFromQueryParams(url.queryParams)
|
|
415
|
+
exposeInstanceOnWindow(this, "explorer")
|
|
416
|
+
this.setUpIntersectionObserver()
|
|
417
|
+
this.attachEventListeners()
|
|
418
|
+
this.updateEntityPickerTable() // call for the first time to initialize EntityPicker
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private attachEventListeners() {
|
|
422
|
+
if (typeof window !== "undefined" && "ResizeObserver" in window) {
|
|
423
|
+
const onResizeThrottled = _.debounce(this.onResize, 200, {
|
|
424
|
+
leading: true,
|
|
425
|
+
})
|
|
426
|
+
const resizeObserver = new ResizeObserver(onResizeThrottled)
|
|
427
|
+
resizeObserver.observe(this.grapherContainerRef.current!)
|
|
428
|
+
this.disposers.push(() => {
|
|
429
|
+
resizeObserver.disconnect()
|
|
430
|
+
})
|
|
431
|
+
} else if (typeof window === "object" && typeof document === "object") {
|
|
432
|
+
// only show the warning when we're in something that roughly resembles a browser
|
|
433
|
+
console.warn(
|
|
434
|
+
"ResizeObserver not available; the explorer will not be responsive to window resizes"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
this.onResize() // fire once to initialize, at least
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// We always prefer the entity picker metric to be sourced from the currently displayed table.
|
|
441
|
+
// To do this properly, we need to also react to the table changing.
|
|
442
|
+
this.disposers.push(
|
|
443
|
+
reaction(
|
|
444
|
+
() => [
|
|
445
|
+
this.entityPickerMetric,
|
|
446
|
+
this.explorerProgram.explorerGrapherConfig.tableSlug,
|
|
447
|
+
],
|
|
448
|
+
() => this.updateEntityPickerTable()
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
this.disposers.push(
|
|
453
|
+
reaction(
|
|
454
|
+
() => this.grapherState.availableEntityNames,
|
|
455
|
+
(availableEntityNames) => {
|
|
456
|
+
availableEntityNames?.forEach((entity) =>
|
|
457
|
+
this.appendOnlyAvailableEntityNames.add(entity)
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if (this.props.isInStandalonePage) this.bindToWindow()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
override componentWillUnmount() {
|
|
467
|
+
this.disposers.forEach((dispose) => dispose())
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private initSlideshow() {
|
|
471
|
+
if (this.grapherState.slideShow) return
|
|
472
|
+
|
|
473
|
+
this.grapherState.slideShow = new SlideShowController(
|
|
474
|
+
this.explorerProgram.decisionMatrix.allDecisionsAsQueryParams(),
|
|
475
|
+
0,
|
|
476
|
+
this
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private persistedGrapherQueryParamsBySelectedRow: Map<
|
|
481
|
+
number,
|
|
482
|
+
Partial<GrapherQueryParams>
|
|
483
|
+
> = new Map()
|
|
484
|
+
|
|
485
|
+
// todo: break this method up and unit test more. this is pretty ugly right now.
|
|
486
|
+
@action.bound async reactToUserChangingSelection(oldSelectedRow: number) {
|
|
487
|
+
if (!this.explorerProgram.currentlySelectedGrapherRow) return
|
|
488
|
+
this.initSlideshow()
|
|
489
|
+
|
|
490
|
+
const oldGrapherParams = this.grapherState.changedParams
|
|
491
|
+
this.persistedGrapherQueryParamsBySelectedRow.set(
|
|
492
|
+
oldSelectedRow,
|
|
493
|
+
oldGrapherParams
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
const newGrapherParams = {
|
|
497
|
+
...this.currentChoiceParams, // Needed for Grapher's embedArchivedUrl.
|
|
498
|
+
...this.persistedGrapherQueryParamsBySelectedRow.get(
|
|
499
|
+
this.explorerProgram.currentlySelectedGrapherRow
|
|
500
|
+
),
|
|
501
|
+
country: oldGrapherParams.country,
|
|
502
|
+
region: oldGrapherParams.region,
|
|
503
|
+
time: this.grapherState.timeParam,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const previousTab = this.grapherState.activeTab
|
|
507
|
+
|
|
508
|
+
await this.updateGrapherFromExplorer()
|
|
509
|
+
|
|
510
|
+
newGrapherParams.tab =
|
|
511
|
+
this.grapherState.mapGrapherTabToQueryParam(previousTab)
|
|
512
|
+
|
|
513
|
+
// reset map state if switching to a chart
|
|
514
|
+
if (newGrapherParams.tab !== GRAPHER_TAB_QUERY_PARAMS.map) {
|
|
515
|
+
newGrapherParams.globe = "0"
|
|
516
|
+
newGrapherParams.mapSelect = ""
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.grapherState.populateFromQueryParams(newGrapherParams)
|
|
520
|
+
|
|
521
|
+
// When switching between explorer views, we usually preserve the tab.
|
|
522
|
+
// However, if the new chart doesn't support the previously selected tab,
|
|
523
|
+
// Grapher automatically switches to a supported one. In such cases,
|
|
524
|
+
// we call onChartSwitching to make adjustments that ensure the new view
|
|
525
|
+
// is sensible (e.g. updating the time selection when switching from a
|
|
526
|
+
// single-time chart like a discrete bar chart to a multi-time chart like
|
|
527
|
+
// a line chart).
|
|
528
|
+
const currentTab = this.grapherState.activeTab
|
|
529
|
+
if (previousTab !== currentTab)
|
|
530
|
+
this.grapherState.onChartSwitching(previousTab, currentTab)
|
|
531
|
+
|
|
532
|
+
this.analytics.logExplorerView(
|
|
533
|
+
this.explorerProgram.slug,
|
|
534
|
+
this.explorerProgram.decisionMatrix.currentParams
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
@action.bound private setGrapherTable(table: OwidTable) {
|
|
539
|
+
this.grapherState.inputTable = this.inputTableTransformer(table)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@computed get availableEntityNames(): EntityName[] {
|
|
543
|
+
const entityNameSet = this.appendOnlyAvailableEntityNames
|
|
544
|
+
return Array.from(entityNameSet)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@computed private get enableMapSelection(): boolean {
|
|
548
|
+
return !this.isNarrow
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private futureGrapherTable = new PromiseSwitcher<OwidTable>({
|
|
552
|
+
onResolve: (table) => this.setGrapherTable(table),
|
|
553
|
+
onReject: (error) => this.grapher?.setError(error),
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
tableLoader = new PromiseCache((slug: TableSlug | undefined) =>
|
|
557
|
+
this.explorerProgram.constructTable(slug)
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
@action.bound async updateGrapherFromExplorer() {
|
|
561
|
+
switch (this.explorerProgram.chartCreationMode) {
|
|
562
|
+
case ExplorerChartCreationMode.FromGrapherId:
|
|
563
|
+
return this.updateGrapherFromExplorerUsingGrapherId()
|
|
564
|
+
case ExplorerChartCreationMode.FromVariableIds:
|
|
565
|
+
return this.updateGrapherFromExplorerUsingVariableIds()
|
|
566
|
+
case ExplorerChartCreationMode.FromExplorerTableColumnSlugs:
|
|
567
|
+
return this.updateGrapherFromExplorerUsingColumnSlugs()
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
@computed private get columnDefsWithoutTableSlugByIdOrSlug(): Record<
|
|
572
|
+
number | string,
|
|
573
|
+
OwidColumnDef
|
|
574
|
+
> {
|
|
575
|
+
const { columnDefsWithoutTableSlug } = this.explorerProgram
|
|
576
|
+
return _.keyBy(
|
|
577
|
+
columnDefsWithoutTableSlug,
|
|
578
|
+
(def: OwidColumnDef) => def.owidVariableId ?? def.slug
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// gets the slugs of all base and intermediate columns that a
|
|
583
|
+
// transformed column depends on; for example, if a column's transform
|
|
584
|
+
// is 'divideBy 170775 other_slug' and 'other_slug' is also a transformed
|
|
585
|
+
// column defined by 'multiplyBy 539022 2', then this function
|
|
586
|
+
// returns ['539022', '170775', 'other_slug']
|
|
587
|
+
private getBaseColumnsForColumnWithTransform(slug: string): string[] {
|
|
588
|
+
const def = this.columnDefsWithoutTableSlugByIdOrSlug[slug]
|
|
589
|
+
if (!def?.transform) return []
|
|
590
|
+
const dataSlugs =
|
|
591
|
+
extractPotentialDataSlugsFromTransform(def.transform) ?? []
|
|
592
|
+
return dataSlugs.flatMap((dataSlug) => [
|
|
593
|
+
...this.getBaseColumnsForColumnWithTransform(dataSlug),
|
|
594
|
+
dataSlug,
|
|
595
|
+
])
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// gets the IDs of all variables that a transformed column depends on;
|
|
599
|
+
// for example, if a there are two columns, 'slug' and 'other_slug', that
|
|
600
|
+
// are defined by the transforms 'divideBy 170775 other_slug' and 'multiplyBy 539022 2',
|
|
601
|
+
// respectively, then getBaseVariableIdsForColumnWithTransform('slug')
|
|
602
|
+
// returns ['539022', '170775'] as these are the IDs of the two variables
|
|
603
|
+
// that the 'slug' column depends on
|
|
604
|
+
private getBaseVariableIdsForColumnWithTransform(slug: string): string[] {
|
|
605
|
+
const { columnDefsWithoutTableSlug } = this.explorerProgram
|
|
606
|
+
const baseVariableIdsAndColumnSlugs =
|
|
607
|
+
this.getBaseColumnsForColumnWithTransform(slug)
|
|
608
|
+
const slugsInColumnBlock: string[] = columnDefsWithoutTableSlug
|
|
609
|
+
.filter((def) => !def.owidVariableId)
|
|
610
|
+
.map((def) => def.slug)
|
|
611
|
+
return baseVariableIdsAndColumnSlugs.filter(
|
|
612
|
+
(variableIdOrColumnSlug) =>
|
|
613
|
+
!slugsInColumnBlock.includes(variableIdOrColumnSlug)
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@action.bound async updateGrapherFromExplorerUsingGrapherId() {
|
|
618
|
+
const grapherState = this.grapherState
|
|
619
|
+
|
|
620
|
+
const { grapherId } = this.explorerProgram.explorerGrapherConfig
|
|
621
|
+
const grapherConfig = this.grapherConfigs.get(grapherId!)
|
|
622
|
+
if (!grapherConfig) {
|
|
623
|
+
if (this.props.throwOnMissingGrapher) {
|
|
624
|
+
throw new Error(`Grapher config not found for ID: ${grapherId}`)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const finalGrapherConfig = grapherConfig ?? {}
|
|
628
|
+
|
|
629
|
+
const config: GrapherProgrammaticInterface = {
|
|
630
|
+
...mergeGrapherConfigs(
|
|
631
|
+
finalGrapherConfig,
|
|
632
|
+
this.explorerProgram.grapherConfig
|
|
633
|
+
),
|
|
634
|
+
baseUrl: this.baseUrl,
|
|
635
|
+
bakedGrapherURL: this.bakedBaseUrl,
|
|
636
|
+
adminBaseUrl: this.adminBaseUrl,
|
|
637
|
+
hideEntityControls: this.showExplorerControls,
|
|
638
|
+
enableMapSelection: this.enableMapSelection,
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// if not empty, respect the explorer's selection
|
|
642
|
+
if (this.selection.hasSelection) {
|
|
643
|
+
config.selectedEntityNames = this.selection.selectedEntityNames
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
grapherState.setAuthoredVersion(config)
|
|
647
|
+
grapherState.reset()
|
|
648
|
+
grapherState.inputTable = BlankOwidTable()
|
|
649
|
+
grapherState.updateFromObject(config)
|
|
650
|
+
if (!config.table) {
|
|
651
|
+
const loadFn =
|
|
652
|
+
this.props.loadInputTableForConfig ?? fetchInputTableForConfig
|
|
653
|
+
const inputTable = loadFn({
|
|
654
|
+
dimensions: config.dimensions,
|
|
655
|
+
selectedEntityColors: config.selectedEntityColors,
|
|
656
|
+
dataApiUrl: this.props.dataApiUrl,
|
|
657
|
+
archiveContext: this.props.archiveContext,
|
|
658
|
+
noCache: this.props.isPreview,
|
|
659
|
+
loadMetadataOnly: this.props.loadMetadataOnly,
|
|
660
|
+
}).then((owidTable) => (owidTable ? owidTable : BlankOwidTable()))
|
|
661
|
+
// We use the PromiseSwitcher here to make sure that only the last
|
|
662
|
+
// of several user triggered load operations in quick succession
|
|
663
|
+
// will actually set the table.
|
|
664
|
+
await this.futureGrapherTable.set(inputTable)
|
|
665
|
+
} else {
|
|
666
|
+
grapherState.inputTable = this.inputTableTransformer(config.table)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
@action.bound async updateGrapherFromExplorerUsingVariableIds() {
|
|
671
|
+
const grapherState = this.grapherState
|
|
672
|
+
const {
|
|
673
|
+
yVariableIds = "",
|
|
674
|
+
xVariableId,
|
|
675
|
+
colorVariableId,
|
|
676
|
+
sizeVariableId,
|
|
677
|
+
ySlugs = "",
|
|
678
|
+
xSlug,
|
|
679
|
+
colorSlug,
|
|
680
|
+
sizeSlug,
|
|
681
|
+
} = this.explorerProgram.explorerGrapherConfig
|
|
682
|
+
|
|
683
|
+
const yVariableIdsList = yVariableIds
|
|
684
|
+
.split(" ")
|
|
685
|
+
.map(parseIntOrUndefined)
|
|
686
|
+
.filter((item) => item !== undefined)
|
|
687
|
+
const ySlugList = ySlugs.split(" ")
|
|
688
|
+
|
|
689
|
+
const partialGrapherConfig =
|
|
690
|
+
this.partialGrapherConfigsByVariableId.get(yVariableIdsList[0]) ??
|
|
691
|
+
// if ySlug references a column that duplicates an indicator via the
|
|
692
|
+
// `duplicate` transform, make sure the partial grapher config for
|
|
693
|
+
// that indicator is pulled in
|
|
694
|
+
this.partialGrapherConfigsBySlug.get(ySlugList[0]) ??
|
|
695
|
+
{}
|
|
696
|
+
|
|
697
|
+
const config: GrapherProgrammaticInterface = {
|
|
698
|
+
...mergeGrapherConfigs(
|
|
699
|
+
partialGrapherConfig,
|
|
700
|
+
this.explorerProgram.grapherConfig
|
|
701
|
+
),
|
|
702
|
+
bakedGrapherURL: this.bakedBaseUrl,
|
|
703
|
+
adminBaseUrl: this.adminBaseUrl,
|
|
704
|
+
hideEntityControls: this.showExplorerControls,
|
|
705
|
+
enableMapSelection: this.enableMapSelection,
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// if not empty, respect the explorer's selection
|
|
709
|
+
if (this.selection.hasSelection) {
|
|
710
|
+
config.selectedEntityNames = this.selection.selectedEntityNames
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// set given variable IDs as dimensions to make Grapher
|
|
714
|
+
// download the data and metadata for these variables
|
|
715
|
+
const dimensions = config.dimensions?.slice() ?? []
|
|
716
|
+
yVariableIdsList.forEach((yVariableId) => {
|
|
717
|
+
dimensions.push({
|
|
718
|
+
variableId: yVariableId,
|
|
719
|
+
property: DimensionProperty.y,
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
if (xVariableId) {
|
|
724
|
+
const maybeXVariableId = parseIntOrUndefined(xVariableId)
|
|
725
|
+
if (maybeXVariableId !== undefined)
|
|
726
|
+
dimensions.push({
|
|
727
|
+
variableId: maybeXVariableId,
|
|
728
|
+
property: DimensionProperty.x,
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
if (colorVariableId) {
|
|
732
|
+
const maybeColorVariableId = parseIntOrUndefined(colorVariableId)
|
|
733
|
+
if (maybeColorVariableId !== undefined)
|
|
734
|
+
dimensions.push({
|
|
735
|
+
variableId: maybeColorVariableId,
|
|
736
|
+
property: DimensionProperty.color,
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
if (sizeVariableId) {
|
|
740
|
+
const maybeSizeVariableId = parseIntOrUndefined(sizeVariableId)
|
|
741
|
+
if (maybeSizeVariableId !== undefined)
|
|
742
|
+
dimensions.push({
|
|
743
|
+
variableId: maybeSizeVariableId,
|
|
744
|
+
property: DimensionProperty.size,
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Slugs that are used to create a chart refer to columns derived from variables
|
|
749
|
+
// by a transform string (e.g. 'multiplyBy 539022 2'). To render such a chart, we
|
|
750
|
+
// need to download the data for all variables the transformed columns depend on
|
|
751
|
+
// and construct an appropriate Grapher table. This is done in three steps:
|
|
752
|
+
// 1. find all variables that the transformed columns depend on and add them to
|
|
753
|
+
// the config's dimensions array
|
|
754
|
+
// 2. download data and metadata of the variables
|
|
755
|
+
// 3. append the transformed columns to the Grapher table (note that this includes
|
|
756
|
+
// intermediate columns that are defined for multi-step transforms but are not
|
|
757
|
+
// referred to in any Grapher row)
|
|
758
|
+
|
|
759
|
+
// all slugs specified by the author in the explorer config
|
|
760
|
+
const uniqueSlugsInGrapherRow = _.uniq(
|
|
761
|
+
[...ySlugs.split(" "), xSlug, colorSlug, sizeSlug].filter(
|
|
762
|
+
_.identity
|
|
763
|
+
)
|
|
764
|
+
) as string[]
|
|
765
|
+
|
|
766
|
+
// find all variables that the transformed columns depend on and add them to the dimensions array
|
|
767
|
+
if (uniqueSlugsInGrapherRow.length) {
|
|
768
|
+
const baseVariableIds = _.uniq(
|
|
769
|
+
uniqueSlugsInGrapherRow.flatMap((slug) =>
|
|
770
|
+
this.getBaseVariableIdsForColumnWithTransform(slug)
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
.map((id) => parseInt(id, 10))
|
|
774
|
+
.filter((id) => !isNaN(id))
|
|
775
|
+
baseVariableIds.forEach((variableId) => {
|
|
776
|
+
const hasDimension = dimensions.some(
|
|
777
|
+
(d) => d.variableId === variableId
|
|
778
|
+
)
|
|
779
|
+
if (!hasDimension) {
|
|
780
|
+
dimensions.push({
|
|
781
|
+
variableId: variableId,
|
|
782
|
+
property: DimensionProperty.table, // no specific dimension
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
config.dimensions = dimensions
|
|
789
|
+
if (ySlugs && yVariableIds) config.ySlugs = ySlugs + " " + yVariableIds
|
|
790
|
+
|
|
791
|
+
this.inputTableTransformer = (table: OwidTable) => {
|
|
792
|
+
// add transformed (and intermediate) columns to the grapher table
|
|
793
|
+
if (uniqueSlugsInGrapherRow.length) {
|
|
794
|
+
const allColumnSlugs = _.uniq(
|
|
795
|
+
uniqueSlugsInGrapherRow.flatMap((slug) => [
|
|
796
|
+
...this.getBaseColumnsForColumnWithTransform(slug),
|
|
797
|
+
slug,
|
|
798
|
+
])
|
|
799
|
+
)
|
|
800
|
+
const existingColumnSlugs = table.columnSlugs
|
|
801
|
+
const outstandingColumnSlugs = allColumnSlugs.filter(
|
|
802
|
+
(slug) => !existingColumnSlugs.includes(slug)
|
|
803
|
+
)
|
|
804
|
+
const requiredColumnDefs = outstandingColumnSlugs
|
|
805
|
+
.map(
|
|
806
|
+
(slug) =>
|
|
807
|
+
this.columnDefsWithoutTableSlugByIdOrSlug[slug]
|
|
808
|
+
)
|
|
809
|
+
.filter(_.identity)
|
|
810
|
+
table = table.appendColumns(requiredColumnDefs)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// update column definitions with manually provided properties
|
|
814
|
+
table = table.updateDefs((def: OwidColumnDef) => {
|
|
815
|
+
const manuallyProvidedDef =
|
|
816
|
+
this.columnDefsWithoutTableSlugByIdOrSlug[def.slug] ?? {}
|
|
817
|
+
const mergedDef = { ...def, ...manuallyProvidedDef }
|
|
818
|
+
|
|
819
|
+
// update display properties
|
|
820
|
+
mergedDef.display ??= {}
|
|
821
|
+
if (manuallyProvidedDef.name)
|
|
822
|
+
mergedDef.display.name = manuallyProvidedDef.name
|
|
823
|
+
if (manuallyProvidedDef.unit)
|
|
824
|
+
mergedDef.display.unit = manuallyProvidedDef.unit
|
|
825
|
+
if (manuallyProvidedDef.shortUnit)
|
|
826
|
+
mergedDef.display.shortUnit = manuallyProvidedDef.shortUnit
|
|
827
|
+
|
|
828
|
+
return mergedDef
|
|
829
|
+
})
|
|
830
|
+
return table
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
grapherState.setAuthoredVersion(config)
|
|
834
|
+
grapherState.reset()
|
|
835
|
+
grapherState.updateFromObject(config)
|
|
836
|
+
if (dimensions.length === 0) {
|
|
837
|
+
// If dimensions are empty, explicitly set the table to an empty table
|
|
838
|
+
// so we don't end up confusingly showing stale data from a previous chart
|
|
839
|
+
grapherState.inputTable = BlankOwidTable()
|
|
840
|
+
} else {
|
|
841
|
+
const loadFn =
|
|
842
|
+
this.props.loadInputTableForConfig ?? fetchInputTableForConfig
|
|
843
|
+
const inputTable = loadFn({
|
|
844
|
+
dimensions: config.dimensions,
|
|
845
|
+
selectedEntityColors: config.selectedEntityColors,
|
|
846
|
+
archiveContext: this.props.archiveContext,
|
|
847
|
+
dataApiUrl: this.props.dataApiUrl,
|
|
848
|
+
noCache: this.props.isPreview,
|
|
849
|
+
loadMetadataOnly: this.props.loadMetadataOnly,
|
|
850
|
+
}).then((owidTable) => (owidTable ? owidTable : BlankOwidTable()))
|
|
851
|
+
// We use the PromiseSwitcher here to make sure that only the last
|
|
852
|
+
// of several user triggered load operations in quick succession
|
|
853
|
+
// will actually set the table.
|
|
854
|
+
await this.futureGrapherTable.set(inputTable)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@action.bound async updateGrapherFromExplorerUsingColumnSlugs() {
|
|
859
|
+
const grapherState = this.grapherState
|
|
860
|
+
const { tableSlug } = this.explorerProgram.explorerGrapherConfig
|
|
861
|
+
|
|
862
|
+
const config: GrapherProgrammaticInterface = {
|
|
863
|
+
...this.explorerProgram.grapherConfig,
|
|
864
|
+
bakedGrapherURL: this.bakedBaseUrl,
|
|
865
|
+
adminBaseUrl: this.adminBaseUrl,
|
|
866
|
+
hideEntityControls: this.showExplorerControls,
|
|
867
|
+
enableMapSelection: this.enableMapSelection,
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// if not empty, respect the explorer's selection
|
|
871
|
+
if (this.selection.hasSelection) {
|
|
872
|
+
config.selectedEntityNames = this.selection.selectedEntityNames
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
grapherState.setAuthoredVersion(config)
|
|
876
|
+
grapherState.reset()
|
|
877
|
+
grapherState.updateFromObject(config)
|
|
878
|
+
|
|
879
|
+
// Clear any error messages, they are likely to be related to dataset loading.
|
|
880
|
+
this.grapher?.clearErrors()
|
|
881
|
+
// Set a table immediately. A BlankTable shows a loading animation.
|
|
882
|
+
this.setGrapherTable(
|
|
883
|
+
BlankOwidTable(tableSlug, `Loading table '${tableSlug}'`)
|
|
884
|
+
)
|
|
885
|
+
// We use the PromiseSwitcher here to make sure that only the last
|
|
886
|
+
// of several user triggered load operations in quick succession
|
|
887
|
+
// will actually set the table.
|
|
888
|
+
await this.futureGrapherTable.set(this.tableLoader.get(tableSlug))
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@action.bound setSlide(choiceParams: ExplorerFullQueryParams) {
|
|
892
|
+
this.explorerProgram.decisionMatrix.setValuesFromChoiceParams(
|
|
893
|
+
choiceParams
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
@computed private get currentChoiceParams(): ExplorerChoiceParams {
|
|
898
|
+
const { decisionMatrix } = this.explorerProgram
|
|
899
|
+
return decisionMatrix.currentParams
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
@computed get queryParams(): ExplorerFullQueryParams {
|
|
903
|
+
if (
|
|
904
|
+
typeof window !== "undefined" &&
|
|
905
|
+
window.location.href.includes(EXPLORERS_PREVIEW_ROUTE)
|
|
906
|
+
)
|
|
907
|
+
localStorage.setItem(
|
|
908
|
+
UNSAVED_EXPLORER_PREVIEW_QUERYPARAMS +
|
|
909
|
+
this.explorerProgram.slug,
|
|
910
|
+
JSON.stringify(this.currentChoiceParams)
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
let url = Url.fromQueryParams(
|
|
914
|
+
omitUndefinedValues({
|
|
915
|
+
...this.grapherState.changedParams,
|
|
916
|
+
pickerSort: this.entityPickerSort,
|
|
917
|
+
pickerMetric: this.entityPickerMetric,
|
|
918
|
+
hideControls: this.initialQueryParams.hideControls || undefined,
|
|
919
|
+
...this.currentChoiceParams,
|
|
920
|
+
})
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
url = setSelectedEntityNamesParam(
|
|
924
|
+
url,
|
|
925
|
+
this.selection.hasSelection
|
|
926
|
+
? this.selection.selectedEntityNames
|
|
927
|
+
: undefined
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
return url.queryParams as ExplorerFullQueryParams
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
@computed get currentUrl(): Url {
|
|
934
|
+
if (this.props.isPreview) return Url.fromQueryParams(this.queryParams)
|
|
935
|
+
return Url.fromURL(window.location.href).setQueryParams(
|
|
936
|
+
this.queryParams
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private bindToWindow() {
|
|
941
|
+
// There is a surprisingly considerable performance overhead to updating the url
|
|
942
|
+
// while animating, so we debounce to allow e.g. smoother timelines
|
|
943
|
+
const pushParams = () =>
|
|
944
|
+
setWindowUrl(Url.fromQueryParams(this.queryParams))
|
|
945
|
+
const debouncedPushParams = _.debounce(pushParams, 100)
|
|
946
|
+
|
|
947
|
+
this.disposers.push(
|
|
948
|
+
reaction(
|
|
949
|
+
() => this.queryParams,
|
|
950
|
+
() =>
|
|
951
|
+
this.grapher?.debounceMode
|
|
952
|
+
? debouncedPushParams()
|
|
953
|
+
: pushParams()
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private get panels() {
|
|
959
|
+
return this.explorerProgram.decisionMatrix.choicesWithAvailability.map(
|
|
960
|
+
(choice) => (
|
|
961
|
+
<ExplorerControlPanel
|
|
962
|
+
key={choice.title}
|
|
963
|
+
explorerSlug={this.explorerProgram.slug}
|
|
964
|
+
choice={choice}
|
|
965
|
+
onChange={this.onChangeChoice(choice.title)}
|
|
966
|
+
isMobile={this.isNarrow}
|
|
967
|
+
/>
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
onChangeChoice = (choiceTitle: string) => async (value: string) => {
|
|
973
|
+
const { currentlySelectedGrapherRow } = this.explorerProgram
|
|
974
|
+
this.explorerProgram.decisionMatrix.setValueCommand(choiceTitle, value)
|
|
975
|
+
if (currentlySelectedGrapherRow)
|
|
976
|
+
await this.reactToUserChangingSelection(currentlySelectedGrapherRow)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private renderHeaderElement() {
|
|
980
|
+
return (
|
|
981
|
+
<div className="ExplorerHeaderBox">
|
|
982
|
+
<div className="ExplorerTitle">
|
|
983
|
+
{this.explorerProgram.explorerTitle} Data Explorer
|
|
984
|
+
</div>
|
|
985
|
+
<div className="ExplorerSubtitle">
|
|
986
|
+
<MarkdownTextWrap
|
|
987
|
+
fontSize={12}
|
|
988
|
+
text={this.explorerProgram.explorerSubtitle || ""}
|
|
989
|
+
/>
|
|
990
|
+
</div>
|
|
991
|
+
{this.explorerProgram.downloadDataLink && (
|
|
992
|
+
<a
|
|
993
|
+
href={this.explorerProgram.downloadDataLink}
|
|
994
|
+
className="ExplorerDownloadLink"
|
|
995
|
+
>
|
|
996
|
+
Download this dataset
|
|
997
|
+
</a>
|
|
998
|
+
)}
|
|
999
|
+
</div>
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private isNarrow = this.props.forceWideMode ? false : isNarrow()
|
|
1004
|
+
|
|
1005
|
+
@computed private get isInIFrame() {
|
|
1006
|
+
return isInIFrame()
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@computed private get showExplorerControls() {
|
|
1010
|
+
if (!this.props.isEmbeddedInAnOwidPage && !this.isInIFrame) return true
|
|
1011
|
+
// Only allow hiding controls on embedded pages
|
|
1012
|
+
return !(
|
|
1013
|
+
this.explorerProgram.hideControls ||
|
|
1014
|
+
this.initialQueryParams.hideControls === "true"
|
|
1015
|
+
)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
@computed private get downloadDataLink(): string | undefined {
|
|
1019
|
+
return this.explorerProgram.downloadDataLink
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private grapherContainerRef = React.createRef<HTMLDivElement>()
|
|
1023
|
+
|
|
1024
|
+
private grapherRef = React.createRef<Grapher>()
|
|
1025
|
+
|
|
1026
|
+
private renderControlBar() {
|
|
1027
|
+
return (
|
|
1028
|
+
<ExplorerControlBar
|
|
1029
|
+
isMobile={this.isNarrow}
|
|
1030
|
+
showControls={this.showMobileControlsPopup}
|
|
1031
|
+
closeControls={this.closeControls}
|
|
1032
|
+
>
|
|
1033
|
+
{this.panels}
|
|
1034
|
+
</ExplorerControlBar>
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private renderEntityPicker() {
|
|
1039
|
+
const selection =
|
|
1040
|
+
this.grapherState.isOnMapTab && this.enableMapSelection
|
|
1041
|
+
? this.grapherState.mapConfig.selection
|
|
1042
|
+
: this.selection
|
|
1043
|
+
|
|
1044
|
+
return (
|
|
1045
|
+
<EntityPicker
|
|
1046
|
+
key="entityPicker"
|
|
1047
|
+
manager={this}
|
|
1048
|
+
selection={selection}
|
|
1049
|
+
onSelectEntity={this.grapherState.onSelectEntity}
|
|
1050
|
+
onDeselectEntity={this.grapherState.onDeselectEntity}
|
|
1051
|
+
onClearEntities={this.grapherState.onClearEntities}
|
|
1052
|
+
isDropdownMenu={this.isNarrow}
|
|
1053
|
+
/>
|
|
1054
|
+
)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
@action.bound private toggleMobileControls() {
|
|
1058
|
+
this.showMobileControlsPopup = !this.showMobileControlsPopup
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@action.bound private onResize() {
|
|
1062
|
+
// Don't bother rendering if the container is hidden
|
|
1063
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
1064
|
+
if (this.grapherContainerRef.current?.offsetParent === null) return
|
|
1065
|
+
|
|
1066
|
+
const oldIsNarrow = this.isNarrow
|
|
1067
|
+
this.isNarrow = this.props.forceWideMode ? false : isNarrow()
|
|
1068
|
+
this.updateGrapherBounds()
|
|
1069
|
+
|
|
1070
|
+
// If we changed between narrow and wide mode, we need to wait for CSS changes to kick in
|
|
1071
|
+
// to properly calculate the new grapher bounds
|
|
1072
|
+
if (this.isNarrow !== oldIsNarrow)
|
|
1073
|
+
window.setTimeout(() => this.updateGrapherBounds(), 0)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Todo: add better logic to maximize the size of the Grapher
|
|
1077
|
+
private updateGrapherBounds() {
|
|
1078
|
+
const grapherContainer = this.grapherContainerRef.current
|
|
1079
|
+
if (grapherContainer)
|
|
1080
|
+
this.grapherState.externalBounds = new Bounds(
|
|
1081
|
+
0,
|
|
1082
|
+
0,
|
|
1083
|
+
grapherContainer.clientWidth,
|
|
1084
|
+
grapherContainer.clientHeight
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private showMobileControlsPopup = false
|
|
1089
|
+
private get mobileCustomizeButton() {
|
|
1090
|
+
return (
|
|
1091
|
+
<a
|
|
1092
|
+
className="btn btn-primary mobile-button"
|
|
1093
|
+
onClick={this.toggleMobileControls}
|
|
1094
|
+
data-track-note="explorer_customize_chart_mobile"
|
|
1095
|
+
>
|
|
1096
|
+
<FontAwesomeIcon icon={faChartLine} /> Customize chart
|
|
1097
|
+
</a>
|
|
1098
|
+
)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@action.bound private closeControls() {
|
|
1102
|
+
this.showMobileControlsPopup = false
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// todo: add tests for this and better tests for this class in general
|
|
1106
|
+
@computed private get showHeaderElement() {
|
|
1107
|
+
return (
|
|
1108
|
+
this.showExplorerControls &&
|
|
1109
|
+
this.explorerProgram.explorerTitle &&
|
|
1110
|
+
this.panels.length > 0
|
|
1111
|
+
)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
override render() {
|
|
1115
|
+
const { showExplorerControls, showHeaderElement } = this
|
|
1116
|
+
|
|
1117
|
+
return (
|
|
1118
|
+
<div
|
|
1119
|
+
className={classNames({
|
|
1120
|
+
Explorer: true,
|
|
1121
|
+
"mobile-explorer": this.isNarrow,
|
|
1122
|
+
HideControls: !showExplorerControls,
|
|
1123
|
+
"is-embed": this.props.isEmbeddedInAnOwidPage,
|
|
1124
|
+
})}
|
|
1125
|
+
>
|
|
1126
|
+
{showHeaderElement && this.renderHeaderElement()}
|
|
1127
|
+
{showHeaderElement && this.renderControlBar()}
|
|
1128
|
+
{showExplorerControls && this.renderEntityPicker()}
|
|
1129
|
+
{showExplorerControls &&
|
|
1130
|
+
this.isNarrow &&
|
|
1131
|
+
this.mobileCustomizeButton}
|
|
1132
|
+
<div className="ExplorerFigure" ref={this.grapherContainerRef}>
|
|
1133
|
+
<Grapher
|
|
1134
|
+
ref={this.grapherRef}
|
|
1135
|
+
grapherState={this.grapherState}
|
|
1136
|
+
/>
|
|
1137
|
+
</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
@computed get editUrl() {
|
|
1143
|
+
return `${EXPLORERS_ROUTE_FOLDER}/${this.props.slug}`
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
@computed get baseUrl() {
|
|
1147
|
+
let archiveUrl = undefined
|
|
1148
|
+
if (this.isOnArchivalPage) {
|
|
1149
|
+
archiveUrl = this.props.archiveContext?.archiveUrl
|
|
1150
|
+
}
|
|
1151
|
+
return (
|
|
1152
|
+
archiveUrl ??
|
|
1153
|
+
`${this.bakedBaseUrl}/${EXPLORERS_ROUTE_FOLDER}/${this.props.slug}`
|
|
1154
|
+
)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
@computed get canonicalUrl() {
|
|
1158
|
+
return (
|
|
1159
|
+
this.props.canonicalUrl ??
|
|
1160
|
+
Url.fromURL(this.baseUrl).setQueryParams(this.queryParams).fullUrl
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
@computed get grapherTable() {
|
|
1165
|
+
return this.grapherState?.tableAfterAuthorTimelineAndEntityFilter
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
entityPickerMetric: string | undefined =
|
|
1169
|
+
this.initialQueryParams.pickerMetric
|
|
1170
|
+
entityPickerSort: SortOrder | undefined = this.initialQueryParams.pickerSort
|
|
1171
|
+
|
|
1172
|
+
entityPickerTable: OwidTable | undefined = undefined
|
|
1173
|
+
entityPickerTableIsLoading: boolean = false
|
|
1174
|
+
|
|
1175
|
+
private futureEntityPickerTable = new PromiseSwitcher<OwidTable>({
|
|
1176
|
+
onResolve: (table) => {
|
|
1177
|
+
this.entityPickerTable = table
|
|
1178
|
+
this.entityPickerTableIsLoading = false
|
|
1179
|
+
},
|
|
1180
|
+
onReject: () => {
|
|
1181
|
+
this.entityPickerTableIsLoading = false
|
|
1182
|
+
},
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
private updateEntityPickerTable(): void {
|
|
1186
|
+
// If we don't currently have a entity picker metric, then set pickerTable to the currently-used table anyways,
|
|
1187
|
+
// so that when we start sorting by entity name we can infer that the column is a string column immediately.
|
|
1188
|
+
const tableSlugToLoad = this.entityPickerMetric
|
|
1189
|
+
? this.getTableSlugOfColumnSlug(this.entityPickerMetric)
|
|
1190
|
+
: this.explorerProgram.explorerGrapherConfig.tableSlug
|
|
1191
|
+
|
|
1192
|
+
this.entityPickerTableIsLoading = true
|
|
1193
|
+
void this.futureEntityPickerTable.set(
|
|
1194
|
+
this.tableLoader.get(tableSlugToLoad)
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
setEntityPicker({
|
|
1199
|
+
metric,
|
|
1200
|
+
sort,
|
|
1201
|
+
}: {
|
|
1202
|
+
metric: string | undefined
|
|
1203
|
+
sort?: SortOrder
|
|
1204
|
+
}) {
|
|
1205
|
+
this.entityPickerMetric = metric
|
|
1206
|
+
if (sort) this.entityPickerSort = sort
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private tableSlugHasColumnSlug(
|
|
1210
|
+
tableSlug: TableSlug | undefined,
|
|
1211
|
+
columnSlug: ColumnSlug,
|
|
1212
|
+
columnDefsByTableSlug: Map<TableSlug | undefined, CoreColumnDef[]>
|
|
1213
|
+
) {
|
|
1214
|
+
return !!columnDefsByTableSlug
|
|
1215
|
+
.get(tableSlug)
|
|
1216
|
+
?.find((def) => def.slug === columnSlug)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private getTableSlugOfColumnSlug(
|
|
1220
|
+
columnSlug: ColumnSlug
|
|
1221
|
+
): TableSlug | undefined {
|
|
1222
|
+
const columnDefsByTableSlug = this.explorerProgram.columnDefsByTableSlug
|
|
1223
|
+
|
|
1224
|
+
// In most cases, column slugs will be duplicated in the tables, e.g. entityName.
|
|
1225
|
+
// Prefer the current Grapher table if it contains the column slug.
|
|
1226
|
+
const grapherTableSlug =
|
|
1227
|
+
this.explorerProgram.explorerGrapherConfig.tableSlug
|
|
1228
|
+
if (
|
|
1229
|
+
this.tableSlugHasColumnSlug(
|
|
1230
|
+
grapherTableSlug,
|
|
1231
|
+
columnSlug,
|
|
1232
|
+
columnDefsByTableSlug
|
|
1233
|
+
)
|
|
1234
|
+
) {
|
|
1235
|
+
return grapherTableSlug
|
|
1236
|
+
}
|
|
1237
|
+
// ...otherwise, search all tables for the column slug
|
|
1238
|
+
return this.explorerProgram.tableSlugs.find((tableSlug) =>
|
|
1239
|
+
this.tableSlugHasColumnSlug(
|
|
1240
|
+
tableSlug,
|
|
1241
|
+
columnSlug,
|
|
1242
|
+
columnDefsByTableSlug
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
@computed get entityPickerColumnDefs(): CoreColumnDef[] {
|
|
1248
|
+
const allColumnDefs = _.uniqBy(
|
|
1249
|
+
Array.from(
|
|
1250
|
+
this.explorerProgram.columnDefsByTableSlug.values()
|
|
1251
|
+
).flat(),
|
|
1252
|
+
(def) => def.slug
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
if (this.explorerProgram.pickerColumnSlugs) {
|
|
1256
|
+
const columnDefsBySlug = keyMap(allColumnDefs, (def) => def.slug)
|
|
1257
|
+
// Preserve the order of columns in the Explorer `pickerColumnSlugs`
|
|
1258
|
+
return excludeUndefined(
|
|
1259
|
+
this.explorerProgram.pickerColumnSlugs.map((slug) =>
|
|
1260
|
+
columnDefsBySlug.get(slug)
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
} else {
|
|
1264
|
+
const discardColumnTypes = new Set([
|
|
1265
|
+
ColumnTypeNames.Year,
|
|
1266
|
+
ColumnTypeNames.Date,
|
|
1267
|
+
ColumnTypeNames.Day,
|
|
1268
|
+
ColumnTypeNames.EntityId,
|
|
1269
|
+
ColumnTypeNames.EntityCode,
|
|
1270
|
+
])
|
|
1271
|
+
return allColumnDefs.filter(
|
|
1272
|
+
(def) =>
|
|
1273
|
+
(def.type === undefined ||
|
|
1274
|
+
!discardColumnTypes.has(def.type)) &&
|
|
1275
|
+
def.slug !== undefined
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
@computed get requiredColumnSlugs() {
|
|
1281
|
+
return this.grapherState?.newSlugs ?? []
|
|
1282
|
+
}
|
|
1283
|
+
}
|