@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,949 @@
|
|
|
1
|
+
import * as _ from "lodash-es"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import { computed, action, observable, makeObservable } from "mobx"
|
|
4
|
+
import { observer } from "mobx-react"
|
|
5
|
+
import {
|
|
6
|
+
geoGraticule,
|
|
7
|
+
geoOrthographic,
|
|
8
|
+
GeoPath,
|
|
9
|
+
geoPath,
|
|
10
|
+
GeoPermissibleObjects,
|
|
11
|
+
} from "d3-geo"
|
|
12
|
+
import { Quadtree, quadtree } from "d3-quadtree"
|
|
13
|
+
import { select } from "d3-selection"
|
|
14
|
+
import { zoom } from "d3-zoom"
|
|
15
|
+
// no types available for versor
|
|
16
|
+
import versor from "versor"
|
|
17
|
+
import {
|
|
18
|
+
makeIdForHumanConsumption,
|
|
19
|
+
Bounds,
|
|
20
|
+
isTouchDevice,
|
|
21
|
+
getRelativeMouse,
|
|
22
|
+
checkIsTouchEvent,
|
|
23
|
+
PointVector,
|
|
24
|
+
MapRegionName,
|
|
25
|
+
excludeUndefined,
|
|
26
|
+
EntityName,
|
|
27
|
+
} from "../../utils/index.js"
|
|
28
|
+
import {
|
|
29
|
+
Annotation,
|
|
30
|
+
ANNOTATION_COLOR_DARK,
|
|
31
|
+
ANNOTATION_COLOR_LIGHT,
|
|
32
|
+
ChoroplethMapManager,
|
|
33
|
+
ChoroplethSeriesByName,
|
|
34
|
+
DEFAULT_GLOBE_SIZE,
|
|
35
|
+
ExternalAnnotation,
|
|
36
|
+
GEO_FEATURES_CLASSNAME,
|
|
37
|
+
GLOBE_COUNTRY_ZOOM,
|
|
38
|
+
GLOBE_LATITUDE_MAX,
|
|
39
|
+
GLOBE_LATITUDE_MIN,
|
|
40
|
+
GLOBE_MAX_ZOOM,
|
|
41
|
+
GLOBE_MIN_ZOOM,
|
|
42
|
+
GlobeRenderFeature,
|
|
43
|
+
InternalAnnotation,
|
|
44
|
+
MAP_HOVER_TARGET_RANGE,
|
|
45
|
+
PROJECTED_DATA_LEGEND_COLOR,
|
|
46
|
+
RenderFeature,
|
|
47
|
+
SVGMouseEvent,
|
|
48
|
+
} from "./MapChartConstants"
|
|
49
|
+
import { MapConfig } from "./MapConfig"
|
|
50
|
+
import { getGeoFeaturesForGlobe } from "./GeoFeatures"
|
|
51
|
+
import {
|
|
52
|
+
BackgroundCountry,
|
|
53
|
+
CountryWithData,
|
|
54
|
+
CountryWithNoData,
|
|
55
|
+
ExternalValueAnnotation,
|
|
56
|
+
InternalValueAnnotation,
|
|
57
|
+
NoDataPattern,
|
|
58
|
+
ProjectedDataPattern,
|
|
59
|
+
} from "./MapComponents"
|
|
60
|
+
import { Patterns } from "../core/GrapherConstants"
|
|
61
|
+
import {
|
|
62
|
+
calculateDistance,
|
|
63
|
+
detectNearbyFeature,
|
|
64
|
+
isPointPlacedOnVisibleHemisphere,
|
|
65
|
+
sortFeaturesByInteractionStateAndSize,
|
|
66
|
+
getForegroundFeatures,
|
|
67
|
+
isValidGlobeRegionName,
|
|
68
|
+
} from "./MapHelpers"
|
|
69
|
+
import {
|
|
70
|
+
makeInternalAnnotationForFeature,
|
|
71
|
+
makeExternalAnnotationForFeature,
|
|
72
|
+
repositionAndFilterExternalAnnotations,
|
|
73
|
+
} from "./MapAnnotations"
|
|
74
|
+
import * as R from "remeda"
|
|
75
|
+
import { GlobeController } from "./GlobeController"
|
|
76
|
+
import { isDarkColor } from "../color/ColorUtils"
|
|
77
|
+
import { MapSelectionArray } from "../selection/MapSelectionArray"
|
|
78
|
+
|
|
79
|
+
const DEFAULT_SCALE = geoOrthographic().scale()
|
|
80
|
+
|
|
81
|
+
@observer
|
|
82
|
+
export class ChoroplethGlobe extends React.Component<{
|
|
83
|
+
manager: ChoroplethMapManager
|
|
84
|
+
}> {
|
|
85
|
+
base = React.createRef<SVGGElement>()
|
|
86
|
+
|
|
87
|
+
private hoverEnterFeature: GlobeRenderFeature | undefined = undefined
|
|
88
|
+
private hoverNearbyFeature: GlobeRenderFeature | undefined = undefined
|
|
89
|
+
|
|
90
|
+
private isPanningOrZooming = false
|
|
91
|
+
|
|
92
|
+
constructor(props: { manager: ChoroplethMapManager }) {
|
|
93
|
+
super(props)
|
|
94
|
+
|
|
95
|
+
makeObservable<
|
|
96
|
+
ChoroplethGlobe,
|
|
97
|
+
"hoverEnterFeature" | "hoverNearbyFeature"
|
|
98
|
+
>(this, {
|
|
99
|
+
hoverEnterFeature: observable,
|
|
100
|
+
hoverNearbyFeature: observable,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@computed private get isTouchDevice(): boolean {
|
|
105
|
+
return isTouchDevice()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@computed private get manager(): ChoroplethMapManager {
|
|
109
|
+
return this.props.manager
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@computed get mapConfig(): MapConfig {
|
|
113
|
+
return this.manager.mapConfig
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@computed private get globeController(): GlobeController {
|
|
117
|
+
return this.manager.globeController ?? new GlobeController(this)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@computed.struct private get bounds(): Bounds {
|
|
121
|
+
return this.manager.choroplethMapBounds
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@computed private get selectionArray(): MapSelectionArray {
|
|
125
|
+
return this.manager.selectionArray ?? new MapSelectionArray()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@computed.struct private get choroplethData(): ChoroplethSeriesByName {
|
|
129
|
+
return this.manager.choroplethData
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@computed private get features(): GlobeRenderFeature[] {
|
|
133
|
+
return getGeoFeaturesForGlobe()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@computed private get featuresById(): Map<string, GlobeRenderFeature> {
|
|
137
|
+
return new Map(this.features.map((feature) => [feature.id, feature]))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@computed private get foregroundFeatures(): GlobeRenderFeature[] {
|
|
141
|
+
return getForegroundFeatures(this.features, this.selectionArray)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@computed
|
|
145
|
+
private get backgroundFeatures(): GlobeRenderFeature[] {
|
|
146
|
+
return _.difference(this.features, this.foregroundFeatures)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@computed private get backgroundFeatureIdSet(): Set<EntityName> {
|
|
150
|
+
return new Set(this.backgroundFeatures.map((feature) => feature.id))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@computed private get featuresWithData(): GlobeRenderFeature[] {
|
|
154
|
+
return this.foregroundFeatures.filter((feature) =>
|
|
155
|
+
this.choroplethData.has(feature.id)
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@computed private get sortedFeaturesWithData(): GlobeRenderFeature[] {
|
|
160
|
+
// sort features so that hovered or selected features are rendered last
|
|
161
|
+
// and smaller countries are rendered on top of bigger ones
|
|
162
|
+
return sortFeaturesByInteractionStateAndSize(this.featuresWithData, {
|
|
163
|
+
isHovered: (featureId: string) =>
|
|
164
|
+
this.manager.getHoverState?.(featureId).active ?? false,
|
|
165
|
+
isSelected: (featureId) =>
|
|
166
|
+
this.manager.isSelected?.(featureId) ?? false,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@computed private get featuresWithNoData(): GlobeRenderFeature[] {
|
|
171
|
+
return _.difference(this.foregroundFeatures, this.featuresWithData)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@computed private get binColors(): string[] {
|
|
175
|
+
return this.manager.binColors ?? []
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Map uses a hybrid approach to mouseover
|
|
179
|
+
// If mouse is inside an element, that is prioritized
|
|
180
|
+
// Otherwise we do a quadtree search for the closest center point of a feature bounds,
|
|
181
|
+
// so that we can hover very small countries without trouble
|
|
182
|
+
@action.bound private detectNearbyFeature(
|
|
183
|
+
event: MouseEvent | TouchEvent,
|
|
184
|
+
maxDistance = MAP_HOVER_TARGET_RANGE
|
|
185
|
+
): GlobeRenderFeature | undefined {
|
|
186
|
+
if (this.hoverEnterFeature || !this.base.current) return
|
|
187
|
+
|
|
188
|
+
const nearbyFeature = detectNearbyFeature({
|
|
189
|
+
quadtree: this.quadtree,
|
|
190
|
+
element: this.base.current,
|
|
191
|
+
event,
|
|
192
|
+
distance: maxDistance,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if (!nearbyFeature) {
|
|
196
|
+
this.hoverNearbyFeature = undefined
|
|
197
|
+
this.manager.onMapMouseLeave?.()
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (nearbyFeature.id !== this.hoverNearbyFeature?.id) {
|
|
202
|
+
this.hoverNearbyFeature = nearbyFeature
|
|
203
|
+
this.manager.onMapMouseOver?.(nearbyFeature.geo)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return nearbyFeature
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@computed private get globeSize(): number {
|
|
210
|
+
return Math.min(this.bounds.width, this.bounds.height)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@computed private get globeCenter(): [number, number] {
|
|
214
|
+
return [
|
|
215
|
+
this.bounds.left + this.bounds.width / 2,
|
|
216
|
+
this.bounds.top + this.bounds.height / 2,
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private computeProjectionScale(zoomScale: number): number {
|
|
221
|
+
return zoomScale * DEFAULT_SCALE * (this.globeSize / DEFAULT_GLOBE_SIZE)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@computed private get minScale(): number {
|
|
225
|
+
return this.computeProjectionScale(GLOBE_MIN_ZOOM)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@computed private get maxScale(): number {
|
|
229
|
+
return this.computeProjectionScale(GLOBE_MAX_ZOOM)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@computed private get globeScale(): number {
|
|
233
|
+
return this.computeProjectionScale(this.zoomScale)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@computed private get globeRadius(): number {
|
|
237
|
+
return (this.globeSize / 2) * this.zoomScale
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@computed private get globeRotation(): [number, number] {
|
|
241
|
+
// d3 projections expect [-lon, -lat] to rotate to [lon, lat]
|
|
242
|
+
return [
|
|
243
|
+
-this.mapConfig.globe.rotation[0],
|
|
244
|
+
-this.mapConfig.globe.rotation[1],
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@computed private get zoomScale(): number {
|
|
249
|
+
return this.mapConfig.globe.zoom
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@computed private get projection(): any {
|
|
253
|
+
return geoOrthographic()
|
|
254
|
+
.scale(this.globeScale)
|
|
255
|
+
.translate(this.globeCenter)
|
|
256
|
+
.rotate(this.globeRotation)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@computed private get globePath(): GeoPath<any, GeoPermissibleObjects> {
|
|
260
|
+
return geoPath().digits(1).projection(this.projection)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private getPath(feature: GlobeRenderFeature): string {
|
|
264
|
+
return this.globePath(feature.geo) ?? ""
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private isFeatureCentroidVisibleOnGlobe(
|
|
268
|
+
feature: GlobeRenderFeature,
|
|
269
|
+
threshold = 0 // 1 = at the exact center, 0 = anywhere on the visible hemisphere
|
|
270
|
+
): boolean {
|
|
271
|
+
return isPointPlacedOnVisibleHemisphere(
|
|
272
|
+
feature.geoCentroid,
|
|
273
|
+
this.mapConfig.globe.rotation,
|
|
274
|
+
threshold
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@computed private get graticulePath(): string {
|
|
279
|
+
const graticule = geoGraticule().step([10, 10])()
|
|
280
|
+
return this.globePath(graticule) ?? ""
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@computed private get equatorPath(): string {
|
|
284
|
+
const equator = geoGraticule().step([0, 360])()
|
|
285
|
+
return this.globePath(equator) ?? ""
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@computed private get visibleFeatures(): GlobeRenderFeature[] {
|
|
289
|
+
return this.foregroundFeatures.filter((feature) =>
|
|
290
|
+
this.isFeatureCentroidVisibleOnGlobe(feature)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@computed private get quadtree(): Quadtree<GlobeRenderFeature> {
|
|
295
|
+
return quadtree<GlobeRenderFeature>()
|
|
296
|
+
.x((feature) => this.projection(feature.geoCentroid)[0])
|
|
297
|
+
.y((feature) => this.projection(feature.geoCentroid)[1])
|
|
298
|
+
.addAll(this.visibleFeatures)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@computed private get shouldShowAnnotations(): boolean {
|
|
302
|
+
return !!(
|
|
303
|
+
this.manager.mapColumn.hasNumberFormatting &&
|
|
304
|
+
!this.mapConfig.tooltipUseCustomLabels
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private formatAnnotationLabel(value: string | number): string {
|
|
309
|
+
return this.manager.mapColumn.formatValueShortWithAbbreviations(value)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@computed private get annotationCandidateFeatures(): GlobeRenderFeature[] {
|
|
313
|
+
if (!this.shouldShowAnnotations) return []
|
|
314
|
+
|
|
315
|
+
return excludeUndefined(
|
|
316
|
+
this.mapConfig.selection.selectedCountryNamesInForeground.map(
|
|
317
|
+
(name) => this.featuresById.get(name)
|
|
318
|
+
)
|
|
319
|
+
).filter((feature) => this.foregroundFeatures.includes(feature))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Naively placed annotations that might be overlapping */
|
|
323
|
+
@computed
|
|
324
|
+
private get annotationCandidates(): Annotation[] {
|
|
325
|
+
return excludeUndefined(
|
|
326
|
+
this.annotationCandidateFeatures
|
|
327
|
+
.filter(
|
|
328
|
+
(feature): feature is GlobeRenderFeature =>
|
|
329
|
+
feature !== undefined &&
|
|
330
|
+
// don't show annotations for countries that are currently
|
|
331
|
+
// on the back side of the globe or at the edge
|
|
332
|
+
this.isFeatureCentroidVisibleOnGlobe(feature, 0.3)
|
|
333
|
+
)
|
|
334
|
+
.map((feature) => {
|
|
335
|
+
const series = this.choroplethData.get(feature.id)
|
|
336
|
+
if (!series) return
|
|
337
|
+
|
|
338
|
+
const labelColor = isDarkColor(series.color)
|
|
339
|
+
? ANNOTATION_COLOR_LIGHT
|
|
340
|
+
: ANNOTATION_COLOR_DARK
|
|
341
|
+
const fontSizeScale = 1 / this.zoomScale
|
|
342
|
+
|
|
343
|
+
const args = {
|
|
344
|
+
feature,
|
|
345
|
+
projection: this.projection,
|
|
346
|
+
formattedValue: this.formatAnnotationLabel(
|
|
347
|
+
series.value
|
|
348
|
+
),
|
|
349
|
+
color: labelColor,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// try to fit the annotation inside the feature
|
|
353
|
+
const internalAnnotation =
|
|
354
|
+
makeInternalAnnotationForFeature(args)
|
|
355
|
+
if (internalAnnotation) return internalAnnotation
|
|
356
|
+
|
|
357
|
+
// place the annotation outside of the feature
|
|
358
|
+
const externalAnnotation = makeExternalAnnotationForFeature(
|
|
359
|
+
{ ...args, fontSizeScale }
|
|
360
|
+
)
|
|
361
|
+
if (externalAnnotation) return externalAnnotation
|
|
362
|
+
|
|
363
|
+
return undefined
|
|
364
|
+
})
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@computed
|
|
369
|
+
private get internalAnnotations(): InternalAnnotation[] {
|
|
370
|
+
return this.annotationCandidates.filter(
|
|
371
|
+
(annotation) => annotation.type === "internal"
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@computed
|
|
376
|
+
private get externalAnnotations(): ExternalAnnotation[] {
|
|
377
|
+
const { projection, backgroundFeatureIdSet } = this
|
|
378
|
+
const annotations = this.annotationCandidates.filter(
|
|
379
|
+
(annotation) => annotation.type === "external"
|
|
380
|
+
)
|
|
381
|
+
return repositionAndFilterExternalAnnotations({
|
|
382
|
+
annotations,
|
|
383
|
+
projection,
|
|
384
|
+
backgroundFeatureIdSet,
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@action.bound private rotateGlobe(targetCoords: [number, number]): void {
|
|
389
|
+
this.mapConfig.globe.rotation = [
|
|
390
|
+
-targetCoords[0],
|
|
391
|
+
// Clamping the latitude to [-90, 90] would allow rotation up to the poles.
|
|
392
|
+
// However, the panning strategy used doesn't work well around the poles.
|
|
393
|
+
// That's why we clamp the latitude to a narrower range.
|
|
394
|
+
-R.clamp(targetCoords[1], {
|
|
395
|
+
min: GLOBE_LATITUDE_MIN,
|
|
396
|
+
max: GLOBE_LATITUDE_MAX,
|
|
397
|
+
}),
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@action.bound private zoomGlobe(delta: number): void {
|
|
402
|
+
const sensitivity = 0.01
|
|
403
|
+
const newZoom = this.zoomScale * (1 + delta * sensitivity)
|
|
404
|
+
this.mapConfig.globe.zoom = R.clamp(newZoom, {
|
|
405
|
+
min: GLOBE_MIN_ZOOM,
|
|
406
|
+
max: GLOBE_MAX_ZOOM,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@computed private get hoverFeature(): GlobeRenderFeature | undefined {
|
|
411
|
+
return this.hoverEnterFeature || this.hoverNearbyFeature
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@action.bound private clearHover(): void {
|
|
415
|
+
this.hoverEnterFeature = undefined
|
|
416
|
+
this.hoverNearbyFeature = undefined
|
|
417
|
+
this.manager.onMapMouseLeave?.()
|
|
418
|
+
this.globeController.dismissCountryFocus()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@action.bound private onMouseMove(event: MouseEvent): void {
|
|
422
|
+
this.detectNearbyFeature(event)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@action.bound private onMouseEnterFeature(
|
|
426
|
+
feature: GlobeRenderFeature
|
|
427
|
+
): void {
|
|
428
|
+
// ignore mouse enter if panning or zooming
|
|
429
|
+
if (this.isPanningOrZooming) return
|
|
430
|
+
this.setHoverEnterFeature(feature)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@action.bound private onMouseLeaveFeature(): void {
|
|
434
|
+
// Fixes an issue where clicking on a country that overlaps with the
|
|
435
|
+
// tooltip causes the tooltip to disappear shortly after being rendered
|
|
436
|
+
if (this.isTouchDevice) return
|
|
437
|
+
|
|
438
|
+
this.clearHoverEnterFeature()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@action.bound private setHoverEnterFeature(
|
|
442
|
+
feature: GlobeRenderFeature
|
|
443
|
+
): void {
|
|
444
|
+
if (this.hoverEnterFeature?.id === feature.id) return
|
|
445
|
+
|
|
446
|
+
this.hoverEnterFeature = feature
|
|
447
|
+
this.manager.onMapMouseOver?.(feature.geo)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@action.bound private clearHoverEnterFeature(): void {
|
|
451
|
+
this.hoverEnterFeature = undefined
|
|
452
|
+
this.manager.onMapMouseLeave?.()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@action.bound private onClick(feature: GlobeRenderFeature): void {
|
|
456
|
+
this.setHoverEnterFeature(feature)
|
|
457
|
+
|
|
458
|
+
// reset the region if necessary
|
|
459
|
+
this.mapConfig.region = MapRegionName.World
|
|
460
|
+
|
|
461
|
+
// select/deselect the country if allowed
|
|
462
|
+
const country = feature.id
|
|
463
|
+
if (this.manager.isMapSelectionEnabled) {
|
|
464
|
+
this.mapConfig.selection.toggleSelection(country)
|
|
465
|
+
|
|
466
|
+
// make sure country focus is dismissed for unselected countries
|
|
467
|
+
if (!this.mapConfig.selection.selectedSet.has(country))
|
|
468
|
+
this.globeController.dismissCountryFocus()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// rotate to the selected country on mobile
|
|
472
|
+
if (!this.manager.isMapSelectionEnabled) {
|
|
473
|
+
// only zoom in on click, never zoom out
|
|
474
|
+
const zoom = Math.max(GLOBE_COUNTRY_ZOOM, this.mapConfig.globe.zoom)
|
|
475
|
+
this.globeController.rotateToCountry(country, zoom)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@action.bound private onTouchStart(feature: GlobeRenderFeature): void {
|
|
480
|
+
this.setHoverEnterFeature(feature)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@action.bound private onDocumentClick(): void {
|
|
484
|
+
this.clearHover()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private setUpPanningAndZooming(): void {
|
|
488
|
+
const base = this.base.current
|
|
489
|
+
if (!base) return
|
|
490
|
+
|
|
491
|
+
// Possible interaction types are
|
|
492
|
+
// - zoom-scroll: zooming by scrolling via the wheel event
|
|
493
|
+
// - zoom-pinch: zooming by pinching using two fingers on touch devices
|
|
494
|
+
// - pan: panning by dragging the mouse or using a finger on touch devices
|
|
495
|
+
type InteractionType = "zoom-scroll" | "zoom-pinch" | "pan"
|
|
496
|
+
|
|
497
|
+
// Panning and zooming are powered by D3.
|
|
498
|
+
//
|
|
499
|
+
// Panning is adapted from https://observablehq.com/d/569d101dd5bd332b.
|
|
500
|
+
// The strategy ensures the geographic start point remains under the cursor
|
|
501
|
+
// where possible. See https://www.jasondavies.com/maps/rotate/ for more
|
|
502
|
+
// details.
|
|
503
|
+
//
|
|
504
|
+
// We could rely on D3's event.transform.k for zooming, but the
|
|
505
|
+
// transform value might be out of sync with the actual zoom level if
|
|
506
|
+
// the zoom level was changed elsewhere (e.g. by automatically zooming
|
|
507
|
+
// in on a country). That's why we compute the target zoom level ourselves.
|
|
508
|
+
|
|
509
|
+
const panAndZoom = (): any => {
|
|
510
|
+
let previousType: InteractionType | undefined
|
|
511
|
+
|
|
512
|
+
// for panning
|
|
513
|
+
let startCoords: [number, number, number],
|
|
514
|
+
startQuat: [number, number, number, number],
|
|
515
|
+
startRot: [number, number, number],
|
|
516
|
+
previousPos: [number, number]
|
|
517
|
+
|
|
518
|
+
// for zooming
|
|
519
|
+
let startDistance: number | undefined
|
|
520
|
+
|
|
521
|
+
const getInteractionType = (event: any): InteractionType => {
|
|
522
|
+
if (event.sourceEvent.type === "wheel") return "zoom-scroll"
|
|
523
|
+
if (isMultiTouchEvent(event.sourceEvent)) return "zoom-pinch"
|
|
524
|
+
return "pan"
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const panningOrZoomingStart = (event: any): void => {
|
|
528
|
+
const type = getInteractionType(event)
|
|
529
|
+
|
|
530
|
+
const startPinching = (): void => {
|
|
531
|
+
startDistance = calculatePinchDistance(event.sourceEvent)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const startPanning = (): void => {
|
|
535
|
+
const posVector = getRelativeMouse(base, event.sourceEvent)
|
|
536
|
+
const pos: [number, number] = [posVector.x, posVector.y]
|
|
537
|
+
|
|
538
|
+
startCoords = versor.cartesian(this.projection.invert(pos))
|
|
539
|
+
startRot = this.projection.rotate()
|
|
540
|
+
startQuat = versor(startRot)
|
|
541
|
+
previousPos = pos
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (type === "zoom-pinch") startPinching()
|
|
545
|
+
else if (type === "pan") startPanning()
|
|
546
|
+
|
|
547
|
+
previousType = type
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const panningOrZooming = action((event: any): void => {
|
|
551
|
+
this.isPanningOrZooming = true
|
|
552
|
+
|
|
553
|
+
this.clearHover() // dismiss the tooltip
|
|
554
|
+
this.mapConfig.region = MapRegionName.World // reset region
|
|
555
|
+
|
|
556
|
+
const wheeling = (): void => {
|
|
557
|
+
this.zoomGlobe(-event.sourceEvent.deltaY)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const pinching = (): void => {
|
|
561
|
+
const distance = calculatePinchDistance(event.sourceEvent)
|
|
562
|
+
|
|
563
|
+
if (!startDistance) {
|
|
564
|
+
startDistance = distance
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const delta = distance - startDistance
|
|
569
|
+
|
|
570
|
+
// We sometimes get two events for the same pinch gesture,
|
|
571
|
+
// with one of the events having a delta of 0. We simply
|
|
572
|
+
// ignore the delta-0 events. This fixes a bug where the
|
|
573
|
+
// rendered SVG country paths would interfere with the
|
|
574
|
+
// pinch-to-zoom gesture.
|
|
575
|
+
if (delta === 0) return
|
|
576
|
+
|
|
577
|
+
this.zoomGlobe(delta)
|
|
578
|
+
startDistance = distance
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const panning = (): void => {
|
|
582
|
+
const posVector = getRelativeMouse(base, event.sourceEvent)
|
|
583
|
+
const pos: [number, number] = [posVector.x, posVector.y]
|
|
584
|
+
|
|
585
|
+
// True if the cursor is currently over the globe
|
|
586
|
+
const isDraggingGlobe = isPointInCircle(posVector, {
|
|
587
|
+
cx: this.globeCenter[0],
|
|
588
|
+
cy: this.globeCenter[1],
|
|
589
|
+
r: this.globeRadius,
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
if (isDraggingGlobe) {
|
|
593
|
+
// If the user is dragging the globe, then use a
|
|
594
|
+
// rotation strategy that ensures the geographic start
|
|
595
|
+
// point remains under the cursor where possible
|
|
596
|
+
|
|
597
|
+
const currCoords = versor.cartesian(
|
|
598
|
+
this.projection.rotate(startRot).invert(pos)
|
|
599
|
+
)
|
|
600
|
+
const delta = versor.delta(startCoords, currCoords)
|
|
601
|
+
const quat = versor.multiply(startQuat, delta)
|
|
602
|
+
const rotation = versor.rotation(quat)
|
|
603
|
+
|
|
604
|
+
// Ignore the gamma channel for more intuitive rotation
|
|
605
|
+
// see https://observablehq.com/@d3/three-axis-rotation
|
|
606
|
+
// for a visual explanation of three-axis rotation.
|
|
607
|
+
// As a side effect, rotation around the poles feels off.
|
|
608
|
+
this.rotateGlobe([rotation[0], rotation[1]])
|
|
609
|
+
} else {
|
|
610
|
+
// If the user's cursor is outside of the globe, then
|
|
611
|
+
// adjust the globe's rotation based on the cursor's
|
|
612
|
+
// movement, applying a sensitivity factor to control
|
|
613
|
+
// the speed of rotation
|
|
614
|
+
|
|
615
|
+
const sensitivity = 0.8
|
|
616
|
+
const r = this.globeRotation
|
|
617
|
+
const dx = pos[0] - previousPos[0]
|
|
618
|
+
const dy = pos[1] - previousPos[1]
|
|
619
|
+
this.rotateGlobe([
|
|
620
|
+
r[0] + sensitivity * dx,
|
|
621
|
+
r[1] - sensitivity * dy,
|
|
622
|
+
])
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
previousPos = pos
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const type = getInteractionType(event)
|
|
629
|
+
|
|
630
|
+
// bail if a zoom-pinch gesture turned into a pan
|
|
631
|
+
// because this might lead to erratic jumps
|
|
632
|
+
if (previousType === "zoom-pinch" && type === "pan") return
|
|
633
|
+
|
|
634
|
+
if (type === "zoom-scroll") wheeling()
|
|
635
|
+
else if (type === "zoom-pinch") pinching()
|
|
636
|
+
else if (type === "pan") panning()
|
|
637
|
+
|
|
638
|
+
previousType = type
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
const panningOrZoomingEnd = (): void => {
|
|
642
|
+
this.isPanningOrZooming = false
|
|
643
|
+
startDistance = undefined
|
|
644
|
+
previousType = undefined
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return zoom()
|
|
648
|
+
.scaleExtent([this.minScale, this.maxScale])
|
|
649
|
+
.touchable(() => this.isTouchDevice)
|
|
650
|
+
.on("start", panningOrZoomingStart)
|
|
651
|
+
.on("zoom", panningOrZooming)
|
|
652
|
+
.on("end", panningOrZoomingEnd)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
select(base).call(panAndZoom())
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
override componentDidMount(): void {
|
|
659
|
+
// rotate to the selected region
|
|
660
|
+
if (isValidGlobeRegionName(this.mapConfig.region)) {
|
|
661
|
+
this.globeController.jumpToOwidContinent(this.mapConfig.region)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
document.addEventListener("touchstart", this.onDocumentClick, {
|
|
665
|
+
capture: true,
|
|
666
|
+
passive: true,
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
this.setUpPanningAndZooming()
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
override componentWillUnmount(): void {
|
|
673
|
+
document.removeEventListener("touchstart", this.onDocumentClick, {
|
|
674
|
+
capture: true,
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
renderGlobeOutline(): React.ReactElement {
|
|
679
|
+
return (
|
|
680
|
+
<>
|
|
681
|
+
<circle
|
|
682
|
+
id={makeIdForHumanConsumption("globe-sphere")}
|
|
683
|
+
cx={this.globeCenter[0]}
|
|
684
|
+
cy={this.globeCenter[1]}
|
|
685
|
+
r={this.globeRadius}
|
|
686
|
+
fill="#fafafa"
|
|
687
|
+
/>
|
|
688
|
+
<path
|
|
689
|
+
id={makeIdForHumanConsumption("globe-graticule")}
|
|
690
|
+
d={this.graticulePath}
|
|
691
|
+
stroke="#e7e7e7"
|
|
692
|
+
strokeWidth={1}
|
|
693
|
+
fill="none"
|
|
694
|
+
style={{ pointerEvents: "none" }}
|
|
695
|
+
/>
|
|
696
|
+
<path
|
|
697
|
+
id={makeIdForHumanConsumption("globe-equator")}
|
|
698
|
+
d={this.equatorPath}
|
|
699
|
+
stroke="#dadada"
|
|
700
|
+
strokeWidth={1}
|
|
701
|
+
fill="none"
|
|
702
|
+
style={{ pointerEvents: "none" }}
|
|
703
|
+
/>
|
|
704
|
+
</>
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
renderFeaturesInBackground(): React.ReactElement | undefined {
|
|
709
|
+
if (this.backgroundFeatures.length === 0) return
|
|
710
|
+
|
|
711
|
+
return (
|
|
712
|
+
<g id={makeIdForHumanConsumption("countries-background")}>
|
|
713
|
+
{this.backgroundFeatures.map((feature) => (
|
|
714
|
+
<BackgroundCountry
|
|
715
|
+
key={feature.id}
|
|
716
|
+
feature={feature}
|
|
717
|
+
path={this.getPath(feature)}
|
|
718
|
+
/>
|
|
719
|
+
))}
|
|
720
|
+
</g>
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
renderFeaturesWithNoData(): React.ReactElement | undefined {
|
|
725
|
+
if (this.featuresWithNoData.length === 0) return
|
|
726
|
+
|
|
727
|
+
const patternId = Patterns.noDataPatternForGlobe
|
|
728
|
+
|
|
729
|
+
return (
|
|
730
|
+
<g
|
|
731
|
+
id={makeIdForHumanConsumption("countries-without-data")}
|
|
732
|
+
className="noDataFeatures"
|
|
733
|
+
>
|
|
734
|
+
<defs>
|
|
735
|
+
<NoDataPattern patternId={patternId} />
|
|
736
|
+
</defs>
|
|
737
|
+
|
|
738
|
+
{this.featuresWithNoData.map((feature) => (
|
|
739
|
+
<CountryWithNoData
|
|
740
|
+
key={feature.id}
|
|
741
|
+
feature={feature}
|
|
742
|
+
path={this.getPath(feature)}
|
|
743
|
+
patternId={patternId}
|
|
744
|
+
isSelected={this.manager.isSelected?.(feature.id)}
|
|
745
|
+
hover={this.manager.getHoverState?.(feature.id)}
|
|
746
|
+
onClick={(event) => {
|
|
747
|
+
// don't invoke a second click on parent that
|
|
748
|
+
// catches clicks on 'nearby' features
|
|
749
|
+
event.stopPropagation()
|
|
750
|
+
|
|
751
|
+
this.onClick(feature)
|
|
752
|
+
}}
|
|
753
|
+
onTouchStart={() => this.onTouchStart(feature)}
|
|
754
|
+
onMouseEnter={this.onMouseEnterFeature}
|
|
755
|
+
onMouseLeave={this.onMouseLeaveFeature}
|
|
756
|
+
/>
|
|
757
|
+
))}
|
|
758
|
+
</g>
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
renderFeaturesWithData(): React.ReactElement | undefined {
|
|
763
|
+
if (this.sortedFeaturesWithData.length === 0) return
|
|
764
|
+
|
|
765
|
+
return (
|
|
766
|
+
<g id={makeIdForHumanConsumption("countries-with-data")}>
|
|
767
|
+
{this.manager.hasProjectedData && (
|
|
768
|
+
<defs>
|
|
769
|
+
{/* Pattern used by the map legend for the projected data bin */}
|
|
770
|
+
<ProjectedDataPattern
|
|
771
|
+
key={PROJECTED_DATA_LEGEND_COLOR}
|
|
772
|
+
color={PROJECTED_DATA_LEGEND_COLOR}
|
|
773
|
+
forLegend
|
|
774
|
+
/>
|
|
775
|
+
|
|
776
|
+
{/* Patterns used by the map legend. These duplicate the patterns below,
|
|
777
|
+
but use a legend-specific id */}
|
|
778
|
+
{this.binColors.map((color, index) => (
|
|
779
|
+
<ProjectedDataPattern
|
|
780
|
+
key={`${color}-${index}`}
|
|
781
|
+
color={color}
|
|
782
|
+
forLegend
|
|
783
|
+
/>
|
|
784
|
+
))}
|
|
785
|
+
|
|
786
|
+
{/* Pattern used by features */}
|
|
787
|
+
{this.binColors.map((color, index) => (
|
|
788
|
+
<ProjectedDataPattern
|
|
789
|
+
key={`${color}-${index}`}
|
|
790
|
+
color={color}
|
|
791
|
+
/>
|
|
792
|
+
))}
|
|
793
|
+
</defs>
|
|
794
|
+
)}
|
|
795
|
+
|
|
796
|
+
{this.sortedFeaturesWithData.map((feature) => {
|
|
797
|
+
const series = this.choroplethData.get(feature.id)
|
|
798
|
+
if (!series) return null
|
|
799
|
+
return (
|
|
800
|
+
<CountryWithData
|
|
801
|
+
key={feature.id}
|
|
802
|
+
feature={feature}
|
|
803
|
+
series={series}
|
|
804
|
+
path={this.getPath(feature)}
|
|
805
|
+
isSelected={this.manager.isSelected?.(feature.id)}
|
|
806
|
+
hover={this.manager.getHoverState?.(feature.id)}
|
|
807
|
+
onClick={(event) => {
|
|
808
|
+
// don't invoke a second click on parent that
|
|
809
|
+
// catches clicks on 'nearby' features
|
|
810
|
+
event.stopPropagation()
|
|
811
|
+
|
|
812
|
+
this.onClick(feature)
|
|
813
|
+
}}
|
|
814
|
+
onTouchStart={() => this.onTouchStart(feature)}
|
|
815
|
+
onMouseEnter={this.onMouseEnterFeature}
|
|
816
|
+
onMouseLeave={this.onMouseLeaveFeature}
|
|
817
|
+
/>
|
|
818
|
+
)
|
|
819
|
+
})}
|
|
820
|
+
</g>
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
renderInternalAnnotations(): React.ReactElement | undefined {
|
|
825
|
+
if (this.internalAnnotations.length === 0) return
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<g id={makeIdForHumanConsumption("annotations-internal")}>
|
|
829
|
+
{this.internalAnnotations.map((annotation) => (
|
|
830
|
+
<InternalValueAnnotation
|
|
831
|
+
key={annotation.id}
|
|
832
|
+
annotation={annotation}
|
|
833
|
+
showOutline={
|
|
834
|
+
this.choroplethData.get(annotation.id)?.isProjection
|
|
835
|
+
}
|
|
836
|
+
/>
|
|
837
|
+
))}
|
|
838
|
+
</g>
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
renderExternalAnnotations(): React.ReactElement | undefined {
|
|
843
|
+
if (this.externalAnnotations.length === 0) return
|
|
844
|
+
|
|
845
|
+
return (
|
|
846
|
+
<g
|
|
847
|
+
id={makeIdForHumanConsumption("annotations-external")}
|
|
848
|
+
className="ExternalAnnotations"
|
|
849
|
+
>
|
|
850
|
+
{this.externalAnnotations.map((annotation) => (
|
|
851
|
+
<ExternalValueAnnotation
|
|
852
|
+
key={annotation.id}
|
|
853
|
+
annotation={annotation}
|
|
854
|
+
onMouseEnter={action((feature: RenderFeature) =>
|
|
855
|
+
this.setHoverEnterFeature(
|
|
856
|
+
feature as GlobeRenderFeature
|
|
857
|
+
)
|
|
858
|
+
)}
|
|
859
|
+
onMouseLeave={action(() =>
|
|
860
|
+
this.clearHoverEnterFeature()
|
|
861
|
+
)}
|
|
862
|
+
/>
|
|
863
|
+
))}
|
|
864
|
+
</g>
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
renderStatic(): React.ReactElement {
|
|
869
|
+
return (
|
|
870
|
+
<>
|
|
871
|
+
{this.renderGlobeOutline()}
|
|
872
|
+
<g id={makeIdForHumanConsumption("globe")}>
|
|
873
|
+
{this.renderFeaturesInBackground()}
|
|
874
|
+
{this.renderFeaturesWithNoData()}
|
|
875
|
+
{this.renderFeaturesWithData()}
|
|
876
|
+
{this.renderInternalAnnotations()}
|
|
877
|
+
{this.renderExternalAnnotations()}
|
|
878
|
+
</g>
|
|
879
|
+
</>
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
renderInteractive(): React.ReactElement {
|
|
884
|
+
// this needs to be referenced here or it will be recomputed on every mousemove
|
|
885
|
+
const _cachedCentroids = this.quadtree
|
|
886
|
+
|
|
887
|
+
return (
|
|
888
|
+
<g
|
|
889
|
+
ref={this.base}
|
|
890
|
+
onMouseDown={
|
|
891
|
+
(ev: SVGMouseEvent): void =>
|
|
892
|
+
ev.preventDefault() /* Without this, title may get selected while shift clicking */
|
|
893
|
+
}
|
|
894
|
+
onMouseMove={(ev: SVGMouseEvent): void =>
|
|
895
|
+
this.onMouseMove(ev.nativeEvent)
|
|
896
|
+
}
|
|
897
|
+
onMouseLeave={this.onMouseLeaveFeature}
|
|
898
|
+
onClick={() => {
|
|
899
|
+
// invoke a click on a feature when clicking nearby one
|
|
900
|
+
if (this.hoverNearbyFeature)
|
|
901
|
+
this.onClick(this.hoverNearbyFeature)
|
|
902
|
+
}}
|
|
903
|
+
style={{ cursor: this.hoverFeature ? "pointer" : undefined }}
|
|
904
|
+
>
|
|
905
|
+
{this.renderGlobeOutline()}
|
|
906
|
+
<g className={GEO_FEATURES_CLASSNAME}>
|
|
907
|
+
{this.renderFeaturesInBackground()}
|
|
908
|
+
{this.renderFeaturesWithNoData()}
|
|
909
|
+
{this.renderFeaturesWithData()}
|
|
910
|
+
{this.renderInternalAnnotations()}
|
|
911
|
+
{this.renderExternalAnnotations()}
|
|
912
|
+
</g>
|
|
913
|
+
</g>
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
override render(): React.ReactElement {
|
|
918
|
+
return this.manager.isStatic
|
|
919
|
+
? this.renderStatic()
|
|
920
|
+
: this.renderInteractive()
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const isMultiTouchEvent = (
|
|
925
|
+
event: MouseEvent | TouchEvent
|
|
926
|
+
): event is TouchEvent => {
|
|
927
|
+
return checkIsTouchEvent(event) && event.touches.length >= 2
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const calculatePinchDistance = (event: TouchEvent): number => {
|
|
931
|
+
const { touches } = event
|
|
932
|
+
return calculateDistance(
|
|
933
|
+
[touches[0].clientX, touches[0].clientY],
|
|
934
|
+
[touches[1].clientX, touches[1].clientY]
|
|
935
|
+
)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function isPointInCircle(
|
|
939
|
+
point: PointVector,
|
|
940
|
+
circle: { cx: number; cy: number; r: number }
|
|
941
|
+
): boolean {
|
|
942
|
+
const { x, y } = point
|
|
943
|
+
const { cx, cy, r } = circle
|
|
944
|
+
|
|
945
|
+
const distanceSquared = (x - cx) ** 2 + (y - cy) ** 2
|
|
946
|
+
const radiusSquared = r ** 2
|
|
947
|
+
|
|
948
|
+
return distanceSquared <= radiusSquared
|
|
949
|
+
}
|