@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,1175 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as _ from "lodash-es"
|
|
3
|
+
import {
|
|
4
|
+
intersectionOfSets,
|
|
5
|
+
findClosestTimeIndex,
|
|
6
|
+
sortNumeric,
|
|
7
|
+
getClosestTimePairs,
|
|
8
|
+
sortedFindClosest,
|
|
9
|
+
cagr,
|
|
10
|
+
makeAnnotationsSlug,
|
|
11
|
+
isPresent,
|
|
12
|
+
TimeBound,
|
|
13
|
+
ColumnSlug,
|
|
14
|
+
imemo,
|
|
15
|
+
ToleranceStrategy,
|
|
16
|
+
differenceOfSets,
|
|
17
|
+
sortedFindClosestIndex,
|
|
18
|
+
} from "../utils/index.js"
|
|
19
|
+
import {
|
|
20
|
+
Time,
|
|
21
|
+
TransformType,
|
|
22
|
+
CoreColumnStore,
|
|
23
|
+
Color,
|
|
24
|
+
CoreValueType,
|
|
25
|
+
ColumnTypeNames,
|
|
26
|
+
EntityName,
|
|
27
|
+
OwidColumnDef,
|
|
28
|
+
OwidRow,
|
|
29
|
+
OwidTableSlugs,
|
|
30
|
+
ErrorValue,
|
|
31
|
+
ToleranceOptions,
|
|
32
|
+
} from "../types/index.js"
|
|
33
|
+
import { CoreTable } from "./CoreTable.js"
|
|
34
|
+
import { ErrorValueTypes, isNotErrorValue } from "./ErrorValues.js"
|
|
35
|
+
import {
|
|
36
|
+
getOriginalTimeColumnSlug,
|
|
37
|
+
makeOriginalValueSlugFromColumnSlug,
|
|
38
|
+
makeOriginalTimeSlugFromColumnSlug,
|
|
39
|
+
makeOriginalStartTimeSlugFromColumnSlug,
|
|
40
|
+
timeColumnSlugFromColumnDef,
|
|
41
|
+
toPercentageColumnDef,
|
|
42
|
+
} from "./OwidTableUtil.js"
|
|
43
|
+
import {
|
|
44
|
+
linearInterpolation,
|
|
45
|
+
toleranceInterpolation,
|
|
46
|
+
replaceDef,
|
|
47
|
+
InterpolationProvider,
|
|
48
|
+
InterpolationContext,
|
|
49
|
+
} from "./CoreTableUtils.js"
|
|
50
|
+
import { CoreColumn, ColumnTypeMap } from "./CoreTableColumns.js"
|
|
51
|
+
|
|
52
|
+
// An OwidTable is a subset of Table. An OwidTable always has EntityName, EntityCode, EntityId, and Time columns,
|
|
53
|
+
// and value column(s). Whether or not we need in the long run is uncertain and it may just be a stepping stone
|
|
54
|
+
// to go from our Variables paradigm to the Table paradigm.
|
|
55
|
+
export class OwidTable extends CoreTable<OwidRow, OwidColumnDef> {
|
|
56
|
+
@imemo get availableEntityNames(): any[] {
|
|
57
|
+
return Array.from(this.availableEntityNameSet)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@imemo get availableEntityNameSet(): Set<string> {
|
|
61
|
+
return this.entityNameColumn.uniqValuesAsSet
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@imemo override get entityNameColumn(): CoreColumn {
|
|
65
|
+
return (
|
|
66
|
+
this.getFirstColumnWithType(ColumnTypeNames.EntityName) ??
|
|
67
|
+
this.get(OwidTableSlugs.entityName)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@imemo get minTime(): Time {
|
|
72
|
+
return _.min(this.allTimes) as Time
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@imemo get maxTime(): number | undefined {
|
|
76
|
+
return _.max(this.allTimes)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@imemo private get allTimes(): Time[] {
|
|
80
|
+
return this.get(this.timeColumn.slug).values
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@imemo get rowIndicesByEntityName(): Map<string, number[]> {
|
|
84
|
+
return this.rowIndex([this.entityNameSlug])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getAnnotationColumnSlug(columnDef: OwidColumnDef): string | undefined {
|
|
88
|
+
return _.isEmpty(columnDef?.annotationsColumnSlug)
|
|
89
|
+
? makeAnnotationsSlug(columnDef.slug)
|
|
90
|
+
: columnDef.annotationsColumnSlug
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// todo: instead of this we should probably make annotations another property on charts—something like "annotationsColumnSlugs"
|
|
94
|
+
getAnnotationColumnForColumn(columnSlug: ColumnSlug): CoreColumn {
|
|
95
|
+
const def = this.get(columnSlug).def as OwidColumnDef
|
|
96
|
+
const slug = this.getAnnotationColumnSlug(def)
|
|
97
|
+
return this.get(slug)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getTimesUniqSortedAscForColumns(columnSlugs: ColumnSlug[]): number[] {
|
|
101
|
+
// todo: should be easy to speed up if necessary.
|
|
102
|
+
return sortNumeric(
|
|
103
|
+
_.uniq(
|
|
104
|
+
this.getColumns(columnSlugs)
|
|
105
|
+
.filter((col) => col)
|
|
106
|
+
.flatMap((col) => col.uniqTimesAsc)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
timeDomainFor(slugs: ColumnSlug[]): [Time | undefined, Time | undefined] {
|
|
112
|
+
const cols = this.getColumns(slugs)
|
|
113
|
+
const mins = cols.map((col) => col.minTime)
|
|
114
|
+
const maxes = cols.map((col) => col.maxTime)
|
|
115
|
+
return [_.min(mins), _.max(maxes)]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
originalTimeDomainFor(
|
|
119
|
+
slugs: ColumnSlug[]
|
|
120
|
+
): [Time | undefined, Time | undefined] {
|
|
121
|
+
const cols = this.getColumns(slugs)
|
|
122
|
+
const mins = cols.map((col) => _.min(col.originalTimes))
|
|
123
|
+
const maxes = cols.map((col) => _.max(col.originalTimes))
|
|
124
|
+
return [_.min(mins), _.max(maxes)]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
filterByEntityNames(names: EntityName[]): this {
|
|
128
|
+
const namesSet = new Set(names)
|
|
129
|
+
return this.columnFilter(
|
|
130
|
+
this.entityNameSlug,
|
|
131
|
+
(value) => namesSet.has(value as string),
|
|
132
|
+
`Filter out all entities except '${names}'`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
filterByEntityNamesUsingIncludeExcludePattern({
|
|
137
|
+
excluded,
|
|
138
|
+
included,
|
|
139
|
+
}: {
|
|
140
|
+
excluded?: EntityName[]
|
|
141
|
+
included?: EntityName[]
|
|
142
|
+
}): this {
|
|
143
|
+
if (!included && !excluded) return this
|
|
144
|
+
|
|
145
|
+
const excludedSet = new Set(excluded)
|
|
146
|
+
const includedSet = new Set(included)
|
|
147
|
+
|
|
148
|
+
const excludeFilter = (entityName: EntityName): boolean =>
|
|
149
|
+
!excludedSet.has(entityName)
|
|
150
|
+
const includeFilter =
|
|
151
|
+
includedSet.size > 0
|
|
152
|
+
? (entityName: EntityName): boolean =>
|
|
153
|
+
includedSet.has(entityName)
|
|
154
|
+
: (): boolean => true
|
|
155
|
+
|
|
156
|
+
const filterFn = (entityName: any): boolean =>
|
|
157
|
+
excludeFilter(entityName) && includeFilter(entityName)
|
|
158
|
+
|
|
159
|
+
const excludedList = excluded ? excluded.join(", ") : ""
|
|
160
|
+
const includedList = included ? included.join(", ") : ""
|
|
161
|
+
|
|
162
|
+
return this.columnFilter(
|
|
163
|
+
this.entityNameSlug,
|
|
164
|
+
filterFn,
|
|
165
|
+
`Excluded entities specified by author: ${excludedList} - Included entities specified by author: ${includedList}`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Does a stable sort by time. You can refer to this table for fast time filtering.
|
|
170
|
+
@imemo private get sortedByTime(): this {
|
|
171
|
+
if (this.timeColumn.isMissing) return this
|
|
172
|
+
return this.sortBy([this.timeColumn.slug])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
filterByTimeRange(start: TimeBound, end: TimeBound): this {
|
|
176
|
+
if (this.isBlank) return this
|
|
177
|
+
|
|
178
|
+
// We may want to do this time adjustment in Grapher instead of here.
|
|
179
|
+
const adjustedStart = start === Infinity ? this.maxTime! : start
|
|
180
|
+
const adjustedEnd = end === -Infinity ? this.minTime! : end
|
|
181
|
+
// todo: we should set a time column onload so we don't have to worry about it again.
|
|
182
|
+
const timeColumnSlug = this.timeColumn?.slug || OwidTableSlugs.time
|
|
183
|
+
|
|
184
|
+
const description = `Keep only rows with Time between ${adjustedStart} - ${adjustedEnd}`
|
|
185
|
+
|
|
186
|
+
// perf: if the time range is greater than the table's time range, we can skip the filter
|
|
187
|
+
if (adjustedStart <= this.minTime && adjustedEnd >= this.maxTime!)
|
|
188
|
+
return this.noopTransform(description).sortedByTime
|
|
189
|
+
|
|
190
|
+
return this.columnFilter(
|
|
191
|
+
timeColumnSlug,
|
|
192
|
+
(time) =>
|
|
193
|
+
(time as number) >= adjustedStart &&
|
|
194
|
+
(time as number) <= adjustedEnd,
|
|
195
|
+
description
|
|
196
|
+
|
|
197
|
+
// Sorting by time, because incidentally some parts of the code depended on this method
|
|
198
|
+
// returning sorted rows.
|
|
199
|
+
).sortedByTime
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
filterByTargetTimes(targetTimes: Time[], tolerance = 0): this {
|
|
203
|
+
const timeColumn = this.timeColumn!
|
|
204
|
+
const timeValues = timeColumn.valuesIncludingErrorValues
|
|
205
|
+
|
|
206
|
+
// The common case here is that the tolerance is set to 0, in which case we can simply filter
|
|
207
|
+
// the time column for the target times.
|
|
208
|
+
if (tolerance === 0) {
|
|
209
|
+
const targetTimesSet = new Set(targetTimes)
|
|
210
|
+
return this.columnFilter(
|
|
211
|
+
timeColumn.slug,
|
|
212
|
+
(time) => targetTimesSet.has(time as number),
|
|
213
|
+
`Keep only rows with time equal to one of the target times: ${targetTimes.join(
|
|
214
|
+
", "
|
|
215
|
+
)}`
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// If tolerance isn't 0, then we need to find the closest time for each entity, while incorporating the tolerance.
|
|
220
|
+
const entityNameToIndices = this.rowIndicesByEntityName
|
|
221
|
+
const matchingIndices = new Set<number>()
|
|
222
|
+
this.availableEntityNames.forEach((entityName) => {
|
|
223
|
+
const indices = entityNameToIndices.get(entityName) || []
|
|
224
|
+
const allTimesAsc = indices
|
|
225
|
+
.map((index) => ({ time: timeValues[index] as number, index }))
|
|
226
|
+
.sort((a, b) => a.time - b.time)
|
|
227
|
+
|
|
228
|
+
targetTimes.forEach((targetTime) => {
|
|
229
|
+
const index = findClosestTimeIndex(
|
|
230
|
+
allTimesAsc.map((t) => t.time),
|
|
231
|
+
targetTime,
|
|
232
|
+
tolerance
|
|
233
|
+
)
|
|
234
|
+
const closest =
|
|
235
|
+
index === undefined ? undefined : allTimesAsc[index]
|
|
236
|
+
if (closest !== undefined) matchingIndices.add(closest.index)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
return this.columnFilter(
|
|
241
|
+
this.entityNameSlug,
|
|
242
|
+
(row, index) => matchingIndices.has(index),
|
|
243
|
+
`Keep a row for each entity for each of the closest times ${targetTimes.join(
|
|
244
|
+
", "
|
|
245
|
+
)} with tolerance ${tolerance}`
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
hasAnyColumnNoValidValue(slugs: ColumnSlug[]): boolean {
|
|
250
|
+
return slugs.some((slug) => this.get(slug).numValues === 0)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
dropAllRows(): this {
|
|
254
|
+
return this.dropRowsAt(this.indices, "Drop all rows")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
dropRowsWithErrorValuesForColumn(slug: ColumnSlug): this {
|
|
258
|
+
return this.columnFilter(
|
|
259
|
+
slug,
|
|
260
|
+
(value) => isNotErrorValue(value),
|
|
261
|
+
`Drop rows with empty or ErrorValues in ${slug} column`
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// TODO rewrite with column ops
|
|
266
|
+
// TODO move to CoreTable
|
|
267
|
+
dropRowsWithErrorValuesForAnyColumn(slugs: ColumnSlug[]): this {
|
|
268
|
+
return this.rowFilter(
|
|
269
|
+
(row) => slugs.every((slug) => isNotErrorValue(row[slug])),
|
|
270
|
+
`Drop rows with empty or ErrorValues in any column: ${slugs.join(
|
|
271
|
+
", "
|
|
272
|
+
)}`
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// TODO rewrite with column ops
|
|
277
|
+
// TODO move to CoreTable
|
|
278
|
+
dropRowsWithErrorValuesForAllColumns(slugs: ColumnSlug[]): this {
|
|
279
|
+
return this.rowFilter(
|
|
280
|
+
(row) => slugs.some((slug) => isNotErrorValue(row[slug])),
|
|
281
|
+
`Drop rows with empty or ErrorValues in every column: ${slugs.join(
|
|
282
|
+
", "
|
|
283
|
+
)}`
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Drop _all rows_ for an entity if there is any column that has no valid values for that entity.
|
|
288
|
+
dropEntitiesThatHaveNoDataInSomeColumn(columnSlugs: ColumnSlug[]): this {
|
|
289
|
+
const indexesByEntityName = this.rowIndicesByEntityName
|
|
290
|
+
|
|
291
|
+
// Iterate over all entities, and remove them as we go if they have no data in some column
|
|
292
|
+
const entityNamesToKeep = new Set(indexesByEntityName.keys())
|
|
293
|
+
|
|
294
|
+
for (const slug of columnSlugs) {
|
|
295
|
+
const col = this.get(slug)
|
|
296
|
+
|
|
297
|
+
// Optimization, if there are no error values in this column, we can skip this column
|
|
298
|
+
if (col.numErrorValues === 0) continue
|
|
299
|
+
|
|
300
|
+
for (const entityName of entityNamesToKeep) {
|
|
301
|
+
const indicesForEntityName = indexesByEntityName.get(entityName)
|
|
302
|
+
if (!indicesForEntityName)
|
|
303
|
+
throw new Error("Unexpected: entity not found in index map")
|
|
304
|
+
|
|
305
|
+
// Optimization: We don't care about the number of valid/error values, we just need
|
|
306
|
+
// to know if there is at least one valid value
|
|
307
|
+
const hasSomeValidValueForEntityInCol =
|
|
308
|
+
indicesForEntityName.some((index) =>
|
|
309
|
+
isNotErrorValue(col.valuesIncludingErrorValues[index])
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// Optimization: If we find a column that this entity has no data in we can remove
|
|
313
|
+
// it immediately, no need to iterate over other columns also
|
|
314
|
+
if (!hasSomeValidValueForEntityInCol)
|
|
315
|
+
entityNamesToKeep.delete(entityName)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const entityNamesToDrop = differenceOfSets([
|
|
320
|
+
this.availableEntityNameSet,
|
|
321
|
+
entityNamesToKeep,
|
|
322
|
+
])
|
|
323
|
+
const droppedEntitiesStr =
|
|
324
|
+
entityNamesToDrop.size > 0
|
|
325
|
+
? [...entityNamesToDrop].join(", ")
|
|
326
|
+
: "(None)"
|
|
327
|
+
|
|
328
|
+
if (entityNamesToDrop.size === 0) {
|
|
329
|
+
return this.noopTransform(
|
|
330
|
+
`Drop entities that have no data in some column`
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return this.columnFilter(
|
|
335
|
+
this.entityNameSlug,
|
|
336
|
+
(rowEntityName) => entityNamesToKeep.has(rowEntityName as string),
|
|
337
|
+
`Drop ${entityNamesToDrop.size} entities that have no data in some column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}`
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Drop _all rows_ for an entity if all columns have at least one invalid or missing value for that entity.
|
|
342
|
+
dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns(
|
|
343
|
+
columnSlugs: ColumnSlug[]
|
|
344
|
+
): this {
|
|
345
|
+
const indexesByEntityName = this.rowIndicesByEntityName
|
|
346
|
+
const uniqTimes = new Set(this.allTimes)
|
|
347
|
+
|
|
348
|
+
// entity names to iterate over
|
|
349
|
+
const entityNamesToIterateOver = new Set(indexesByEntityName.keys())
|
|
350
|
+
|
|
351
|
+
// set of entities we want to keep
|
|
352
|
+
const entityNamesToKeep = new Set<string>()
|
|
353
|
+
|
|
354
|
+
// total number of entities
|
|
355
|
+
const entityCount = entityNamesToIterateOver.size
|
|
356
|
+
|
|
357
|
+
// helper function to generate operation name
|
|
358
|
+
const makeOpName = (entityNamesToKeep: Set<EntityName>): string => {
|
|
359
|
+
const entityNamesToDrop = differenceOfSets([
|
|
360
|
+
this.availableEntityNameSet,
|
|
361
|
+
entityNamesToKeep,
|
|
362
|
+
])
|
|
363
|
+
const droppedEntitiesStr =
|
|
364
|
+
entityNamesToDrop.size > 0
|
|
365
|
+
? [...entityNamesToDrop].join(", ")
|
|
366
|
+
: "(None)"
|
|
367
|
+
return `Drop entities that have some missing or error value in all column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Optimization: if there is a column that has a valid data entry for
|
|
371
|
+
// every entity and every time, we are done
|
|
372
|
+
for (const slug of columnSlugs) {
|
|
373
|
+
const col = this.get(slug)
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
col.numValues === entityCount * uniqTimes.size &&
|
|
377
|
+
col.numErrorValues === 0
|
|
378
|
+
) {
|
|
379
|
+
const entityNamesToKeep = new Set(indexesByEntityName.keys())
|
|
380
|
+
|
|
381
|
+
return this.columnFilter(
|
|
382
|
+
this.entityNameSlug,
|
|
383
|
+
(rowEntityName) =>
|
|
384
|
+
entityNamesToKeep.has(rowEntityName as string),
|
|
385
|
+
makeOpName(entityNamesToKeep)
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const slug of columnSlugs) {
|
|
391
|
+
const col = this.get(slug)
|
|
392
|
+
|
|
393
|
+
for (const entityName of entityNamesToIterateOver) {
|
|
394
|
+
const indicesForEntityName = indexesByEntityName.get(entityName)
|
|
395
|
+
if (!indicesForEntityName)
|
|
396
|
+
throw new Error("Unexpected: entity not found in index map")
|
|
397
|
+
|
|
398
|
+
// Optimization: If the column is missing values for the entity,
|
|
399
|
+
// we know we can't make a decision yet, so we skip this entity
|
|
400
|
+
if (indicesForEntityName.length < uniqTimes.size) continue
|
|
401
|
+
|
|
402
|
+
// Optimization: We don't care about the number of valid/error
|
|
403
|
+
// values, we just need to know if there is at least one invalid value
|
|
404
|
+
const hasSomeInvalidValueForEntityInCol =
|
|
405
|
+
indicesForEntityName.some(
|
|
406
|
+
(index) =>
|
|
407
|
+
!isNotErrorValue(
|
|
408
|
+
col.valuesIncludingErrorValues[index]
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
// Optimization: If all values are valid, we know we want to keep this entity,
|
|
413
|
+
// so we remove it from the entities to iterate over
|
|
414
|
+
if (!hasSomeInvalidValueForEntityInCol) {
|
|
415
|
+
entityNamesToKeep.add(entityName)
|
|
416
|
+
entityNamesToIterateOver.delete(entityName)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return this.columnFilter(
|
|
422
|
+
this.entityNameSlug,
|
|
423
|
+
(rowEntityName) => entityNamesToKeep.has(rowEntityName as string),
|
|
424
|
+
makeOpName(entityNamesToKeep)
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private sumsByTime(columnSlug: ColumnSlug): Map<number, number> {
|
|
429
|
+
const timeValues = this.timeColumn.values
|
|
430
|
+
const values = this.get(columnSlug).values as number[]
|
|
431
|
+
if (timeValues.length !== values.length)
|
|
432
|
+
// Throwing here may seem drastic but this will lead to an error in any case if the lengths are different
|
|
433
|
+
// (can happen when dropping rows with errors is done incorrectly).
|
|
434
|
+
// When it happens then throwing here is an easier to diagnose error
|
|
435
|
+
throw Error(
|
|
436
|
+
`Tried to run sumsByTime when timeValues (${timeValues.length}) and values (${values.length}) had different length`
|
|
437
|
+
)
|
|
438
|
+
const map = new Map<number, number>()
|
|
439
|
+
timeValues.forEach((time, index) =>
|
|
440
|
+
map.set(time, (map.get(time) ?? 0) + values[index])
|
|
441
|
+
)
|
|
442
|
+
return map
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// todo: this needs tests (and/or drop in favor of someone else's package)
|
|
446
|
+
// Shows how much each entity contributed to the given column for each time period
|
|
447
|
+
toPercentageFromEachEntityForEachTime(columnSlug: ColumnSlug): this {
|
|
448
|
+
if (!this.has(columnSlug)) return this
|
|
449
|
+
const timeColumn = this.timeColumn!
|
|
450
|
+
const col = this.get(columnSlug)
|
|
451
|
+
const timeTotals = this.sumsByTime(columnSlug)
|
|
452
|
+
const timeValues = timeColumn.values
|
|
453
|
+
const newDefs = replaceDef(this.defs, [
|
|
454
|
+
toPercentageColumnDef(col.def, ColumnTypeNames.RelativePercentage),
|
|
455
|
+
])
|
|
456
|
+
const newColumnStore: CoreColumnStore = {
|
|
457
|
+
...this.columnStore,
|
|
458
|
+
[columnSlug]: this.columnStore[columnSlug].map((val, index) => {
|
|
459
|
+
const timeTotal = timeTotals.get(timeValues[index])
|
|
460
|
+
if (timeTotal === 0) return ErrorValueTypes.DivideByZeroError
|
|
461
|
+
return (100 * (val as number)) / timeTotal!
|
|
462
|
+
}),
|
|
463
|
+
}
|
|
464
|
+
return this.transform(
|
|
465
|
+
newColumnStore,
|
|
466
|
+
newDefs,
|
|
467
|
+
`Transformed ${columnSlug} column to be % contribution of each entity for that time`,
|
|
468
|
+
TransformType.UpdateColumnDefsAndApply
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// If you want to see how much each column contributed to the entity for that year, use this.
|
|
473
|
+
// NB: Uses absolute value. So if one entity added 100, and another -100, they both would have contributed "50%" to that year.
|
|
474
|
+
// Otherwise we'd have NaN.
|
|
475
|
+
toPercentageFromEachColumnForEachEntityAndTime(
|
|
476
|
+
columnSlugs: ColumnSlug[]
|
|
477
|
+
): this {
|
|
478
|
+
columnSlugs = columnSlugs.filter((slug) => this.has(slug))
|
|
479
|
+
if (!columnSlugs.length) return this
|
|
480
|
+
|
|
481
|
+
const newDefs = this.defs.map((def) => {
|
|
482
|
+
if (columnSlugs.includes(def.slug))
|
|
483
|
+
return toPercentageColumnDef(
|
|
484
|
+
def,
|
|
485
|
+
ColumnTypeNames.RelativePercentage
|
|
486
|
+
)
|
|
487
|
+
return def
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const columnStore = this.columnStore
|
|
491
|
+
const columnStorePatch: CoreColumnStore = {}
|
|
492
|
+
|
|
493
|
+
const totals = new Array(this.numRows).fill(0).map((__, i) =>
|
|
494
|
+
_.sumBy(columnSlugs, (slug) => {
|
|
495
|
+
const value = columnStore[slug][i]
|
|
496
|
+
return _.isNumber(value) ? Math.abs(value) : 0
|
|
497
|
+
})
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
columnSlugs.forEach((slug) => {
|
|
501
|
+
columnStorePatch[slug] = columnStore[slug].map((value, i) => {
|
|
502
|
+
const total = totals[i]
|
|
503
|
+
if (!_.isNumber(value) || !_.isNumber(total)) return value
|
|
504
|
+
if (total === 0) return ErrorValueTypes.DivideByZeroError
|
|
505
|
+
return (100 * Math.abs(value)) / total
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const newColumnStore = {
|
|
510
|
+
...columnStore,
|
|
511
|
+
...columnStorePatch,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return this.transform(
|
|
515
|
+
newColumnStore,
|
|
516
|
+
newDefs,
|
|
517
|
+
`Transformed columns from absolute numbers to % of abs sum of ${columnSlugs.join(
|
|
518
|
+
","
|
|
519
|
+
)} `,
|
|
520
|
+
TransformType.UpdateColumnDefs
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// todo: this needs tests (and/or drop in favor of someone else's package)
|
|
525
|
+
// If you wanted to build a table showing something like GDP growth relative to 1950, use this.
|
|
526
|
+
toTotalGrowthForEachColumnComparedToStartTime(
|
|
527
|
+
startTimeBound: TimeBound,
|
|
528
|
+
columnSlugs: ColumnSlug[]
|
|
529
|
+
): this {
|
|
530
|
+
if (this.timeColumn.isMissing) return this
|
|
531
|
+
const timeColumnSlug = this.timeColumn.slug
|
|
532
|
+
const newDefs = this.defs.map((def) => {
|
|
533
|
+
if (columnSlugs.includes(def.slug))
|
|
534
|
+
return toPercentageColumnDef(
|
|
535
|
+
def,
|
|
536
|
+
ColumnTypeNames.PercentChangeOverTime
|
|
537
|
+
)
|
|
538
|
+
return def
|
|
539
|
+
})
|
|
540
|
+
const newRows = Object.values(
|
|
541
|
+
_.groupBy(this.sortedByTime.rows, (row) => row[this.entityNameSlug])
|
|
542
|
+
).flatMap((rowsForSingleEntity) => {
|
|
543
|
+
columnSlugs.forEach((valueSlug) => {
|
|
544
|
+
let comparisonValue: number
|
|
545
|
+
rowsForSingleEntity = rowsForSingleEntity.map(
|
|
546
|
+
(row: Readonly<OwidRow>) => {
|
|
547
|
+
const newRow = {
|
|
548
|
+
...row,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const value = row[valueSlug]
|
|
552
|
+
|
|
553
|
+
if (row[timeColumnSlug] < startTimeBound) {
|
|
554
|
+
newRow[valueSlug] =
|
|
555
|
+
ErrorValueTypes.MissingValuePlaceholder
|
|
556
|
+
} else if (!_.isNumber(value)) {
|
|
557
|
+
newRow[valueSlug] =
|
|
558
|
+
ErrorValueTypes.NaNButShouldBeNumber
|
|
559
|
+
} else if (comparisonValue !== undefined) {
|
|
560
|
+
// Note: comparisonValue can be negative!
|
|
561
|
+
// +value / -comparisonValue = negative growth, which is incorrect.
|
|
562
|
+
newRow[valueSlug] =
|
|
563
|
+
(100 * (value - comparisonValue)) /
|
|
564
|
+
Math.abs(comparisonValue)
|
|
565
|
+
} else if (value === 0) {
|
|
566
|
+
newRow[valueSlug] =
|
|
567
|
+
ErrorValueTypes.MissingValuePlaceholder
|
|
568
|
+
} else {
|
|
569
|
+
comparisonValue = value
|
|
570
|
+
newRow[valueSlug] = 0
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return newRow
|
|
574
|
+
}
|
|
575
|
+
)
|
|
576
|
+
})
|
|
577
|
+
return rowsForSingleEntity
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
return this.transform(
|
|
581
|
+
newRows,
|
|
582
|
+
newDefs,
|
|
583
|
+
`Transformed columns from absolute values to % of time ${startTimeBound} for columns ${columnSlugs.join(
|
|
584
|
+
","
|
|
585
|
+
)} `,
|
|
586
|
+
TransformType.UpdateColumnDefs
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
keepMinTimeAndMaxTimeForEachEntityOnly(): this {
|
|
591
|
+
const indexMap = this.rowIndicesByEntityName
|
|
592
|
+
const timeColumn = this.timeColumn
|
|
593
|
+
if (timeColumn.isMissing) return this
|
|
594
|
+
const timeValues = timeColumn.valuesIncludingErrorValues
|
|
595
|
+
const matchingIndices = new Set<number>()
|
|
596
|
+
indexMap.forEach((indices) =>
|
|
597
|
+
[_.minBy, _.maxBy]
|
|
598
|
+
.map((f) => f(indices, (index) => timeValues[index]))
|
|
599
|
+
.filter(isPresent)
|
|
600
|
+
.forEach((index) => matchingIndices.add(index))
|
|
601
|
+
)
|
|
602
|
+
return this.columnFilter(
|
|
603
|
+
timeColumn.slug,
|
|
604
|
+
(row, index) => matchingIndices.has(index),
|
|
605
|
+
`Keep minTime & maxTime rows only for each entity`
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private getAverageAnnualChangeIndicesByEntity(
|
|
610
|
+
columnSlugs: ColumnSlug[]
|
|
611
|
+
): Map<EntityName, [number, number]> {
|
|
612
|
+
const columns = columnSlugs.map((slug) => this.get(slug))
|
|
613
|
+
const indexMap = this.rowIndicesByEntityName
|
|
614
|
+
const timeValues = this.timeColumn.valuesIncludingErrorValues
|
|
615
|
+
|
|
616
|
+
// Find indices of min & max rows
|
|
617
|
+
const entityNameToIndices = new Map<EntityName, [number, number]>()
|
|
618
|
+
indexMap.forEach((indices, entityName) => {
|
|
619
|
+
// We are discarding every row which contains a 0 for any columnSlug.
|
|
620
|
+
// Technically, to be more correct, we should support distinct min/max indices for each
|
|
621
|
+
// columnSlug, but that only makes a tiny difference in a tiny subset of charts.
|
|
622
|
+
const nonZeroValueIndices = indices.filter((index) =>
|
|
623
|
+
columns.every((col) => {
|
|
624
|
+
const value = col.valuesIncludingErrorValues[index]
|
|
625
|
+
return _.isNumber(value) && value !== 0
|
|
626
|
+
})
|
|
627
|
+
)
|
|
628
|
+
const minIndex = _.minBy(
|
|
629
|
+
nonZeroValueIndices,
|
|
630
|
+
(index) => timeValues[index]
|
|
631
|
+
)
|
|
632
|
+
const maxIndex = _.maxBy(indices, (index) => timeValues[index])
|
|
633
|
+
if (minIndex === undefined || maxIndex === undefined) return
|
|
634
|
+
|
|
635
|
+
const allValuePairsHaveDistinctTime = columns.every((col) => {
|
|
636
|
+
const originalTimes =
|
|
637
|
+
this.columnStore[col.originalTimeColumnSlug]
|
|
638
|
+
return originalTimes[minIndex] !== originalTimes[maxIndex]
|
|
639
|
+
})
|
|
640
|
+
if (allValuePairsHaveDistinctTime)
|
|
641
|
+
entityNameToIndices.set(entityName, [minIndex, maxIndex])
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
return entityNameToIndices
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
toAverageAnnualChangeForEachEntity(columnSlugs: ColumnSlug[]): this {
|
|
648
|
+
columnSlugs = columnSlugs.filter((slug) => this.has(slug))
|
|
649
|
+
if (
|
|
650
|
+
this.timeColumn.isMissing ||
|
|
651
|
+
!(this.timeColumn instanceof ColumnTypeMap.Year) ||
|
|
652
|
+
columnSlugs.length === 0
|
|
653
|
+
)
|
|
654
|
+
return this
|
|
655
|
+
|
|
656
|
+
const columns = columnSlugs.map((slug) => this.get(slug))
|
|
657
|
+
const entityNameToIndices =
|
|
658
|
+
this.getAverageAnnualChangeIndicesByEntity(columnSlugs)
|
|
659
|
+
|
|
660
|
+
// Overwrite table rows
|
|
661
|
+
const rows: OwidRow[] = []
|
|
662
|
+
entityNameToIndices.forEach((indices) => {
|
|
663
|
+
const [startRow, endRow] = this.rowsAt(indices)
|
|
664
|
+
|
|
665
|
+
const newRow: OwidRow = { ...endRow }
|
|
666
|
+
columns.forEach((col) => {
|
|
667
|
+
const timeSlug = col.originalTimeColumnSlug
|
|
668
|
+
|
|
669
|
+
const startTime = startRow[timeSlug]
|
|
670
|
+
const endTime = endRow[timeSlug]
|
|
671
|
+
const yearsElapsed = endTime - startTime
|
|
672
|
+
|
|
673
|
+
const startValue = startRow[col.slug]
|
|
674
|
+
const endValue = endRow[col.slug]
|
|
675
|
+
|
|
676
|
+
// Update to average annual change
|
|
677
|
+
newRow[col.slug] = cagr(startValue, endValue, yearsElapsed)
|
|
678
|
+
|
|
679
|
+
// Add original start time column
|
|
680
|
+
const startTimeSlug = makeOriginalStartTimeSlugFromColumnSlug(
|
|
681
|
+
col.slug
|
|
682
|
+
)
|
|
683
|
+
newRow[startTimeSlug] = startTime
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
rows.push(newRow)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
const newDefs = [
|
|
690
|
+
...replaceDef(
|
|
691
|
+
this.defs,
|
|
692
|
+
columns.map((col) =>
|
|
693
|
+
toPercentageColumnDef(
|
|
694
|
+
col.def,
|
|
695
|
+
ColumnTypeNames.PercentChangeOverTime
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
),
|
|
699
|
+
...columns.map((col) => {
|
|
700
|
+
return {
|
|
701
|
+
...this.timeColumn.def,
|
|
702
|
+
slug: makeOriginalStartTimeSlugFromColumnSlug(col.slug),
|
|
703
|
+
}
|
|
704
|
+
}),
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
return this.transform(
|
|
708
|
+
rows,
|
|
709
|
+
newDefs,
|
|
710
|
+
`Average annual change for columns: ${columnSlugs.join(", ")}`,
|
|
711
|
+
TransformType.UpdateRows
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Give our users a clean CSV of each Grapher. Assumes an Owid Table with entityName.
|
|
716
|
+
toPrettyCsv(
|
|
717
|
+
useShortNames: boolean = false,
|
|
718
|
+
activeColumnSlugs: string[] | undefined = undefined
|
|
719
|
+
): string {
|
|
720
|
+
let table
|
|
721
|
+
if (activeColumnSlugs?.length) {
|
|
722
|
+
const timeColumnToInclude = [
|
|
723
|
+
OwidTableSlugs.year,
|
|
724
|
+
OwidTableSlugs.day,
|
|
725
|
+
this.timeColumn.slug, // needed for explorers, where the time column may be called anything
|
|
726
|
+
].find((colSlug) => this.has(colSlug))
|
|
727
|
+
|
|
728
|
+
if (!timeColumnToInclude)
|
|
729
|
+
throw new Error(
|
|
730
|
+
"Expected to find a time column to include in the CSV"
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
table = this.select([
|
|
734
|
+
timeColumnToInclude,
|
|
735
|
+
this.entityNameSlug,
|
|
736
|
+
...activeColumnSlugs,
|
|
737
|
+
])
|
|
738
|
+
} else {
|
|
739
|
+
table = this.dropColumns([
|
|
740
|
+
OwidTableSlugs.entityId,
|
|
741
|
+
OwidTableSlugs.time,
|
|
742
|
+
OwidTableSlugs.entityColor,
|
|
743
|
+
])
|
|
744
|
+
}
|
|
745
|
+
return table
|
|
746
|
+
.sortBy([this.entityNameSlug])
|
|
747
|
+
.toCsvWithColumnNames(useShortNames)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
@imemo get entityNameColorIndex(): Map<EntityName, Color> {
|
|
751
|
+
return this.valueIndex(
|
|
752
|
+
this.entityNameSlug,
|
|
753
|
+
OwidTableSlugs.entityColor
|
|
754
|
+
) as Map<EntityName, Color>
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
getColorForEntityName(entityName: EntityName): Color | undefined {
|
|
758
|
+
return this.entityNameColorIndex.get(entityName)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
@imemo get columnDisplayNameToColorMap(): Map<string, Color> {
|
|
762
|
+
return new Map(
|
|
763
|
+
this.columnsAsArray
|
|
764
|
+
.filter((col) => col.def.color)
|
|
765
|
+
.map((col) => [col.displayName, col.def.color!])
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// This assumes the table is sorted where the times for entity names go in asc order.
|
|
770
|
+
// The whole table does not have to be sorted by time.
|
|
771
|
+
getLatestValueForEntity(
|
|
772
|
+
entityName: EntityName,
|
|
773
|
+
columnSlug: ColumnSlug
|
|
774
|
+
): CoreValueType | undefined {
|
|
775
|
+
const indices = this.rowIndicesByEntityName.get(entityName)
|
|
776
|
+
if (!indices) return undefined
|
|
777
|
+
const values = this.get(columnSlug).valuesIncludingErrorValues
|
|
778
|
+
const descending = indices.toReversed()
|
|
779
|
+
const index = descending.find(
|
|
780
|
+
(index) => !(values[index] instanceof ErrorValue)
|
|
781
|
+
)
|
|
782
|
+
return index !== undefined ? values[index] : undefined
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
entitiesWith(columnSlugs: ColumnSlug[]): Set<EntityName> {
|
|
786
|
+
if (!columnSlugs.length) return new Set()
|
|
787
|
+
if (columnSlugs.length === 1)
|
|
788
|
+
return new Set(this.get(columnSlugs[0]).uniqEntityNames)
|
|
789
|
+
|
|
790
|
+
return intersectionOfSets<EntityName>(
|
|
791
|
+
columnSlugs.map((slug) => new Set(this.get(slug).uniqEntityNames))
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Retrieves the two columns `columnSlug` and `timeColumnSlug` from the table and
|
|
796
|
+
// passes their values to the respective interpolation method.
|
|
797
|
+
// `withAllRows` is expected to be completed and sorted.
|
|
798
|
+
private interpolate<K extends InterpolationContext>(
|
|
799
|
+
withAllRows: this,
|
|
800
|
+
columnSlug: ColumnSlug,
|
|
801
|
+
timeColumnSlug: ColumnSlug,
|
|
802
|
+
interpolation: InterpolationProvider<K>,
|
|
803
|
+
context: K
|
|
804
|
+
): { values: CoreValueType[]; times: number[] } {
|
|
805
|
+
const groupBoundaries = withAllRows.groupBoundaries(this.entityNameSlug)
|
|
806
|
+
const col = withAllRows.get(columnSlug)
|
|
807
|
+
const validIndices = col.validRowIndices
|
|
808
|
+
const newValues = col.valuesIncludingErrorValues.slice()
|
|
809
|
+
const newTimes = withAllRows
|
|
810
|
+
.get(timeColumnSlug)
|
|
811
|
+
.valuesIncludingErrorValues.slice() as Time[]
|
|
812
|
+
groupBoundaries.forEach((_, index) => {
|
|
813
|
+
interpolation(
|
|
814
|
+
newValues,
|
|
815
|
+
newTimes,
|
|
816
|
+
validIndices,
|
|
817
|
+
context,
|
|
818
|
+
groupBoundaries[index],
|
|
819
|
+
groupBoundaries[index + 1]
|
|
820
|
+
)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
values: newValues,
|
|
825
|
+
times: newTimes,
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// TODO generalize `interpolateColumnWithTolerance` and `interpolateColumnLinearly` more
|
|
830
|
+
// There are finicky details in both of them that complicate this
|
|
831
|
+
interpolateColumnWithTolerance(
|
|
832
|
+
columnSlug: ColumnSlug,
|
|
833
|
+
{ toleranceStrategyOverride, toleranceOverride }: ToleranceOptions = {}
|
|
834
|
+
): this {
|
|
835
|
+
// If the column doesn't exist, return the table unchanged.
|
|
836
|
+
if (!this.has(columnSlug)) return this
|
|
837
|
+
|
|
838
|
+
const column = this.get(columnSlug)
|
|
839
|
+
const columnDef = column.def as OwidColumnDef
|
|
840
|
+
const tolerance = toleranceOverride ?? column.tolerance ?? 0
|
|
841
|
+
const toleranceStrategy =
|
|
842
|
+
toleranceStrategyOverride ??
|
|
843
|
+
column.toleranceStrategy ??
|
|
844
|
+
ToleranceStrategy.closest
|
|
845
|
+
|
|
846
|
+
const timeColumnOfTable = !this.timeColumn.isMissing
|
|
847
|
+
? this.timeColumn
|
|
848
|
+
: // CovidTable does not have a day or year column so we need to use time.
|
|
849
|
+
(this.get(OwidTableSlugs.time) as CoreColumn)
|
|
850
|
+
|
|
851
|
+
const maybeTimeColumnOfValue =
|
|
852
|
+
getOriginalTimeColumnSlug(this, columnSlug) ??
|
|
853
|
+
timeColumnSlugFromColumnDef(columnDef)
|
|
854
|
+
const timeColumnOfValue = this.get(maybeTimeColumnOfValue)
|
|
855
|
+
const originalTimeSlug = makeOriginalTimeSlugFromColumnSlug(columnSlug)
|
|
856
|
+
|
|
857
|
+
let columnStore: CoreColumnStore
|
|
858
|
+
if (tolerance) {
|
|
859
|
+
const withAllRows = this.complete([
|
|
860
|
+
this.entityNameSlug,
|
|
861
|
+
timeColumnOfTable.slug,
|
|
862
|
+
]).sortBy([this.entityNameSlug, timeColumnOfTable.slug])
|
|
863
|
+
|
|
864
|
+
const interpolateInBothDirections =
|
|
865
|
+
!toleranceStrategy ||
|
|
866
|
+
toleranceStrategy === ToleranceStrategy.closest
|
|
867
|
+
const interpolateBackwards =
|
|
868
|
+
interpolateInBothDirections ||
|
|
869
|
+
toleranceStrategy === ToleranceStrategy.backwards
|
|
870
|
+
const interpolateForwards =
|
|
871
|
+
interpolateInBothDirections ||
|
|
872
|
+
toleranceStrategy === ToleranceStrategy.forwards
|
|
873
|
+
|
|
874
|
+
const interpolationResult = this.interpolate(
|
|
875
|
+
withAllRows,
|
|
876
|
+
columnSlug,
|
|
877
|
+
timeColumnOfValue.slug,
|
|
878
|
+
toleranceInterpolation,
|
|
879
|
+
{
|
|
880
|
+
timeToleranceBackwards: interpolateBackwards
|
|
881
|
+
? tolerance
|
|
882
|
+
: 0,
|
|
883
|
+
timeToleranceForwards: interpolateForwards ? tolerance : 0,
|
|
884
|
+
}
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
columnStore = {
|
|
888
|
+
...withAllRows.columnStore,
|
|
889
|
+
[columnSlug]: interpolationResult.values,
|
|
890
|
+
[originalTimeSlug]: interpolationResult.times,
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
// If there is no tolerance still append the tolerance column
|
|
894
|
+
columnStore = {
|
|
895
|
+
...this.columnStore,
|
|
896
|
+
[originalTimeSlug]:
|
|
897
|
+
timeColumnOfValue.valuesIncludingErrorValues,
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return this.transform(
|
|
902
|
+
columnStore,
|
|
903
|
+
[
|
|
904
|
+
...this.defs,
|
|
905
|
+
{
|
|
906
|
+
...timeColumnOfValue.def,
|
|
907
|
+
slug: originalTimeSlug,
|
|
908
|
+
display: {
|
|
909
|
+
includeInTable: false,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
`Interpolated values in column ${columnSlug} with tolerance ${tolerance} and appended column ${originalTimeSlug} with the original times`,
|
|
914
|
+
TransformType.UpdateColumnDefs
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
interpolateColumnLinearly(
|
|
919
|
+
columnSlug: ColumnSlug,
|
|
920
|
+
extrapolate: boolean = false
|
|
921
|
+
): this {
|
|
922
|
+
// If the column doesn't exist, return the table unchanged.
|
|
923
|
+
if (!this.has(columnSlug)) return this
|
|
924
|
+
|
|
925
|
+
const column = this.get(columnSlug)
|
|
926
|
+
const columnDef = column?.def as OwidColumnDef
|
|
927
|
+
|
|
928
|
+
const maybeTimeColumnSlug =
|
|
929
|
+
getOriginalTimeColumnSlug(this, columnSlug) ??
|
|
930
|
+
timeColumnSlugFromColumnDef(columnDef)
|
|
931
|
+
const timeColumn =
|
|
932
|
+
this.get(maybeTimeColumnSlug) ??
|
|
933
|
+
(this.get(OwidTableSlugs.time) as CoreColumn) // CovidTable does not have a day or year column so we need to use time.
|
|
934
|
+
|
|
935
|
+
const originalColumnSlug =
|
|
936
|
+
makeOriginalValueSlugFromColumnSlug(columnSlug)
|
|
937
|
+
const originalColumnDef = {
|
|
938
|
+
...columnDef,
|
|
939
|
+
slug: originalColumnSlug,
|
|
940
|
+
display: { includeInTable: false },
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// todo: we can probably do this once early in the pipeline so we dont have to do it again since complete and sort can be expensive.
|
|
944
|
+
const withAllRows = this.complete([
|
|
945
|
+
this.entityNameSlug,
|
|
946
|
+
timeColumn.slug,
|
|
947
|
+
]).sortBy([this.entityNameSlug, timeColumn.slug])
|
|
948
|
+
|
|
949
|
+
const interpolationResult = this.interpolate(
|
|
950
|
+
withAllRows,
|
|
951
|
+
columnSlug,
|
|
952
|
+
timeColumn.slug,
|
|
953
|
+
linearInterpolation,
|
|
954
|
+
{ extrapolateAtStart: extrapolate, extrapolateAtEnd: extrapolate }
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
const columnStore = {
|
|
958
|
+
...withAllRows.columnStore,
|
|
959
|
+
[originalColumnSlug]: withAllRows.columnStore[columnSlug],
|
|
960
|
+
[columnSlug]: interpolationResult.values,
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return this.transform(
|
|
964
|
+
columnStore,
|
|
965
|
+
[
|
|
966
|
+
...this.defs,
|
|
967
|
+
originalColumnDef,
|
|
968
|
+
{
|
|
969
|
+
...timeColumn.def,
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
`Interpolated values in column ${columnSlug} linearly and appended column ${originalColumnSlug} with the original values`,
|
|
973
|
+
TransformType.UpdateColumnDefs
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
interpolateColumnsByClosestTimeMatch(
|
|
978
|
+
columnSlugA: ColumnSlug,
|
|
979
|
+
columnSlugB: ColumnSlug
|
|
980
|
+
): this {
|
|
981
|
+
if (!this.has(columnSlugA) || !this.has(columnSlugB)) return this
|
|
982
|
+
|
|
983
|
+
const columnA = this.get(columnSlugA)
|
|
984
|
+
const columnB = this.get(columnSlugB)
|
|
985
|
+
|
|
986
|
+
const toleranceA = columnA.tolerance ?? 0
|
|
987
|
+
const toleranceB = columnB.tolerance ?? 0
|
|
988
|
+
|
|
989
|
+
// If the columns are of mismatching time types, then we can't do any time matching.
|
|
990
|
+
// This can happen when we have a ScatterPlot with days in one column, and a column with
|
|
991
|
+
// xOverrideYear.
|
|
992
|
+
// We also don't need to do any time matching when the tolerance of both columns is 0.
|
|
993
|
+
if (
|
|
994
|
+
this.timeColumn.isMissing ||
|
|
995
|
+
this.timeColumn.slug !== columnA.originalTimeColumnSlug ||
|
|
996
|
+
this.timeColumn.slug !== columnB.originalTimeColumnSlug ||
|
|
997
|
+
(toleranceA === 0 && toleranceB === 0)
|
|
998
|
+
) {
|
|
999
|
+
return this
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const maxDiff = Math.max(toleranceA, toleranceB)
|
|
1003
|
+
|
|
1004
|
+
const withAllRows = this.complete([
|
|
1005
|
+
this.entityNameSlug,
|
|
1006
|
+
this.timeColumn.slug,
|
|
1007
|
+
]).sortBy([this.entityNameSlug, this.timeColumn.slug])
|
|
1008
|
+
|
|
1009
|
+
// Existing columns
|
|
1010
|
+
const valuesA = withAllRows.get(columnA.slug).valuesIncludingErrorValues
|
|
1011
|
+
const valuesB = withAllRows.get(columnB.slug).valuesIncludingErrorValues
|
|
1012
|
+
const times = withAllRows.timeColumn
|
|
1013
|
+
.valuesIncludingErrorValues as Time[]
|
|
1014
|
+
|
|
1015
|
+
// New columns
|
|
1016
|
+
const newValuesA = new Array(times.length).fill(
|
|
1017
|
+
ErrorValueTypes.NoValueWithinTolerance
|
|
1018
|
+
)
|
|
1019
|
+
const newValuesB = new Array(times.length).fill(
|
|
1020
|
+
ErrorValueTypes.NoValueWithinTolerance
|
|
1021
|
+
)
|
|
1022
|
+
const newTimesA = new Array(times.length).fill(
|
|
1023
|
+
ErrorValueTypes.NoValueWithinTolerance
|
|
1024
|
+
)
|
|
1025
|
+
const newTimesB = new Array(times.length).fill(
|
|
1026
|
+
ErrorValueTypes.NoValueWithinTolerance
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
const groupBoundaries = withAllRows.groupBoundaries(this.entityNameSlug)
|
|
1030
|
+
|
|
1031
|
+
for (let i = 1; i < groupBoundaries.length; i++) {
|
|
1032
|
+
const startIndex = groupBoundaries[i - 1]
|
|
1033
|
+
const endIndex = groupBoundaries[i]
|
|
1034
|
+
const numRows = endIndex - startIndex
|
|
1035
|
+
|
|
1036
|
+
const availableTimesA = []
|
|
1037
|
+
const availableTimesB = []
|
|
1038
|
+
|
|
1039
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
1040
|
+
if (isNotErrorValue(valuesA[index]))
|
|
1041
|
+
availableTimesA.push(times[index])
|
|
1042
|
+
if (isNotErrorValue(valuesB[index]))
|
|
1043
|
+
availableTimesB.push(times[index])
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (availableTimesA.length === 0 || availableTimesB.length === 0) {
|
|
1047
|
+
// If one of the columns has no values, we can't do any interpolation
|
|
1048
|
+
continue
|
|
1049
|
+
}
|
|
1050
|
+
if (
|
|
1051
|
+
availableTimesA.length === numRows &&
|
|
1052
|
+
availableTimesB.length === numRows
|
|
1053
|
+
) {
|
|
1054
|
+
// If all values are available, we can skip the interpolation and just copy all values
|
|
1055
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
1056
|
+
newValuesA[index] = valuesA[index]
|
|
1057
|
+
newValuesB[index] = valuesB[index]
|
|
1058
|
+
newTimesA[index] = times[index]
|
|
1059
|
+
newTimesB[index] = times[index]
|
|
1060
|
+
}
|
|
1061
|
+
continue
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const timePairs = getClosestTimePairs(
|
|
1065
|
+
availableTimesA,
|
|
1066
|
+
availableTimesB,
|
|
1067
|
+
maxDiff
|
|
1068
|
+
)
|
|
1069
|
+
const timeAtoTimeB = new Map(timePairs)
|
|
1070
|
+
const pairedTimesInA = sortNumeric(
|
|
1071
|
+
Array.from(timeAtoTimeB.keys())
|
|
1072
|
+
) as Time[]
|
|
1073
|
+
|
|
1074
|
+
for (let index = startIndex; index < endIndex; index++) {
|
|
1075
|
+
const currentTime = times[index]
|
|
1076
|
+
|
|
1077
|
+
const candidateTimeA = sortedFindClosest(
|
|
1078
|
+
pairedTimesInA,
|
|
1079
|
+
currentTime
|
|
1080
|
+
)
|
|
1081
|
+
if (candidateTimeA === undefined) continue
|
|
1082
|
+
|
|
1083
|
+
if (Math.abs(currentTime - candidateTimeA) > toleranceA)
|
|
1084
|
+
continue
|
|
1085
|
+
|
|
1086
|
+
const candidateTimeB = timeAtoTimeB.get(candidateTimeA)
|
|
1087
|
+
|
|
1088
|
+
if (
|
|
1089
|
+
candidateTimeB === undefined ||
|
|
1090
|
+
Math.abs(currentTime - candidateTimeB) > toleranceB
|
|
1091
|
+
) {
|
|
1092
|
+
continue
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const candidateIndexA = sortedFindClosestIndex(
|
|
1096
|
+
times,
|
|
1097
|
+
candidateTimeA,
|
|
1098
|
+
startIndex,
|
|
1099
|
+
endIndex
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
const candidateIndexB = sortedFindClosestIndex(
|
|
1103
|
+
times,
|
|
1104
|
+
candidateTimeB,
|
|
1105
|
+
startIndex,
|
|
1106
|
+
endIndex
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
newValuesA[index] = valuesA[candidateIndexA]
|
|
1110
|
+
newValuesB[index] = valuesB[candidateIndexB]
|
|
1111
|
+
newTimesA[index] = times[candidateIndexA]
|
|
1112
|
+
newTimesB[index] = times[candidateIndexB]
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const originalTimeColumnASlug = makeOriginalTimeSlugFromColumnSlug(
|
|
1117
|
+
columnA.slug
|
|
1118
|
+
)
|
|
1119
|
+
const originalTimeColumnBSlug = makeOriginalTimeSlugFromColumnSlug(
|
|
1120
|
+
columnB.slug
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
const columnStore = {
|
|
1124
|
+
...withAllRows.columnStore,
|
|
1125
|
+
[columnA.slug]: newValuesA,
|
|
1126
|
+
[columnB.slug]: newValuesB,
|
|
1127
|
+
[originalTimeColumnASlug]: newTimesA,
|
|
1128
|
+
[originalTimeColumnBSlug]: newTimesB,
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return withAllRows.transform(
|
|
1132
|
+
columnStore,
|
|
1133
|
+
[
|
|
1134
|
+
...withAllRows.defs,
|
|
1135
|
+
{
|
|
1136
|
+
...withAllRows.timeColumn.def,
|
|
1137
|
+
slug: originalTimeColumnASlug,
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
...withAllRows.timeColumn.def,
|
|
1141
|
+
slug: originalTimeColumnBSlug,
|
|
1142
|
+
},
|
|
1143
|
+
],
|
|
1144
|
+
`Interpolated values`,
|
|
1145
|
+
TransformType.UpdateColumnDefs
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
sampleEntityName(howMany = 1): any[] {
|
|
1150
|
+
return this.availableEntityNames.slice(0, howMany)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
get isBlank(): boolean {
|
|
1154
|
+
return !this.numRows
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const BLANK_TABLE_MESSAGE = `Table is empty.`
|
|
1159
|
+
|
|
1160
|
+
// This just assures that even an emtpty OwidTable will have an entityName column. Probably a cleaner way to do this pattern (add a defaultColumns prop??)
|
|
1161
|
+
export const BlankOwidTable = (
|
|
1162
|
+
tableSlug = `blankOwidTable`,
|
|
1163
|
+
extraTableDescription = ""
|
|
1164
|
+
): OwidTable =>
|
|
1165
|
+
new OwidTable(
|
|
1166
|
+
undefined,
|
|
1167
|
+
[
|
|
1168
|
+
{ slug: OwidTableSlugs.entityName },
|
|
1169
|
+
{ slug: OwidTableSlugs.year, type: ColumnTypeNames.Year },
|
|
1170
|
+
],
|
|
1171
|
+
{
|
|
1172
|
+
tableDescription: BLANK_TABLE_MESSAGE + extraTableDescription,
|
|
1173
|
+
tableSlug,
|
|
1174
|
+
}
|
|
1175
|
+
)
|