@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,3659 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MarkdownTextWrap,
|
|
3
|
+
sumTextWrapHeights,
|
|
4
|
+
reactRenderToStringClientOnly,
|
|
5
|
+
} from "../../components/index.js"
|
|
6
|
+
import {
|
|
7
|
+
OwidTable,
|
|
8
|
+
BlankOwidTable,
|
|
9
|
+
CoreColumn,
|
|
10
|
+
ColumnTypeMap,
|
|
11
|
+
} from "../../core-table/index.js"
|
|
12
|
+
import {
|
|
13
|
+
GrapherChartType,
|
|
14
|
+
GRAPHER_CHART_TYPES,
|
|
15
|
+
AnnotationFieldsInTitle,
|
|
16
|
+
TimeBound,
|
|
17
|
+
Time,
|
|
18
|
+
EntitySelectionMode,
|
|
19
|
+
StackMode,
|
|
20
|
+
LogoOption,
|
|
21
|
+
GrapherTabConfigOption,
|
|
22
|
+
GRAPHER_TAB_CONFIG_OPTIONS,
|
|
23
|
+
ColorSchemeName,
|
|
24
|
+
ScatterPointLabelStrategy,
|
|
25
|
+
MissingDataStrategy,
|
|
26
|
+
ColumnSlugs,
|
|
27
|
+
ColumnSlug,
|
|
28
|
+
EntityName,
|
|
29
|
+
SeriesName,
|
|
30
|
+
ComparisonLineConfig,
|
|
31
|
+
RelatedQuestionsConfig,
|
|
32
|
+
FacetStrategy,
|
|
33
|
+
SortBy,
|
|
34
|
+
SortOrder,
|
|
35
|
+
QueryParams,
|
|
36
|
+
LegacyGrapherInterface,
|
|
37
|
+
ArchiveContext,
|
|
38
|
+
AdditionalGrapherDataFetchFn,
|
|
39
|
+
GrapherInterface,
|
|
40
|
+
grapherKeysToSerialize,
|
|
41
|
+
GrapherQueryParams,
|
|
42
|
+
GRAPHER_QUERY_PARAM_KEYS,
|
|
43
|
+
ScaleType,
|
|
44
|
+
FacetAxisDomain,
|
|
45
|
+
TimeBounds,
|
|
46
|
+
GrapherTabName,
|
|
47
|
+
GRAPHER_TAB_NAMES,
|
|
48
|
+
AxisConfigInterface,
|
|
49
|
+
SeriesStrategy,
|
|
50
|
+
AssetMap,
|
|
51
|
+
GRAPHER_MAP_TYPE,
|
|
52
|
+
GrapherVariant,
|
|
53
|
+
SeriesColorMap,
|
|
54
|
+
OwidChartDimensionInterface,
|
|
55
|
+
DimensionProperty,
|
|
56
|
+
DetailsMarker,
|
|
57
|
+
EnrichedDetail,
|
|
58
|
+
ProjectionColumnInfo,
|
|
59
|
+
OwidColumnDef,
|
|
60
|
+
OwidVariableRow,
|
|
61
|
+
SortConfig,
|
|
62
|
+
Color,
|
|
63
|
+
GlobeRegionName,
|
|
64
|
+
GrapherWindowType,
|
|
65
|
+
MapRegionName,
|
|
66
|
+
} from "../../types/index.js"
|
|
67
|
+
import {
|
|
68
|
+
objectWithPersistablesToObject,
|
|
69
|
+
deleteRuntimeAndUnchangedProps,
|
|
70
|
+
minTimeToJSON,
|
|
71
|
+
maxTimeToJSON,
|
|
72
|
+
updatePersistables,
|
|
73
|
+
minTimeBoundFromJSONOrNegativeInfinity,
|
|
74
|
+
maxTimeBoundFromJSONOrPositiveInfinity,
|
|
75
|
+
parseFloatOrUndefined,
|
|
76
|
+
Url,
|
|
77
|
+
getTimeDomainFromQueryString,
|
|
78
|
+
findClosestTime,
|
|
79
|
+
Bounds,
|
|
80
|
+
excludeUndefined,
|
|
81
|
+
isInIFrame,
|
|
82
|
+
bind,
|
|
83
|
+
slugify,
|
|
84
|
+
extractDetailsFromSyntax,
|
|
85
|
+
lowerCaseFirstLetterUnlessAbbreviation,
|
|
86
|
+
getOriginAttributionFragments,
|
|
87
|
+
isMobile,
|
|
88
|
+
isTouchDevice,
|
|
89
|
+
omitUndefinedValues,
|
|
90
|
+
firstOfNonEmptyArray,
|
|
91
|
+
getWindowUrl,
|
|
92
|
+
isArrayDifferentFromReference,
|
|
93
|
+
differenceObj,
|
|
94
|
+
queryParamsToStr,
|
|
95
|
+
timeBoundToTimeBoundString,
|
|
96
|
+
getRegionByName,
|
|
97
|
+
checkIsCountry,
|
|
98
|
+
checkIsOwidContinent,
|
|
99
|
+
checkIsIncomeGroup,
|
|
100
|
+
checkHasMembers,
|
|
101
|
+
sortNumeric,
|
|
102
|
+
} from "../../utils/index.js"
|
|
103
|
+
import Cookies from "js-cookie"
|
|
104
|
+
import * as _ from "lodash-es"
|
|
105
|
+
import {
|
|
106
|
+
computed,
|
|
107
|
+
action,
|
|
108
|
+
makeObservable,
|
|
109
|
+
observable,
|
|
110
|
+
autorun,
|
|
111
|
+
runInAction,
|
|
112
|
+
} from "mobx"
|
|
113
|
+
import React from "react"
|
|
114
|
+
import * as R from "remeda"
|
|
115
|
+
import { match } from "ts-pattern"
|
|
116
|
+
import { AxisConfig } from "../axis/AxisConfig.js"
|
|
117
|
+
import {
|
|
118
|
+
GrapherRasterizeFn,
|
|
119
|
+
StaticChartRasterizer,
|
|
120
|
+
} from "../captionedChart/StaticChartRasterizer.js"
|
|
121
|
+
import { Chart } from "../chart/Chart.js"
|
|
122
|
+
import { ChartDimension } from "../chart/ChartDimension.js"
|
|
123
|
+
import { ChartState } from "../chart/ChartInterface.js"
|
|
124
|
+
import {
|
|
125
|
+
isChartTypeName,
|
|
126
|
+
isChartTab,
|
|
127
|
+
isMapTab,
|
|
128
|
+
findValidChartTypeCombination,
|
|
129
|
+
isValidTabConfigOption,
|
|
130
|
+
mapChartTypeNameToTabConfigOption,
|
|
131
|
+
mapTabConfigOptionToChartTypeName,
|
|
132
|
+
} from "../chart/ChartTabs.js"
|
|
133
|
+
import { makeChartState } from "../chart/ChartTypeMap.js"
|
|
134
|
+
import {
|
|
135
|
+
autoDetectSeriesStrategy,
|
|
136
|
+
autoDetectYColumnSlugs,
|
|
137
|
+
} from "../chart/ChartUtils.js"
|
|
138
|
+
import { DimensionSlot } from "../chart/DimensionSlot.js"
|
|
139
|
+
import {
|
|
140
|
+
GRAPHER_LIGHT_TEXT,
|
|
141
|
+
GRAPHER_BACKGROUND_BEIGE,
|
|
142
|
+
GRAPHER_BACKGROUND_DEFAULT,
|
|
143
|
+
} from "../color/ColorConstants.js"
|
|
144
|
+
import { ColorScaleConfig } from "../color/ColorScaleConfig.js"
|
|
145
|
+
import { isValidDataTableFilter } from "../dataTable/DataTable.js"
|
|
146
|
+
import { DataTableConfig } from "../dataTable/DataTableConstants.js"
|
|
147
|
+
import {
|
|
148
|
+
type EntitySelectorState,
|
|
149
|
+
EntitySelector,
|
|
150
|
+
} from "../entitySelector/EntitySelector.js"
|
|
151
|
+
import { FacetChart } from "../facet/FacetChart.js"
|
|
152
|
+
import { FocusArray } from "../focus/FocusArray.js"
|
|
153
|
+
import { GlobeController } from "../mapCharts/GlobeController.js"
|
|
154
|
+
import {
|
|
155
|
+
MAP_REGION_LABELS,
|
|
156
|
+
MAP_REGION_NAMES,
|
|
157
|
+
} from "../mapCharts/MapChartConstants.js"
|
|
158
|
+
import { MapConfig } from "../mapCharts/MapConfig.js"
|
|
159
|
+
import {
|
|
160
|
+
isValidMapRegionName,
|
|
161
|
+
isOnTheMap,
|
|
162
|
+
getCountriesByRegion,
|
|
163
|
+
} from "../mapCharts/MapHelpers.js"
|
|
164
|
+
import { DownloadModalTabName } from "../modal/DownloadModal.js"
|
|
165
|
+
import { SelectionArray } from "../selection/SelectionArray.js"
|
|
166
|
+
import { SlideShowController } from "../slideshowController/SlideShowController.js"
|
|
167
|
+
import {
|
|
168
|
+
TimelineDragTarget,
|
|
169
|
+
TimelineController,
|
|
170
|
+
} from "../timeline/TimelineController.js"
|
|
171
|
+
import { TooltipManager } from "../tooltip/TooltipProps.js"
|
|
172
|
+
import {
|
|
173
|
+
EntityRegionTypeGroup,
|
|
174
|
+
groupEntityNamesByRegionType,
|
|
175
|
+
EntityNamesByRegionType,
|
|
176
|
+
isEntityRegionType,
|
|
177
|
+
} from "./EntitiesByRegionType.js"
|
|
178
|
+
import {
|
|
179
|
+
getEntityNamesParam,
|
|
180
|
+
getSelectedEntityNamesParam,
|
|
181
|
+
getFocusedSeriesNamesParam,
|
|
182
|
+
} from "./EntityUrlBuilder.js"
|
|
183
|
+
import {
|
|
184
|
+
MinimalNarrativeChartInfo,
|
|
185
|
+
GrapherProgrammaticInterface,
|
|
186
|
+
GrapherManager,
|
|
187
|
+
DEFAULT_MS_PER_TICK,
|
|
188
|
+
} from "./Grapher.js"
|
|
189
|
+
import { GrapherAnalytics } from "./GrapherAnalytics.js"
|
|
190
|
+
import {
|
|
191
|
+
type GrapherAnalyticsContext,
|
|
192
|
+
type EntitySelectorEvent,
|
|
193
|
+
type GrapherImageDownloadEvent,
|
|
194
|
+
type GrapherInteractionEvent,
|
|
195
|
+
} from "../../types/index.js"
|
|
196
|
+
import {
|
|
197
|
+
latestGrapherConfigSchema,
|
|
198
|
+
DEFAULT_GRAPHER_ENTITY_TYPE,
|
|
199
|
+
DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL,
|
|
200
|
+
GRAPHER_FRAME_PADDING_HORIZONTAL,
|
|
201
|
+
GRAPHER_FRAME_PADDING_VERTICAL,
|
|
202
|
+
DEFAULT_GRAPHER_BOUNDS,
|
|
203
|
+
GrapherModal,
|
|
204
|
+
CookieKey,
|
|
205
|
+
GRAPHER_PROD_URL,
|
|
206
|
+
BASE_FONT_SIZE,
|
|
207
|
+
isContinentsVariableId,
|
|
208
|
+
isPopulationVariableETLPath,
|
|
209
|
+
DEFAULT_GRAPHER_WIDTH,
|
|
210
|
+
DEFAULT_GRAPHER_HEIGHT,
|
|
211
|
+
STATIC_EXPORT_DETAIL_SPACING,
|
|
212
|
+
DEFAULT_GRAPHER_BOUNDS_SQUARE,
|
|
213
|
+
CHART_TYPES_THAT_SHOW_ALL_ENTITIES,
|
|
214
|
+
} from "./GrapherConstants.js"
|
|
215
|
+
import { parseGlobeRotation, grapherObjectToQueryParams } from "./GrapherUrl.js"
|
|
216
|
+
import { legacyToCurrentGrapherQueryParams } from "./GrapherUrlMigrations.js"
|
|
217
|
+
import { getErrorMessageRelatedQuestionUrl } from "./relatedQuestion.js"
|
|
218
|
+
|
|
219
|
+
export class GrapherState {
|
|
220
|
+
$schema = latestGrapherConfigSchema
|
|
221
|
+
chartTypes: GrapherChartType[] = [
|
|
222
|
+
GRAPHER_CHART_TYPES.LineChart,
|
|
223
|
+
GRAPHER_CHART_TYPES.DiscreteBar,
|
|
224
|
+
]
|
|
225
|
+
id: number | undefined = undefined
|
|
226
|
+
version = 1
|
|
227
|
+
slug: string | undefined = undefined
|
|
228
|
+
|
|
229
|
+
// Initializing text fields with `undefined` ensures that empty strings get serialised
|
|
230
|
+
title: string | undefined = undefined
|
|
231
|
+
subtitle: string | undefined = undefined
|
|
232
|
+
sourceDesc: string | undefined = undefined
|
|
233
|
+
note: string | undefined = undefined
|
|
234
|
+
// Missing from GrapherInterface: details
|
|
235
|
+
internalNotes: string | undefined = undefined
|
|
236
|
+
variantName: string | undefined = undefined
|
|
237
|
+
originUrl: string | undefined = undefined
|
|
238
|
+
hideAnnotationFieldsInTitle: AnnotationFieldsInTitle | undefined = undefined
|
|
239
|
+
|
|
240
|
+
minTime: TimeBound | undefined = undefined
|
|
241
|
+
maxTime: TimeBound | undefined = undefined
|
|
242
|
+
timelineMinTime: Time | undefined = undefined
|
|
243
|
+
timelineMaxTime: Time | undefined = undefined
|
|
244
|
+
addCountryMode = EntitySelectionMode.MultipleEntities
|
|
245
|
+
stackMode = StackMode.absolute
|
|
246
|
+
showNoDataArea = true
|
|
247
|
+
hideLegend: boolean | undefined = false
|
|
248
|
+
logo: LogoOption | undefined = undefined
|
|
249
|
+
hideLogo: boolean | undefined = undefined
|
|
250
|
+
hideRelativeToggle: boolean | undefined = true
|
|
251
|
+
entityType = DEFAULT_GRAPHER_ENTITY_TYPE
|
|
252
|
+
entityTypePlural = DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL
|
|
253
|
+
facettingLabelByYVariables = "metric"
|
|
254
|
+
hideTimeline: boolean | undefined = undefined
|
|
255
|
+
zoomToSelection: boolean | undefined = undefined
|
|
256
|
+
showYearLabels: boolean | undefined = undefined // Always show year in labels for bar charts
|
|
257
|
+
hasMapTab = false
|
|
258
|
+
tab: GrapherTabConfigOption = GRAPHER_TAB_CONFIG_OPTIONS.chart
|
|
259
|
+
isPublished: boolean | undefined = undefined
|
|
260
|
+
baseColorScheme: ColorSchemeName | undefined = undefined
|
|
261
|
+
invertColorScheme: boolean | undefined = undefined
|
|
262
|
+
hideConnectedScatterLines: boolean | undefined = undefined // Hides lines between points when timeline spans multiple years. Requested by core-econ for certain charts
|
|
263
|
+
hideScatterLabels: boolean | undefined = undefined
|
|
264
|
+
scatterPointLabelStrategy: ScatterPointLabelStrategy | undefined = undefined
|
|
265
|
+
compareEndPointsOnly: boolean | undefined = undefined
|
|
266
|
+
matchingEntitiesOnly: boolean | undefined = undefined
|
|
267
|
+
/** Hides the total value label that is normally displayed for stacked bar charts */
|
|
268
|
+
hideTotalValueLabel: boolean | undefined = undefined
|
|
269
|
+
|
|
270
|
+
missingDataStrategy: MissingDataStrategy | undefined = undefined
|
|
271
|
+
|
|
272
|
+
xAxis = new AxisConfig(undefined, this)
|
|
273
|
+
yAxis = new AxisConfig(undefined, this)
|
|
274
|
+
colorScale = new ColorScaleConfig()
|
|
275
|
+
map = new MapConfig()
|
|
276
|
+
dimensions: ChartDimension[] = []
|
|
277
|
+
ySlugs: ColumnSlugs | undefined = undefined
|
|
278
|
+
xSlug: ColumnSlug | undefined = undefined
|
|
279
|
+
colorSlug: ColumnSlug | undefined = undefined
|
|
280
|
+
sizeSlug: ColumnSlug | undefined = undefined
|
|
281
|
+
tableSlugs: ColumnSlugs | undefined = undefined
|
|
282
|
+
selectedEntityColors: {
|
|
283
|
+
[entityName: string]: string | undefined
|
|
284
|
+
} = {}
|
|
285
|
+
selectedEntityNames: EntityName[] = []
|
|
286
|
+
focusedSeriesNames: SeriesName[] = []
|
|
287
|
+
excludedEntityNames: EntityName[] | undefined = undefined
|
|
288
|
+
includedEntityNames: EntityName[] | undefined = undefined
|
|
289
|
+
comparisonLines: ComparisonLineConfig[] | undefined = undefined // todo: Persistables?
|
|
290
|
+
relatedQuestions: RelatedQuestionsConfig[] | undefined = undefined // todo: Persistables?
|
|
291
|
+
|
|
292
|
+
dataTableConfig: DataTableConfig = {
|
|
293
|
+
filter: "all",
|
|
294
|
+
search: "",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Used to highlight particular times in a line chart.
|
|
299
|
+
* The sparkline in map tooltips makes use of this.
|
|
300
|
+
*/
|
|
301
|
+
highlightedTimesInLineChart?: Time[]
|
|
302
|
+
|
|
303
|
+
hideFacetControl = true
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Indicates whether the chart is embedded alongside a complementary table.
|
|
307
|
+
* If that's the case, the chart can be simplified (e.g. hide legends or
|
|
308
|
+
* annotations) since the table serves as an additional source of information.
|
|
309
|
+
*/
|
|
310
|
+
isDisplayedAlongsideComplementaryTable = false
|
|
311
|
+
|
|
312
|
+
// the desired faceting strategy, which might not be possible if we change the data
|
|
313
|
+
selectedFacetStrategy: FacetStrategy | undefined = undefined
|
|
314
|
+
sortBy: SortBy | undefined = SortBy.total
|
|
315
|
+
sortOrder: SortOrder | undefined = SortOrder.desc
|
|
316
|
+
sortColumnSlug: string | undefined = undefined
|
|
317
|
+
_isInFullScreenMode = false
|
|
318
|
+
windowInnerWidth: number | undefined = undefined
|
|
319
|
+
windowInnerHeight: number | undefined = undefined
|
|
320
|
+
manuallyProvideData? = false // This will be removed.
|
|
321
|
+
|
|
322
|
+
// This will be removed.
|
|
323
|
+
@computed get isDev(): boolean {
|
|
324
|
+
return this.initialOptions.env === "dev"
|
|
325
|
+
}
|
|
326
|
+
isEditor =
|
|
327
|
+
typeof window !== "undefined" && (window as any).isEditor === true
|
|
328
|
+
bakedGrapherURL: string | undefined = undefined
|
|
329
|
+
adminBaseUrl: string | undefined = undefined
|
|
330
|
+
externalQueryParams: QueryParams = {}
|
|
331
|
+
private framePaddingHorizontal = GRAPHER_FRAME_PADDING_HORIZONTAL
|
|
332
|
+
private framePaddingVertical = GRAPHER_FRAME_PADDING_VERTICAL
|
|
333
|
+
_inputTable: OwidTable = new OwidTable()
|
|
334
|
+
|
|
335
|
+
// TODO Daniel: probably obsolete?
|
|
336
|
+
// @observable.ref interpolatedSortColumnsBySlug:
|
|
337
|
+
// | CoreColumnBySlug
|
|
338
|
+
// | undefined = {}
|
|
339
|
+
get inputTable(): OwidTable {
|
|
340
|
+
return this._inputTable
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@action set inputTable(table: OwidTable) {
|
|
344
|
+
this._inputTable = table
|
|
345
|
+
|
|
346
|
+
if (this.manager?.selection?.hasSelection) {
|
|
347
|
+
// Selection is managed externally, do nothing.
|
|
348
|
+
} else if (this.areSelectedEntitiesDifferentThanAuthors) {
|
|
349
|
+
// User has changed the selection, use theirs
|
|
350
|
+
} else this.applyOriginalSelectionAsAuthored()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
legacyConfigAsAuthored: Partial<LegacyGrapherInterface> = {}
|
|
354
|
+
entitySelectorState: Partial<EntitySelectorState> = {}
|
|
355
|
+
@computed get dataTableSlugs(): ColumnSlug[] {
|
|
356
|
+
return this.tableSlugs ? this.tableSlugs.split(" ") : this.newSlugs
|
|
357
|
+
}
|
|
358
|
+
isEmbeddedInAnOwidPage?: boolean = false
|
|
359
|
+
isEmbeddedInADataPage?: boolean = false
|
|
360
|
+
|
|
361
|
+
// These are explicitly set to `false` if FetchingGrapher or some other
|
|
362
|
+
// external code is fetching the config and data
|
|
363
|
+
isConfigReady: boolean | undefined = true
|
|
364
|
+
isDataReady: boolean | undefined = true
|
|
365
|
+
|
|
366
|
+
/** Whether external grapher controls can be hidden in embeds. */
|
|
367
|
+
canHideExternalControlsInEmbed: boolean = false
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Value of the query parameter in the embed URL that hides external grapher
|
|
371
|
+
* controls.
|
|
372
|
+
*/
|
|
373
|
+
hideExternalControlsInEmbedUrl: boolean =
|
|
374
|
+
this.canHideExternalControlsInEmbed
|
|
375
|
+
|
|
376
|
+
narrativeChartInfo?: MinimalNarrativeChartInfo = undefined
|
|
377
|
+
archiveContext?: ArchiveContext
|
|
378
|
+
|
|
379
|
+
selection: SelectionArray = new SelectionArray()
|
|
380
|
+
focusArray = new FocusArray()
|
|
381
|
+
analytics: GrapherAnalytics
|
|
382
|
+
|
|
383
|
+
_additionalDataLoaderFn: AdditionalGrapherDataFetchFn | undefined =
|
|
384
|
+
undefined
|
|
385
|
+
/**
|
|
386
|
+
* todo: factor this out and make more RAII.
|
|
387
|
+
*
|
|
388
|
+
* Explorers create 1 Grapher instance, but as the user clicks around the Explorer loads other author created Graphers.
|
|
389
|
+
* But currently some Grapher features depend on knowing how the current state is different than the "authored state".
|
|
390
|
+
* So when an Explorer updates the grapher, it also needs to update this "original state".
|
|
391
|
+
*/
|
|
392
|
+
@action.bound setAuthoredVersion(
|
|
393
|
+
config: Partial<LegacyGrapherInterface>
|
|
394
|
+
): void {
|
|
395
|
+
this.legacyConfigAsAuthored = config
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@action.bound updateAuthoredVersion(
|
|
399
|
+
config: Partial<LegacyGrapherInterface>
|
|
400
|
+
): void {
|
|
401
|
+
this.legacyConfigAsAuthored = {
|
|
402
|
+
...this.legacyConfigAsAuthored,
|
|
403
|
+
...config,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
constructor(options: GrapherProgrammaticInterface) {
|
|
407
|
+
makeObservable(this, {
|
|
408
|
+
$schema: observable.ref,
|
|
409
|
+
chartTypes: observable.ref,
|
|
410
|
+
id: observable.ref,
|
|
411
|
+
version: observable.ref,
|
|
412
|
+
slug: observable.ref,
|
|
413
|
+
title: observable.ref,
|
|
414
|
+
subtitle: observable.ref,
|
|
415
|
+
sourceDesc: observable.ref,
|
|
416
|
+
note: observable.ref,
|
|
417
|
+
internalNotes: observable.ref,
|
|
418
|
+
variantName: observable.ref,
|
|
419
|
+
originUrl: observable.ref,
|
|
420
|
+
hideAnnotationFieldsInTitle: observable,
|
|
421
|
+
minTime: observable.ref,
|
|
422
|
+
maxTime: observable.ref,
|
|
423
|
+
timelineMinTime: observable.ref,
|
|
424
|
+
timelineMaxTime: observable.ref,
|
|
425
|
+
addCountryMode: observable.ref,
|
|
426
|
+
stackMode: observable.ref,
|
|
427
|
+
showNoDataArea: observable.ref,
|
|
428
|
+
hideLegend: observable.ref,
|
|
429
|
+
logo: observable.ref,
|
|
430
|
+
hideLogo: observable.ref,
|
|
431
|
+
hideRelativeToggle: observable.ref,
|
|
432
|
+
entityType: observable.ref,
|
|
433
|
+
entityTypePlural: observable.ref,
|
|
434
|
+
facettingLabelByYVariables: observable.ref,
|
|
435
|
+
hideTimeline: observable.ref,
|
|
436
|
+
zoomToSelection: observable.ref,
|
|
437
|
+
showYearLabels: observable.ref,
|
|
438
|
+
hasMapTab: observable.ref,
|
|
439
|
+
tab: observable.ref,
|
|
440
|
+
isPublished: observable.ref,
|
|
441
|
+
baseColorScheme: observable.ref,
|
|
442
|
+
invertColorScheme: observable.ref,
|
|
443
|
+
hideConnectedScatterLines: observable,
|
|
444
|
+
hideScatterLabels: observable.ref,
|
|
445
|
+
scatterPointLabelStrategy: observable,
|
|
446
|
+
compareEndPointsOnly: observable.ref,
|
|
447
|
+
matchingEntitiesOnly: observable.ref,
|
|
448
|
+
hideTotalValueLabel: observable.ref,
|
|
449
|
+
missingDataStrategy: observable.ref,
|
|
450
|
+
xAxis: observable.ref,
|
|
451
|
+
yAxis: observable.ref,
|
|
452
|
+
colorScale: observable,
|
|
453
|
+
map: observable,
|
|
454
|
+
dimensions: observable.ref,
|
|
455
|
+
ySlugs: observable,
|
|
456
|
+
xSlug: observable,
|
|
457
|
+
colorSlug: observable,
|
|
458
|
+
sizeSlug: observable,
|
|
459
|
+
tableSlugs: observable,
|
|
460
|
+
selectedEntityColors: observable,
|
|
461
|
+
selectedEntityNames: observable,
|
|
462
|
+
focusedSeriesNames: observable,
|
|
463
|
+
excludedEntityNames: observable,
|
|
464
|
+
includedEntityNames: observable,
|
|
465
|
+
comparisonLines: observable,
|
|
466
|
+
relatedQuestions: observable,
|
|
467
|
+
dataTableConfig: observable,
|
|
468
|
+
highlightedTimesInLineChart: observable.ref,
|
|
469
|
+
hideFacetControl: observable.ref,
|
|
470
|
+
selectedFacetStrategy: observable,
|
|
471
|
+
sortBy: observable,
|
|
472
|
+
sortOrder: observable,
|
|
473
|
+
sortColumnSlug: observable,
|
|
474
|
+
_isInFullScreenMode: observable.ref,
|
|
475
|
+
windowInnerWidth: observable.ref,
|
|
476
|
+
windowInnerHeight: observable.ref,
|
|
477
|
+
bakedGrapherURL: observable,
|
|
478
|
+
externalQueryParams: observable.ref,
|
|
479
|
+
_inputTable: observable.ref,
|
|
480
|
+
legacyConfigAsAuthored: observable.ref,
|
|
481
|
+
entitySelectorState: observable,
|
|
482
|
+
isConfigReady: observable,
|
|
483
|
+
isDataReady: observable,
|
|
484
|
+
canHideExternalControlsInEmbed: observable.ref,
|
|
485
|
+
hideExternalControlsInEmbedUrl: observable.ref,
|
|
486
|
+
isExportingToSvgOrPng: observable.ref,
|
|
487
|
+
isSocialMediaExport: observable.ref,
|
|
488
|
+
isWikimediaExport: observable.ref,
|
|
489
|
+
variant: observable.ref,
|
|
490
|
+
staticBounds: observable,
|
|
491
|
+
isPlaying: observable.ref,
|
|
492
|
+
isTimelineAnimationActive: observable.ref,
|
|
493
|
+
animationStartTime: observable.ref,
|
|
494
|
+
areHandlesOnSameTimeBeforeAnimation: observable.ref,
|
|
495
|
+
timelineDragTarget: observable.ref,
|
|
496
|
+
isEntitySelectorModalOrDrawerOpen: observable.ref,
|
|
497
|
+
activeModal: observable.ref,
|
|
498
|
+
activeDownloadModalTab: observable.ref,
|
|
499
|
+
shouldIncludeDetailsInStaticExport: observable,
|
|
500
|
+
_externalBounds: observable,
|
|
501
|
+
slideShow: observable,
|
|
502
|
+
_baseFontSize: observable,
|
|
503
|
+
isShareMenuActive: observable,
|
|
504
|
+
hideTitle: observable,
|
|
505
|
+
hideSubtitle: observable,
|
|
506
|
+
hideNote: observable,
|
|
507
|
+
hideOriginUrl: observable,
|
|
508
|
+
hideEntityControls: observable,
|
|
509
|
+
enableMapSelection: observable,
|
|
510
|
+
forceHideAnnotationFieldsInTitle: observable,
|
|
511
|
+
hasTableTab: observable,
|
|
512
|
+
hideShareButton: observable,
|
|
513
|
+
hideExploreTheDataButton: observable,
|
|
514
|
+
isDisplayedAlongsideComplementaryTable: observable,
|
|
515
|
+
})
|
|
516
|
+
// prefer the manager's selection over the config's selectedEntityNames
|
|
517
|
+
// if both are passed in and the manager's selection is not empty.
|
|
518
|
+
// this is necessary for the global entity selector to work correctly.
|
|
519
|
+
if (options.manager?.selection?.hasSelection) {
|
|
520
|
+
this.updateFromObject(_.omit(options, "selectedEntityNames"))
|
|
521
|
+
} else {
|
|
522
|
+
this.updateFromObject(options)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this._additionalDataLoaderFn = options.additionalDataLoaderFn
|
|
526
|
+
this.isEmbeddedInAnOwidPage = options.isEmbeddedInAnOwidPage ?? false
|
|
527
|
+
this.isEmbeddedInADataPage = options.isEmbeddedInADataPage ?? false
|
|
528
|
+
|
|
529
|
+
this._inputTable =
|
|
530
|
+
options.table ?? BlankOwidTable(`initialGrapherTable`)
|
|
531
|
+
this.initialOptions = options
|
|
532
|
+
this.analytics = new GrapherAnalytics(this.initialOptions.env ?? "")
|
|
533
|
+
this.selection =
|
|
534
|
+
this.manager?.selection ??
|
|
535
|
+
new SelectionArray(this.initialOptions.selectedEntityNames ?? [])
|
|
536
|
+
this.setAuthoredVersion(options)
|
|
537
|
+
this.canHideExternalControlsInEmbed =
|
|
538
|
+
options.canHideExternalControlsInEmbed ?? false
|
|
539
|
+
|
|
540
|
+
// make sure the static bounds are set
|
|
541
|
+
this.staticBounds = options.staticBounds ?? DEFAULT_GRAPHER_BOUNDS
|
|
542
|
+
|
|
543
|
+
this.narrativeChartInfo = options.narrativeChartInfo
|
|
544
|
+
this.archiveContext = options.archiveContext
|
|
545
|
+
|
|
546
|
+
this.populateFromQueryParams(
|
|
547
|
+
legacyToCurrentGrapherQueryParams(
|
|
548
|
+
this.initialOptions.queryStr ?? ""
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
if (this.isEditor) {
|
|
552
|
+
this.ensureValidConfigWhenEditing()
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
toObject(deleteUnchanged: boolean = true): GrapherInterface {
|
|
557
|
+
const obj: GrapherInterface = objectWithPersistablesToObject(
|
|
558
|
+
this,
|
|
559
|
+
grapherKeysToSerialize
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
obj.selectedEntityNames = this.selection.selectedEntityNames
|
|
563
|
+
obj.focusedSeriesNames = this.focusArray.seriesNames
|
|
564
|
+
|
|
565
|
+
if (deleteUnchanged) {
|
|
566
|
+
deleteRuntimeAndUnchangedProps(obj, defaultObject)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// always include the schema, even if it's the default
|
|
570
|
+
obj.$schema = this.$schema || latestGrapherConfigSchema
|
|
571
|
+
|
|
572
|
+
// JSON doesn't support Infinity, so we use strings instead.
|
|
573
|
+
if (obj.minTime) obj.minTime = minTimeToJSON(this.minTime) as any
|
|
574
|
+
if (obj.maxTime) obj.maxTime = maxTimeToJSON(this.maxTime) as any
|
|
575
|
+
|
|
576
|
+
if (obj.timelineMinTime)
|
|
577
|
+
obj.timelineMinTime = minTimeToJSON(this.timelineMinTime) as any
|
|
578
|
+
if (obj.timelineMaxTime)
|
|
579
|
+
obj.timelineMaxTime = maxTimeToJSON(this.timelineMaxTime) as any
|
|
580
|
+
|
|
581
|
+
// don't serialise tab if the default chart is currently shown
|
|
582
|
+
if (
|
|
583
|
+
this.activeChartType &&
|
|
584
|
+
this.activeChartType === this.defaultChartType
|
|
585
|
+
) {
|
|
586
|
+
delete obj.tab
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// todo: remove dimensions concept
|
|
590
|
+
// if (this.legacyConfigAsAuthored?.dimensions)
|
|
591
|
+
// obj.dimensions = this.legacyConfigAsAuthored.dimensions
|
|
592
|
+
return obj
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
@action.bound updateFromObject(obj?: GrapherProgrammaticInterface): void {
|
|
596
|
+
if (!obj) return
|
|
597
|
+
|
|
598
|
+
updatePersistables(this, obj)
|
|
599
|
+
|
|
600
|
+
this.bindUrlToWindow = obj.bindUrlToWindow ?? false
|
|
601
|
+
|
|
602
|
+
// Regression fix: some legacies have this set to Null. Todo: clean DB.
|
|
603
|
+
if (obj.originUrl === null) this.originUrl = ""
|
|
604
|
+
|
|
605
|
+
// update selection
|
|
606
|
+
if (obj.selectedEntityNames)
|
|
607
|
+
this.selection.setSelectedEntities(obj.selectedEntityNames)
|
|
608
|
+
|
|
609
|
+
// update focus
|
|
610
|
+
if (obj.focusedSeriesNames)
|
|
611
|
+
this.focusArray.clearAllAndAdd(...obj.focusedSeriesNames)
|
|
612
|
+
|
|
613
|
+
// JSON doesn't support Infinity, so we use strings instead.
|
|
614
|
+
this.minTime = minTimeBoundFromJSONOrNegativeInfinity(obj.minTime)
|
|
615
|
+
this.maxTime = maxTimeBoundFromJSONOrPositiveInfinity(obj.maxTime)
|
|
616
|
+
|
|
617
|
+
this.timelineMinTime = minTimeBoundFromJSONOrNegativeInfinity(
|
|
618
|
+
obj.timelineMinTime
|
|
619
|
+
)
|
|
620
|
+
this.timelineMaxTime = maxTimeBoundFromJSONOrPositiveInfinity(
|
|
621
|
+
obj.timelineMaxTime
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
// Todo: remove once we are more RAII.
|
|
625
|
+
if (obj?.dimensions?.length)
|
|
626
|
+
this.setDimensionsFromConfigs(obj.dimensions)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
@action.bound populateFromQueryParams(params: GrapherQueryParams): void {
|
|
630
|
+
this.externalQueryParams = _.omit(params, GRAPHER_QUERY_PARAM_KEYS)
|
|
631
|
+
|
|
632
|
+
// Set tab if specified
|
|
633
|
+
const parsedTab = params.tab
|
|
634
|
+
? this.mapQueryParamToTabName(params.tab)
|
|
635
|
+
: undefined
|
|
636
|
+
if (parsedTab) this.setTab(parsedTab)
|
|
637
|
+
else if (params.tab !== undefined)
|
|
638
|
+
console.error("Unexpected tab: " + params.tab)
|
|
639
|
+
|
|
640
|
+
// Set overlay if specified
|
|
641
|
+
const overlay = params.overlay
|
|
642
|
+
if (overlay) {
|
|
643
|
+
if (overlay === "sources") {
|
|
644
|
+
this.activeModal = GrapherModal.Sources
|
|
645
|
+
} else if (overlay === "download-data") {
|
|
646
|
+
this.activeModal = GrapherModal.Download
|
|
647
|
+
this.activeDownloadModalTab = DownloadModalTabName.Data
|
|
648
|
+
} else if (overlay === "download-vis") {
|
|
649
|
+
this.activeModal = GrapherModal.Download
|
|
650
|
+
this.activeDownloadModalTab = DownloadModalTabName.Vis
|
|
651
|
+
} else if (overlay === "download") {
|
|
652
|
+
this.activeModal = GrapherModal.Download
|
|
653
|
+
} else if (overlay === "embed") {
|
|
654
|
+
// We could include the embed modal in the `overlay=` params, but there has been an issue in the past
|
|
655
|
+
// where we accidentally included that in the Embed dialog's URL, and then embeds would always show
|
|
656
|
+
// the modal.
|
|
657
|
+
// So, if it is specified in the query params, we just ignore it.
|
|
658
|
+
// Linking directly to the modal doesn't have much of a use case, anyway.
|
|
659
|
+
} else {
|
|
660
|
+
console.error("Unexpected overlay: " + overlay)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Stack mode for bar and stacked area charts
|
|
665
|
+
this.stackMode = (params.stackMode ?? this.stackMode) as StackMode
|
|
666
|
+
|
|
667
|
+
this.zoomToSelection =
|
|
668
|
+
params.zoomToSelection === "true" ? true : this.zoomToSelection
|
|
669
|
+
|
|
670
|
+
// Axis scale mode
|
|
671
|
+
const xScaleType = params.xScale
|
|
672
|
+
if (xScaleType) {
|
|
673
|
+
if (xScaleType === ScaleType.linear || xScaleType === ScaleType.log)
|
|
674
|
+
this.xAxis.scaleType = xScaleType
|
|
675
|
+
else console.error("Unexpected xScale: " + xScaleType)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const yScaleType = params.yScale
|
|
679
|
+
if (yScaleType) {
|
|
680
|
+
if (yScaleType === ScaleType.linear || yScaleType === ScaleType.log)
|
|
681
|
+
this.yAxis.scaleType = yScaleType
|
|
682
|
+
else console.error("Unexpected xScale: " + yScaleType)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const time = params.time
|
|
686
|
+
if (time !== undefined && time !== "")
|
|
687
|
+
this.setTimeFromTimeQueryParam(time)
|
|
688
|
+
|
|
689
|
+
const endpointsOnly = params.endpointsOnly
|
|
690
|
+
if (endpointsOnly !== undefined)
|
|
691
|
+
this.compareEndPointsOnly = endpointsOnly === "1" ? true : undefined
|
|
692
|
+
|
|
693
|
+
// globe
|
|
694
|
+
const globe = params.globe
|
|
695
|
+
if (globe !== undefined) {
|
|
696
|
+
this.mapConfig.globe.isActive = globe === "1"
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// globe rotation
|
|
700
|
+
const globeRotation = params.globeRotation
|
|
701
|
+
if (globeRotation !== undefined) {
|
|
702
|
+
this.mapConfig.globe.rotation = parseGlobeRotation(globeRotation)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// globe zoom
|
|
706
|
+
const globeZoom = params.globeZoom
|
|
707
|
+
if (globeZoom !== undefined) {
|
|
708
|
+
const parsedZoom = parseFloatOrUndefined(globeZoom)
|
|
709
|
+
if (parsedZoom !== undefined) this.mapConfig.globe.zoom = parsedZoom
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// region
|
|
713
|
+
const region = params.region
|
|
714
|
+
if (region !== undefined && isValidMapRegionName(region)) {
|
|
715
|
+
this.map.region = region
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// map selection
|
|
719
|
+
const mapSelection = getEntityNamesParam(params.mapSelect)
|
|
720
|
+
if (mapSelection) {
|
|
721
|
+
this.mapConfig.selection.setSelectedEntities(mapSelection)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// selection
|
|
725
|
+
const url = Url.fromQueryParams(params)
|
|
726
|
+
const selection = getSelectedEntityNamesParam(url)
|
|
727
|
+
if (this.addCountryMode !== EntitySelectionMode.Disabled && selection)
|
|
728
|
+
this.selection.setSelectedEntities(selection)
|
|
729
|
+
|
|
730
|
+
// focus
|
|
731
|
+
const focusedSeriesNames = getFocusedSeriesNamesParam(params.focus)
|
|
732
|
+
if (focusedSeriesNames) {
|
|
733
|
+
this.focusArray.clearAllAndAdd(...focusedSeriesNames)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// faceting
|
|
737
|
+
if (params.facet && params.facet in FacetStrategy) {
|
|
738
|
+
this.selectedFacetStrategy = params.facet as FacetStrategy
|
|
739
|
+
}
|
|
740
|
+
if (params.uniformYAxis === "0") {
|
|
741
|
+
this.yAxis.facetDomain = FacetAxisDomain.independent
|
|
742
|
+
} else if (params.uniformYAxis === "1") {
|
|
743
|
+
this.yAxis.facetDomain = FacetAxisDomain.shared
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// no data area in marimekko charts
|
|
747
|
+
if (params.showNoDataArea) {
|
|
748
|
+
this.showNoDataArea = params.showNoDataArea === "1"
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// deprecated; support for legacy URLs
|
|
752
|
+
if (params.showSelectionOnlyInTable) {
|
|
753
|
+
this.dataTableConfig.filter =
|
|
754
|
+
params.showSelectionOnlyInTable === "1" ? "selection" : "all"
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// data table filter
|
|
758
|
+
if (params.tableFilter) {
|
|
759
|
+
this.dataTableConfig.filter = isValidDataTableFilter(
|
|
760
|
+
params.tableFilter
|
|
761
|
+
)
|
|
762
|
+
? params.tableFilter
|
|
763
|
+
: "all"
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// data table search
|
|
767
|
+
if (params.tableSearch) {
|
|
768
|
+
this.dataTableConfig.search = params.tableSearch
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
@action.bound setTimeFromTimeQueryParam(time: string): void {
|
|
773
|
+
this.timelineHandleTimeBounds = getTimeDomainFromQueryString(time).map(
|
|
774
|
+
(time) => findClosestTime(this.times, time) ?? time
|
|
775
|
+
) as TimeBounds
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
@computed get activeTab(): GrapherTabName {
|
|
779
|
+
return this.mapTabConfigOptionToTabName(this.tab)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
@computed get activeChartType(): GrapherChartType | undefined {
|
|
783
|
+
return isChartTypeName(this.activeTab) ? this.activeTab : undefined
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
@computed private get defaultChartType(): GrapherChartType {
|
|
787
|
+
return this.chartType ?? GRAPHER_CHART_TYPES.LineChart
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
@computed private get defaultTab(): GrapherTabName {
|
|
791
|
+
if (this.chartType) return this.chartType
|
|
792
|
+
if (this.hasMapTab) return GRAPHER_TAB_NAMES.WorldMap
|
|
793
|
+
return GRAPHER_TAB_NAMES.Table
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
@computed get chartType(): GrapherChartType | undefined {
|
|
797
|
+
return this.validChartTypes[0]
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
@computed get hasChartTab(): boolean {
|
|
801
|
+
return this.validChartTypes.length > 0
|
|
802
|
+
}
|
|
803
|
+
@computed get isOnChartTab(): boolean {
|
|
804
|
+
return !this.isOnMapTab && !this.isOnTableTab
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
@computed get isOnMapTab(): boolean {
|
|
808
|
+
return this.activeTab === GRAPHER_TAB_NAMES.WorldMap
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
@computed get isOnTableTab(): boolean {
|
|
812
|
+
return this.activeTab === GRAPHER_TAB_NAMES.Table
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
@computed get isOnChartOrMapTab(): boolean {
|
|
816
|
+
return this.isOnChartTab || this.isOnMapTab
|
|
817
|
+
}
|
|
818
|
+
@computed get yAxisConfig(): Readonly<AxisConfigInterface> {
|
|
819
|
+
return this.yAxis.toObject()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
@computed get xAxisConfig(): Readonly<AxisConfigInterface> {
|
|
823
|
+
return this.xAxis.toObject()
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
@computed get showLegend(): boolean {
|
|
827
|
+
// hide the legend for stacked bar charts
|
|
828
|
+
// if the legend only ever shows a single entity
|
|
829
|
+
if (this.isOnStackedBarTab) {
|
|
830
|
+
const seriesStrategy =
|
|
831
|
+
this.chartState.seriesStrategy ||
|
|
832
|
+
autoDetectSeriesStrategy(this, true)
|
|
833
|
+
const isEntityStrategy = seriesStrategy === SeriesStrategy.entity
|
|
834
|
+
const hasSingleEntity = this.selection.numSelectedEntities === 1
|
|
835
|
+
const hideLegend =
|
|
836
|
+
this.hideLegend || (isEntityStrategy && hasSingleEntity)
|
|
837
|
+
return !hideLegend
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return !this.hideLegend
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private isChartTypeThatShowsAllEntities(
|
|
844
|
+
chartType: GrapherChartType
|
|
845
|
+
): boolean {
|
|
846
|
+
return CHART_TYPES_THAT_SHOW_ALL_ENTITIES.includes(chartType)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
@computed private get hasChartThatShowsAllEntities(): boolean {
|
|
850
|
+
return this.validChartTypes.some((chartType) =>
|
|
851
|
+
this.isChartTypeThatShowsAllEntities(chartType)
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
@computed get isOnArchivalPage(): boolean {
|
|
856
|
+
return this.archiveContext?.type === "archive-page"
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
@computed get hasArchivedPage(): boolean {
|
|
860
|
+
return this.archiveContext?.type === "archived-page-version"
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
@computed private get runtimeAssetMap(): AssetMap | undefined {
|
|
864
|
+
return this.archiveContext?.type === "archive-page"
|
|
865
|
+
? this.archiveContext.assets.runtime
|
|
866
|
+
: undefined
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
@computed get additionalDataLoaderFn():
|
|
870
|
+
| AdditionalGrapherDataFetchFn
|
|
871
|
+
| undefined {
|
|
872
|
+
if (this.isOnArchivalPage) return undefined
|
|
873
|
+
return this._additionalDataLoaderFn
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* We only show the selected entities in the data table if entity selection
|
|
878
|
+
* is disabled – unless there is a view that displays all data points, like
|
|
879
|
+
* a map or a scatter plot.
|
|
880
|
+
*/
|
|
881
|
+
@computed get shouldShowSelectionOnlyInDataTable(): boolean {
|
|
882
|
+
return (
|
|
883
|
+
this.selection.hasSelection &&
|
|
884
|
+
!this.canChangeAddOrHighlightEntities &&
|
|
885
|
+
this.hasChartTab &&
|
|
886
|
+
!this.hasChartThatShowsAllEntities &&
|
|
887
|
+
!this.hasMapTab
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Selection used in Grapher's data table.
|
|
893
|
+
*
|
|
894
|
+
* If a map selection is set, it is preferred over the chart selection.
|
|
895
|
+
*/
|
|
896
|
+
@computed get dataTableSelection(): SelectionArray {
|
|
897
|
+
return this.mapConfig.selection.hasSelection
|
|
898
|
+
? this.mapConfig.selection
|
|
899
|
+
: this.selection
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// table that is used for display in the table tab
|
|
903
|
+
@computed get tableForDisplay(): OwidTable {
|
|
904
|
+
let table = this.table
|
|
905
|
+
|
|
906
|
+
if (!this.isReady || !this.isOnTableTab) return table
|
|
907
|
+
|
|
908
|
+
if (this.chartState.transformTableForDisplay) {
|
|
909
|
+
table = this.chartState.transformTableForDisplay(table)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (this.shouldShowSelectionOnlyInDataTable) {
|
|
913
|
+
table = table.filterByEntityNames(
|
|
914
|
+
this.selection.selectedEntityNames
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return table
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@computed get filteredTableForDisplay(): OwidTable {
|
|
922
|
+
let table = this.tableForDisplay
|
|
923
|
+
const { filter } = this.dataTableConfig
|
|
924
|
+
|
|
925
|
+
const availableEntities = table.availableEntityNames
|
|
926
|
+
|
|
927
|
+
// Determine which entities should be visible based on the filter
|
|
928
|
+
const visibleEntities = match(filter)
|
|
929
|
+
.with("all", () => availableEntities)
|
|
930
|
+
.with("selection", () =>
|
|
931
|
+
this.selection.hasSelection
|
|
932
|
+
? this.selection.selectedEntityNames
|
|
933
|
+
: availableEntities
|
|
934
|
+
)
|
|
935
|
+
.when(isEntityRegionType, (filter) => {
|
|
936
|
+
const regionNames = this.entityNamesByRegionType.get(filter)
|
|
937
|
+
return regionNames ?? availableEntities
|
|
938
|
+
})
|
|
939
|
+
.exhaustive()
|
|
940
|
+
|
|
941
|
+
// Apply entity filter if necessary
|
|
942
|
+
if (visibleEntities.length < availableEntities.length)
|
|
943
|
+
table = table.filterByEntityNames(visibleEntities)
|
|
944
|
+
|
|
945
|
+
return table
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
@computed get tableForSelection(): OwidTable {
|
|
949
|
+
// This table specifies which entities can be selected in the charts EntitySelectorModal.
|
|
950
|
+
// It should contain all entities that can be selected, and none more.
|
|
951
|
+
// Depending on the chart type, the criteria for being able to select an entity are
|
|
952
|
+
// different; e.g. for scatterplots, the entity needs to (1) not be excluded and
|
|
953
|
+
// (2) needs to have data for the x and y dimension.
|
|
954
|
+
let table =
|
|
955
|
+
this.isOnScatterTab || this.isOnMarimekkoTab
|
|
956
|
+
? this.tableAfterAuthorTimelineAndActiveChartTransform
|
|
957
|
+
: this.table
|
|
958
|
+
|
|
959
|
+
if (!this.isReady) return table
|
|
960
|
+
|
|
961
|
+
// Some chart types (e.g. stacked area charts) choose not to show an entity
|
|
962
|
+
// with incomplete data. Such chart types define a custom transform function
|
|
963
|
+
// to ensure that the entity selector only offers entities that are actually plotted.
|
|
964
|
+
if (this.chartState.transformTableForSelection) {
|
|
965
|
+
table = this.chartState.transformTableForSelection(table)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return table
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Input table with color and size tolerance applied.
|
|
973
|
+
*
|
|
974
|
+
* This happens _before_ applying the author's timeline filter to avoid
|
|
975
|
+
* accidentally dropping all color values before applying tolerance.
|
|
976
|
+
* This is especially important for scatter plots and Marimekko charts,
|
|
977
|
+
* where color and size columns are often transformed with infinite tolerance.
|
|
978
|
+
*
|
|
979
|
+
* Line and discrete bar charts also support a color dimension, but their
|
|
980
|
+
* tolerance transformations run in their respective transformTable functions
|
|
981
|
+
* since it's more efficient to run them on a table that has been filtered
|
|
982
|
+
* by selected entities.
|
|
983
|
+
*/
|
|
984
|
+
@computed get tableAfterColorAndSizeToleranceApplication(): OwidTable {
|
|
985
|
+
let table = this.inputTable
|
|
986
|
+
|
|
987
|
+
if (this.hasScatter && this.sizeColumnSlug) {
|
|
988
|
+
const tolerance =
|
|
989
|
+
table.get(this.sizeColumnSlug)?.display?.tolerance ?? Infinity
|
|
990
|
+
table = table.interpolateColumnWithTolerance(this.sizeColumnSlug, {
|
|
991
|
+
toleranceOverride: tolerance,
|
|
992
|
+
})
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (
|
|
996
|
+
(this.hasScatter || this.hasMarimekko) &&
|
|
997
|
+
this.categoricalColorColumnSlug
|
|
998
|
+
) {
|
|
999
|
+
const tolerance =
|
|
1000
|
+
table.get(this.categoricalColorColumnSlug)?.display
|
|
1001
|
+
?.tolerance ?? Infinity
|
|
1002
|
+
table = table.interpolateColumnWithTolerance(
|
|
1003
|
+
this.categoricalColorColumnSlug,
|
|
1004
|
+
{ toleranceOverride: tolerance }
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return table
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// If an author sets a timeline or entity filter, run it early in the pipeline
|
|
1012
|
+
// so to the charts it's as if the filtered times and entities do not exist
|
|
1013
|
+
@computed get tableAfterAuthorTimelineAndEntityFilter(): OwidTable {
|
|
1014
|
+
let table = this.tableAfterColorAndSizeToleranceApplication
|
|
1015
|
+
|
|
1016
|
+
// Filter entities
|
|
1017
|
+
table = table.filterByEntityNamesUsingIncludeExcludePattern({
|
|
1018
|
+
excluded: this.excludedEntityNames,
|
|
1019
|
+
included: this.includedEntityNames,
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
// Filter times
|
|
1023
|
+
if (
|
|
1024
|
+
this.timelineMinTime === undefined &&
|
|
1025
|
+
this.timelineMaxTime === undefined
|
|
1026
|
+
)
|
|
1027
|
+
return table
|
|
1028
|
+
return table.filterByTimeRange(
|
|
1029
|
+
this.timelineMinTime ?? -Infinity,
|
|
1030
|
+
this.timelineMaxTime ?? Infinity
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
@computed
|
|
1035
|
+
get tableAfterAuthorTimelineAndActiveChartTransform(): OwidTable {
|
|
1036
|
+
const table = this.table
|
|
1037
|
+
if (!this.isReady || !this.isOnChartOrMapTab) return table
|
|
1038
|
+
|
|
1039
|
+
const startMark = performance.now()
|
|
1040
|
+
|
|
1041
|
+
const transformedTable = this.chartState.transformTable(table)
|
|
1042
|
+
|
|
1043
|
+
this.createPerformanceMeasurement(
|
|
1044
|
+
"chartInstance.transformTable",
|
|
1045
|
+
startMark
|
|
1046
|
+
)
|
|
1047
|
+
return transformedTable
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
@computed get chartState(): ChartState {
|
|
1051
|
+
// Note: when timeline handles on a LineChart are collapsed into a single handle, the
|
|
1052
|
+
// LineChart turns into a DiscreteBar.
|
|
1053
|
+
return this.isOnMapTab
|
|
1054
|
+
? makeChartState(GRAPHER_MAP_TYPE, this)
|
|
1055
|
+
: this.chartStateExceptMap
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
@computed get facetChartInstance(): FacetChart | undefined {
|
|
1059
|
+
if (!this.isFaceted) return undefined
|
|
1060
|
+
return new FacetChart({
|
|
1061
|
+
manager: this,
|
|
1062
|
+
chartTypeName: this.activeChartType,
|
|
1063
|
+
})
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// When Map becomes a first-class chart instance, we should drop this
|
|
1067
|
+
@computed get chartStateExceptMap(): ChartState {
|
|
1068
|
+
const chartType = this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart
|
|
1069
|
+
|
|
1070
|
+
return makeChartState(chartType, this)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
@computed get chartSeriesNames(): SeriesName[] {
|
|
1074
|
+
if (!this.isReady) return []
|
|
1075
|
+
|
|
1076
|
+
// collect series names from all chart instances when faceted
|
|
1077
|
+
if (this.isFaceted) {
|
|
1078
|
+
return _.uniq(
|
|
1079
|
+
this.facetChartInstance?.intermediateChartInstances.flatMap(
|
|
1080
|
+
(chartInstance) =>
|
|
1081
|
+
chartInstance.chartState.series.map(
|
|
1082
|
+
(series) => series.seriesName
|
|
1083
|
+
)
|
|
1084
|
+
) ?? []
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return this.chartState.series.map((series) => series.seriesName)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
@computed get table(): OwidTable {
|
|
1092
|
+
return this.tableAfterAuthorTimelineAndEntityFilter
|
|
1093
|
+
}
|
|
1094
|
+
@computed
|
|
1095
|
+
private get tableAfterAllTransformsAndFilters(): OwidTable {
|
|
1096
|
+
const { startTime, endTime } = this
|
|
1097
|
+
const table = this.tableAfterAuthorTimelineAndActiveChartTransform
|
|
1098
|
+
|
|
1099
|
+
if (startTime === undefined || endTime === undefined) return table
|
|
1100
|
+
|
|
1101
|
+
if (this.isOnMapTab) {
|
|
1102
|
+
const targetTimes = this.isFaceted
|
|
1103
|
+
? [startTime, endTime]
|
|
1104
|
+
: [endTime]
|
|
1105
|
+
|
|
1106
|
+
return table.filterByTargetTimes(targetTimes)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (this.isOnDiscreteBarTab || this.isOnMarimekkoTab)
|
|
1110
|
+
return table.filterByTargetTimes([endTime])
|
|
1111
|
+
|
|
1112
|
+
if (this.isOnSlopeChartTab)
|
|
1113
|
+
return table.filterByTargetTimes([startTime, endTime])
|
|
1114
|
+
|
|
1115
|
+
return table.filterByTimeRange(startTime, endTime)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
@computed get transformedTable(): OwidTable {
|
|
1119
|
+
return this.tableAfterAllTransformsAndFilters
|
|
1120
|
+
}
|
|
1121
|
+
isExportingToSvgOrPng = false
|
|
1122
|
+
isSocialMediaExport = false
|
|
1123
|
+
isWikimediaExport = false
|
|
1124
|
+
|
|
1125
|
+
variant = GrapherVariant.Default
|
|
1126
|
+
|
|
1127
|
+
staticBounds: Bounds = DEFAULT_GRAPHER_BOUNDS
|
|
1128
|
+
|
|
1129
|
+
enableKeyboardShortcuts: boolean = false
|
|
1130
|
+
bindUrlToWindow: boolean = false
|
|
1131
|
+
tooltip?: TooltipManager["tooltip"] = observable.box(undefined, {
|
|
1132
|
+
deep: false,
|
|
1133
|
+
})
|
|
1134
|
+
isPlaying = false
|
|
1135
|
+
isTimelineAnimationActive = false // true if the timeline animation is either playing or paused but not finished
|
|
1136
|
+
|
|
1137
|
+
animationStartTime: Time | undefined = undefined
|
|
1138
|
+
areHandlesOnSameTimeBeforeAnimation: boolean | undefined = undefined
|
|
1139
|
+
timelineDragTarget: TimelineDragTarget | undefined = undefined
|
|
1140
|
+
|
|
1141
|
+
isEntitySelectorModalOrDrawerOpen = false
|
|
1142
|
+
|
|
1143
|
+
activeModal?: GrapherModal
|
|
1144
|
+
activeDownloadModalTab: DownloadModalTabName = DownloadModalTabName.Vis
|
|
1145
|
+
|
|
1146
|
+
@computed get isStatic(): boolean {
|
|
1147
|
+
return this.isExportingToSvgOrPng
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private get isStaging(): boolean {
|
|
1151
|
+
if (typeof location === "undefined") return false
|
|
1152
|
+
return location.host.includes("staging")
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private get isLocalhost(): boolean {
|
|
1156
|
+
if (typeof location === "undefined") return false
|
|
1157
|
+
return location.host.includes("localhost")
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
@computed get editUrl(): string | undefined {
|
|
1161
|
+
let editPath = this.manager?.adminEditPath
|
|
1162
|
+
if (!editPath && this.id) {
|
|
1163
|
+
editPath = `charts/${this.id}/edit`
|
|
1164
|
+
}
|
|
1165
|
+
if (this.showAdminControls && this.adminBaseUrl && editPath) {
|
|
1166
|
+
return `${this.adminBaseUrl}/admin/${editPath}`
|
|
1167
|
+
}
|
|
1168
|
+
return undefined
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
@computed get createNarrativeChartUrl(): string | undefined {
|
|
1172
|
+
const adminPath = this.manager?.adminCreateNarrativeChartPath
|
|
1173
|
+
if (this.showAdminControls && this.isPublished && adminPath) {
|
|
1174
|
+
return `${this.adminBaseUrl}/admin/${adminPath}`
|
|
1175
|
+
}
|
|
1176
|
+
return undefined
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
@computed get isAdminObjectAvailable(): boolean {
|
|
1180
|
+
if (typeof window === "undefined") return false
|
|
1181
|
+
return (
|
|
1182
|
+
window.admin !== undefined &&
|
|
1183
|
+
// Ensure that we're not accidentally matching on a DOM element with an ID of "admin"
|
|
1184
|
+
typeof window.admin.isSuperuser === "boolean"
|
|
1185
|
+
)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
@computed get isAdmin(): boolean {
|
|
1189
|
+
if (typeof window === "undefined") return false
|
|
1190
|
+
if (this.isAdminObjectAvailable) return true
|
|
1191
|
+
// Using this.isAdminObjectAvailable is not enough because it's not
|
|
1192
|
+
// available in gdoc previews, which render in an iframe without the
|
|
1193
|
+
// admin scaffolding.
|
|
1194
|
+
if (this.adminBaseUrl) {
|
|
1195
|
+
try {
|
|
1196
|
+
const adminUrl = new URL(this.adminBaseUrl)
|
|
1197
|
+
const currentUrl = new URL(window.location.href)
|
|
1198
|
+
return adminUrl.host === currentUrl.host
|
|
1199
|
+
} catch {
|
|
1200
|
+
return false
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return false
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
@computed get isUserLoggedInAsAdmin(): boolean {
|
|
1207
|
+
// This cookie is set by visiting ourworldindata.org/identifyadmin on the static site.
|
|
1208
|
+
// There is an iframe on owid.cloud to trigger a visit to that page.
|
|
1209
|
+
try {
|
|
1210
|
+
// Cookie access can be restricted by iframe sandboxing, in which case the below code will throw an error
|
|
1211
|
+
// see https://github.com/owid/owid-grapher/pull/2452
|
|
1212
|
+
return !!Cookies.get(CookieKey.isAdmin)
|
|
1213
|
+
} catch {
|
|
1214
|
+
return false
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
@computed get showAdminControls(): boolean {
|
|
1218
|
+
return (
|
|
1219
|
+
this.isUserLoggedInAsAdmin ||
|
|
1220
|
+
this.isAdmin || // Useful for gdoc previews.
|
|
1221
|
+
this.isDev ||
|
|
1222
|
+
this.isLocalhost ||
|
|
1223
|
+
this.isStaging
|
|
1224
|
+
)
|
|
1225
|
+
}
|
|
1226
|
+
// Exclusively used for the performance.measurement API, so that DevTools can show some context
|
|
1227
|
+
createPerformanceMeasurement(name: string, startMark: number): void {
|
|
1228
|
+
const endMark = performance.now()
|
|
1229
|
+
const detail = {
|
|
1230
|
+
devtools: {
|
|
1231
|
+
track: "Grapher",
|
|
1232
|
+
properties: [
|
|
1233
|
+
// might be missing for charts within explorers or mdims
|
|
1234
|
+
["slug", this.slug ?? "missing-slug"],
|
|
1235
|
+
["chartTypes", this.validChartTypes],
|
|
1236
|
+
["tab", this.tab],
|
|
1237
|
+
],
|
|
1238
|
+
},
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
performance.measure(name, {
|
|
1243
|
+
start: startMark,
|
|
1244
|
+
end: endMark,
|
|
1245
|
+
detail,
|
|
1246
|
+
})
|
|
1247
|
+
} catch {
|
|
1248
|
+
// In old browsers, the above may throw an error - just ignore it
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
@action.bound private applyOriginalFocusAsAuthored(): void {
|
|
1253
|
+
if (this.focusedSeriesNames?.length)
|
|
1254
|
+
this.focusArray.clearAllAndAdd(...this.focusedSeriesNames)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
@action.bound applyOriginalSelectionAsAuthored(): void {
|
|
1258
|
+
if (this.selectedEntityNames?.length)
|
|
1259
|
+
this.selection.setSelectedEntities(this.selectedEntityNames)
|
|
1260
|
+
}
|
|
1261
|
+
// The below properties are here so the admin can access them
|
|
1262
|
+
@computed get hasData(): boolean {
|
|
1263
|
+
return this.dimensions.length > 0 || this.newSlugs.length > 0
|
|
1264
|
+
}
|
|
1265
|
+
// Ready to go iff we have retrieved data for every variable associated with the chart
|
|
1266
|
+
@computed get isReady(): boolean {
|
|
1267
|
+
if (!this.isConfigReady) return false
|
|
1268
|
+
if (!this.isDataReady) return false
|
|
1269
|
+
return this.whatAreWeWaitingFor === ""
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
@computed get whatAreWeWaitingFor(): string {
|
|
1273
|
+
const { newSlugs, inputTable, dimensions } = this
|
|
1274
|
+
if (newSlugs.length || dimensions.length === 0) {
|
|
1275
|
+
const missingColumns = newSlugs.filter(
|
|
1276
|
+
(slug) => !inputTable.has(slug)
|
|
1277
|
+
)
|
|
1278
|
+
return missingColumns.length
|
|
1279
|
+
? `Waiting for columns ${missingColumns.join(",")} in table '${inputTable.tableSlug}'. ${inputTable.tableDescription}`
|
|
1280
|
+
: ""
|
|
1281
|
+
}
|
|
1282
|
+
if (dimensions.length > 0 && this.loadingDimensions.length === 0)
|
|
1283
|
+
return ""
|
|
1284
|
+
return `Waiting for dimensions ${this.loadingDimensions.join(",")}.`
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// If we are using new slugs and not dimensions, Grapher is ready.
|
|
1288
|
+
@computed get newSlugs(): string[] {
|
|
1289
|
+
const { xSlug, colorSlug, sizeSlug } = this
|
|
1290
|
+
const ySlugs = this.ySlugs ? this.ySlugs.split(" ") : []
|
|
1291
|
+
return excludeUndefined([...ySlugs, xSlug, colorSlug, sizeSlug])
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
@computed private get loadingDimensions(): ChartDimension[] {
|
|
1295
|
+
return this.dimensions.filter(
|
|
1296
|
+
(dim) => !this.inputTable.has(dim.columnSlug)
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
@computed get isInIFrame(): boolean {
|
|
1300
|
+
return isInIFrame()
|
|
1301
|
+
}
|
|
1302
|
+
@computed get times(): Time[] {
|
|
1303
|
+
const { mapColumnSlug, projectionColumnInfoBySlug, yColumnSlugs } = this
|
|
1304
|
+
|
|
1305
|
+
// If the map shows historical and projected data, then the time range
|
|
1306
|
+
// has to extend to the full range of both indicators
|
|
1307
|
+
const mapColumnInfo = projectionColumnInfoBySlug.get(mapColumnSlug)
|
|
1308
|
+
const mapColumnSlugs = mapColumnInfo
|
|
1309
|
+
? [mapColumnInfo.projectedSlug, mapColumnInfo.historicalSlug]
|
|
1310
|
+
: [mapColumnSlug]
|
|
1311
|
+
|
|
1312
|
+
const columnSlugs = this.isOnMapTab ? mapColumnSlugs : yColumnSlugs
|
|
1313
|
+
|
|
1314
|
+
// Generate the times only after the chart transform has been applied, so that we don't show
|
|
1315
|
+
// times on the timeline for which data may not exist, e.g. when the selected entity
|
|
1316
|
+
// doesn't contain data for all years in the table.
|
|
1317
|
+
// -@danielgavrilov, 2020-10-22
|
|
1318
|
+
return this.tableAfterAuthorTimelineAndActiveChartTransform.getTimesUniqSortedAscForColumns(
|
|
1319
|
+
columnSlugs
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Plots time on the x-axis.
|
|
1325
|
+
*/
|
|
1326
|
+
@computed private get hasTimeDimension(): boolean {
|
|
1327
|
+
return this.isStackedBar || this.isStackedArea || this.isLineChart
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
@computed private get hasTimeDimensionButTimelineIsHidden(): boolean {
|
|
1331
|
+
return this.hasTimeDimension && !!this.hideTimeline
|
|
1332
|
+
}
|
|
1333
|
+
@computed get startHandleTimeBound(): TimeBound {
|
|
1334
|
+
if (this.isSingleTimeSelectionActive) return this.endHandleTimeBound
|
|
1335
|
+
return this.timelineHandleTimeBounds[0]
|
|
1336
|
+
}
|
|
1337
|
+
set startHandleTimeBound(newValue: TimeBound) {
|
|
1338
|
+
if (this.isSingleTimeSelectionActive)
|
|
1339
|
+
this.timelineHandleTimeBounds = [newValue, newValue]
|
|
1340
|
+
else
|
|
1341
|
+
this.timelineHandleTimeBounds = [
|
|
1342
|
+
newValue,
|
|
1343
|
+
this.timelineHandleTimeBounds[1],
|
|
1344
|
+
]
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
set endHandleTimeBound(newValue: TimeBound) {
|
|
1348
|
+
if (this.isSingleTimeSelectionActive)
|
|
1349
|
+
this.timelineHandleTimeBounds = [newValue, newValue]
|
|
1350
|
+
else
|
|
1351
|
+
this.timelineHandleTimeBounds = [
|
|
1352
|
+
this.timelineHandleTimeBounds[0],
|
|
1353
|
+
newValue,
|
|
1354
|
+
]
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
@computed get endHandleTimeBound(): TimeBound {
|
|
1358
|
+
return this.timelineHandleTimeBounds[1]
|
|
1359
|
+
}
|
|
1360
|
+
@action.bound resetHandleTimeBounds(): void {
|
|
1361
|
+
this.startHandleTimeBound = this.timelineMinTime ?? -Infinity
|
|
1362
|
+
this.endHandleTimeBound = this.timelineMaxTime ?? Infinity
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Keeps a running cache of series colors at the Grapher level.
|
|
1366
|
+
seriesColorMap: SeriesColorMap = new Map()
|
|
1367
|
+
|
|
1368
|
+
@computed get closestTimelineMinTime(): Time | undefined {
|
|
1369
|
+
return findClosestTime(this.times, this.timelineMinTime ?? -Infinity)
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
@computed get closestTimelineMaxTime(): Time | undefined {
|
|
1373
|
+
return findClosestTime(this.times, this.timelineMaxTime ?? Infinity)
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
@computed get startTime(): Time | undefined {
|
|
1377
|
+
return findClosestTime(this.times, this.startHandleTimeBound)
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
@computed get endTime(): Time | undefined {
|
|
1381
|
+
return findClosestTime(this.times, this.endHandleTimeBound)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
@computed get isSingleTimeScatterAnimationActive(): boolean {
|
|
1385
|
+
return (
|
|
1386
|
+
this.isTimelineAnimationActive &&
|
|
1387
|
+
this.isOnScatterTab &&
|
|
1388
|
+
!this.isRelativeMode &&
|
|
1389
|
+
!!this.areHandlesOnSameTimeBeforeAnimation
|
|
1390
|
+
)
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
@computed get isSingleTimeMapAnimationActive(): boolean {
|
|
1394
|
+
return (
|
|
1395
|
+
this.isTimelineAnimationActive &&
|
|
1396
|
+
this.isOnMapTab &&
|
|
1397
|
+
!!this.areHandlesOnSameTimeBeforeAnimation
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
@computed private get onlySingleTimeSelectionPossible(): boolean {
|
|
1402
|
+
return this.checkOnlySingleTimeSelectionPossible(this.activeTab)
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
@computed get onlyTimeRangeSelectionPossible(): boolean {
|
|
1406
|
+
return this.checkOnlyTimeRangeSelectionPossible(this.activeTab)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
@computed get isSingleTimeSelectionActive(): boolean {
|
|
1410
|
+
return (
|
|
1411
|
+
this.onlySingleTimeSelectionPossible ||
|
|
1412
|
+
this.isSingleTimeScatterAnimationActive ||
|
|
1413
|
+
this.isSingleTimeMapAnimationActive
|
|
1414
|
+
)
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
@computed get shouldLinkToOwid(): boolean {
|
|
1418
|
+
if (
|
|
1419
|
+
this.isEmbeddedInAnOwidPage ||
|
|
1420
|
+
this.isExportingToSvgOrPng ||
|
|
1421
|
+
!this.isInIFrame
|
|
1422
|
+
)
|
|
1423
|
+
return false
|
|
1424
|
+
|
|
1425
|
+
return true
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
@computed get hasOWIDLogo(): boolean {
|
|
1429
|
+
return (
|
|
1430
|
+
!this.hideLogo && (this.logo === undefined || this.logo === "owid")
|
|
1431
|
+
)
|
|
1432
|
+
}
|
|
1433
|
+
@computed get hasFatalErrors(): boolean {
|
|
1434
|
+
const { relatedQuestions = [] } = this
|
|
1435
|
+
return relatedQuestions.some(
|
|
1436
|
+
(question) => !!getErrorMessageRelatedQuestionUrl(question)
|
|
1437
|
+
)
|
|
1438
|
+
}
|
|
1439
|
+
disposers: (() => void)[] = []
|
|
1440
|
+
|
|
1441
|
+
@bind dispose(): void {
|
|
1442
|
+
this.disposers.forEach((dispose) => dispose())
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
@action.bound setTab(newTab: GrapherTabName): void {
|
|
1446
|
+
this.tab = this.mapTabNameToTabConfigOption(newTab)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
@action.bound private ensureHandlesAreOnSameTime(): void {
|
|
1450
|
+
if (this.areHandlesOnSameTime) return
|
|
1451
|
+
|
|
1452
|
+
this.timelineHandleTimeBounds = [
|
|
1453
|
+
this.endHandleTimeBound,
|
|
1454
|
+
this.endHandleTimeBound,
|
|
1455
|
+
]
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
@action.bound private ensureHandlesAreOnDifferentTimes(): void {
|
|
1459
|
+
if (!this.areHandlesOnSameTime) return
|
|
1460
|
+
|
|
1461
|
+
const time = this.startTime // startTime = endTime
|
|
1462
|
+
if (time === this.closestTimelineMinTime) {
|
|
1463
|
+
this.timelineHandleTimeBounds = [time ?? -Infinity, Infinity]
|
|
1464
|
+
} else {
|
|
1465
|
+
this.timelineHandleTimeBounds = [-Infinity, time ?? Infinity]
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
@computed get entitySelector(): EntitySelector {
|
|
1470
|
+
const entitySelectorArray = this.isOnMapTab
|
|
1471
|
+
? this.mapConfig.selection
|
|
1472
|
+
: this.selection
|
|
1473
|
+
return new EntitySelector({
|
|
1474
|
+
manager: this,
|
|
1475
|
+
selection: entitySelectorArray,
|
|
1476
|
+
})
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
private checkOnlySingleTimeSelectionPossible = (
|
|
1480
|
+
tabName: GrapherTabName
|
|
1481
|
+
): boolean => {
|
|
1482
|
+
// Scatters aren't included here because although single-time selection
|
|
1483
|
+
// is preferred, start and end time selection is still possible
|
|
1484
|
+
return [
|
|
1485
|
+
GRAPHER_TAB_NAMES.DiscreteBar,
|
|
1486
|
+
GRAPHER_TAB_NAMES.StackedDiscreteBar,
|
|
1487
|
+
GRAPHER_TAB_NAMES.Marimekko,
|
|
1488
|
+
].includes(tabName as any)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private checkOnlyTimeRangeSelectionPossible = (
|
|
1492
|
+
tabName: GrapherTabName
|
|
1493
|
+
): boolean => {
|
|
1494
|
+
return [
|
|
1495
|
+
GRAPHER_TAB_NAMES.LineChart,
|
|
1496
|
+
GRAPHER_TAB_NAMES.SlopeChart,
|
|
1497
|
+
GRAPHER_TAB_NAMES.StackedArea,
|
|
1498
|
+
GRAPHER_TAB_NAMES.StackedBar,
|
|
1499
|
+
].includes(tabName as any)
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
@action.bound ensureTimeHandlesAreSensibleForTab(
|
|
1503
|
+
tab: GrapherTabName
|
|
1504
|
+
): void {
|
|
1505
|
+
if (this.checkOnlySingleTimeSelectionPossible(tab)) {
|
|
1506
|
+
this.ensureHandlesAreOnSameTime()
|
|
1507
|
+
} else if (this.checkOnlyTimeRangeSelectionPossible(tab)) {
|
|
1508
|
+
this.ensureHandlesAreOnDifferentTimes()
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
@action.bound private ensureEntitySelectionIsSensibleForTab(
|
|
1513
|
+
tab: GrapherTabName
|
|
1514
|
+
): void {
|
|
1515
|
+
// No-op if the current tab is a map or table tab
|
|
1516
|
+
if (!isChartTab(tab)) return
|
|
1517
|
+
|
|
1518
|
+
const isChartTypeThatShowsAllEntities =
|
|
1519
|
+
this.isChartTypeThatShowsAllEntities(tab)
|
|
1520
|
+
|
|
1521
|
+
// If the chart show all entities (e.g. scatter plot or Marimekko chart),
|
|
1522
|
+
// then we typically prefer no selection unless the user has explicitly
|
|
1523
|
+
// made changes to the default selection
|
|
1524
|
+
if (
|
|
1525
|
+
isChartTypeThatShowsAllEntities &&
|
|
1526
|
+
!this.areSelectedEntitiesDifferentThanAuthors
|
|
1527
|
+
) {
|
|
1528
|
+
this.selection.clearSelection()
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// If the chart type only shows a subset of entities at a time
|
|
1532
|
+
// (e.g. line chart), then an empty selection is not useful, so we
|
|
1533
|
+
// automatically apply the author's selection
|
|
1534
|
+
if (!isChartTypeThatShowsAllEntities && !this.selection.hasSelection) {
|
|
1535
|
+
this.applyOriginalSelectionAsAuthored()
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
@action.bound onChartSwitching(
|
|
1540
|
+
_oldTab: GrapherTabName,
|
|
1541
|
+
newTab: GrapherTabName
|
|
1542
|
+
): void {
|
|
1543
|
+
if (!this.isReady)
|
|
1544
|
+
console.warn(
|
|
1545
|
+
"onChartSwitching has been called before grapher has loaded its data, this is probably a mistake"
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
this.ensureTimeHandlesAreSensibleForTab(newTab)
|
|
1549
|
+
this.ensureEntitySelectionIsSensibleForTab(newTab)
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
@action.bound syncEntitySelectionBetweenChartAndMap(
|
|
1553
|
+
oldTab: GrapherTabName,
|
|
1554
|
+
newTab: GrapherTabName
|
|
1555
|
+
): void {
|
|
1556
|
+
// sync entity selection between the map and the chart tab if entity
|
|
1557
|
+
// selection is enabled for the map, and the map has been interacted
|
|
1558
|
+
// with, i.e. at least one country has been selected on the map
|
|
1559
|
+
const shouldSyncSelection =
|
|
1560
|
+
this.addCountryMode !== EntitySelectionMode.Disabled &&
|
|
1561
|
+
this.isMapSelectionEnabled &&
|
|
1562
|
+
this.mapConfig.selection.hasSelection
|
|
1563
|
+
|
|
1564
|
+
// switching from the chart tab to the map tab
|
|
1565
|
+
if (!isMapTab(oldTab) && isMapTab(newTab) && shouldSyncSelection) {
|
|
1566
|
+
this.mapConfig.selection.setSelectedEntities(
|
|
1567
|
+
this.selection.selectedEntityNames
|
|
1568
|
+
)
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// switching from the map tab to the chart tab
|
|
1572
|
+
if (isMapTab(oldTab) && !isMapTab(newTab) && shouldSyncSelection) {
|
|
1573
|
+
this.selection.setSelectedEntities(
|
|
1574
|
+
this.mapConfig.selection.selectedEntityNames
|
|
1575
|
+
)
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
@action.bound private validateEntitySelectorState(
|
|
1580
|
+
newTab: GrapherTabName
|
|
1581
|
+
): void {
|
|
1582
|
+
if (isMapTab(newTab) || isChartTab(newTab)) {
|
|
1583
|
+
const { entitySelector } = this
|
|
1584
|
+
|
|
1585
|
+
// the map and chart tab might have a different set of sort columns;
|
|
1586
|
+
// if the currently selected sort column is invalid, reset it to the default
|
|
1587
|
+
const sortSlug = entitySelector.sortConfig.slug
|
|
1588
|
+
if (!entitySelector.isSortSlugValid(sortSlug)) {
|
|
1589
|
+
this.entitySelectorState.sortConfig =
|
|
1590
|
+
entitySelector.getDefaultSortConfig()
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// the map and chart tab might have a different set of entity filters;
|
|
1594
|
+
// if the currently selected entity filter is invalid, reset it
|
|
1595
|
+
const { entityFilter } = this.entitySelectorState
|
|
1596
|
+
if (entityFilter) {
|
|
1597
|
+
if (!this.entitySelector.isEntityFilterValid(entityFilter)) {
|
|
1598
|
+
this.entitySelectorState.entityFilter = undefined
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// the map column slug might be interpolated with different
|
|
1603
|
+
// tolerance values on the chart and the map tab
|
|
1604
|
+
entitySelector.resetInterpolatedMapColumn()
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
@action.bound onTabChange(
|
|
1609
|
+
oldTab: GrapherTabName,
|
|
1610
|
+
newTab: GrapherTabName
|
|
1611
|
+
): void {
|
|
1612
|
+
this.onChartSwitching(oldTab, newTab)
|
|
1613
|
+
this.syncEntitySelectionBetweenChartAndMap(oldTab, newTab)
|
|
1614
|
+
this.validateEntitySelectorState(newTab)
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// todo: can we remove this?
|
|
1618
|
+
// I believe these states can only occur during editing.
|
|
1619
|
+
@action.bound private ensureValidConfigWhenEditing(): void {
|
|
1620
|
+
const disposers = [
|
|
1621
|
+
autorun(() => {
|
|
1622
|
+
if (!this.availableTabs.includes(this.activeTab))
|
|
1623
|
+
runInAction(() => this.setTab(this.availableTabs[0]))
|
|
1624
|
+
}),
|
|
1625
|
+
autorun(() => {
|
|
1626
|
+
const validDimensions = this.validDimensions
|
|
1627
|
+
if (!_.isEqual(this.dimensions, validDimensions))
|
|
1628
|
+
this.dimensions = validDimensions
|
|
1629
|
+
}),
|
|
1630
|
+
]
|
|
1631
|
+
this.disposers.push(...disposers)
|
|
1632
|
+
}
|
|
1633
|
+
@computed private get validDimensions(): ChartDimension[] {
|
|
1634
|
+
const { dimensions } = this
|
|
1635
|
+
const validProperties = this.dimensionSlots.map((d) => d.property)
|
|
1636
|
+
let validDimensions = dimensions.filter((dim) =>
|
|
1637
|
+
validProperties.includes(dim.property)
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
this.dimensionSlots.forEach((slot) => {
|
|
1641
|
+
if (!slot.allowMultiple)
|
|
1642
|
+
validDimensions = _.uniqWith(
|
|
1643
|
+
validDimensions,
|
|
1644
|
+
(
|
|
1645
|
+
a: OwidChartDimensionInterface,
|
|
1646
|
+
b: OwidChartDimensionInterface
|
|
1647
|
+
) =>
|
|
1648
|
+
a.property === slot.property &&
|
|
1649
|
+
a.property === b.property
|
|
1650
|
+
)
|
|
1651
|
+
})
|
|
1652
|
+
|
|
1653
|
+
return validDimensions
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
@computed get originUrlWithProtocol(): string {
|
|
1657
|
+
if (!this.originUrl) return ""
|
|
1658
|
+
let url = this.originUrl
|
|
1659
|
+
|
|
1660
|
+
// If the URL is relative, make it absolute to ourworldindata.org.
|
|
1661
|
+
// we could also opt to make it relative to bakedGrapherUrl, but then we'd
|
|
1662
|
+
// end up with different URLs than prod on staging servers and in the SVG tester,
|
|
1663
|
+
// which could also affect positioning.
|
|
1664
|
+
if (url.startsWith("/")) {
|
|
1665
|
+
url = new URL(url, GRAPHER_PROD_URL).href
|
|
1666
|
+
}
|
|
1667
|
+
if (!url.startsWith("http")) url = `https://${url}`
|
|
1668
|
+
return url
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
@computed get timelineHandleTimeBounds(): TimeBounds {
|
|
1672
|
+
if (this.isOnMapTab) {
|
|
1673
|
+
const endTime = maxTimeBoundFromJSONOrPositiveInfinity(
|
|
1674
|
+
this.map.time
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
// If a start time is provided, use it; otherwise, set it to the end time
|
|
1678
|
+
// so that a single map (not a faceted one) is shown by default
|
|
1679
|
+
const startTime =
|
|
1680
|
+
this.map.startTime === undefined
|
|
1681
|
+
? endTime
|
|
1682
|
+
: minTimeBoundFromJSONOrNegativeInfinity(this.map.startTime)
|
|
1683
|
+
|
|
1684
|
+
return [startTime, endTime]
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// If the timeline is hidden on the chart tab but displayed on the table tab
|
|
1688
|
+
// (which is the case for charts that plot time on the x-axis),
|
|
1689
|
+
// we always want to use the authored `minTime` and `maxTime` for the chart,
|
|
1690
|
+
// irrespective of the time range the user might have selected on the table tab
|
|
1691
|
+
if (this.isOnChartTab && this.hasTimeDimensionButTimelineIsHidden) {
|
|
1692
|
+
const { minTime, maxTime } = this.authorsVersion
|
|
1693
|
+
return [
|
|
1694
|
+
minTimeBoundFromJSONOrNegativeInfinity(minTime),
|
|
1695
|
+
maxTimeBoundFromJSONOrPositiveInfinity(maxTime),
|
|
1696
|
+
]
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
return [
|
|
1700
|
+
// Handle `undefined` values in minTime/maxTime
|
|
1701
|
+
minTimeBoundFromJSONOrNegativeInfinity(this.minTime),
|
|
1702
|
+
maxTimeBoundFromJSONOrPositiveInfinity(this.maxTime),
|
|
1703
|
+
]
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
set timelineHandleTimeBounds(value: TimeBounds) {
|
|
1707
|
+
if (this.isOnMapTab) {
|
|
1708
|
+
this.map.startTime = value[0]
|
|
1709
|
+
this.map.time = value[1]
|
|
1710
|
+
} else {
|
|
1711
|
+
this.minTime = value[0]
|
|
1712
|
+
this.maxTime = value[1]
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Get the dimension slots appropriate for this type of chart
|
|
1717
|
+
@computed get dimensionSlots(): DimensionSlot[] {
|
|
1718
|
+
const xAxis = new DimensionSlot(this, DimensionProperty.x)
|
|
1719
|
+
const yAxis = new DimensionSlot(this, DimensionProperty.y)
|
|
1720
|
+
const color = new DimensionSlot(this, DimensionProperty.color)
|
|
1721
|
+
const size = new DimensionSlot(this, DimensionProperty.size)
|
|
1722
|
+
|
|
1723
|
+
if (this.hasScatter) return [yAxis, xAxis, size, color]
|
|
1724
|
+
if (this.hasMarimekko) return [yAxis, xAxis, color]
|
|
1725
|
+
if (this.hasLineChart || this.hasDiscreteBar) return [yAxis, color]
|
|
1726
|
+
return [yAxis]
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
@computed.struct get filledDimensions(): ChartDimension[] {
|
|
1730
|
+
return this.isReady ? this.dimensions : []
|
|
1731
|
+
}
|
|
1732
|
+
@action.bound addDimension(config: OwidChartDimensionInterface): void {
|
|
1733
|
+
this.dimensions.push(new ChartDimension(config, this))
|
|
1734
|
+
}
|
|
1735
|
+
@action.bound setDimensionsForProperty(
|
|
1736
|
+
property: DimensionProperty,
|
|
1737
|
+
newConfigs: OwidChartDimensionInterface[]
|
|
1738
|
+
): void {
|
|
1739
|
+
let newDimensions: ChartDimension[] = []
|
|
1740
|
+
this.dimensionSlots.forEach((slot) => {
|
|
1741
|
+
if (slot.property === property)
|
|
1742
|
+
newDimensions = newDimensions.concat(
|
|
1743
|
+
newConfigs.map((config) => new ChartDimension(config, this))
|
|
1744
|
+
)
|
|
1745
|
+
else newDimensions = newDimensions.concat(slot.dimensions)
|
|
1746
|
+
})
|
|
1747
|
+
this.dimensions = newDimensions
|
|
1748
|
+
}
|
|
1749
|
+
@action.bound setDimensionsFromConfigs(
|
|
1750
|
+
configs: OwidChartDimensionInterface[]
|
|
1751
|
+
): void {
|
|
1752
|
+
this.dimensions = configs.map(
|
|
1753
|
+
(config) => new ChartDimension(config, this)
|
|
1754
|
+
)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
@computed get defaultSlug(): string {
|
|
1758
|
+
return slugify(this.displayTitle)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
@computed get displaySlug(): string {
|
|
1762
|
+
return this.slug ?? this.defaultSlug
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
shouldIncludeDetailsInStaticExport = true
|
|
1766
|
+
// Used for superscript numbers in static exports
|
|
1767
|
+
@computed get detailsOrderedByReference(): string[] {
|
|
1768
|
+
if (typeof window === "undefined") return []
|
|
1769
|
+
|
|
1770
|
+
// extract details from supporting text
|
|
1771
|
+
const subtitleDetails = !this.hideSubtitle
|
|
1772
|
+
? extractDetailsFromSyntax(this.currentSubtitle)
|
|
1773
|
+
: []
|
|
1774
|
+
const noteDetails = !this.hideNote
|
|
1775
|
+
? extractDetailsFromSyntax(this.note ?? "")
|
|
1776
|
+
: []
|
|
1777
|
+
|
|
1778
|
+
// extract details from axis labels
|
|
1779
|
+
const yAxisDetails = extractDetailsFromSyntax(
|
|
1780
|
+
this.yAxisConfig.label || ""
|
|
1781
|
+
)
|
|
1782
|
+
const xAxisDetails = extractDetailsFromSyntax(
|
|
1783
|
+
this.xAxisConfig.label || ""
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
// text fragments are ordered by appearance
|
|
1787
|
+
const uniqueDetails = _.uniq([
|
|
1788
|
+
...subtitleDetails,
|
|
1789
|
+
...yAxisDetails,
|
|
1790
|
+
...xAxisDetails,
|
|
1791
|
+
...noteDetails,
|
|
1792
|
+
])
|
|
1793
|
+
|
|
1794
|
+
return uniqueDetails
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
@computed get detailsMarkerInSvg(): DetailsMarker {
|
|
1798
|
+
const { isStatic, shouldIncludeDetailsInStaticExport } = this
|
|
1799
|
+
return !isStatic
|
|
1800
|
+
? "underline"
|
|
1801
|
+
: shouldIncludeDetailsInStaticExport
|
|
1802
|
+
? "superscript"
|
|
1803
|
+
: "none"
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Used for static exports. Defined at this level because they need to
|
|
1807
|
+
// be accessed by CaptionedChart and DownloadModal
|
|
1808
|
+
@computed get detailRenderers(): MarkdownTextWrap[] {
|
|
1809
|
+
if (typeof window === "undefined") return []
|
|
1810
|
+
return this.detailsOrderedByReference.map((term, i) => {
|
|
1811
|
+
let text = `**${i + 1}.** `
|
|
1812
|
+
const detail: EnrichedDetail | undefined = window.details?.[term]
|
|
1813
|
+
if (detail && detail.text) {
|
|
1814
|
+
const lines = detail.text.split("\n")
|
|
1815
|
+
const title = lines[0]
|
|
1816
|
+
const description = lines.slice(2).join("\n")
|
|
1817
|
+
text += `**${title}** ${description}`
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// can't use the computed property here because Grapher might not currently be in static mode
|
|
1821
|
+
const baseFontSize = this.areStaticBoundsSmall
|
|
1822
|
+
? this.computeBaseFontSizeFromHeight(this.staticBounds)
|
|
1823
|
+
: 18
|
|
1824
|
+
|
|
1825
|
+
return new MarkdownTextWrap({
|
|
1826
|
+
text,
|
|
1827
|
+
fontSize: (11 / BASE_FONT_SIZE) * baseFontSize,
|
|
1828
|
+
// leave room for padding on the left and right
|
|
1829
|
+
maxWidth:
|
|
1830
|
+
this.staticBounds.width - 2 * this.framePaddingHorizontal,
|
|
1831
|
+
lineHeight: 1.2,
|
|
1832
|
+
style: { fill: GRAPHER_LIGHT_TEXT },
|
|
1833
|
+
})
|
|
1834
|
+
})
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
@computed get hasProjectedData(): boolean {
|
|
1838
|
+
return this.inputTable.numericColumnSlugs.some(
|
|
1839
|
+
(slug) => this.inputTable.get(slug).isProjection
|
|
1840
|
+
)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
@computed get projectionColumnInfoBySlug(): Map<
|
|
1844
|
+
ColumnSlug,
|
|
1845
|
+
ProjectionColumnInfo
|
|
1846
|
+
> {
|
|
1847
|
+
const table = this.inputTable
|
|
1848
|
+
|
|
1849
|
+
const [projectionSlugs, nonProjectionSlugs] = R.partition(
|
|
1850
|
+
this.yColumnSlugs,
|
|
1851
|
+
(slug) => table.get(slug).isProjection
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
if (!projectionSlugs.length) return new Map()
|
|
1855
|
+
|
|
1856
|
+
const projectionColumnInfoBySlug = new Map<
|
|
1857
|
+
ColumnSlug,
|
|
1858
|
+
ProjectionColumnInfo
|
|
1859
|
+
>()
|
|
1860
|
+
|
|
1861
|
+
const findHistoricalSlugForProjection = (
|
|
1862
|
+
projectedSlug: ColumnSlug
|
|
1863
|
+
): ColumnSlug | undefined => {
|
|
1864
|
+
// If there is only one non-projection column, we trivially match it to the projection
|
|
1865
|
+
if (nonProjectionSlugs.length === 1) return nonProjectionSlugs[0]
|
|
1866
|
+
|
|
1867
|
+
// Try to find a historical column with the same display name
|
|
1868
|
+
const displayName = table.get(projectedSlug).displayName
|
|
1869
|
+
return nonProjectionSlugs.find(
|
|
1870
|
+
(slug) => table.get(slug).displayName === displayName
|
|
1871
|
+
)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
for (const projectedSlug of projectionSlugs) {
|
|
1875
|
+
const historicalSlug =
|
|
1876
|
+
findHistoricalSlugForProjection(projectedSlug)
|
|
1877
|
+
if (historicalSlug) {
|
|
1878
|
+
const combinedSlug = `${projectedSlug}-${historicalSlug}`
|
|
1879
|
+
const slugForIsProjectionColumn = `${combinedSlug}-isProjection`
|
|
1880
|
+
|
|
1881
|
+
projectionColumnInfoBySlug.set(projectedSlug, {
|
|
1882
|
+
projectedSlug,
|
|
1883
|
+
historicalSlug,
|
|
1884
|
+
combinedSlug,
|
|
1885
|
+
slugForIsProjectionColumn,
|
|
1886
|
+
})
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
return projectionColumnInfoBySlug
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
@computed get validChartTypes(): GrapherChartType[] {
|
|
1894
|
+
const chartTypeSet = new Set(this.chartTypes)
|
|
1895
|
+
|
|
1896
|
+
// all single-chart Graphers are valid
|
|
1897
|
+
if (chartTypeSet.size <= 1) return Array.from(chartTypeSet)
|
|
1898
|
+
|
|
1899
|
+
// find valid combination in a pre-defined list
|
|
1900
|
+
const validChartTypes = findValidChartTypeCombination(
|
|
1901
|
+
Array.from(chartTypeSet)
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
// if the given combination is not valid, then ignore all but the first chart type
|
|
1905
|
+
if (!validChartTypes) return this.chartTypes.slice(0, 1)
|
|
1906
|
+
|
|
1907
|
+
// projected data is only supported for line charts
|
|
1908
|
+
const isLineChart = validChartTypes[0] === GRAPHER_CHART_TYPES.LineChart
|
|
1909
|
+
if (isLineChart && this.hasProjectedData) {
|
|
1910
|
+
return [
|
|
1911
|
+
GRAPHER_CHART_TYPES.LineChart,
|
|
1912
|
+
GRAPHER_CHART_TYPES.DiscreteBar,
|
|
1913
|
+
]
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
return validChartTypes
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
@computed get validChartTypeSet(): Set<GrapherChartType> {
|
|
1920
|
+
return new Set(this.validChartTypes)
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
@computed get availableTabs(): GrapherTabName[] {
|
|
1924
|
+
const availableTabs: GrapherTabName[] = []
|
|
1925
|
+
if (this.hasTableTab) availableTabs.push(GRAPHER_TAB_NAMES.Table)
|
|
1926
|
+
if (this.hasMapTab) availableTabs.push(GRAPHER_TAB_NAMES.WorldMap)
|
|
1927
|
+
availableTabs.push(...this.validChartTypes)
|
|
1928
|
+
return availableTabs
|
|
1929
|
+
}
|
|
1930
|
+
@computed get hasMultipleChartTypes(): boolean {
|
|
1931
|
+
return this.validChartTypes.length > 1
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
@computed get currentSubtitle(): string {
|
|
1935
|
+
const subtitle = this.subtitle
|
|
1936
|
+
if (subtitle !== undefined) return subtitle
|
|
1937
|
+
const yColumns = this.yColumnsFromDimensions
|
|
1938
|
+
if (yColumns.length === 1) return yColumns[0].def.descriptionShort ?? ""
|
|
1939
|
+
return ""
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
@computed get shouldAddEntitySuffixToTitle(): boolean {
|
|
1943
|
+
const selectedEntityNames = this.selection.selectedEntityNames
|
|
1944
|
+
const showEntityAnnotation = !this.hideAnnotationFieldsInTitle?.entity
|
|
1945
|
+
|
|
1946
|
+
const seriesStrategy =
|
|
1947
|
+
this.chartState.seriesStrategy ||
|
|
1948
|
+
autoDetectSeriesStrategy(this, true)
|
|
1949
|
+
|
|
1950
|
+
return !!(
|
|
1951
|
+
!this.forceHideAnnotationFieldsInTitle?.entity &&
|
|
1952
|
+
this.isOnChartTab &&
|
|
1953
|
+
(seriesStrategy !== SeriesStrategy.entity || !this.showLegend) &&
|
|
1954
|
+
selectedEntityNames.length === 1 &&
|
|
1955
|
+
(showEntityAnnotation ||
|
|
1956
|
+
this.canChangeEntity ||
|
|
1957
|
+
this.canSelectMultipleEntities)
|
|
1958
|
+
)
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
@computed get shouldAddTimeSuffixToTitle(): boolean {
|
|
1962
|
+
const showTimeAnnotation = !this.hideAnnotationFieldsInTitle?.time
|
|
1963
|
+
return (
|
|
1964
|
+
!this.forceHideAnnotationFieldsInTitle?.time &&
|
|
1965
|
+
this.isReady &&
|
|
1966
|
+
(showTimeAnnotation ||
|
|
1967
|
+
(this.hasTimeline &&
|
|
1968
|
+
// chart types that refer to the current time only in the timeline
|
|
1969
|
+
(this.isOnDiscreteBarTab ||
|
|
1970
|
+
this.isOnStackedDiscreteBarTab ||
|
|
1971
|
+
this.isOnMarimekkoTab ||
|
|
1972
|
+
this.isOnMapTab)))
|
|
1973
|
+
)
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
@computed get shouldAddChangeInPrefixToTitle(): boolean {
|
|
1977
|
+
const showChangeInPrefix =
|
|
1978
|
+
!this.hideAnnotationFieldsInTitle?.changeInPrefix
|
|
1979
|
+
return (
|
|
1980
|
+
!this.forceHideAnnotationFieldsInTitle?.changeInPrefix &&
|
|
1981
|
+
(this.isOnLineChartTab || this.isOnSlopeChartTab) &&
|
|
1982
|
+
this.isRelativeMode &&
|
|
1983
|
+
showChangeInPrefix
|
|
1984
|
+
)
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
@computed get currentTitle(): string {
|
|
1988
|
+
let text = this.displayTitle.trim()
|
|
1989
|
+
if (text.length === 0) return text
|
|
1990
|
+
|
|
1991
|
+
// helper function to add an annotation fragment to the title
|
|
1992
|
+
// only adds a comma if the text does not end with a question mark
|
|
1993
|
+
const appendAnnotationField = (
|
|
1994
|
+
text: string,
|
|
1995
|
+
annotation: string
|
|
1996
|
+
): string => {
|
|
1997
|
+
const separator = text.endsWith("?") ? "" : ","
|
|
1998
|
+
return `${text}${separator} ${annotation}`
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (this.shouldAddEntitySuffixToTitle) {
|
|
2002
|
+
const selectedEntityNames = this.selection.selectedEntityNames
|
|
2003
|
+
const entityStr = selectedEntityNames[0]
|
|
2004
|
+
if (entityStr?.length) text = appendAnnotationField(text, entityStr)
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (this.shouldAddChangeInPrefixToTitle)
|
|
2008
|
+
text = "Change in " + lowerCaseFirstLetterUnlessAbbreviation(text)
|
|
2009
|
+
|
|
2010
|
+
if (this.shouldAddTimeSuffixToTitle && this.timeTitleSuffix)
|
|
2011
|
+
text = appendAnnotationField(text, this.timeTitleSuffix)
|
|
2012
|
+
|
|
2013
|
+
return text.trim()
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Uses some explicit and implicit information to decide whether a timeline is shown.
|
|
2018
|
+
*/
|
|
2019
|
+
@computed get hasTimeline(): boolean {
|
|
2020
|
+
// we don't have more than one distinct time point in our data, so it doesn't make sense to show a timeline
|
|
2021
|
+
if (this.times.length <= 1) return false
|
|
2022
|
+
|
|
2023
|
+
switch (this.activeTab) {
|
|
2024
|
+
// the map tab has its own `hideTimeline` option
|
|
2025
|
+
case GRAPHER_TAB_NAMES.WorldMap:
|
|
2026
|
+
return !this.map.hideTimeline
|
|
2027
|
+
|
|
2028
|
+
// use the chart-level `hideTimeline` option for the table, with some exceptions
|
|
2029
|
+
case GRAPHER_TAB_NAMES.Table:
|
|
2030
|
+
// always show the timeline for charts that plot time on the x-axis
|
|
2031
|
+
if (this.hasTimeDimension) return true
|
|
2032
|
+
return !this.hideTimeline
|
|
2033
|
+
|
|
2034
|
+
// use the chart-level `hideTimeline` option
|
|
2035
|
+
default:
|
|
2036
|
+
return !this.hideTimeline
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
@computed private get areHandlesOnSameTime(): boolean {
|
|
2041
|
+
const times = sortNumeric(this.table.timeColumn.uniqValues.slice())
|
|
2042
|
+
const [start, end] = this.timelineHandleTimeBounds.map((time) =>
|
|
2043
|
+
findClosestTime(times, time)
|
|
2044
|
+
)
|
|
2045
|
+
return start === end
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
@computed get mapColumnSlug(): string {
|
|
2049
|
+
const mapColumnSlug = this.map.columnSlug
|
|
2050
|
+
// If there's no mapColumnSlug or there is one but it's not in the dimensions array, use the first ycolumn
|
|
2051
|
+
if (
|
|
2052
|
+
!mapColumnSlug ||
|
|
2053
|
+
!this.dimensions.some((dim) => dim.columnSlug === mapColumnSlug)
|
|
2054
|
+
)
|
|
2055
|
+
return this.yColumnSlug!
|
|
2056
|
+
return mapColumnSlug
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
getColumnForProperty(property: DimensionProperty): CoreColumn | undefined {
|
|
2060
|
+
return this.dimensions.find((dim) => dim.property === property)?.column
|
|
2061
|
+
}
|
|
2062
|
+
getSlugForProperty(property: DimensionProperty): string | undefined {
|
|
2063
|
+
return this.dimensions.find((dim) => dim.property === property)
|
|
2064
|
+
?.columnSlug
|
|
2065
|
+
}
|
|
2066
|
+
@computed get yColumnsFromDimensions(): CoreColumn[] {
|
|
2067
|
+
return this.filledDimensions
|
|
2068
|
+
.filter((dim) => dim.property === DimensionProperty.y)
|
|
2069
|
+
.map((dim) => dim.column)
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
@computed get yColumnSlugs(): string[] {
|
|
2073
|
+
return this.ySlugs
|
|
2074
|
+
? this.ySlugs.split(" ")
|
|
2075
|
+
: this.dimensions
|
|
2076
|
+
.filter((dim) => dim.property === DimensionProperty.y)
|
|
2077
|
+
.map((dim) => dim.columnSlug)
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
@computed get yColumnSlug(): string | undefined {
|
|
2081
|
+
return this.ySlugs
|
|
2082
|
+
? this.ySlugs.split(" ")[0]
|
|
2083
|
+
: this.getSlugForProperty(DimensionProperty.y)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
@computed get xColumnSlug(): string | undefined {
|
|
2087
|
+
return this.xSlug ?? this.getSlugForProperty(DimensionProperty.x)
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
@computed get sizeColumnSlug(): string | undefined {
|
|
2091
|
+
return this.sizeSlug ?? this.getSlugForProperty(DimensionProperty.size)
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
@computed get colorColumnSlug(): string | undefined {
|
|
2095
|
+
return (
|
|
2096
|
+
this.colorSlug ?? this.getSlugForProperty(DimensionProperty.color)
|
|
2097
|
+
)
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
@computed get numericColorColumnSlug(): string | undefined {
|
|
2101
|
+
if (!this.colorColumnSlug) return undefined
|
|
2102
|
+
|
|
2103
|
+
const colorColumn = this.inputTable.get(this.colorColumnSlug)
|
|
2104
|
+
if (!colorColumn.isMissing && colorColumn.hasNumberFormatting)
|
|
2105
|
+
return this.colorColumnSlug
|
|
2106
|
+
|
|
2107
|
+
return undefined
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
@computed get categoricalColorColumnSlug(): string | undefined {
|
|
2111
|
+
if (!this.colorColumnSlug) return undefined
|
|
2112
|
+
return this.numericColorColumnSlug ? undefined : this.colorColumnSlug
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
@computed get yScaleType(): ScaleType | undefined {
|
|
2116
|
+
return this.yAxis.scaleType
|
|
2117
|
+
}
|
|
2118
|
+
@computed get xScaleType(): ScaleType | undefined {
|
|
2119
|
+
return this.xAxis.scaleType
|
|
2120
|
+
}
|
|
2121
|
+
@computed private get timeTitleSuffix(): string | undefined {
|
|
2122
|
+
const timeColumn = this.table.timeColumn
|
|
2123
|
+
if (timeColumn.isMissing) return undefined // Do not show year until data is loaded
|
|
2124
|
+
const { startTime, endTime } = this
|
|
2125
|
+
if (startTime === undefined || endTime === undefined) return undefined
|
|
2126
|
+
|
|
2127
|
+
const time =
|
|
2128
|
+
startTime === endTime
|
|
2129
|
+
? timeColumn.formatValue(startTime)
|
|
2130
|
+
: timeColumn.formatValue(startTime) +
|
|
2131
|
+
" to " +
|
|
2132
|
+
timeColumn.formatValue(endTime)
|
|
2133
|
+
|
|
2134
|
+
return time
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
@computed get sourcesLine(): string {
|
|
2138
|
+
return this.sourceDesc ?? this.defaultSourcesLine
|
|
2139
|
+
}
|
|
2140
|
+
// Columns that are used as a dimension in the currently active view
|
|
2141
|
+
@computed get activeColumnSlugs(): string[] {
|
|
2142
|
+
const { yColumnSlugs, xColumnSlug, sizeColumnSlug, colorColumnSlug } =
|
|
2143
|
+
this
|
|
2144
|
+
|
|
2145
|
+
return excludeUndefined([
|
|
2146
|
+
...yColumnSlugs,
|
|
2147
|
+
xColumnSlug,
|
|
2148
|
+
sizeColumnSlug,
|
|
2149
|
+
colorColumnSlug,
|
|
2150
|
+
])
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
@computed get columnsWithSourcesExtensive(): CoreColumn[] {
|
|
2154
|
+
const { yColumnSlugs, xColumnSlug, sizeColumnSlug, colorColumnSlug } =
|
|
2155
|
+
this
|
|
2156
|
+
|
|
2157
|
+
const columnSlugs = excludeUndefined([
|
|
2158
|
+
...yColumnSlugs,
|
|
2159
|
+
xColumnSlug,
|
|
2160
|
+
sizeColumnSlug,
|
|
2161
|
+
colorColumnSlug,
|
|
2162
|
+
])
|
|
2163
|
+
|
|
2164
|
+
return this.inputTable
|
|
2165
|
+
.getColumns(_.uniq(columnSlugs))
|
|
2166
|
+
.filter(
|
|
2167
|
+
(column) =>
|
|
2168
|
+
!!column.source.name || !_.isEmpty(column.def.origins)
|
|
2169
|
+
)
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
set facetStrategy(facet: FacetStrategy) {
|
|
2173
|
+
this.selectedFacetStrategy = facet
|
|
2174
|
+
}
|
|
2175
|
+
set baseFontSize(val: number) {
|
|
2176
|
+
this._baseFontSize = val
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
getColumnSlugsForCondensedSources(): string[] {
|
|
2180
|
+
const { xColumnSlug, sizeColumnSlug, colorColumnSlug, hasMarimekko } =
|
|
2181
|
+
this
|
|
2182
|
+
const columnSlugs: string[] = []
|
|
2183
|
+
|
|
2184
|
+
// exclude "Countries Continent" if it's used as the color dimension in a scatter plot, slope chart etc.
|
|
2185
|
+
if (
|
|
2186
|
+
colorColumnSlug !== undefined &&
|
|
2187
|
+
!isContinentsVariableId(colorColumnSlug)
|
|
2188
|
+
)
|
|
2189
|
+
columnSlugs.push(colorColumnSlug)
|
|
2190
|
+
|
|
2191
|
+
if (xColumnSlug !== undefined) {
|
|
2192
|
+
const xColumn = this.inputTable.get(xColumnSlug)
|
|
2193
|
+
.def as OwidColumnDef
|
|
2194
|
+
// exclude population variable if it's used as the x dimension in a marimekko
|
|
2195
|
+
if (
|
|
2196
|
+
!hasMarimekko ||
|
|
2197
|
+
!isPopulationVariableETLPath(xColumn?.catalogPath ?? "")
|
|
2198
|
+
)
|
|
2199
|
+
columnSlugs.push(xColumnSlug)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// exclude population variable if it's used as the size dimension in a scatter plot
|
|
2203
|
+
if (sizeColumnSlug !== undefined) {
|
|
2204
|
+
const sizeColumn = this.inputTable.get(sizeColumnSlug)
|
|
2205
|
+
.def as OwidColumnDef
|
|
2206
|
+
if (!isPopulationVariableETLPath(sizeColumn?.catalogPath ?? ""))
|
|
2207
|
+
columnSlugs.push(sizeColumnSlug)
|
|
2208
|
+
}
|
|
2209
|
+
return columnSlugs
|
|
2210
|
+
}
|
|
2211
|
+
@computed get columnsWithSourcesCondensed(): CoreColumn[] {
|
|
2212
|
+
const { yColumnSlugs } = this
|
|
2213
|
+
|
|
2214
|
+
const columnSlugs = [...yColumnSlugs]
|
|
2215
|
+
columnSlugs.push(...this.getColumnSlugsForCondensedSources())
|
|
2216
|
+
|
|
2217
|
+
return this.inputTable
|
|
2218
|
+
.getColumns(_.uniq(columnSlugs))
|
|
2219
|
+
.filter(
|
|
2220
|
+
(column) =>
|
|
2221
|
+
!!column.source.name || !_.isEmpty(column.def.origins)
|
|
2222
|
+
)
|
|
2223
|
+
}
|
|
2224
|
+
@computed private get defaultSourcesLine(): string {
|
|
2225
|
+
const attributions = this.columnsWithSourcesCondensed.flatMap(
|
|
2226
|
+
(column) => {
|
|
2227
|
+
const { presentation = {} } = column.def
|
|
2228
|
+
// if the variable metadata specifies an attribution on the
|
|
2229
|
+
// variable level then this is preferred over assembling it from
|
|
2230
|
+
// the source and origins
|
|
2231
|
+
if (
|
|
2232
|
+
presentation.attribution !== undefined &&
|
|
2233
|
+
presentation.attribution !== ""
|
|
2234
|
+
)
|
|
2235
|
+
return [presentation.attribution]
|
|
2236
|
+
else {
|
|
2237
|
+
const originFragments = getOriginAttributionFragments(
|
|
2238
|
+
column.def.origins
|
|
2239
|
+
)
|
|
2240
|
+
return [column.source.name, ...originFragments]
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
)
|
|
2244
|
+
|
|
2245
|
+
const uniqueAttributions = _.uniq(_.compact(attributions))
|
|
2246
|
+
|
|
2247
|
+
if (uniqueAttributions.length > 3)
|
|
2248
|
+
return `${uniqueAttributions[0]} and other sources`
|
|
2249
|
+
|
|
2250
|
+
return uniqueAttributions.join("; ")
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
@computed private get axisDimensions(): ChartDimension[] {
|
|
2254
|
+
return this.filledDimensions.filter(
|
|
2255
|
+
(dim) =>
|
|
2256
|
+
dim.property === DimensionProperty.y ||
|
|
2257
|
+
dim.property === DimensionProperty.x
|
|
2258
|
+
)
|
|
2259
|
+
}
|
|
2260
|
+
@computed get yColumnsFromDimensionsOrSlugsOrAuto(): CoreColumn[] {
|
|
2261
|
+
return this.yColumnsFromDimensions.length
|
|
2262
|
+
? this.yColumnsFromDimensions
|
|
2263
|
+
: this.table.getColumns(autoDetectYColumnSlugs(this))
|
|
2264
|
+
}
|
|
2265
|
+
@computed get defaultTitle(): string {
|
|
2266
|
+
const yColumns = this.yColumnsFromDimensionsOrSlugsOrAuto
|
|
2267
|
+
|
|
2268
|
+
if (this.isScatter)
|
|
2269
|
+
return this.axisDimensions
|
|
2270
|
+
.map(
|
|
2271
|
+
(dimension) =>
|
|
2272
|
+
dimension.column.titlePublicOrDisplayName.title
|
|
2273
|
+
)
|
|
2274
|
+
.join(" vs. ")
|
|
2275
|
+
|
|
2276
|
+
const uniqueDatasetNames = _.uniq(
|
|
2277
|
+
excludeUndefined(
|
|
2278
|
+
yColumns.map((col) => (col.def as OwidColumnDef).datasetName)
|
|
2279
|
+
)
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
if (this.hasMultipleYColumns && uniqueDatasetNames.length === 1)
|
|
2283
|
+
return uniqueDatasetNames[0]
|
|
2284
|
+
|
|
2285
|
+
if (yColumns.length === 2)
|
|
2286
|
+
return yColumns
|
|
2287
|
+
.map((col) => col.titlePublicOrDisplayName.title)
|
|
2288
|
+
.join(" and ")
|
|
2289
|
+
|
|
2290
|
+
return yColumns
|
|
2291
|
+
.map((col) => col.titlePublicOrDisplayName.title)
|
|
2292
|
+
.join(", ")
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
@computed get displayTitle(): string {
|
|
2296
|
+
if (this.title) return this.title
|
|
2297
|
+
if (this.isReady) return this.defaultTitle
|
|
2298
|
+
return ""
|
|
2299
|
+
}
|
|
2300
|
+
// Returns an object ready to be serialized to JSON
|
|
2301
|
+
@computed get object(): GrapherInterface {
|
|
2302
|
+
return this.toObject()
|
|
2303
|
+
}
|
|
2304
|
+
@computed get isLineChart(): boolean {
|
|
2305
|
+
return (
|
|
2306
|
+
this.chartType === GRAPHER_CHART_TYPES.LineChart || !this.chartType
|
|
2307
|
+
)
|
|
2308
|
+
}
|
|
2309
|
+
@computed get isScatter(): boolean {
|
|
2310
|
+
return this.chartType === GRAPHER_CHART_TYPES.ScatterPlot
|
|
2311
|
+
}
|
|
2312
|
+
@computed get isStackedArea(): boolean {
|
|
2313
|
+
return this.chartType === GRAPHER_CHART_TYPES.StackedArea
|
|
2314
|
+
}
|
|
2315
|
+
@computed get isSlopeChart(): boolean {
|
|
2316
|
+
return this.chartType === GRAPHER_CHART_TYPES.SlopeChart
|
|
2317
|
+
}
|
|
2318
|
+
@computed get isDiscreteBar(): boolean {
|
|
2319
|
+
return this.chartType === GRAPHER_CHART_TYPES.DiscreteBar
|
|
2320
|
+
}
|
|
2321
|
+
@computed get isStackedBar(): boolean {
|
|
2322
|
+
return this.chartType === GRAPHER_CHART_TYPES.StackedBar
|
|
2323
|
+
}
|
|
2324
|
+
@computed get isMarimekko(): boolean {
|
|
2325
|
+
return this.chartType === GRAPHER_CHART_TYPES.Marimekko
|
|
2326
|
+
}
|
|
2327
|
+
@computed get isStackedDiscreteBar(): boolean {
|
|
2328
|
+
return this.chartType === GRAPHER_CHART_TYPES.StackedDiscreteBar
|
|
2329
|
+
}
|
|
2330
|
+
@computed get isOnLineChartTab(): boolean {
|
|
2331
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.LineChart
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
@computed get isOnScatterTab(): boolean {
|
|
2335
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.ScatterPlot
|
|
2336
|
+
}
|
|
2337
|
+
@computed get isOnStackedAreaTab(): boolean {
|
|
2338
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.StackedArea
|
|
2339
|
+
}
|
|
2340
|
+
@computed get isOnSlopeChartTab(): boolean {
|
|
2341
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.SlopeChart
|
|
2342
|
+
}
|
|
2343
|
+
@computed get isOnDiscreteBarTab(): boolean {
|
|
2344
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.DiscreteBar
|
|
2345
|
+
}
|
|
2346
|
+
@computed get isOnStackedBarTab(): boolean {
|
|
2347
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.StackedBar
|
|
2348
|
+
}
|
|
2349
|
+
@computed get isOnMarimekkoTab(): boolean {
|
|
2350
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.Marimekko
|
|
2351
|
+
}
|
|
2352
|
+
@computed get isOnStackedDiscreteBarTab(): boolean {
|
|
2353
|
+
return this.activeChartType === GRAPHER_CHART_TYPES.StackedDiscreteBar
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
@computed get hasLineChart(): boolean {
|
|
2357
|
+
return this.validChartTypeSet.has(GRAPHER_CHART_TYPES.LineChart)
|
|
2358
|
+
}
|
|
2359
|
+
@computed get hasSlopeChart(): boolean {
|
|
2360
|
+
return this.validChartTypeSet.has(GRAPHER_CHART_TYPES.SlopeChart)
|
|
2361
|
+
}
|
|
2362
|
+
@computed get hasDiscreteBar(): boolean {
|
|
2363
|
+
return this.validChartTypeSet.has(GRAPHER_CHART_TYPES.DiscreteBar)
|
|
2364
|
+
}
|
|
2365
|
+
@computed get hasMarimekko(): boolean {
|
|
2366
|
+
return this.validChartTypeSet.has(GRAPHER_CHART_TYPES.Marimekko)
|
|
2367
|
+
}
|
|
2368
|
+
@computed get hasScatter(): boolean {
|
|
2369
|
+
return this.validChartTypeSet.has(GRAPHER_CHART_TYPES.ScatterPlot)
|
|
2370
|
+
}
|
|
2371
|
+
@computed get supportsMultipleYColumns(): boolean {
|
|
2372
|
+
return !this.isScatter
|
|
2373
|
+
}
|
|
2374
|
+
@computed get xDimension(): ChartDimension | undefined {
|
|
2375
|
+
return this.filledDimensions.find(
|
|
2376
|
+
(d) => d.property === DimensionProperty.x
|
|
2377
|
+
)
|
|
2378
|
+
}
|
|
2379
|
+
// todo: this is only relevant for scatter plots and Marimekko. move to scatter plot class?
|
|
2380
|
+
// todo: remove this. Should be done as a simple column transform at the data level.
|
|
2381
|
+
// Possible to override the x axis dimension to target a special year
|
|
2382
|
+
// In case you want to graph say, education in the past and democracy today https://ourworldindata.org/grapher/correlation-between-education-and-democracy
|
|
2383
|
+
@computed get xOverrideTime(): number | undefined {
|
|
2384
|
+
return this.xDimension?.targetYear
|
|
2385
|
+
}
|
|
2386
|
+
// todo: this is only relevant for scatter plots and Marimekko. move to scatter plot class?
|
|
2387
|
+
set xOverrideTime(value: number | undefined) {
|
|
2388
|
+
this.xDimension!.targetYear = value
|
|
2389
|
+
}
|
|
2390
|
+
@computed get defaultBounds(): Bounds {
|
|
2391
|
+
return new Bounds(0, 0, DEFAULT_GRAPHER_WIDTH, DEFAULT_GRAPHER_HEIGHT)
|
|
2392
|
+
}
|
|
2393
|
+
@computed get hasYDimension(): boolean {
|
|
2394
|
+
return this.dimensions.some((d) => d.property === DimensionProperty.y)
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
generateStaticSvg(
|
|
2398
|
+
renderToHtmlString: (element: React.ReactElement) => string
|
|
2399
|
+
): string {
|
|
2400
|
+
const _isExportingToSvgOrPng = this.isExportingToSvgOrPng
|
|
2401
|
+
this.isExportingToSvgOrPng = true
|
|
2402
|
+
|
|
2403
|
+
const innerHTML = renderToHtmlString(<Chart manager={this} />)
|
|
2404
|
+
|
|
2405
|
+
this.isExportingToSvgOrPng = _isExportingToSvgOrPng
|
|
2406
|
+
return innerHTML
|
|
2407
|
+
}
|
|
2408
|
+
@computed get staticBoundsWithDetails(): Bounds {
|
|
2409
|
+
const includeDetails =
|
|
2410
|
+
this.shouldIncludeDetailsInStaticExport &&
|
|
2411
|
+
!_.isEmpty(this.detailRenderers)
|
|
2412
|
+
|
|
2413
|
+
let height = this.staticBounds.height
|
|
2414
|
+
if (includeDetails) {
|
|
2415
|
+
height +=
|
|
2416
|
+
2 * this.framePaddingVertical +
|
|
2417
|
+
sumTextWrapHeights(
|
|
2418
|
+
this.detailRenderers,
|
|
2419
|
+
STATIC_EXPORT_DETAIL_SPACING
|
|
2420
|
+
)
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
return new Bounds(0, 0, this.staticBounds.width, height)
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
rasterize: GrapherRasterizeFn = ({ includeDetails }) => {
|
|
2427
|
+
const _shouldIncludeDetailsInStaticExport =
|
|
2428
|
+
this.shouldIncludeDetailsInStaticExport
|
|
2429
|
+
this.shouldIncludeDetailsInStaticExport = includeDetails
|
|
2430
|
+
|
|
2431
|
+
const { width, height } = this.staticBoundsWithDetails
|
|
2432
|
+
|
|
2433
|
+
try {
|
|
2434
|
+
// We need to ensure `rasterize` is only called on the client-side, otherwise this will fail
|
|
2435
|
+
const staticSVG = this.generateStaticSvg(
|
|
2436
|
+
reactRenderToStringClientOnly
|
|
2437
|
+
)
|
|
2438
|
+
return new StaticChartRasterizer(staticSVG, width, height).render()
|
|
2439
|
+
} finally {
|
|
2440
|
+
this.shouldIncludeDetailsInStaticExport =
|
|
2441
|
+
_shouldIncludeDetailsInStaticExport
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
@computed get disableIntroAnimation(): boolean {
|
|
2445
|
+
return this.isStatic
|
|
2446
|
+
}
|
|
2447
|
+
@computed get mapConfig(): MapConfig {
|
|
2448
|
+
return this.map
|
|
2449
|
+
}
|
|
2450
|
+
@computed get cacheTag(): string {
|
|
2451
|
+
return this.version.toString()
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
@computed get relativeToggleLabel(): string {
|
|
2455
|
+
if (this.isOnScatterTab) return "Display average annual change"
|
|
2456
|
+
else if (this.isOnLineChartTab || this.isOnSlopeChartTab)
|
|
2457
|
+
return "Display relative change"
|
|
2458
|
+
return "Display relative values"
|
|
2459
|
+
}
|
|
2460
|
+
// NB: The timeline scatterplot in relative mode calculates changes relative
|
|
2461
|
+
// to the lower bound year rather than creating an arrow chart
|
|
2462
|
+
@computed get isRelativeMode(): boolean {
|
|
2463
|
+
// don't allow relative mode in some cases
|
|
2464
|
+
if (
|
|
2465
|
+
this.hasSingleMetricInFacets ||
|
|
2466
|
+
this.hasSingleEntityInFacets ||
|
|
2467
|
+
this.isStackedChartSplitByMetric
|
|
2468
|
+
)
|
|
2469
|
+
return false
|
|
2470
|
+
return this.stackMode === StackMode.relative
|
|
2471
|
+
}
|
|
2472
|
+
@computed get canToggleRelativeMode(): boolean {
|
|
2473
|
+
const {
|
|
2474
|
+
isOnLineChartTab,
|
|
2475
|
+
isOnSlopeChartTab,
|
|
2476
|
+
hideRelativeToggle,
|
|
2477
|
+
areHandlesOnSameTime,
|
|
2478
|
+
yScaleType,
|
|
2479
|
+
hasSingleEntityInFacets,
|
|
2480
|
+
hasSingleMetricInFacets,
|
|
2481
|
+
xColumnSlug,
|
|
2482
|
+
isOnMarimekkoTab,
|
|
2483
|
+
isStackedChartSplitByMetric,
|
|
2484
|
+
} = this
|
|
2485
|
+
|
|
2486
|
+
if (isOnLineChartTab || isOnSlopeChartTab)
|
|
2487
|
+
return (
|
|
2488
|
+
!hideRelativeToggle &&
|
|
2489
|
+
!areHandlesOnSameTime &&
|
|
2490
|
+
yScaleType !== ScaleType.log
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
// actually trying to exclude relative mode with just one metric or entity
|
|
2494
|
+
if (
|
|
2495
|
+
hasSingleEntityInFacets ||
|
|
2496
|
+
hasSingleMetricInFacets ||
|
|
2497
|
+
isStackedChartSplitByMetric
|
|
2498
|
+
)
|
|
2499
|
+
return false
|
|
2500
|
+
|
|
2501
|
+
if (isOnMarimekkoTab && xColumnSlug === undefined) return false
|
|
2502
|
+
return !hideRelativeToggle
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// Filter data to what can be display on the map (across all times)
|
|
2506
|
+
@computed get mappableData(): OwidVariableRow<any>[] {
|
|
2507
|
+
return this.inputTable
|
|
2508
|
+
.get(this.mapColumnSlug)
|
|
2509
|
+
.owidRows.filter((row) => isOnTheMap(row.entityName))
|
|
2510
|
+
}
|
|
2511
|
+
@computed get isMobile(): boolean {
|
|
2512
|
+
return isMobile()
|
|
2513
|
+
}
|
|
2514
|
+
@computed get isTouchDevice(): boolean {
|
|
2515
|
+
return isTouchDevice()
|
|
2516
|
+
}
|
|
2517
|
+
_externalBounds: Bounds | undefined = undefined
|
|
2518
|
+
/** externalBounds should be set to the available plotting area for a
|
|
2519
|
+
Grapher that resizes itself to fit. When this area changes,
|
|
2520
|
+
externalBounds should be updated. Updating externalBounds can
|
|
2521
|
+
trigger a bunch of somewhat expensive recalculations so it might
|
|
2522
|
+
be worth debouncing updates (e.g. when drag-resizing) */
|
|
2523
|
+
@computed get externalBounds(): Bounds {
|
|
2524
|
+
const { _externalBounds, initialOptions } = this
|
|
2525
|
+
return (
|
|
2526
|
+
_externalBounds ?? initialOptions.bounds ?? DEFAULT_GRAPHER_BOUNDS
|
|
2527
|
+
)
|
|
2528
|
+
}
|
|
2529
|
+
set externalBounds(bounds: Bounds) {
|
|
2530
|
+
this._externalBounds = bounds
|
|
2531
|
+
}
|
|
2532
|
+
@computed get isPortrait(): boolean {
|
|
2533
|
+
return (
|
|
2534
|
+
this.externalBounds.width < this.externalBounds.height &&
|
|
2535
|
+
this.externalBounds.width < DEFAULT_GRAPHER_WIDTH
|
|
2536
|
+
)
|
|
2537
|
+
}
|
|
2538
|
+
@computed private get widthForDeviceOrientation(): number {
|
|
2539
|
+
return this.isPortrait ? 400 : 680
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
@computed private get heightForDeviceOrientation(): number {
|
|
2543
|
+
return this.isPortrait ? 640 : 480
|
|
2544
|
+
}
|
|
2545
|
+
@computed private get useIdealBounds(): boolean {
|
|
2546
|
+
const {
|
|
2547
|
+
isEditor,
|
|
2548
|
+
isExportingToSvgOrPng,
|
|
2549
|
+
externalBounds,
|
|
2550
|
+
widthForDeviceOrientation,
|
|
2551
|
+
heightForDeviceOrientation,
|
|
2552
|
+
isInIFrame,
|
|
2553
|
+
isInFullScreenMode,
|
|
2554
|
+
windowInnerWidth,
|
|
2555
|
+
windowInnerHeight,
|
|
2556
|
+
} = this
|
|
2557
|
+
|
|
2558
|
+
// In full-screen mode, we usually use all space available to us
|
|
2559
|
+
// We use the ideal bounds only if the available space is very large
|
|
2560
|
+
if (isInFullScreenMode) {
|
|
2561
|
+
if (
|
|
2562
|
+
windowInnerHeight! > 2 * heightForDeviceOrientation &&
|
|
2563
|
+
windowInnerWidth! > 2 * widthForDeviceOrientation
|
|
2564
|
+
)
|
|
2565
|
+
return true
|
|
2566
|
+
return false
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// For these, defer to the bounds that are set externally
|
|
2570
|
+
if (
|
|
2571
|
+
this.isEmbeddedInADataPage ||
|
|
2572
|
+
this.isEmbeddedInAnOwidPage ||
|
|
2573
|
+
this.manager ||
|
|
2574
|
+
isInIFrame
|
|
2575
|
+
)
|
|
2576
|
+
return false
|
|
2577
|
+
|
|
2578
|
+
// If the user is using interactive version and then goes to export chart, use current bounds to maintain WYSIWYG
|
|
2579
|
+
if (isExportingToSvgOrPng) return false
|
|
2580
|
+
|
|
2581
|
+
// In the editor, we usually want ideal bounds, except when we're rendering a static preview;
|
|
2582
|
+
// in that case, we want to use the given static bounds
|
|
2583
|
+
if (isEditor) return !this.isExportingToSvgOrPng
|
|
2584
|
+
|
|
2585
|
+
// If the available space is very small, we use all of the space given to us
|
|
2586
|
+
if (
|
|
2587
|
+
externalBounds.height < heightForDeviceOrientation ||
|
|
2588
|
+
externalBounds.width < widthForDeviceOrientation
|
|
2589
|
+
)
|
|
2590
|
+
return false
|
|
2591
|
+
|
|
2592
|
+
return true
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// If we have a big screen to be in, we can define our own aspect ratio and sit in the center
|
|
2596
|
+
@computed private get scaleToFitIdeal(): number {
|
|
2597
|
+
return Math.min(
|
|
2598
|
+
(this.availableWidth * 0.95) / this.widthForDeviceOrientation,
|
|
2599
|
+
(this.availableHeight * 0.95) / this.heightForDeviceOrientation
|
|
2600
|
+
)
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
@computed private get fullScreenPadding(): number {
|
|
2604
|
+
const { windowInnerWidth } = this
|
|
2605
|
+
if (!windowInnerWidth) return 0
|
|
2606
|
+
return windowInnerWidth < 940 ? 0 : 40
|
|
2607
|
+
}
|
|
2608
|
+
@computed get hideFullScreenButton(): boolean {
|
|
2609
|
+
if (this.isInFullScreenMode) return false
|
|
2610
|
+
if (!this.isSmall) return false
|
|
2611
|
+
// hide the full screen button if the full screen height
|
|
2612
|
+
// is barely larger than the current chart height
|
|
2613
|
+
const fullScreenHeight = this.windowInnerHeight!
|
|
2614
|
+
return fullScreenHeight < this.frameBounds.height + 80
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
@computed private get availableWidth(): number {
|
|
2618
|
+
const {
|
|
2619
|
+
externalBounds,
|
|
2620
|
+
isInFullScreenMode,
|
|
2621
|
+
windowInnerWidth,
|
|
2622
|
+
fullScreenPadding,
|
|
2623
|
+
} = this
|
|
2624
|
+
|
|
2625
|
+
return Math.floor(
|
|
2626
|
+
isInFullScreenMode
|
|
2627
|
+
? windowInnerWidth! - 2 * fullScreenPadding
|
|
2628
|
+
: externalBounds.width
|
|
2629
|
+
)
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
@computed private get availableHeight(): number {
|
|
2633
|
+
const {
|
|
2634
|
+
externalBounds,
|
|
2635
|
+
isInFullScreenMode,
|
|
2636
|
+
windowInnerHeight,
|
|
2637
|
+
fullScreenPadding,
|
|
2638
|
+
} = this
|
|
2639
|
+
|
|
2640
|
+
return Math.floor(
|
|
2641
|
+
isInFullScreenMode
|
|
2642
|
+
? windowInnerHeight! - 2 * fullScreenPadding
|
|
2643
|
+
: externalBounds.height
|
|
2644
|
+
)
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
@computed private get idealWidth(): number {
|
|
2648
|
+
return Math.floor(this.widthForDeviceOrientation * this.scaleToFitIdeal)
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
@computed private get idealHeight(): number {
|
|
2652
|
+
return Math.floor(
|
|
2653
|
+
this.heightForDeviceOrientation * this.scaleToFitIdeal
|
|
2654
|
+
)
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/** Bounds of the entire Grapher frame including the chart area and entity selector panel */
|
|
2658
|
+
@computed get frameBounds(): Bounds {
|
|
2659
|
+
return this.useIdealBounds
|
|
2660
|
+
? new Bounds(0, 0, this.idealWidth, this.idealHeight)
|
|
2661
|
+
: new Bounds(0, 0, this.availableWidth, this.availableHeight)
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
@computed get activeBounds(): Bounds {
|
|
2665
|
+
return this.isExportingToSvgOrPng ? this.staticBounds : this.frameBounds
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/** Bounds of the CaptionedChart that renders the header, chart area and footer */
|
|
2669
|
+
@computed get captionedChartBounds(): Bounds {
|
|
2670
|
+
// if there's no panel, the chart takes up the whole frame
|
|
2671
|
+
if (!this.isEntitySelectorPanelActive) return this.frameBounds
|
|
2672
|
+
|
|
2673
|
+
return new Bounds(
|
|
2674
|
+
0,
|
|
2675
|
+
0,
|
|
2676
|
+
// the chart takes up 9 columns in 12-column grid
|
|
2677
|
+
(9 / 12) * this.frameBounds.width,
|
|
2678
|
+
this.frameBounds.height - 2 // 2px accounts for the border
|
|
2679
|
+
)
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
@computed get chartAreaPadding(): number {
|
|
2683
|
+
// Choose padding based on chart size, ensuring it's at most 24px
|
|
2684
|
+
return Math.min(24, Math.ceil(0.025 * this.activeBounds.width))
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/** Bounds of the chart area if no CaptionedChart is rendered */
|
|
2688
|
+
@computed get chartAreaBounds(): Bounds {
|
|
2689
|
+
return this.activeBounds.pad(this.chartAreaPadding)
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/** Bounds of the entity selector if rendered into the side panel */
|
|
2693
|
+
@computed get sidePanelBounds(): Bounds | undefined {
|
|
2694
|
+
if (!this.isEntitySelectorPanelActive) return
|
|
2695
|
+
|
|
2696
|
+
return new Bounds(
|
|
2697
|
+
0, // not in use; intentionally set to zero
|
|
2698
|
+
0, // not in use; intentionally set to zero
|
|
2699
|
+
this.frameBounds.width - this.captionedChartBounds.width,
|
|
2700
|
+
this.captionedChartBounds.height
|
|
2701
|
+
)
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
base = React.createRef<HTMLDivElement>()
|
|
2705
|
+
@computed get containerElement(): HTMLDivElement | undefined {
|
|
2706
|
+
return this.base.current || undefined
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// private hasLoggedGAViewEvent = false
|
|
2710
|
+
// @observable private hasBeenVisible = false
|
|
2711
|
+
// @observable private uncaughtError?: Error
|
|
2712
|
+
@computed private get analyticsContext(): GrapherAnalyticsContext {
|
|
2713
|
+
const ctx = this.manager?.analyticsContext
|
|
2714
|
+
return {
|
|
2715
|
+
slug: ctx?.mdimSlug ?? this.slug,
|
|
2716
|
+
viewConfigId: ctx?.mdimViewConfigId,
|
|
2717
|
+
narrativeChartName: this.narrativeChartInfo?.name,
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
logEntitySelectorEvent(action: EntitySelectorEvent, target?: string): void {
|
|
2722
|
+
this.analytics.logEntitySelectorEvent(action, {
|
|
2723
|
+
...this.analyticsContext,
|
|
2724
|
+
target,
|
|
2725
|
+
})
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
logImageDownloadEvent(action: GrapherImageDownloadEvent): void {
|
|
2729
|
+
this.analytics.logGrapherImageDownloadEvent(action, {
|
|
2730
|
+
...this.analyticsContext,
|
|
2731
|
+
context: omitUndefinedValues({
|
|
2732
|
+
tab: this.activeTab,
|
|
2733
|
+
globe: this.isOnMapTab
|
|
2734
|
+
? this.mapConfig.globe.isActive
|
|
2735
|
+
: undefined,
|
|
2736
|
+
}),
|
|
2737
|
+
})
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
logGrapherInteractionEvent(
|
|
2741
|
+
action: GrapherInteractionEvent,
|
|
2742
|
+
target?: string
|
|
2743
|
+
): void {
|
|
2744
|
+
this.analytics.logGrapherInteractionEvent(action, {
|
|
2745
|
+
...this.analyticsContext,
|
|
2746
|
+
target,
|
|
2747
|
+
})
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// @action.bound setError(err: Error): void {
|
|
2751
|
+
// this.uncaughtError = err
|
|
2752
|
+
// }
|
|
2753
|
+
// @action.bound clearErrors(): void {
|
|
2754
|
+
// this.uncaughtError = undefined
|
|
2755
|
+
// }
|
|
2756
|
+
// private get commandPalette(): React.ReactElement | null {
|
|
2757
|
+
// return this.props.enableKeyboardShortcuts ? (
|
|
2758
|
+
// <CommandPalette commands={this.keyboardShortcuts} display="none" />
|
|
2759
|
+
// ) : null
|
|
2760
|
+
// }
|
|
2761
|
+
formatTimeFn(time: Time): string {
|
|
2762
|
+
return this.inputTable.timeColumn.formatTime(time)
|
|
2763
|
+
}
|
|
2764
|
+
@computed get availableEntityNames(): EntityName[] {
|
|
2765
|
+
return this.tableForSelection.availableEntityNames
|
|
2766
|
+
}
|
|
2767
|
+
@computed get entityRegionTypeGroups(): EntityRegionTypeGroup[] {
|
|
2768
|
+
return groupEntityNamesByRegionType(this.availableEntityNames)
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
@computed get entityNamesByRegionType(): EntityNamesByRegionType {
|
|
2772
|
+
return new Map(
|
|
2773
|
+
this.entityRegionTypeGroups.map(({ regionType, entityNames }) => [
|
|
2774
|
+
regionType,
|
|
2775
|
+
entityNames,
|
|
2776
|
+
])
|
|
2777
|
+
)
|
|
2778
|
+
}
|
|
2779
|
+
slideShow: SlideShowController<any> | undefined = undefined
|
|
2780
|
+
@computed get _sortConfig(): Readonly<SortConfig> {
|
|
2781
|
+
return {
|
|
2782
|
+
sortBy: this.sortBy ?? SortBy.total,
|
|
2783
|
+
sortOrder: this.sortOrder ?? SortOrder.desc,
|
|
2784
|
+
sortColumnSlug: this.sortColumnSlug,
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
@computed get sortConfig(): SortConfig {
|
|
2789
|
+
const sortConfig = { ...this._sortConfig }
|
|
2790
|
+
// In relative mode, where the values for every entity sum up to 100%, sorting by total
|
|
2791
|
+
// doesn't make sense. It's also jumpy because of some rounding errors. For this reason,
|
|
2792
|
+
// we sort by entity name instead.
|
|
2793
|
+
// Marimekko charts are special and there we don't do this forcing of sort order
|
|
2794
|
+
if (
|
|
2795
|
+
!this.isOnMarimekkoTab &&
|
|
2796
|
+
this.isRelativeMode &&
|
|
2797
|
+
sortConfig.sortBy === SortBy.total
|
|
2798
|
+
) {
|
|
2799
|
+
sortConfig.sortBy = SortBy.entityName
|
|
2800
|
+
sortConfig.sortOrder = SortOrder.asc
|
|
2801
|
+
}
|
|
2802
|
+
return sortConfig
|
|
2803
|
+
}
|
|
2804
|
+
@computed get hasMultipleYColumns(): boolean {
|
|
2805
|
+
return this.yColumnSlugs.length > 1
|
|
2806
|
+
}
|
|
2807
|
+
@computed private get hasSingleMetricInFacets(): boolean {
|
|
2808
|
+
const {
|
|
2809
|
+
isOnStackedDiscreteBarTab,
|
|
2810
|
+
isOnStackedAreaTab,
|
|
2811
|
+
isOnStackedBarTab,
|
|
2812
|
+
selectedFacetStrategy,
|
|
2813
|
+
hasMultipleYColumns,
|
|
2814
|
+
} = this
|
|
2815
|
+
|
|
2816
|
+
if (isOnStackedDiscreteBarTab) {
|
|
2817
|
+
return (
|
|
2818
|
+
selectedFacetStrategy === FacetStrategy.entity ||
|
|
2819
|
+
selectedFacetStrategy === FacetStrategy.metric
|
|
2820
|
+
)
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
if (isOnStackedAreaTab || isOnStackedBarTab) {
|
|
2824
|
+
return (
|
|
2825
|
+
selectedFacetStrategy === FacetStrategy.entity &&
|
|
2826
|
+
!hasMultipleYColumns
|
|
2827
|
+
)
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
return false
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
@computed private get hasSingleEntityInFacets(): boolean {
|
|
2834
|
+
const {
|
|
2835
|
+
isOnStackedAreaTab,
|
|
2836
|
+
isOnStackedBarTab,
|
|
2837
|
+
selectedFacetStrategy,
|
|
2838
|
+
selection,
|
|
2839
|
+
} = this
|
|
2840
|
+
|
|
2841
|
+
if (isOnStackedAreaTab || isOnStackedBarTab) {
|
|
2842
|
+
return (
|
|
2843
|
+
selectedFacetStrategy === FacetStrategy.metric &&
|
|
2844
|
+
selection.numSelectedEntities === 1
|
|
2845
|
+
)
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
return false
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// TODO: remove once #2136 is fixed
|
|
2852
|
+
// issue #2136 describes a serious bug that relates to relative mode and
|
|
2853
|
+
// affects all stacked area/bar charts that are split by metric. for now,
|
|
2854
|
+
// we simply turn off relative mode in such cases. once the bug is properly
|
|
2855
|
+
// addressed, this computed property and its references can be removed
|
|
2856
|
+
@computed
|
|
2857
|
+
private get isStackedChartSplitByMetric(): boolean {
|
|
2858
|
+
return (
|
|
2859
|
+
(this.isOnStackedAreaTab || this.isOnStackedBarTab) &&
|
|
2860
|
+
this.selectedFacetStrategy === FacetStrategy.metric
|
|
2861
|
+
)
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
@computed get availableFacetStrategies(): FacetStrategy[] {
|
|
2865
|
+
return this.chartState.availableFacetStrategies?.length
|
|
2866
|
+
? this.chartState.availableFacetStrategies
|
|
2867
|
+
: [FacetStrategy.none]
|
|
2868
|
+
}
|
|
2869
|
+
// the actual facet setting used by a chart, potentially overriding selectedFacetStrategy
|
|
2870
|
+
@computed get facetStrategy(): FacetStrategy {
|
|
2871
|
+
if (
|
|
2872
|
+
this.selectedFacetStrategy &&
|
|
2873
|
+
this.availableFacetStrategies.includes(this.selectedFacetStrategy)
|
|
2874
|
+
)
|
|
2875
|
+
return this.selectedFacetStrategy
|
|
2876
|
+
|
|
2877
|
+
if (
|
|
2878
|
+
this.addCountryMode === EntitySelectionMode.SingleEntity &&
|
|
2879
|
+
this.selection.selectedEntityNames.length > 1
|
|
2880
|
+
) {
|
|
2881
|
+
return FacetStrategy.entity
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
if (this.availableFacetStrategies.length === 0)
|
|
2885
|
+
throw new Error("No facet strategy available")
|
|
2886
|
+
|
|
2887
|
+
return firstOfNonEmptyArray(this.availableFacetStrategies)
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
@computed get isFaceted(): boolean {
|
|
2891
|
+
// Show map facets if start and end time are different
|
|
2892
|
+
if (this.isOnMapTab) return this.startTime !== this.endTime
|
|
2893
|
+
|
|
2894
|
+
const hasFacetStrategy = this.facetStrategy !== FacetStrategy.none
|
|
2895
|
+
return this.isOnChartTab && hasFacetStrategy
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
@computed get hasMultipleSeriesPerFacet(): boolean {
|
|
2899
|
+
return (
|
|
2900
|
+
this.isFaceted &&
|
|
2901
|
+
this.selection.numSelectedEntities > 1 &&
|
|
2902
|
+
this.yColumnSlugs.length > 1
|
|
2903
|
+
)
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
@computed get isInFullScreenMode(): boolean {
|
|
2907
|
+
return this._isInFullScreenMode
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
set isInFullScreenMode(newValue: boolean) {
|
|
2911
|
+
// prevent scrolling when in full-screen mode
|
|
2912
|
+
if (newValue) {
|
|
2913
|
+
document.documentElement.classList.add("no-scroll")
|
|
2914
|
+
} else {
|
|
2915
|
+
document.documentElement.classList.remove("no-scroll")
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// dismiss the share menu
|
|
2919
|
+
this.isShareMenuActive = false
|
|
2920
|
+
|
|
2921
|
+
this._isInFullScreenMode = newValue
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
@action.bound toggleFullScreenMode(): void {
|
|
2925
|
+
this.isInFullScreenMode = !this.isInFullScreenMode
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
@action.bound dismissFullScreen(): void {
|
|
2929
|
+
// if a modal is open, dismiss it instead of exiting full-screen mode
|
|
2930
|
+
if (this.isModalOpen || this.isShareMenuActive) {
|
|
2931
|
+
this.isEntitySelectorModalOrDrawerOpen = false
|
|
2932
|
+
this.activeModal = undefined
|
|
2933
|
+
this.isShareMenuActive = false
|
|
2934
|
+
} else {
|
|
2935
|
+
this.isInFullScreenMode = false
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
@action.bound setHideExternalControlsInEmbedUrl(value: boolean): void {
|
|
2940
|
+
this.hideExternalControlsInEmbedUrl = value
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
@computed get isModalOpen(): boolean {
|
|
2944
|
+
return this.isEntitySelectorModalOpen || this.activeModal !== undefined
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// Whether a server-side download is available for the download modal
|
|
2948
|
+
@computed get isServerSideDownloadAvailable(): boolean {
|
|
2949
|
+
return (
|
|
2950
|
+
// We're not on an archival grapher page
|
|
2951
|
+
!this.isOnArchivalPage &&
|
|
2952
|
+
// We're not inside the admin
|
|
2953
|
+
window.admin === undefined &&
|
|
2954
|
+
// We're not in a narrative chart
|
|
2955
|
+
!this.narrativeChartInfo &&
|
|
2956
|
+
// We have a baseUrl to send the request to
|
|
2957
|
+
!!this.baseUrl
|
|
2958
|
+
)
|
|
2959
|
+
}
|
|
2960
|
+
_baseFontSize = BASE_FONT_SIZE
|
|
2961
|
+
@computed get baseFontSize(): number {
|
|
2962
|
+
if (this.isStatic && this.initialOptions.baseFontSize)
|
|
2963
|
+
return this.initialOptions.baseFontSize
|
|
2964
|
+
if (this.isStaticAndSmall) {
|
|
2965
|
+
return this.computeBaseFontSizeFromHeight(this.staticBounds)
|
|
2966
|
+
}
|
|
2967
|
+
if (this.isStatic) return 18
|
|
2968
|
+
return this._baseFontSize
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// the header and footer don't rely on the base font size unless explicitly specified
|
|
2972
|
+
@computed get useBaseFontSize(): boolean {
|
|
2973
|
+
return this.initialOptions.baseFontSize !== undefined
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
private computeBaseFontSizeFromHeight(bounds: Bounds): number {
|
|
2977
|
+
const squareBounds = DEFAULT_GRAPHER_BOUNDS_SQUARE
|
|
2978
|
+
const factor = squareBounds.height / 21
|
|
2979
|
+
return Math.max(10, bounds.height / factor)
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
computeBaseFontSizeFromWidth(bounds: Bounds): number {
|
|
2983
|
+
if (bounds.width <= 400) return 14
|
|
2984
|
+
else if (bounds.width < 1080) return 16
|
|
2985
|
+
else if (bounds.width >= 1080) return 18
|
|
2986
|
+
else return 16
|
|
2987
|
+
}
|
|
2988
|
+
@computed get fontSize(): number {
|
|
2989
|
+
return this.baseFontSize
|
|
2990
|
+
}
|
|
2991
|
+
@computed get isNarrow(): boolean {
|
|
2992
|
+
if (this.isStatic) return false
|
|
2993
|
+
return this.frameBounds.width <= 420
|
|
2994
|
+
}
|
|
2995
|
+
@computed get isSemiNarrow(): boolean {
|
|
2996
|
+
if (this.isStatic) return false
|
|
2997
|
+
return this.frameBounds.width <= 550
|
|
2998
|
+
}
|
|
2999
|
+
// Small charts are rendered into 6 or 7 columns in a 12-column grid layout
|
|
3000
|
+
// (e.g. side-by-side charts or charts in the All Charts block)
|
|
3001
|
+
@computed get isSmall(): boolean {
|
|
3002
|
+
if (this.isStatic) return false
|
|
3003
|
+
return this.frameBounds.width <= 740
|
|
3004
|
+
}
|
|
3005
|
+
// Medium charts are rendered into 8 columns in a 12-column grid layout
|
|
3006
|
+
// (e.g. stand-alone charts in the main text of an article)
|
|
3007
|
+
@computed get isMedium(): boolean {
|
|
3008
|
+
if (this.isStatic) return false
|
|
3009
|
+
return this.frameBounds.width <= 845
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
@computed get isStaticAndSmall(): boolean {
|
|
3013
|
+
if (!this.isStatic) return false
|
|
3014
|
+
return this.areStaticBoundsSmall
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
@computed get areStaticBoundsSmall(): boolean {
|
|
3018
|
+
const { defaultBounds, staticBounds } = this
|
|
3019
|
+
const idealPixelCount = defaultBounds.width * defaultBounds.height
|
|
3020
|
+
const staticPixelCount = staticBounds.width * staticBounds.height
|
|
3021
|
+
return staticPixelCount < 0.66 * idealPixelCount
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
@computed get isExportingForSocialMedia(): boolean {
|
|
3025
|
+
return (
|
|
3026
|
+
this.isExportingToSvgOrPng &&
|
|
3027
|
+
this.isStaticAndSmall &&
|
|
3028
|
+
this.isSocialMediaExport
|
|
3029
|
+
)
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
@computed get isExportingForWikimedia(): boolean {
|
|
3033
|
+
return this.isExportingToSvgOrPng && this.isWikimediaExport
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
@computed get backgroundColor(): Color {
|
|
3037
|
+
return this.isExportingForSocialMedia
|
|
3038
|
+
? GRAPHER_BACKGROUND_BEIGE
|
|
3039
|
+
: GRAPHER_BACKGROUND_DEFAULT
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
@computed get shouldPinTooltipToBottom(): boolean {
|
|
3043
|
+
return this.isTouchDevice
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
isShareMenuActive = false
|
|
3047
|
+
@computed get hasRelatedQuestion(): boolean {
|
|
3048
|
+
if (
|
|
3049
|
+
this.hideRelatedQuestion ||
|
|
3050
|
+
!this.relatedQuestions ||
|
|
3051
|
+
!this.relatedQuestions.length
|
|
3052
|
+
)
|
|
3053
|
+
return false
|
|
3054
|
+
const question = this.relatedQuestions[0]
|
|
3055
|
+
return !!question && !!question.text && !!question.url
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
@computed get isRelatedQuestionTargetDifferentFromCurrentPage(): boolean {
|
|
3059
|
+
// comparing paths rather than full URLs for this to work as
|
|
3060
|
+
// expected on local and staging where the origin (e.g.
|
|
3061
|
+
// hans.owid.cloud) doesn't match the production origin that has
|
|
3062
|
+
// been entered in the related question URL field:
|
|
3063
|
+
// "ourworldindata.org" and yet should still yield a match.
|
|
3064
|
+
// - Note that this won't work on production previews (where the
|
|
3065
|
+
// path is /admin/posts/preview/ID)
|
|
3066
|
+
const { relatedQuestions = [], hasRelatedQuestion } = this
|
|
3067
|
+
const relatedQuestion = relatedQuestions[0]
|
|
3068
|
+
return (
|
|
3069
|
+
hasRelatedQuestion &&
|
|
3070
|
+
!!relatedQuestion &&
|
|
3071
|
+
getWindowUrl().pathname !==
|
|
3072
|
+
Url.fromURL(relatedQuestion.url).pathname
|
|
3073
|
+
)
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
@computed get showRelatedQuestion(): boolean {
|
|
3077
|
+
return (
|
|
3078
|
+
!!this.relatedQuestions &&
|
|
3079
|
+
!!this.hasRelatedQuestion &&
|
|
3080
|
+
!!this.isRelatedQuestionTargetDifferentFromCurrentPage
|
|
3081
|
+
)
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
@action.bound clearSelection(): void {
|
|
3085
|
+
this.selection.clearSelection()
|
|
3086
|
+
this.applyOriginalSelectionAsAuthored()
|
|
3087
|
+
}
|
|
3088
|
+
@action.bound clearFocus(): void {
|
|
3089
|
+
this.focusArray.clear()
|
|
3090
|
+
this.applyOriginalFocusAsAuthored()
|
|
3091
|
+
}
|
|
3092
|
+
@action.bound clearQueryParams(): void {
|
|
3093
|
+
const { authorsVersion } = this
|
|
3094
|
+
this.tab = authorsVersion.tab
|
|
3095
|
+
this.xAxis.scaleType = authorsVersion.xAxis.scaleType
|
|
3096
|
+
this.yAxis.scaleType = authorsVersion.yAxis.scaleType
|
|
3097
|
+
this.stackMode = authorsVersion.stackMode
|
|
3098
|
+
this.zoomToSelection = authorsVersion.zoomToSelection
|
|
3099
|
+
this.compareEndPointsOnly = authorsVersion.compareEndPointsOnly
|
|
3100
|
+
this.minTime = authorsVersion.minTime
|
|
3101
|
+
this.maxTime = authorsVersion.maxTime
|
|
3102
|
+
this.map.time = authorsVersion.map.time
|
|
3103
|
+
this.map.startTime = authorsVersion.map.startTime
|
|
3104
|
+
this.map.region = authorsVersion.map.region
|
|
3105
|
+
this.showNoDataArea = authorsVersion.showNoDataArea
|
|
3106
|
+
this.dataTableConfig.filter = authorsVersion.dataTableConfig.filter
|
|
3107
|
+
this.dataTableConfig.search = authorsVersion.dataTableConfig.search
|
|
3108
|
+
this.mapConfig.globe.isActive = authorsVersion.mapConfig.globe.isActive
|
|
3109
|
+
this.clearSelection()
|
|
3110
|
+
this.clearFocus()
|
|
3111
|
+
this.mapConfig.selection.clearSelection()
|
|
3112
|
+
}
|
|
3113
|
+
// Todo: come up with a more general pattern?
|
|
3114
|
+
// The idea here is to reset the Grapher to a blank slate, so that if you updateFromObject and the object contains some blanks, those blanks
|
|
3115
|
+
// won't overwrite defaults (like type == LineChart). RAII would probably be better, but this works for now.
|
|
3116
|
+
@action.bound reset(): void {
|
|
3117
|
+
const grapherState = new GrapherState({})
|
|
3118
|
+
for (const key of grapherKeysToSerialize) {
|
|
3119
|
+
// grapherKeysToSerialize is not properly typed
|
|
3120
|
+
this[key] = grapherState[key]
|
|
3121
|
+
}
|
|
3122
|
+
this.seriesColorMap = new Map()
|
|
3123
|
+
|
|
3124
|
+
this.ySlugs = grapherState.ySlugs
|
|
3125
|
+
this.xSlug = grapherState.xSlug
|
|
3126
|
+
this.colorSlug = grapherState.colorSlug
|
|
3127
|
+
this.sizeSlug = grapherState.sizeSlug
|
|
3128
|
+
|
|
3129
|
+
this.selection.clearSelection()
|
|
3130
|
+
this.focusArray.clear()
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
debounceMode: boolean = false
|
|
3134
|
+
|
|
3135
|
+
@computed.struct get allParams(): GrapherQueryParams {
|
|
3136
|
+
return grapherObjectToQueryParams(this)
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
@computed get overlayParam(): string | undefined {
|
|
3140
|
+
if (!this.activeModal) return undefined
|
|
3141
|
+
return match(this.activeModal)
|
|
3142
|
+
.with(GrapherModal.Download, () => {
|
|
3143
|
+
return match(this.activeDownloadModalTab)
|
|
3144
|
+
.with(DownloadModalTabName.Data, () => "download-data")
|
|
3145
|
+
.with(DownloadModalTabName.Vis, () => "download-vis")
|
|
3146
|
+
.exhaustive()
|
|
3147
|
+
})
|
|
3148
|
+
.with(GrapherModal.Embed, () => {
|
|
3149
|
+
// We could include the embed modal in the `overlay=` params, but there has been an issue in the past
|
|
3150
|
+
// where we accidentally included that in the Embed dialog's URL, and then embeds would always show
|
|
3151
|
+
// the modal.
|
|
3152
|
+
// Linking directly to the modal doesn't have much of a use case, anyway.
|
|
3153
|
+
return undefined
|
|
3154
|
+
})
|
|
3155
|
+
.with(GrapherModal.Sources, () => "sources")
|
|
3156
|
+
.exhaustive()
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
@computed get areSelectedEntitiesDifferentThanAuthors(): boolean {
|
|
3160
|
+
const authoredConfig = this.legacyConfigAsAuthored
|
|
3161
|
+
const currentSelectedEntityNames = this.selection.selectedEntityNames
|
|
3162
|
+
const originalSelectedEntityNames =
|
|
3163
|
+
authoredConfig.selectedEntityNames ?? []
|
|
3164
|
+
|
|
3165
|
+
return isArrayDifferentFromReference(
|
|
3166
|
+
currentSelectedEntityNames,
|
|
3167
|
+
originalSelectedEntityNames
|
|
3168
|
+
)
|
|
3169
|
+
}
|
|
3170
|
+
@computed get areFocusedSeriesNamesDifferentThanAuthors(): boolean {
|
|
3171
|
+
const authoredConfig = this.legacyConfigAsAuthored
|
|
3172
|
+
const currentFocusedSeriesNames = this.focusArray.seriesNames
|
|
3173
|
+
const originalFocusedSeriesNames =
|
|
3174
|
+
authoredConfig.focusedSeriesNames ?? []
|
|
3175
|
+
|
|
3176
|
+
return isArrayDifferentFromReference(
|
|
3177
|
+
currentFocusedSeriesNames,
|
|
3178
|
+
originalFocusedSeriesNames
|
|
3179
|
+
)
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Autocomputed url params to reflect difference between current grapher state
|
|
3183
|
+
// and original config state
|
|
3184
|
+
@computed.struct get changedParams(): Partial<GrapherQueryParams> {
|
|
3185
|
+
return differenceObj(this.allParams, this.authorsVersion.allParams)
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// If you want to compare current state against the published grapher.
|
|
3189
|
+
@computed get authorsVersion(): GrapherState {
|
|
3190
|
+
return new GrapherState({
|
|
3191
|
+
...this.legacyConfigAsAuthored,
|
|
3192
|
+
manager: undefined,
|
|
3193
|
+
queryStr: "",
|
|
3194
|
+
})
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
@computed get queryStr(): string {
|
|
3198
|
+
return queryParamsToStr({
|
|
3199
|
+
...this.changedParams,
|
|
3200
|
+
...this.externalQueryParams,
|
|
3201
|
+
})
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
/** Static root URL of the chart, e.g. https://ourworldindata.org/grapher/life-expectancy */
|
|
3205
|
+
@computed get baseUrl(): string | undefined {
|
|
3206
|
+
if (this.isOnArchivalPage) return this.archiveContext?.archiveUrl
|
|
3207
|
+
|
|
3208
|
+
if (this.manager?.baseUrl) return this.manager.baseUrl
|
|
3209
|
+
|
|
3210
|
+
return this.isPublished
|
|
3211
|
+
? `${this.bakedGrapherURL ?? "/grapher"}/${this.displaySlug}`
|
|
3212
|
+
: undefined
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
readonly manager: GrapherManager | undefined = undefined
|
|
3216
|
+
@computed get canonicalUrlIfIsNarrativeChart(): string | undefined {
|
|
3217
|
+
if (!this.narrativeChartInfo) return undefined
|
|
3218
|
+
|
|
3219
|
+
const { parentChartSlug, queryParamsForParentChart } =
|
|
3220
|
+
this.narrativeChartInfo
|
|
3221
|
+
|
|
3222
|
+
const combinedQueryParams = {
|
|
3223
|
+
...queryParamsForParentChart,
|
|
3224
|
+
...this.changedParams,
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
return `${this.bakedGrapherURL}/${parentChartSlug}${queryParamsToStr(
|
|
3228
|
+
combinedQueryParams
|
|
3229
|
+
)}`
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/**
|
|
3233
|
+
* Full URL representing the canonical location of this grapher state,
|
|
3234
|
+
* e.g. https://ourworldindata.org/grapher/life-expectancy?tab=map
|
|
3235
|
+
*/
|
|
3236
|
+
@computed get canonicalUrl(): string | undefined {
|
|
3237
|
+
return (
|
|
3238
|
+
this.manager?.canonicalUrl ??
|
|
3239
|
+
this.canonicalUrlIfIsNarrativeChart ??
|
|
3240
|
+
(this.baseUrl ? this.baseUrl + this.queryStr : undefined)
|
|
3241
|
+
)
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
@computed get isOnCanonicalUrl(): boolean {
|
|
3245
|
+
if (!this.canonicalUrl) return false
|
|
3246
|
+
return (
|
|
3247
|
+
getWindowUrl().pathname === Url.fromURL(this.canonicalUrl).pathname
|
|
3248
|
+
)
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
private makeEmbedUrl(baseUrl: string): string {
|
|
3252
|
+
let url = Url.fromURL(baseUrl)
|
|
3253
|
+
// We want to preserve the tab in the embed URL so that if we change the
|
|
3254
|
+
// default view of the chart, it won't change existing embeds.
|
|
3255
|
+
// See https://github.com/owid/owid-grapher/issues/2805
|
|
3256
|
+
const { tab } = this.allParams
|
|
3257
|
+
if (tab && !url.queryParams.tab) {
|
|
3258
|
+
url = url.updateQueryParams({ tab })
|
|
3259
|
+
}
|
|
3260
|
+
if (this.canHideExternalControlsInEmbed) {
|
|
3261
|
+
url = url.updateQueryParams({
|
|
3262
|
+
hideControls: this.hideExternalControlsInEmbedUrl.toString(),
|
|
3263
|
+
})
|
|
3264
|
+
}
|
|
3265
|
+
return url.fullUrl
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
@computed get embedUrl(): string | undefined {
|
|
3269
|
+
const baseUrl = this.canonicalUrl
|
|
3270
|
+
if (!baseUrl) return undefined
|
|
3271
|
+
return this.makeEmbedUrl(baseUrl)
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
@computed get embedArchivedUrl(): string | undefined {
|
|
3275
|
+
if (!this.archiveContext) return undefined
|
|
3276
|
+
const baseUrl = this.archiveContext.archiveUrl + this.queryStr
|
|
3277
|
+
return this.makeEmbedUrl(baseUrl)
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
@computed get hasUserChangedTimeHandles(): boolean {
|
|
3281
|
+
const authorsVersion = this.authorsVersion
|
|
3282
|
+
return (
|
|
3283
|
+
this.minTime !== authorsVersion.minTime ||
|
|
3284
|
+
this.maxTime !== authorsVersion.maxTime
|
|
3285
|
+
)
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
@computed private get hasUserChangedMapTimeHandle(): boolean {
|
|
3289
|
+
const authorsVersion = this.authorsVersion
|
|
3290
|
+
return (
|
|
3291
|
+
this.map.startTime !== authorsVersion.map.startTime ||
|
|
3292
|
+
this.map.time !== authorsVersion.map.time
|
|
3293
|
+
)
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
@computed get timeParam(): string | undefined {
|
|
3297
|
+
const { timeColumn } = this.table
|
|
3298
|
+
const formatTime = (time: Time): string =>
|
|
3299
|
+
timeBoundToTimeBoundString(
|
|
3300
|
+
time,
|
|
3301
|
+
timeColumn instanceof ColumnTypeMap.Day
|
|
3302
|
+
)
|
|
3303
|
+
|
|
3304
|
+
if (this.isOnMapTab) {
|
|
3305
|
+
if (!this.hasUserChangedMapTimeHandle) return undefined
|
|
3306
|
+
if (this.map.time === undefined) return undefined
|
|
3307
|
+
|
|
3308
|
+
if (this.map.startTime === undefined)
|
|
3309
|
+
return formatTime(this.map.time)
|
|
3310
|
+
|
|
3311
|
+
const startTime = formatTime(this.map.startTime)
|
|
3312
|
+
const endTime = formatTime(this.map.time)
|
|
3313
|
+
|
|
3314
|
+
return startTime === endTime
|
|
3315
|
+
? startTime
|
|
3316
|
+
: `${startTime}..${endTime}`
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
if (!this.hasUserChangedTimeHandles) return undefined
|
|
3320
|
+
|
|
3321
|
+
const [startTime, endTime] =
|
|
3322
|
+
this.timelineHandleTimeBounds.map(formatTime)
|
|
3323
|
+
return startTime === endTime ? startTime : `${startTime}..${endTime}`
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
msPerTick = DEFAULT_MS_PER_TICK
|
|
3327
|
+
timelineController = new TimelineController(this)
|
|
3328
|
+
globeController = new GlobeController(this)
|
|
3329
|
+
|
|
3330
|
+
private dismissTooltip(): void {
|
|
3331
|
+
const tooltip = this.tooltip?.get()
|
|
3332
|
+
if (tooltip) tooltip.dismiss?.()
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
@action.bound onTimelineClick(): void {
|
|
3336
|
+
this.dismissTooltip()
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// called when an entity is selected in the entity selector
|
|
3340
|
+
@action.bound onSelectEntity(entityName: EntityName): void {
|
|
3341
|
+
const { selectedCountryNamesInForeground } = this.mapConfig.selection
|
|
3342
|
+
|
|
3343
|
+
if (!this.isOnMapTab || !this.isMapSelectionEnabled) return
|
|
3344
|
+
|
|
3345
|
+
const region = getRegionByName(entityName)
|
|
3346
|
+
if (!region) return
|
|
3347
|
+
|
|
3348
|
+
if (this.mapConfig.globe.isActive) {
|
|
3349
|
+
if (
|
|
3350
|
+
checkIsCountry(region) &&
|
|
3351
|
+
region.isMappable &&
|
|
3352
|
+
selectedCountryNamesInForeground.includes(region.name)
|
|
3353
|
+
) {
|
|
3354
|
+
// Rotate to the selected country
|
|
3355
|
+
this.globeController.rotateToCountry(region.name)
|
|
3356
|
+
this.mapConfig.region = MapRegionName.World
|
|
3357
|
+
} else if (checkIsOwidContinent(region)) {
|
|
3358
|
+
// Rotate to the selected owid continent
|
|
3359
|
+
const regionName = MAP_REGION_NAMES[
|
|
3360
|
+
region.name
|
|
3361
|
+
] as GlobeRegionName
|
|
3362
|
+
this.globeController.rotateToOwidContinent(regionName)
|
|
3363
|
+
this.mapConfig.region = regionName
|
|
3364
|
+
} else if (checkIsIncomeGroup(region)) {
|
|
3365
|
+
// Switch back to the 2d map if an income group is selected
|
|
3366
|
+
this.globeController.hideGlobe()
|
|
3367
|
+
this.globeController.resetGlobe()
|
|
3368
|
+
this.mapConfig.region = MapRegionName.World
|
|
3369
|
+
} else if (checkHasMembers(region)) {
|
|
3370
|
+
// Rotate to the selected region
|
|
3371
|
+
this.globeController.rotateToRegion(region.name)
|
|
3372
|
+
this.mapConfig.region = MapRegionName.World
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// called when an entity is deselected in the entity selector
|
|
3378
|
+
@action.bound onDeselectEntity(entityName: EntityName): void {
|
|
3379
|
+
// Remove focus from an entity that has been removed from the selection
|
|
3380
|
+
this.focusArray.remove(entityName)
|
|
3381
|
+
|
|
3382
|
+
// Remove focus from the deselected country
|
|
3383
|
+
this.globeController.dismissCountryFocus()
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// called when all entities are cleared in the entity selector
|
|
3387
|
+
@action.bound onClearEntities(): void {
|
|
3388
|
+
// remove focus from all entities if all entities have been deselected
|
|
3389
|
+
this.focusArray.clear()
|
|
3390
|
+
|
|
3391
|
+
// switch back to the 2d map if all entities were deselected
|
|
3392
|
+
if (this.isOnMapTab) {
|
|
3393
|
+
this.globeController.hideGlobe()
|
|
3394
|
+
this.globeController.resetGlobe()
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
isEntityMutedInSelector(entityName: EntityName): boolean {
|
|
3399
|
+
// For now, muted entities are only relevant on the map tab
|
|
3400
|
+
if (!this.isOnMapTab) return false
|
|
3401
|
+
|
|
3402
|
+
// Entities disabled on the map are muted
|
|
3403
|
+
if (
|
|
3404
|
+
this.mapConfig.selection.selectedCountryNamesInBackground.includes(
|
|
3405
|
+
entityName
|
|
3406
|
+
)
|
|
3407
|
+
)
|
|
3408
|
+
return true
|
|
3409
|
+
|
|
3410
|
+
// If a 2d continent is active, then all countries outside of the continent
|
|
3411
|
+
// are not shown on the map, so they're muted in the entity selector as well
|
|
3412
|
+
if (this.mapConfig.is2dContinentActive()) {
|
|
3413
|
+
const region = getRegionByName(entityName)
|
|
3414
|
+
if (!region) return false
|
|
3415
|
+
|
|
3416
|
+
// Don't mute the selected continent
|
|
3417
|
+
if (checkIsOwidContinent(region))
|
|
3418
|
+
return region.name !== MAP_REGION_LABELS[this.mapConfig.region]
|
|
3419
|
+
|
|
3420
|
+
const countriesInRegion = getCountriesByRegion(
|
|
3421
|
+
MAP_REGION_LABELS[this.mapConfig.region]
|
|
3422
|
+
)
|
|
3423
|
+
if (!countriesInRegion) return false
|
|
3424
|
+
|
|
3425
|
+
return !countriesInRegion.has(entityName)
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
return false
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// todo: restore this behavior??
|
|
3432
|
+
onStartPlayOrDrag(): void {
|
|
3433
|
+
this.debounceMode = true
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
onStopPlayOrDrag(): void {
|
|
3437
|
+
this.debounceMode = false
|
|
3438
|
+
}
|
|
3439
|
+
@computed get disablePlay(): boolean {
|
|
3440
|
+
return false
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
@computed get animationEndTime(): Time {
|
|
3444
|
+
const { timeColumn } = this.table
|
|
3445
|
+
if (this.timelineMaxTime) {
|
|
3446
|
+
const timesAsc = sortNumeric(timeColumn.uniqValues.slice())
|
|
3447
|
+
return (
|
|
3448
|
+
findClosestTime(timesAsc, this.timelineMaxTime) ??
|
|
3449
|
+
timeColumn.maxTime
|
|
3450
|
+
)
|
|
3451
|
+
}
|
|
3452
|
+
return timeColumn.maxTime
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
formatTime(value: Time): string {
|
|
3456
|
+
const timeColumn = this.table.timeColumn
|
|
3457
|
+
return isMobile()
|
|
3458
|
+
? timeColumn.formatValueForMobile(value)
|
|
3459
|
+
: timeColumn.formatValue(value)
|
|
3460
|
+
}
|
|
3461
|
+
@computed get canSelectMultipleEntities(): boolean {
|
|
3462
|
+
if (this.isOnMapTab) return true
|
|
3463
|
+
|
|
3464
|
+
if (this.numSelectableEntityNames < 2) return false
|
|
3465
|
+
if (this.addCountryMode === EntitySelectionMode.MultipleEntities)
|
|
3466
|
+
return true
|
|
3467
|
+
|
|
3468
|
+
// if the chart is currently faceted by entity, then use multi-entity
|
|
3469
|
+
// selection, even if the author specified single-entity selection
|
|
3470
|
+
if (
|
|
3471
|
+
this.addCountryMode === EntitySelectionMode.SingleEntity &&
|
|
3472
|
+
this.facetStrategy === FacetStrategy.entity
|
|
3473
|
+
)
|
|
3474
|
+
return true
|
|
3475
|
+
|
|
3476
|
+
return false
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
@computed get canChangeEntity(): boolean {
|
|
3480
|
+
return (
|
|
3481
|
+
this.hasChartTab &&
|
|
3482
|
+
!this.isOnScatterTab &&
|
|
3483
|
+
!this.canSelectMultipleEntities &&
|
|
3484
|
+
this.addCountryMode === EntitySelectionMode.SingleEntity &&
|
|
3485
|
+
this.numSelectableEntityNames > 1
|
|
3486
|
+
)
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
@computed get canAddEntities(): boolean {
|
|
3490
|
+
return (
|
|
3491
|
+
this.hasChartTab &&
|
|
3492
|
+
this.canSelectMultipleEntities &&
|
|
3493
|
+
(this.isOnLineChartTab ||
|
|
3494
|
+
this.isOnSlopeChartTab ||
|
|
3495
|
+
this.isOnStackedAreaTab ||
|
|
3496
|
+
this.isOnStackedBarTab ||
|
|
3497
|
+
this.isOnDiscreteBarTab ||
|
|
3498
|
+
this.isOnStackedDiscreteBarTab)
|
|
3499
|
+
)
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
@computed get canHighlightEntities(): boolean {
|
|
3503
|
+
return (
|
|
3504
|
+
this.hasChartTab &&
|
|
3505
|
+
this.addCountryMode !== EntitySelectionMode.Disabled &&
|
|
3506
|
+
this.numSelectableEntityNames > 1 &&
|
|
3507
|
+
!this.canAddEntities &&
|
|
3508
|
+
!this.canChangeEntity
|
|
3509
|
+
)
|
|
3510
|
+
}
|
|
3511
|
+
@computed get canChangeAddOrHighlightEntities(): boolean {
|
|
3512
|
+
return (
|
|
3513
|
+
this.canChangeEntity ||
|
|
3514
|
+
this.canAddEntities ||
|
|
3515
|
+
this.canHighlightEntities
|
|
3516
|
+
)
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
@computed get shouldShowEntitySelectorAs(): GrapherWindowType {
|
|
3520
|
+
if (
|
|
3521
|
+
this.frameBounds.width > 940 &&
|
|
3522
|
+
// don't use the panel if the grapher is embedded
|
|
3523
|
+
((!this.isInIFrame && !this.isEmbeddedInAnOwidPage) ||
|
|
3524
|
+
// unless we're in full-screen mode
|
|
3525
|
+
this.isInFullScreenMode)
|
|
3526
|
+
)
|
|
3527
|
+
return GrapherWindowType.panel
|
|
3528
|
+
|
|
3529
|
+
return this.isSemiNarrow
|
|
3530
|
+
? GrapherWindowType.modal
|
|
3531
|
+
: GrapherWindowType.drawer
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
@computed get isEntitySelectorPanelActive(): boolean {
|
|
3535
|
+
if (this.hideEntityControls) return false
|
|
3536
|
+
|
|
3537
|
+
const shouldShowPanel =
|
|
3538
|
+
this.shouldShowEntitySelectorAs === GrapherWindowType.panel
|
|
3539
|
+
|
|
3540
|
+
if (this.isOnMapTab && this.isMapSelectionEnabled && shouldShowPanel)
|
|
3541
|
+
return true
|
|
3542
|
+
|
|
3543
|
+
return (
|
|
3544
|
+
this.isOnChartTab &&
|
|
3545
|
+
this.canChangeAddOrHighlightEntities &&
|
|
3546
|
+
shouldShowPanel
|
|
3547
|
+
)
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
@computed get isEntitySelectorModalOpen(): boolean {
|
|
3551
|
+
return (
|
|
3552
|
+
this.isEntitySelectorModalOrDrawerOpen &&
|
|
3553
|
+
this.shouldShowEntitySelectorAs === GrapherWindowType.modal
|
|
3554
|
+
)
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
@computed get isEntitySelectorDrawerOpen(): boolean {
|
|
3558
|
+
return (
|
|
3559
|
+
this.isEntitySelectorModalOrDrawerOpen &&
|
|
3560
|
+
this.shouldShowEntitySelectorAs === GrapherWindowType.drawer
|
|
3561
|
+
)
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
@computed get isMapSelectionEnabled(): boolean {
|
|
3565
|
+
if (this.enableMapSelection) return true
|
|
3566
|
+
|
|
3567
|
+
return (
|
|
3568
|
+
// If the entity controls are hidden, then selecting entities from
|
|
3569
|
+
// the map should also be disabled
|
|
3570
|
+
!this.hideEntityControls &&
|
|
3571
|
+
// Only show the entity selector on the map tab if it's rendered
|
|
3572
|
+
// into the side panel or into the slide-in drawer
|
|
3573
|
+
this.shouldShowEntitySelectorAs !== GrapherWindowType.modal
|
|
3574
|
+
)
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// This is just a helper method to return the correct table for providing entity choices. We want to
|
|
3578
|
+
// provide the root table, not the transformed table.
|
|
3579
|
+
// A user may have added time or other filters that would filter out all rows from certain entities, but
|
|
3580
|
+
// we may still want to show those entities as available in a picker. We also do not want to do things like
|
|
3581
|
+
// hide the Add Entity button as the user drags the timeline.
|
|
3582
|
+
@computed private get numSelectableEntityNames(): number {
|
|
3583
|
+
return this.availableEntityNames.length
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
private mapQueryParamToTabName(tab: string): GrapherTabName | undefined {
|
|
3587
|
+
return isValidTabConfigOption(tab)
|
|
3588
|
+
? this.mapTabConfigOptionToTabName(tab)
|
|
3589
|
+
: this.defaultTab
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
mapGrapherTabToQueryParam(tabName: GrapherTabName): string {
|
|
3593
|
+
return this.mapTabNameToTabConfigOption(tabName)
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
private mapTabNameToTabConfigOption(
|
|
3597
|
+
tabName: GrapherTabName
|
|
3598
|
+
): GrapherTabConfigOption {
|
|
3599
|
+
switch (tabName) {
|
|
3600
|
+
case GRAPHER_TAB_NAMES.Table:
|
|
3601
|
+
return GRAPHER_TAB_CONFIG_OPTIONS.table
|
|
3602
|
+
case GRAPHER_TAB_NAMES.WorldMap:
|
|
3603
|
+
return GRAPHER_TAB_CONFIG_OPTIONS.map
|
|
3604
|
+
default:
|
|
3605
|
+
return this.hasMultipleChartTypes
|
|
3606
|
+
? mapChartTypeNameToTabConfigOption(tabName)
|
|
3607
|
+
: GRAPHER_TAB_CONFIG_OPTIONS.chart
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
private mapTabConfigOptionToTabName(
|
|
3612
|
+
tabOption: GrapherTabConfigOption
|
|
3613
|
+
): GrapherTabName {
|
|
3614
|
+
if (tabOption === GRAPHER_TAB_CONFIG_OPTIONS.table)
|
|
3615
|
+
return GRAPHER_TAB_NAMES.Table
|
|
3616
|
+
|
|
3617
|
+
if (tabOption === GRAPHER_TAB_CONFIG_OPTIONS.map)
|
|
3618
|
+
return this.hasMapTab ? GRAPHER_TAB_NAMES.WorldMap : this.defaultTab
|
|
3619
|
+
|
|
3620
|
+
if (tabOption === GRAPHER_TAB_CONFIG_OPTIONS.chart) {
|
|
3621
|
+
return this.defaultTab
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
const chartTypeName = mapTabConfigOptionToChartTypeName(tabOption)
|
|
3625
|
+
return this.validChartTypeSet.has(chartTypeName)
|
|
3626
|
+
? chartTypeName
|
|
3627
|
+
: this.defaultTab
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
hideTitle = false
|
|
3631
|
+
hideSubtitle = false
|
|
3632
|
+
hideNote = false
|
|
3633
|
+
hideOriginUrl = false
|
|
3634
|
+
|
|
3635
|
+
// For now I am only exposing this programmatically for the dashboard builder. Setting this to true
|
|
3636
|
+
// allows you to still use add country "modes" without showing the buttons in order to prioritize
|
|
3637
|
+
// another entity selector over the built in ones.
|
|
3638
|
+
hideEntityControls = false
|
|
3639
|
+
|
|
3640
|
+
enableMapSelection = false
|
|
3641
|
+
|
|
3642
|
+
// enforces hiding an annotation, even if that means that a crucial piece of information is missing from the chart title
|
|
3643
|
+
forceHideAnnotationFieldsInTitle: AnnotationFieldsInTitle = {
|
|
3644
|
+
entity: false,
|
|
3645
|
+
time: false,
|
|
3646
|
+
changeInPrefix: false,
|
|
3647
|
+
}
|
|
3648
|
+
hasTableTab = true
|
|
3649
|
+
hideShareButton = false
|
|
3650
|
+
hideExploreTheDataButton = true
|
|
3651
|
+
hideRelatedQuestion = false
|
|
3652
|
+
|
|
3653
|
+
initialOptions: GrapherProgrammaticInterface
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
export const defaultObject = objectWithPersistablesToObject(
|
|
3657
|
+
new GrapherState({}),
|
|
3658
|
+
grapherKeysToSerialize
|
|
3659
|
+
)
|