@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,1332 @@
|
|
|
1
|
+
import * as _ from "lodash-es"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { computed, observable, action, makeObservable } from "mobx"
|
|
4
|
+
import { observer } from "mobx-react"
|
|
5
|
+
import classnames from "classnames"
|
|
6
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
7
|
+
import {
|
|
8
|
+
faArrowDownLong,
|
|
9
|
+
faArrowUpLong,
|
|
10
|
+
faInfoCircle,
|
|
11
|
+
} from "@fortawesome/free-solid-svg-icons"
|
|
12
|
+
import { scaleLinear } from "d3-scale"
|
|
13
|
+
import { extent } from "d3-array"
|
|
14
|
+
import { line } from "d3-shape"
|
|
15
|
+
import {
|
|
16
|
+
SortOrder,
|
|
17
|
+
Time,
|
|
18
|
+
EntityName,
|
|
19
|
+
OwidTableSlugs,
|
|
20
|
+
OwidVariableRoundingMode,
|
|
21
|
+
OwidVariableRow,
|
|
22
|
+
} from "../../types/index.js"
|
|
23
|
+
import { OwidTable, CoreColumn } from "../../core-table/index.js"
|
|
24
|
+
import {
|
|
25
|
+
valuesByEntityAtTimes,
|
|
26
|
+
es6mapValues,
|
|
27
|
+
exposeInstanceOnWindow,
|
|
28
|
+
DataValue,
|
|
29
|
+
Bounds,
|
|
30
|
+
TickFormattingOptions,
|
|
31
|
+
Tippy,
|
|
32
|
+
excludeUndefined,
|
|
33
|
+
joinTitleFragments,
|
|
34
|
+
FuzzySearch,
|
|
35
|
+
} from "../../utils/index.js"
|
|
36
|
+
import { SelectionArray } from "../selection/SelectionArray"
|
|
37
|
+
import {
|
|
38
|
+
DEFAULT_GRAPHER_BOUNDS,
|
|
39
|
+
DEFAULT_GRAPHER_ENTITY_TYPE,
|
|
40
|
+
SVG_STYLE_PROPS,
|
|
41
|
+
} from "../core/GrapherConstants"
|
|
42
|
+
import * as R from "remeda"
|
|
43
|
+
import { makeSelectionArray } from "../chart/ChartUtils"
|
|
44
|
+
import { isEntityRegionType } from "../core/EntitiesByRegionType"
|
|
45
|
+
import { NoDataModal } from "../noDataModal/NoDataModal"
|
|
46
|
+
import {
|
|
47
|
+
DataTableColumnKey,
|
|
48
|
+
DisplayDataTableDimension,
|
|
49
|
+
DataTableRow,
|
|
50
|
+
DataTableDimension,
|
|
51
|
+
DataTableColumnDefinition,
|
|
52
|
+
DataTableValuesForEntity,
|
|
53
|
+
RangeValuesForEntity,
|
|
54
|
+
RangeColumnKey,
|
|
55
|
+
PointValuesForEntity,
|
|
56
|
+
PointColumnKey,
|
|
57
|
+
SparklineHighlight,
|
|
58
|
+
TargetTimeMode,
|
|
59
|
+
MinimalOwidRow,
|
|
60
|
+
DataTableConfig,
|
|
61
|
+
DataTableSortState,
|
|
62
|
+
DataTableState,
|
|
63
|
+
DimensionSortIndex,
|
|
64
|
+
DataTableManager,
|
|
65
|
+
CommonDataTableFilter,
|
|
66
|
+
COMMON_DATA_TABLE_FILTERS,
|
|
67
|
+
DataTableFilter,
|
|
68
|
+
SparklineKey,
|
|
69
|
+
} from "./DataTableConstants"
|
|
70
|
+
import { GRAY_30 } from "../color/ColorConstants"
|
|
71
|
+
|
|
72
|
+
const ENTITY_SORT_INDEX = -1
|
|
73
|
+
|
|
74
|
+
const DEFAULT_SORT_STATE: DataTableSortState = {
|
|
75
|
+
dimIndex: ENTITY_SORT_INDEX,
|
|
76
|
+
columnKey: undefined,
|
|
77
|
+
order: SortOrder.asc,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const columnNameByType: Record<DataTableColumnKey, string> = {
|
|
81
|
+
single: "Value",
|
|
82
|
+
start: "Start",
|
|
83
|
+
end: "End",
|
|
84
|
+
delta: "Absolute Change",
|
|
85
|
+
deltaRatio: "Relative Change",
|
|
86
|
+
sparkline: "Sparkline",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const inverseSortOrder = (order: SortOrder): SortOrder =>
|
|
90
|
+
order === SortOrder.asc ? SortOrder.desc : SortOrder.asc
|
|
91
|
+
|
|
92
|
+
interface DataTableProps {
|
|
93
|
+
manager: DataTableManager
|
|
94
|
+
bounds?: Bounds
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@observer
|
|
98
|
+
export class DataTable extends React.Component<DataTableProps> {
|
|
99
|
+
private storedState: DataTableState = {
|
|
100
|
+
sort: DEFAULT_SORT_STATE,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
constructor(props: DataTableProps) {
|
|
104
|
+
super(props)
|
|
105
|
+
makeObservable<DataTable, "storedState">(this, {
|
|
106
|
+
storedState: observable,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@computed get manager(): DataTableManager {
|
|
111
|
+
return this.props.manager
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@computed private get selectionArray(): SelectionArray {
|
|
115
|
+
return makeSelectionArray(this.manager.dataTableSelection)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@computed private get tableConfig(): DataTableConfig {
|
|
119
|
+
return this.manager.dataTableConfig
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@computed private get timelineMinTime(): Time | undefined {
|
|
123
|
+
return this.manager.closestTimelineMinTime
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@computed private get timelineMaxTime(): Time | undefined {
|
|
127
|
+
return this.manager.closestTimelineMaxTime
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@computed get table(): OwidTable {
|
|
131
|
+
let table = this.manager.filteredTableForDisplay
|
|
132
|
+
|
|
133
|
+
// make sure the given table doesn't contain any rows outside of the time range
|
|
134
|
+
table = table.filterByTimeRange(
|
|
135
|
+
this.manager.closestTimelineMinTime ?? -Infinity,
|
|
136
|
+
this.manager.closestTimelineMaxTime ?? Infinity
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return table
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@computed private get tableState(): DataTableState {
|
|
143
|
+
return { sort: this.sortState }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@computed private get sortState(): DataTableSortState {
|
|
147
|
+
let { dimIndex, columnKey, order } = {
|
|
148
|
+
...DEFAULT_SORT_STATE,
|
|
149
|
+
...this.storedState.sort,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If not sorted by entity, then make sure the index of the chosen column exists
|
|
153
|
+
dimIndex = Math.min(dimIndex, this.table.numColumns - 1)
|
|
154
|
+
if (dimIndex !== ENTITY_SORT_INDEX) {
|
|
155
|
+
const availableColumns = this.dataTableDimensionsWithValues[
|
|
156
|
+
dimIndex
|
|
157
|
+
].columnDefinitions.map((colDef) => colDef.key)
|
|
158
|
+
if (
|
|
159
|
+
columnKey === undefined ||
|
|
160
|
+
!availableColumns.includes(columnKey)
|
|
161
|
+
)
|
|
162
|
+
columnKey = availableColumns[0]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
dimIndex,
|
|
167
|
+
columnKey,
|
|
168
|
+
order,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@computed private get entityType(): string {
|
|
173
|
+
return this.manager.entityType ?? DEFAULT_GRAPHER_ENTITY_TYPE
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@computed private get sortValueMapper(): (
|
|
177
|
+
row: DataTableRow
|
|
178
|
+
) => number | string {
|
|
179
|
+
const { dimIndex: sortIndex, columnKey, order } = this.tableState.sort
|
|
180
|
+
if (sortIndex === ENTITY_SORT_INDEX)
|
|
181
|
+
return (row): string => row.entityName
|
|
182
|
+
|
|
183
|
+
return (row): string | number => {
|
|
184
|
+
const dv = row.values[sortIndex] as DataTableValuesForEntity
|
|
185
|
+
|
|
186
|
+
let value: number | string | undefined
|
|
187
|
+
if (dv) {
|
|
188
|
+
if (isSingleValue(dv)) {
|
|
189
|
+
value = dv.single?.value
|
|
190
|
+
} else if (
|
|
191
|
+
isRangeValue(dv) &&
|
|
192
|
+
columnKey !== undefined &&
|
|
193
|
+
isRangeColumnKey(columnKey)
|
|
194
|
+
) {
|
|
195
|
+
value = dv[columnKey]?.value
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// We always want undefined values to be last
|
|
200
|
+
if (
|
|
201
|
+
value === undefined ||
|
|
202
|
+
(typeof value === "number" &&
|
|
203
|
+
(!isFinite(value) || isNaN(value)))
|
|
204
|
+
)
|
|
205
|
+
return order === SortOrder.asc ? Infinity : -Infinity
|
|
206
|
+
|
|
207
|
+
return value
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@computed private get hasSubheaders(): boolean {
|
|
212
|
+
return (
|
|
213
|
+
!this.hasDimensionHeaders ||
|
|
214
|
+
this.dataTableDimensionsWithValues.some(
|
|
215
|
+
(header) => header.columnDefinitions.length > 1
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@action.bound private updateSort(
|
|
221
|
+
dimIndex: DimensionSortIndex,
|
|
222
|
+
columnKey?: DataTableColumnKey
|
|
223
|
+
): void {
|
|
224
|
+
const { sort } = this.tableState
|
|
225
|
+
const order =
|
|
226
|
+
sort.dimIndex === dimIndex && sort.columnKey === columnKey
|
|
227
|
+
? inverseSortOrder(sort.order)
|
|
228
|
+
: dimIndex === ENTITY_SORT_INDEX
|
|
229
|
+
? SortOrder.asc
|
|
230
|
+
: SortOrder.desc
|
|
231
|
+
|
|
232
|
+
this.storedState.sort.dimIndex = dimIndex
|
|
233
|
+
this.storedState.sort.columnKey = columnKey
|
|
234
|
+
this.storedState.sort.order = order
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private get entityHeaderText(): string {
|
|
238
|
+
return _.capitalize(this.entityType)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private get entityHeader(): React.ReactElement {
|
|
242
|
+
const { sort } = this.tableState
|
|
243
|
+
return (
|
|
244
|
+
<ColumnHeader
|
|
245
|
+
key="entity"
|
|
246
|
+
sortable={this.entityCount > 1}
|
|
247
|
+
sortedCol={sort.dimIndex === ENTITY_SORT_INDEX}
|
|
248
|
+
sortOrder={sort.order}
|
|
249
|
+
onClick={(): void => this.updateSort(ENTITY_SORT_INDEX)}
|
|
250
|
+
headerText={this.entityHeaderText}
|
|
251
|
+
colType="entity"
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@computed private get hasDimensionHeaders(): boolean {
|
|
257
|
+
return this.dataTableDimensionsWithValues.length > 1
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// If the table has a single data column, we move the data column
|
|
261
|
+
// closer to the entity column to make it easier to read the table
|
|
262
|
+
@computed private get singleDataColumnStyle():
|
|
263
|
+
| { minWidth: number; contentMaxWidth: number }
|
|
264
|
+
| undefined {
|
|
265
|
+
// no need to do this on mobile
|
|
266
|
+
if (this.manager.isNarrow) return
|
|
267
|
+
|
|
268
|
+
const hasSingleDataColumn =
|
|
269
|
+
this.displayDimensions.length === 1 &&
|
|
270
|
+
this.displayDimensions[0].columnDefinitions.length === 1
|
|
271
|
+
|
|
272
|
+
if (!hasSingleDataColumn) return
|
|
273
|
+
|
|
274
|
+
// header text
|
|
275
|
+
const dimension = this.displayDimensions[0]
|
|
276
|
+
const column = this.displayDimensions[0].columnDefinitions[0]
|
|
277
|
+
const headerText = this.subheaderText(column, dimension)
|
|
278
|
+
|
|
279
|
+
// display values
|
|
280
|
+
const values = excludeUndefined(
|
|
281
|
+
this.displayRows.map(
|
|
282
|
+
(row) => (row?.values[0] as PointValuesForEntity).single
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const accessor = (row: MinimalOwidRow): number | undefined =>
|
|
287
|
+
typeof row.value === "string" ? row.value.length : row.value
|
|
288
|
+
const maxValue = _.maxBy(values, accessor)
|
|
289
|
+
const minValue = _.minBy(values, accessor)
|
|
290
|
+
|
|
291
|
+
const measureWidth = (text: string): number =>
|
|
292
|
+
Bounds.forText(text, { fontSize: 14 }).width
|
|
293
|
+
|
|
294
|
+
// in theory, we should be measuring the length of all values
|
|
295
|
+
// but we might have a lot of values, so we just measure the length
|
|
296
|
+
// of the min and max values as a proxy
|
|
297
|
+
const contentMaxWidth = Math.ceil(
|
|
298
|
+
Math.max(
|
|
299
|
+
measureWidth(maxValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon
|
|
300
|
+
measureWidth(minValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon
|
|
301
|
+
measureWidth(headerText) + 26 // 26px accounts for the sort icon
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// minimum width of the column
|
|
306
|
+
const minWidth = 0.66 * this.bounds.width
|
|
307
|
+
|
|
308
|
+
// only do this if there is an actual need
|
|
309
|
+
if (minWidth - contentMaxWidth < 320) return
|
|
310
|
+
|
|
311
|
+
return { minWidth, contentMaxWidth }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private get dimensionHeaders(): React.ReactElement[] | null {
|
|
315
|
+
const { sort } = this.tableState
|
|
316
|
+
return this.displayDimensions.map((dim, dimIndex) => {
|
|
317
|
+
const { coreTableColumn, display } = dim
|
|
318
|
+
const singleColumn = dim.columnDefinitions.find(
|
|
319
|
+
(column) => column.key === PointColumnKey.single
|
|
320
|
+
)
|
|
321
|
+
const targetTime = singleColumn?.targetTime
|
|
322
|
+
|
|
323
|
+
const title = _.upperFirst(display.columnName.title)
|
|
324
|
+
const titleFragments = joinTitleFragments(
|
|
325
|
+
display.columnName.attributionShort,
|
|
326
|
+
display.columnName.titleVariant
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
const dimensionHeaderText = (
|
|
330
|
+
<React.Fragment>
|
|
331
|
+
<div
|
|
332
|
+
className="name"
|
|
333
|
+
title={
|
|
334
|
+
titleFragments
|
|
335
|
+
? `${title} – ${titleFragments}`
|
|
336
|
+
: title
|
|
337
|
+
}
|
|
338
|
+
>
|
|
339
|
+
<span className="title-text">{title} </span>
|
|
340
|
+
<span className="title-fragments">
|
|
341
|
+
{titleFragments}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="description">
|
|
345
|
+
<span className="unit">{display.unit}</span>
|
|
346
|
+
<span>
|
|
347
|
+
{display.unit && targetTime !== undefined && ","}
|
|
348
|
+
</span>{" "}
|
|
349
|
+
<span className="time">
|
|
350
|
+
{targetTime !== undefined &&
|
|
351
|
+
coreTableColumn.formatTime(targetTime)}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
</React.Fragment>
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const onClick = dim.sortable
|
|
358
|
+
? (): void => this.updateSort(dimIndex, PointColumnKey.single)
|
|
359
|
+
: undefined
|
|
360
|
+
|
|
361
|
+
const props = {
|
|
362
|
+
sortable: dim.sortable,
|
|
363
|
+
sortedCol: dim.sortable && sort.dimIndex === dimIndex,
|
|
364
|
+
sortOrder: sort.order,
|
|
365
|
+
onClick,
|
|
366
|
+
colSpan: dim.columnDefinitions.length,
|
|
367
|
+
headerText: dimensionHeaderText,
|
|
368
|
+
colType: "dimension" as const,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return <ColumnHeader key={coreTableColumn.slug} {...props} />
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private subheaderText(
|
|
376
|
+
column: DataTableColumnDefinition,
|
|
377
|
+
dimension: DisplayDataTableDimension
|
|
378
|
+
): string {
|
|
379
|
+
const col = dimension.coreTableColumn
|
|
380
|
+
|
|
381
|
+
if (column.key === SparklineKey.sparkline) {
|
|
382
|
+
const minTime = col.formatTime(this.timelineMinTime!)
|
|
383
|
+
const maxTime = col.formatTime(this.timelineMaxTime!)
|
|
384
|
+
return `${minTime}–${maxTime}`
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return isDeltaColumn(column.key)
|
|
388
|
+
? columnNameByType[column.key]
|
|
389
|
+
: col.formatTime(column.targetTime!)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private get dimensionSubheaders(): React.ReactElement[][] {
|
|
393
|
+
const { sort } = this.tableState
|
|
394
|
+
return this.displayDimensions.map((dim, dimIndex) =>
|
|
395
|
+
dim.columnDefinitions.map((column, colIndex) => {
|
|
396
|
+
const headerText = this.subheaderText(column, dim)
|
|
397
|
+
const onClick = column.sortable
|
|
398
|
+
? (): void => this.updateSort(dimIndex, column.key)
|
|
399
|
+
: undefined
|
|
400
|
+
return (
|
|
401
|
+
<ColumnHeader
|
|
402
|
+
key={column.key}
|
|
403
|
+
sortable={column.sortable}
|
|
404
|
+
sortedCol={
|
|
405
|
+
sort.dimIndex === dimIndex &&
|
|
406
|
+
sort.columnKey === column.key
|
|
407
|
+
}
|
|
408
|
+
sortOrder={sort.order}
|
|
409
|
+
onClick={onClick}
|
|
410
|
+
headerText={headerText}
|
|
411
|
+
colType="subdimension"
|
|
412
|
+
classNames={classnames({
|
|
413
|
+
"subdimension-first": colIndex === 0,
|
|
414
|
+
})}
|
|
415
|
+
minWidth={this.singleDataColumnStyle?.minWidth}
|
|
416
|
+
contentMaxWidth={
|
|
417
|
+
this.singleDataColumnStyle?.contentMaxWidth
|
|
418
|
+
}
|
|
419
|
+
/>
|
|
420
|
+
)
|
|
421
|
+
})
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private get headerRow(): React.ReactElement {
|
|
426
|
+
const { hasDimensionHeaders, hasSubheaders } = this
|
|
427
|
+
return hasDimensionHeaders && hasSubheaders ? (
|
|
428
|
+
<>
|
|
429
|
+
<tr>
|
|
430
|
+
<th className="above-entity" />
|
|
431
|
+
{this.dimensionHeaders}
|
|
432
|
+
</tr>
|
|
433
|
+
<tr>
|
|
434
|
+
{this.entityHeader}
|
|
435
|
+
{this.dimensionSubheaders}
|
|
436
|
+
</tr>
|
|
437
|
+
</>
|
|
438
|
+
) : (
|
|
439
|
+
<tr>
|
|
440
|
+
{this.entityHeader}
|
|
441
|
+
{hasSubheaders
|
|
442
|
+
? this.dimensionSubheaders
|
|
443
|
+
: this.dimensionHeaders}
|
|
444
|
+
</tr>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private renderValueCellContent({
|
|
449
|
+
columnDefinition,
|
|
450
|
+
valuesForEntity,
|
|
451
|
+
formatTime,
|
|
452
|
+
}: {
|
|
453
|
+
columnDefinition: DataTableColumnDefinition
|
|
454
|
+
valuesForEntity?: DataTableValuesForEntity
|
|
455
|
+
formatTime: (time: Time) => string
|
|
456
|
+
}): React.ReactElement | null {
|
|
457
|
+
if (!valuesForEntity) return null
|
|
458
|
+
if (!(columnDefinition.key in valuesForEntity)) return null
|
|
459
|
+
|
|
460
|
+
const value = getValueForEntityByKey(
|
|
461
|
+
valuesForEntity,
|
|
462
|
+
columnDefinition.key
|
|
463
|
+
)
|
|
464
|
+
if (!value) return null
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<>
|
|
468
|
+
<ClosestTimeNotice
|
|
469
|
+
value={value}
|
|
470
|
+
columnDefinition={columnDefinition}
|
|
471
|
+
formatTime={formatTime}
|
|
472
|
+
/>
|
|
473
|
+
<span>{value.displayValue}</span>
|
|
474
|
+
</>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private renderSparklineCellContent({
|
|
479
|
+
columnDefinition,
|
|
480
|
+
valuesForEntity,
|
|
481
|
+
isProjection,
|
|
482
|
+
}: {
|
|
483
|
+
columnDefinition: DataTableColumnDefinition
|
|
484
|
+
valuesForEntity?: DataTableValuesForEntity
|
|
485
|
+
isProjection?: boolean
|
|
486
|
+
}): React.ReactElement | null {
|
|
487
|
+
if (
|
|
488
|
+
columnDefinition.key !== SparklineKey.sparkline ||
|
|
489
|
+
!valuesForEntity?.sparkline
|
|
490
|
+
)
|
|
491
|
+
return null
|
|
492
|
+
|
|
493
|
+
const highlights: SparklineHighlight[] = []
|
|
494
|
+
|
|
495
|
+
const start = isRangeValue(valuesForEntity)
|
|
496
|
+
? valuesForEntity.start
|
|
497
|
+
: valuesForEntity.single
|
|
498
|
+
const startTime = start?.time ?? this.targetTimes?.[0]
|
|
499
|
+
|
|
500
|
+
const end = isRangeValue(valuesForEntity)
|
|
501
|
+
? valuesForEntity.end
|
|
502
|
+
: valuesForEntity.single
|
|
503
|
+
const endTime = end?.time ?? this.targetTimes?.[1]
|
|
504
|
+
|
|
505
|
+
// Add a highlight for the start time
|
|
506
|
+
if (startTime !== undefined) {
|
|
507
|
+
const value =
|
|
508
|
+
typeof start?.value === "string" ? undefined : start?.value
|
|
509
|
+
|
|
510
|
+
const showMarker =
|
|
511
|
+
this.manager.timelineDragTarget === "start" ||
|
|
512
|
+
this.manager.timelineDragTarget === "both"
|
|
513
|
+
|
|
514
|
+
highlights.push({ time: startTime, value, showMarker })
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Add a highlight for the end time
|
|
518
|
+
if (endTime !== undefined && endTime !== startTime) {
|
|
519
|
+
const value =
|
|
520
|
+
typeof end?.value === "string" ? undefined : end?.value
|
|
521
|
+
|
|
522
|
+
const showMarker =
|
|
523
|
+
this.manager.timelineDragTarget === "end" ||
|
|
524
|
+
this.manager.timelineDragTarget === "both"
|
|
525
|
+
|
|
526
|
+
highlights.push({ time: endTime, value, showMarker })
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<Sparkline
|
|
531
|
+
owidRows={valuesForEntity.sparkline}
|
|
532
|
+
minTime={this.timelineMinTime!}
|
|
533
|
+
maxTime={this.timelineMaxTime!}
|
|
534
|
+
highlights={highlights}
|
|
535
|
+
strokeStyle={isProjection ? "dotted" : "solid"}
|
|
536
|
+
/>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private renderEntityRow(
|
|
541
|
+
row: DataTableRow,
|
|
542
|
+
dimensions: DisplayDataTableDimension[]
|
|
543
|
+
): React.ReactElement {
|
|
544
|
+
return (
|
|
545
|
+
<tr key={row.entityName}>
|
|
546
|
+
<td key="entity" className={classnames({ entity: true })}>
|
|
547
|
+
{row.entityName}
|
|
548
|
+
</td>
|
|
549
|
+
|
|
550
|
+
{row.values.map((valuesForEntity, dimIndex) => {
|
|
551
|
+
const dimension = dimensions[dimIndex]
|
|
552
|
+
const { isProjection } = dimension.coreTableColumn
|
|
553
|
+
|
|
554
|
+
return dimension.columnDefinitions.map(
|
|
555
|
+
(columnDefinition, colIndex) => {
|
|
556
|
+
const key = `${dimIndex}-${colIndex}`
|
|
557
|
+
const formatTime = (time: Time): string =>
|
|
558
|
+
dimension.coreTableColumn.formatTime(time)
|
|
559
|
+
|
|
560
|
+
if (columnDefinition.key === SparklineKey.sparkline)
|
|
561
|
+
return (
|
|
562
|
+
<ValueCell
|
|
563
|
+
key={key}
|
|
564
|
+
columnKey={columnDefinition.key}
|
|
565
|
+
isFirstColumn={colIndex === 0}
|
|
566
|
+
>
|
|
567
|
+
{this.renderSparklineCellContent({
|
|
568
|
+
columnDefinition,
|
|
569
|
+
valuesForEntity,
|
|
570
|
+
isProjection,
|
|
571
|
+
})}
|
|
572
|
+
</ValueCell>
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<ValueCell
|
|
577
|
+
key={key}
|
|
578
|
+
columnKey={columnDefinition.key}
|
|
579
|
+
isFirstColumn={colIndex === 0}
|
|
580
|
+
maxWidth={
|
|
581
|
+
this.singleDataColumnStyle
|
|
582
|
+
?.contentMaxWidth
|
|
583
|
+
}
|
|
584
|
+
>
|
|
585
|
+
{this.renderValueCellContent({
|
|
586
|
+
columnDefinition,
|
|
587
|
+
valuesForEntity,
|
|
588
|
+
formatTime,
|
|
589
|
+
})}
|
|
590
|
+
</ValueCell>
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
)
|
|
594
|
+
})}
|
|
595
|
+
</tr>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@computed get bounds(): Bounds {
|
|
600
|
+
return this.props.bounds ?? DEFAULT_GRAPHER_BOUNDS
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@computed private get tableCaption(): React.ReactElement | null {
|
|
604
|
+
if (this.hasDimensionHeaders) return null
|
|
605
|
+
if (this.displayDimensions.length === 0) return null
|
|
606
|
+
|
|
607
|
+
const singleDimension = this.displayDimensions[0]
|
|
608
|
+
const titleFragments =
|
|
609
|
+
singleDimension.display.columnName.attributionShort ||
|
|
610
|
+
singleDimension.display.columnName.titleVariant
|
|
611
|
+
? joinTitleFragments(
|
|
612
|
+
singleDimension.display.columnName.attributionShort,
|
|
613
|
+
singleDimension.display.columnName.titleVariant
|
|
614
|
+
)
|
|
615
|
+
: undefined
|
|
616
|
+
|
|
617
|
+
return singleDimension ? (
|
|
618
|
+
<div className="caption">
|
|
619
|
+
{singleDimension.display.columnName.title}{" "}
|
|
620
|
+
<span className="title-fragments">
|
|
621
|
+
{titleFragments}
|
|
622
|
+
{singleDimension.display.unit &&
|
|
623
|
+
` (${singleDimension.display.unit})`}
|
|
624
|
+
</span>
|
|
625
|
+
</div>
|
|
626
|
+
) : null
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private renderNoDataModal(): React.ReactElement {
|
|
630
|
+
return (
|
|
631
|
+
<svg
|
|
632
|
+
width={this.bounds.width}
|
|
633
|
+
height={this.bounds.height}
|
|
634
|
+
style={SVG_STYLE_PROPS}
|
|
635
|
+
>
|
|
636
|
+
<NoDataModal
|
|
637
|
+
manager={this.manager}
|
|
638
|
+
bounds={this.bounds}
|
|
639
|
+
message={`No ${this.entityType} matches this query`}
|
|
640
|
+
helpText="Try checking for typos or searching for something else"
|
|
641
|
+
hideTextOutline
|
|
642
|
+
/>
|
|
643
|
+
</svg>
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
override render(): React.ReactElement | null {
|
|
648
|
+
if (this.sortedDisplayRows.length === 0) return this.renderNoDataModal()
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
<div className="DataTable">
|
|
652
|
+
{this.tableCaption}
|
|
653
|
+
<div className="table-wrapper">
|
|
654
|
+
<table>
|
|
655
|
+
<thead>{this.headerRow}</thead>
|
|
656
|
+
<tbody>
|
|
657
|
+
{this.sortedDisplayRows.map((row) =>
|
|
658
|
+
this.renderEntityRow(
|
|
659
|
+
row,
|
|
660
|
+
this.displayDimensions
|
|
661
|
+
)
|
|
662
|
+
)}
|
|
663
|
+
</tbody>
|
|
664
|
+
</table>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
@computed private get loadedWithData(): boolean {
|
|
671
|
+
return this.columnsToShow.length > 0
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private readonly AUTO_SELECTION_THRESHOLD_PERCENTAGE = 0.5
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* If the user or the editor hasn't specified a start, auto-select a start time
|
|
678
|
+
* where AUTO_SELECTION_THRESHOLD_PERCENTAGE of the entities have values.
|
|
679
|
+
*/
|
|
680
|
+
@computed get autoSelectedStartTime(): number | undefined {
|
|
681
|
+
let autoSelectedStartTime: number | undefined = undefined
|
|
682
|
+
|
|
683
|
+
if (!this.loadedWithData) return undefined
|
|
684
|
+
|
|
685
|
+
const numEntitiesInTable = this.entityNames.length
|
|
686
|
+
|
|
687
|
+
this.columnsToShow.forEach((column): boolean => {
|
|
688
|
+
const numberOfEntitiesWithDataSortedByTime = _.sortBy(
|
|
689
|
+
Object.entries(R.countBy(column.uniqTimesAsc, R.identity())),
|
|
690
|
+
([time, _count]) => parseInt(time)
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
const firstTimeWithSufficientData =
|
|
694
|
+
numberOfEntitiesWithDataSortedByTime.find((time) => {
|
|
695
|
+
const numEntitiesWithData = time[1]
|
|
696
|
+
const percentEntitiesWithData =
|
|
697
|
+
numEntitiesWithData / numEntitiesInTable
|
|
698
|
+
return (
|
|
699
|
+
percentEntitiesWithData >=
|
|
700
|
+
this.AUTO_SELECTION_THRESHOLD_PERCENTAGE
|
|
701
|
+
)
|
|
702
|
+
})?.[0]
|
|
703
|
+
|
|
704
|
+
if (firstTimeWithSufficientData) {
|
|
705
|
+
autoSelectedStartTime = parseInt(firstTimeWithSufficientData)
|
|
706
|
+
return false
|
|
707
|
+
}
|
|
708
|
+
return true
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
return autoSelectedStartTime
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@computed private get columnsToShow(): CoreColumn[] {
|
|
715
|
+
const slugs = this.manager.dataTableSlugs ?? []
|
|
716
|
+
if (slugs.length)
|
|
717
|
+
return slugs
|
|
718
|
+
.map((slug: string) => {
|
|
719
|
+
const col = this.table.get(slug)
|
|
720
|
+
if (!col)
|
|
721
|
+
console.warn(`Warning: column '${slug}' not found`)
|
|
722
|
+
return col
|
|
723
|
+
})
|
|
724
|
+
.filter((col) => col)
|
|
725
|
+
|
|
726
|
+
const skips = new Set(Object.keys(OwidTableSlugs))
|
|
727
|
+
return this.table.columnsAsArray.filter(
|
|
728
|
+
(column) =>
|
|
729
|
+
!skips.has(column.slug) &&
|
|
730
|
+
// dim.property !== "color" &&
|
|
731
|
+
(column.display?.includeInTable ?? true)
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@computed private get availableEntityNames(): EntityName[] {
|
|
736
|
+
return _.union(
|
|
737
|
+
...this.columnsToShow.map(
|
|
738
|
+
(col) => this.table.get(col.slug).uniqEntityNames
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
@computed get fuzzy(): FuzzySearch<string> {
|
|
744
|
+
return FuzzySearch.withKey(
|
|
745
|
+
this.availableEntityNames,
|
|
746
|
+
(entityName) => entityName
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
@computed private get entityNames(): EntityName[] {
|
|
751
|
+
if (!this.tableConfig.search) return this.availableEntityNames
|
|
752
|
+
return this.fuzzy.search(this.tableConfig.search)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
@computed private get entityCount(): number {
|
|
756
|
+
return this.entityNames.length
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@computed private get isSortable(): boolean {
|
|
760
|
+
return this.entityCount > 1
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
override componentDidMount(): void {
|
|
764
|
+
exposeInstanceOnWindow(this, "dataTable")
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private formatValue(
|
|
768
|
+
column: CoreColumn,
|
|
769
|
+
value: number | string | undefined,
|
|
770
|
+
formattingOverrides?: TickFormattingOptions
|
|
771
|
+
): string | undefined {
|
|
772
|
+
if (value === undefined) return undefined
|
|
773
|
+
return column.formatValueShort(value, {
|
|
774
|
+
roundingMode: OwidVariableRoundingMode.decimalPlaces,
|
|
775
|
+
numberAbbreviation: false,
|
|
776
|
+
trailingZeroes: true,
|
|
777
|
+
useNoBreakSpace: true,
|
|
778
|
+
...formattingOverrides,
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
@computed get targetTimes(): [number] | [number, number] | undefined {
|
|
783
|
+
const { startTime, endTime } = this.manager
|
|
784
|
+
if (startTime === undefined || endTime === undefined) return undefined
|
|
785
|
+
|
|
786
|
+
if (startTime !== endTime) return [startTime, endTime]
|
|
787
|
+
return [endTime]
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private getTargetTimesForColumn(
|
|
791
|
+
coreTableColumn: CoreColumn
|
|
792
|
+
): [number] | [number, number] {
|
|
793
|
+
// Respect the column's target time if it's set
|
|
794
|
+
if (coreTableColumn.def.targetTime !== undefined)
|
|
795
|
+
return [coreTableColumn.def.targetTime]
|
|
796
|
+
|
|
797
|
+
// Otherwise, use the table's target times
|
|
798
|
+
if (this.targetTimes !== undefined) return this.targetTimes
|
|
799
|
+
|
|
800
|
+
return [coreTableColumn.maxTime]
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
@computed get dataTableDimensionsWithValues(): DataTableDimension[] {
|
|
804
|
+
return this.columnsToShow.map((coreTableColumn) => {
|
|
805
|
+
const targetTimes = this.getTargetTimesForColumn(coreTableColumn)
|
|
806
|
+
const targetTimeMode =
|
|
807
|
+
targetTimes.length < 2
|
|
808
|
+
? TargetTimeMode.point
|
|
809
|
+
: TargetTimeMode.range
|
|
810
|
+
|
|
811
|
+
// Get values for the given target times and apply tolerance
|
|
812
|
+
const targetValuesByEntity = this.interpolateTargetValues({
|
|
813
|
+
coreTableColumn,
|
|
814
|
+
targetTimes,
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
// Add absolute and relative change columns if necessary
|
|
818
|
+
const valuesByEntityName =
|
|
819
|
+
this.calculateDataValuesForTargetTimeMode({
|
|
820
|
+
targetValuesByEntity,
|
|
821
|
+
coreTableColumn,
|
|
822
|
+
targetTimeMode,
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
// Add data for sparklines
|
|
826
|
+
if (this.columnHasSparkline(coreTableColumn)) {
|
|
827
|
+
for (const [entityName, values] of valuesByEntityName) {
|
|
828
|
+
values.sparkline =
|
|
829
|
+
coreTableColumn.owidRowsByEntityName.get(entityName)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Construct column definitions for the given target time mode
|
|
834
|
+
const columnDefinitions = this.constructColumnDefinitions({
|
|
835
|
+
coreTableColumn,
|
|
836
|
+
targetTimes,
|
|
837
|
+
targetTimeMode,
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
return { columnDefinitions, valuesByEntityName, coreTableColumn }
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private columnHasSparkline(coreTableColumn: CoreColumn): boolean {
|
|
845
|
+
return (
|
|
846
|
+
this.timelineMinTime !== undefined &&
|
|
847
|
+
this.timelineMaxTime !== undefined &&
|
|
848
|
+
this.timelineMinTime !== this.timelineMaxTime &&
|
|
849
|
+
coreTableColumn.hasNumberFormatting &&
|
|
850
|
+
// For columns with a target time, the data table is fixed at that time.
|
|
851
|
+
// It thus doesn't make sense to show a sparkline
|
|
852
|
+
coreTableColumn.def.targetTime === undefined
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private constructColumnDefinitions({
|
|
857
|
+
coreTableColumn,
|
|
858
|
+
targetTimes,
|
|
859
|
+
targetTimeMode,
|
|
860
|
+
}: {
|
|
861
|
+
coreTableColumn: CoreColumn
|
|
862
|
+
targetTimes: number[]
|
|
863
|
+
targetTimeMode: TargetTimeMode
|
|
864
|
+
}): DataTableColumnDefinition[] {
|
|
865
|
+
// Inject delta columns if the data is numerical and we have start & end
|
|
866
|
+
// values to compare in the table. One column for absolute difference,
|
|
867
|
+
// another for % difference.
|
|
868
|
+
const deltaColumns: DataTableColumnDefinition[] = []
|
|
869
|
+
if (coreTableColumn.hasNumberFormatting) {
|
|
870
|
+
if (targetTimeMode === TargetTimeMode.range) {
|
|
871
|
+
const { tableDisplay = {} } = coreTableColumn.display ?? {}
|
|
872
|
+
if (!tableDisplay.hideAbsoluteChange)
|
|
873
|
+
deltaColumns.push({
|
|
874
|
+
key: RangeColumnKey.delta,
|
|
875
|
+
sortable: this.isSortable,
|
|
876
|
+
})
|
|
877
|
+
if (!tableDisplay.hideRelativeChange)
|
|
878
|
+
deltaColumns.push({
|
|
879
|
+
key: RangeColumnKey.deltaRatio,
|
|
880
|
+
sortable: this.isSortable,
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const valueColumns: DataTableColumnDefinition[] = targetTimes.map(
|
|
886
|
+
(targetTime, index) => ({
|
|
887
|
+
key:
|
|
888
|
+
targetTimeMode === TargetTimeMode.range
|
|
889
|
+
? index === 0
|
|
890
|
+
? RangeColumnKey.start
|
|
891
|
+
: RangeColumnKey.end
|
|
892
|
+
: PointColumnKey.single,
|
|
893
|
+
targetTime,
|
|
894
|
+
sortable: this.isSortable,
|
|
895
|
+
})
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
// Show a column with sparklines if appropriate
|
|
899
|
+
const sparklineColumn = this.columnHasSparkline(coreTableColumn)
|
|
900
|
+
? { key: SparklineKey.sparkline, sortable: false }
|
|
901
|
+
: undefined
|
|
902
|
+
|
|
903
|
+
return excludeUndefined([
|
|
904
|
+
...valueColumns,
|
|
905
|
+
sparklineColumn,
|
|
906
|
+
...deltaColumns,
|
|
907
|
+
])
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private interpolateTargetValues({
|
|
911
|
+
coreTableColumn,
|
|
912
|
+
targetTimes,
|
|
913
|
+
}: {
|
|
914
|
+
coreTableColumn: CoreColumn
|
|
915
|
+
targetTimes: number[]
|
|
916
|
+
}): Map<string, (DataValue | undefined)[]> {
|
|
917
|
+
return valuesByEntityAtTimes(
|
|
918
|
+
coreTableColumn.valueByEntityNameAndOriginalTime,
|
|
919
|
+
targetTimes,
|
|
920
|
+
coreTableColumn.tolerance
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private calculateDataValuesForTargetTimeMode({
|
|
925
|
+
targetValuesByEntity,
|
|
926
|
+
targetTimeMode,
|
|
927
|
+
coreTableColumn,
|
|
928
|
+
}: {
|
|
929
|
+
targetValuesByEntity: Map<string, (DataValue | undefined)[]>
|
|
930
|
+
targetTimeMode: TargetTimeMode
|
|
931
|
+
coreTableColumn: CoreColumn
|
|
932
|
+
}): Map<string, DataTableValuesForEntity> {
|
|
933
|
+
return es6mapValues(targetValuesByEntity, (dvs) => {
|
|
934
|
+
// There is always a column, but not always a data value (in the delta column the
|
|
935
|
+
// value needs to be calculated)
|
|
936
|
+
if (targetTimeMode === TargetTimeMode.range) {
|
|
937
|
+
const [start, end]: (MinimalOwidRow | undefined)[] = dvs
|
|
938
|
+
const result: RangeValuesForEntity = {
|
|
939
|
+
start: {
|
|
940
|
+
...start,
|
|
941
|
+
displayValue: this.formatValue(
|
|
942
|
+
coreTableColumn,
|
|
943
|
+
start?.value
|
|
944
|
+
),
|
|
945
|
+
},
|
|
946
|
+
end: {
|
|
947
|
+
...end,
|
|
948
|
+
displayValue: this.formatValue(
|
|
949
|
+
coreTableColumn,
|
|
950
|
+
end?.value
|
|
951
|
+
),
|
|
952
|
+
},
|
|
953
|
+
delta: undefined,
|
|
954
|
+
deltaRatio: undefined,
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (
|
|
958
|
+
start !== undefined &&
|
|
959
|
+
end !== undefined &&
|
|
960
|
+
typeof start.value === "number" &&
|
|
961
|
+
typeof end.value === "number" &&
|
|
962
|
+
// sanity check: start time should always be <= end time
|
|
963
|
+
start.time !== undefined &&
|
|
964
|
+
end.time !== undefined &&
|
|
965
|
+
start.time <= end.time
|
|
966
|
+
) {
|
|
967
|
+
const deltaValue = end.value - start.value
|
|
968
|
+
const deltaRatioValue = deltaValue / Math.abs(start.value)
|
|
969
|
+
|
|
970
|
+
result.delta = {
|
|
971
|
+
value: deltaValue,
|
|
972
|
+
displayValue: this.formatValue(
|
|
973
|
+
coreTableColumn,
|
|
974
|
+
deltaValue,
|
|
975
|
+
{
|
|
976
|
+
showPlus: true,
|
|
977
|
+
unit:
|
|
978
|
+
coreTableColumn.shortUnit === "%"
|
|
979
|
+
? "pp"
|
|
980
|
+
: coreTableColumn.shortUnit,
|
|
981
|
+
}
|
|
982
|
+
),
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
result.deltaRatio = {
|
|
986
|
+
value: deltaRatioValue,
|
|
987
|
+
displayValue:
|
|
988
|
+
isFinite(deltaRatioValue) && !isNaN(deltaRatioValue)
|
|
989
|
+
? this.formatValue(
|
|
990
|
+
coreTableColumn,
|
|
991
|
+
deltaRatioValue * 100,
|
|
992
|
+
{
|
|
993
|
+
unit: "%",
|
|
994
|
+
numDecimalPlaces: 0,
|
|
995
|
+
showPlus: true,
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
: undefined,
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return result
|
|
1002
|
+
} else {
|
|
1003
|
+
// if single time
|
|
1004
|
+
const dv = dvs[0]
|
|
1005
|
+
const result: PointValuesForEntity = {
|
|
1006
|
+
single: { ...dv },
|
|
1007
|
+
}
|
|
1008
|
+
if (dv !== undefined)
|
|
1009
|
+
result.single!.displayValue = this.formatValue(
|
|
1010
|
+
coreTableColumn,
|
|
1011
|
+
dv.value
|
|
1012
|
+
)
|
|
1013
|
+
return result
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
@computed get displayDimensions(): DisplayDataTableDimension[] {
|
|
1019
|
+
return this.dataTableDimensionsWithValues.map((d) => {
|
|
1020
|
+
const coreTableColumn = d.coreTableColumn
|
|
1021
|
+
const columnName = coreTableColumn.titlePublicOrDisplayName
|
|
1022
|
+
const unit =
|
|
1023
|
+
coreTableColumn.unit === "%" ? "percent" : coreTableColumn.unit
|
|
1024
|
+
|
|
1025
|
+
return {
|
|
1026
|
+
coreTableColumn,
|
|
1027
|
+
columnDefinitions: d.columnDefinitions,
|
|
1028
|
+
display: { columnName, unit },
|
|
1029
|
+
sortable: !this.hasSubheaders,
|
|
1030
|
+
}
|
|
1031
|
+
})
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
@computed private get displayRows(): DataTableRow[] {
|
|
1035
|
+
return this.entityNames.map((entityName) => {
|
|
1036
|
+
return {
|
|
1037
|
+
entityName,
|
|
1038
|
+
values: this.dataTableDimensionsWithValues.map((d) =>
|
|
1039
|
+
d.valuesByEntityName.get(entityName)
|
|
1040
|
+
),
|
|
1041
|
+
}
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
@computed private get sortedDisplayRows(): DataTableRow[] {
|
|
1046
|
+
const { order } = this.tableState.sort
|
|
1047
|
+
return _.orderBy(this.displayRows, this.sortValueMapper, order)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function ColumnHeader(props: {
|
|
1052
|
+
classNames?: string
|
|
1053
|
+
sortable: boolean
|
|
1054
|
+
sortedCol: boolean
|
|
1055
|
+
sortOrder: SortOrder
|
|
1056
|
+
onClick?: () => void
|
|
1057
|
+
rowSpan?: number
|
|
1058
|
+
colSpan?: number
|
|
1059
|
+
headerText: React.ReactNode
|
|
1060
|
+
colType: "entity" | "dimension" | "subdimension"
|
|
1061
|
+
minWidth?: number
|
|
1062
|
+
contentMaxWidth?: number
|
|
1063
|
+
}): React.ReactElement {
|
|
1064
|
+
const { sortable, sortedCol, colType } = props
|
|
1065
|
+
const isEntityColumn = colType === "entity"
|
|
1066
|
+
const sortIcon = sortable && (
|
|
1067
|
+
<SortIcon
|
|
1068
|
+
isActiveIcon={sortedCol}
|
|
1069
|
+
order={
|
|
1070
|
+
sortedCol
|
|
1071
|
+
? props.sortOrder
|
|
1072
|
+
: isEntityColumn
|
|
1073
|
+
? SortOrder.asc
|
|
1074
|
+
: SortOrder.desc
|
|
1075
|
+
}
|
|
1076
|
+
/>
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
return (
|
|
1080
|
+
<th
|
|
1081
|
+
className={classnames(props.classNames, colType, { sortable })}
|
|
1082
|
+
rowSpan={props.rowSpan ?? 1}
|
|
1083
|
+
colSpan={props.colSpan ?? 1}
|
|
1084
|
+
onClick={props.onClick}
|
|
1085
|
+
style={{ minWidth: props.minWidth }}
|
|
1086
|
+
>
|
|
1087
|
+
<CellContent maxWidth={props.contentMaxWidth}>
|
|
1088
|
+
<div className="content">
|
|
1089
|
+
{!isEntityColumn && sortIcon}
|
|
1090
|
+
<span>{props.headerText}</span>
|
|
1091
|
+
{isEntityColumn && sortIcon}
|
|
1092
|
+
</div>
|
|
1093
|
+
</CellContent>
|
|
1094
|
+
</th>
|
|
1095
|
+
)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function CellContent(props: {
|
|
1099
|
+
maxWidth?: number
|
|
1100
|
+
children?: React.ReactNode
|
|
1101
|
+
}): React.ReactElement {
|
|
1102
|
+
if (!props.maxWidth) return <>{props.children}</>
|
|
1103
|
+
return <div style={{ maxWidth: props.maxWidth }}>{props.children}</div>
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function ValueCell(props: {
|
|
1107
|
+
columnKey: DataTableColumnKey
|
|
1108
|
+
isFirstColumn?: boolean
|
|
1109
|
+
maxWidth?: number
|
|
1110
|
+
children?: React.ReactNode
|
|
1111
|
+
}): React.ReactElement {
|
|
1112
|
+
const className = classnames([
|
|
1113
|
+
"cell",
|
|
1114
|
+
`cell-${props.columnKey}`,
|
|
1115
|
+
{ "cell-first": props.isFirstColumn },
|
|
1116
|
+
])
|
|
1117
|
+
return (
|
|
1118
|
+
<td className={className}>
|
|
1119
|
+
<CellContent maxWidth={props.maxWidth}>
|
|
1120
|
+
{props.children}
|
|
1121
|
+
</CellContent>
|
|
1122
|
+
</td>
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function SortIcon(props: {
|
|
1127
|
+
isActiveIcon?: boolean
|
|
1128
|
+
order: SortOrder
|
|
1129
|
+
}): React.ReactElement {
|
|
1130
|
+
const isActiveIcon = props.isActiveIcon ?? false
|
|
1131
|
+
const activeIcon =
|
|
1132
|
+
props.order === SortOrder.desc ? faArrowUpLong : faArrowDownLong
|
|
1133
|
+
|
|
1134
|
+
return (
|
|
1135
|
+
<span
|
|
1136
|
+
className={classnames({ "sort-icon": true, active: isActiveIcon })}
|
|
1137
|
+
>
|
|
1138
|
+
{isActiveIcon ? (
|
|
1139
|
+
<FontAwesomeIcon icon={activeIcon} />
|
|
1140
|
+
) : (
|
|
1141
|
+
<span style={{ display: "inline-block", width: "max-content" }}>
|
|
1142
|
+
<FontAwesomeIcon icon={faArrowUpLong} />
|
|
1143
|
+
<FontAwesomeIcon
|
|
1144
|
+
icon={faArrowDownLong}
|
|
1145
|
+
style={{ marginLeft: "-2px" }}
|
|
1146
|
+
/>
|
|
1147
|
+
</span>
|
|
1148
|
+
)}
|
|
1149
|
+
</span>
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function ClosestTimeNotice({
|
|
1154
|
+
value,
|
|
1155
|
+
columnDefinition,
|
|
1156
|
+
formatTime,
|
|
1157
|
+
}: {
|
|
1158
|
+
value: MinimalOwidRow
|
|
1159
|
+
columnDefinition: DataTableColumnDefinition
|
|
1160
|
+
formatTime: (time: Time) => string
|
|
1161
|
+
}): React.ReactElement | null {
|
|
1162
|
+
const shouldShowClosestTimeNotice =
|
|
1163
|
+
!isDeltaColumn(columnDefinition.key) &&
|
|
1164
|
+
columnDefinition.targetTime !== value.time
|
|
1165
|
+
|
|
1166
|
+
if (
|
|
1167
|
+
value.time === undefined ||
|
|
1168
|
+
columnDefinition.targetTime === undefined ||
|
|
1169
|
+
!shouldShowClosestTimeNotice
|
|
1170
|
+
)
|
|
1171
|
+
return null
|
|
1172
|
+
|
|
1173
|
+
const targetTime = formatTime(columnDefinition.targetTime)
|
|
1174
|
+
const closestTime = formatTime(value.time)
|
|
1175
|
+
|
|
1176
|
+
return (
|
|
1177
|
+
<Tippy
|
|
1178
|
+
content={
|
|
1179
|
+
<div className="closest-time-notice-tippy">
|
|
1180
|
+
<strong>Data not available for {targetTime}</strong>
|
|
1181
|
+
<br />
|
|
1182
|
+
Showing closest available data point ({closestTime})
|
|
1183
|
+
</div>
|
|
1184
|
+
}
|
|
1185
|
+
arrow={false}
|
|
1186
|
+
>
|
|
1187
|
+
<span className="closest-time-notice-icon">
|
|
1188
|
+
<span className="icon">
|
|
1189
|
+
<FontAwesomeIcon icon={faInfoCircle} />
|
|
1190
|
+
</span>
|
|
1191
|
+
</span>
|
|
1192
|
+
</Tippy>
|
|
1193
|
+
)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function Sparkline({
|
|
1197
|
+
width = 75,
|
|
1198
|
+
height = 18,
|
|
1199
|
+
owidRows,
|
|
1200
|
+
minTime,
|
|
1201
|
+
maxTime,
|
|
1202
|
+
highlights = [],
|
|
1203
|
+
dotSize = 3.5,
|
|
1204
|
+
color = "#4C6A9C",
|
|
1205
|
+
strokeStyle = "solid",
|
|
1206
|
+
}: {
|
|
1207
|
+
width?: number
|
|
1208
|
+
height?: number
|
|
1209
|
+
owidRows: OwidVariableRow<number>[]
|
|
1210
|
+
minTime: number
|
|
1211
|
+
maxTime: number
|
|
1212
|
+
highlights?: SparklineHighlight[]
|
|
1213
|
+
dotSize?: number
|
|
1214
|
+
color?: string
|
|
1215
|
+
strokeStyle?: "solid" | "dotted"
|
|
1216
|
+
}): React.ReactElement | null {
|
|
1217
|
+
if (owidRows.length <= 1) return null
|
|
1218
|
+
|
|
1219
|
+
// add a little padding so the dots don't overflow
|
|
1220
|
+
const bounds = new Bounds(0, 0, width, height).padWidth(dotSize)
|
|
1221
|
+
|
|
1222
|
+
// calculate x-scale
|
|
1223
|
+
const xDomain = [minTime, maxTime]
|
|
1224
|
+
const xScale = scaleLinear()
|
|
1225
|
+
.domain(xDomain)
|
|
1226
|
+
.range([bounds.left, bounds.right])
|
|
1227
|
+
|
|
1228
|
+
// calculate y-scale
|
|
1229
|
+
const yDomain = extent(owidRows.map((row) => row.value)) as [number, number]
|
|
1230
|
+
const yScale = scaleLinear()
|
|
1231
|
+
.domain(yDomain)
|
|
1232
|
+
.range([bounds.bottom, bounds.top])
|
|
1233
|
+
|
|
1234
|
+
const makePath = line<OwidVariableRow<number>>()
|
|
1235
|
+
.x((row) => xScale(row.originalTime))
|
|
1236
|
+
.y((row) => yScale(row.value))
|
|
1237
|
+
|
|
1238
|
+
const path = makePath(owidRows)
|
|
1239
|
+
if (!path) return null
|
|
1240
|
+
|
|
1241
|
+
const strokeDasharray = strokeStyle === "dotted" ? "2,3" : undefined
|
|
1242
|
+
|
|
1243
|
+
return (
|
|
1244
|
+
<svg
|
|
1245
|
+
width={width}
|
|
1246
|
+
height={height}
|
|
1247
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
1248
|
+
style={{ overflow: "visible" }}
|
|
1249
|
+
>
|
|
1250
|
+
{/* marker lines of highlights */}
|
|
1251
|
+
{highlights
|
|
1252
|
+
.filter((highlight) => highlight.showMarker)
|
|
1253
|
+
.map((highlight) => (
|
|
1254
|
+
<line
|
|
1255
|
+
key={highlight.time}
|
|
1256
|
+
x1={xScale(highlight.time)}
|
|
1257
|
+
x2={xScale(highlight.time)}
|
|
1258
|
+
y1={0}
|
|
1259
|
+
y2={height}
|
|
1260
|
+
stroke={GRAY_30}
|
|
1261
|
+
/>
|
|
1262
|
+
))}
|
|
1263
|
+
|
|
1264
|
+
{/* sparkline */}
|
|
1265
|
+
<path
|
|
1266
|
+
d={path}
|
|
1267
|
+
stroke={color}
|
|
1268
|
+
fill="none"
|
|
1269
|
+
strokeWidth={1.5}
|
|
1270
|
+
strokeDasharray={strokeDasharray}
|
|
1271
|
+
/>
|
|
1272
|
+
|
|
1273
|
+
{/* highlighted data points */}
|
|
1274
|
+
{highlights
|
|
1275
|
+
.filter((highlight) => highlight.value !== undefined)
|
|
1276
|
+
.map((highlight) => (
|
|
1277
|
+
<circle
|
|
1278
|
+
key={highlight.time}
|
|
1279
|
+
cx={xScale(highlight.time)}
|
|
1280
|
+
cy={yScale(highlight.value!)}
|
|
1281
|
+
r={dotSize}
|
|
1282
|
+
fill={color}
|
|
1283
|
+
stroke="#fff"
|
|
1284
|
+
/>
|
|
1285
|
+
))}
|
|
1286
|
+
</svg>
|
|
1287
|
+
)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function getValueForEntityByKey(
|
|
1291
|
+
dimensionValue: DataTableValuesForEntity,
|
|
1292
|
+
columnKey: DataTableColumnKey
|
|
1293
|
+
): MinimalOwidRow | undefined {
|
|
1294
|
+
if (isSingleValue(dimensionValue)) {
|
|
1295
|
+
return dimensionValue[columnKey as PointColumnKey] as MinimalOwidRow
|
|
1296
|
+
} else if (isRangeValue(dimensionValue)) {
|
|
1297
|
+
return dimensionValue[columnKey as RangeColumnKey] as MinimalOwidRow
|
|
1298
|
+
}
|
|
1299
|
+
return undefined
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function isRangeColumnKey(key: string): key is RangeColumnKey {
|
|
1303
|
+
return Object.values(RangeColumnKey).includes(key as any)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function isRangeValue(
|
|
1307
|
+
value: DataTableValuesForEntity
|
|
1308
|
+
): value is RangeValuesForEntity {
|
|
1309
|
+
return "start" in value
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function isSingleValue(
|
|
1313
|
+
value: DataTableValuesForEntity
|
|
1314
|
+
): value is PointValuesForEntity {
|
|
1315
|
+
return "single" in value
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function isDeltaColumn(columnKey?: DataTableColumnKey): boolean {
|
|
1319
|
+
return columnKey === "delta" || columnKey === "deltaRatio"
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function isCommonDataTableFilter(
|
|
1323
|
+
candidate: string
|
|
1324
|
+
): candidate is CommonDataTableFilter {
|
|
1325
|
+
return COMMON_DATA_TABLE_FILTERS.includes(candidate as any)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
export function isValidDataTableFilter(
|
|
1329
|
+
candidate: string
|
|
1330
|
+
): candidate is DataTableFilter {
|
|
1331
|
+
return isCommonDataTableFilter(candidate) || isEntityRegionType(candidate)
|
|
1332
|
+
}
|