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