@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,1144 @@
|
|
|
1
|
+
import * as _ from "lodash-es"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import * as R from "remeda"
|
|
4
|
+
import {
|
|
5
|
+
Bounds,
|
|
6
|
+
excludeUndefined,
|
|
7
|
+
HorizontalAlign,
|
|
8
|
+
Position,
|
|
9
|
+
SortConfig,
|
|
10
|
+
SortOrder,
|
|
11
|
+
getRelativeMouse,
|
|
12
|
+
EntitySelectionMode,
|
|
13
|
+
makeIdForHumanConsumption,
|
|
14
|
+
dyFromAlign,
|
|
15
|
+
exposeInstanceOnWindow,
|
|
16
|
+
} from "../../utils/index.js"
|
|
17
|
+
import { action, computed, makeObservable, observable } from "mobx"
|
|
18
|
+
import { observer } from "mobx-react"
|
|
19
|
+
import {
|
|
20
|
+
BASE_FONT_SIZE,
|
|
21
|
+
DEFAULT_GRAPHER_BOUNDS,
|
|
22
|
+
GRAPHER_FONT_SCALE_12,
|
|
23
|
+
} from "../core/GrapherConstants"
|
|
24
|
+
import { DualAxisComponent } from "../axis/AxisViews"
|
|
25
|
+
import { NoDataModal } from "../noDataModal/NoDataModal"
|
|
26
|
+
import { AxisConfig, AxisManager } from "../axis/AxisConfig"
|
|
27
|
+
import { ChartInterface } from "../chart/ChartInterface"
|
|
28
|
+
import {
|
|
29
|
+
EntityName,
|
|
30
|
+
VerticalAlign,
|
|
31
|
+
ColorScaleConfigInterface,
|
|
32
|
+
} from "../../types/index.js"
|
|
33
|
+
import { OwidTable, CoreColumn } from "../../core-table/index.js"
|
|
34
|
+
import { getShortNameForEntity } from "../chart/ChartUtils"
|
|
35
|
+
import {
|
|
36
|
+
LEGEND_STYLE_FOR_STACKED_CHARTS,
|
|
37
|
+
StackedSeries,
|
|
38
|
+
} from "./StackedConstants"
|
|
39
|
+
import { TooltipFooterIcon } from "../tooltip/TooltipProps.js"
|
|
40
|
+
import {
|
|
41
|
+
Tooltip,
|
|
42
|
+
TooltipValue,
|
|
43
|
+
TooltipState,
|
|
44
|
+
makeTooltipRoundingNotice,
|
|
45
|
+
makeTooltipToleranceNotice,
|
|
46
|
+
} from "../tooltip/Tooltip"
|
|
47
|
+
import {
|
|
48
|
+
HorizontalCategoricalColorLegend,
|
|
49
|
+
HorizontalColorLegendManager,
|
|
50
|
+
} from "../legend/HorizontalColorLegends"
|
|
51
|
+
import { CategoricalBin, ColorScaleBin } from "../color/ColorScaleBin"
|
|
52
|
+
import {
|
|
53
|
+
LegendInteractionState,
|
|
54
|
+
LegendStyleConfig,
|
|
55
|
+
} from "../legend/LegendInteractionState"
|
|
56
|
+
import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis"
|
|
57
|
+
import { ColorScale } from "../color/ColorScale"
|
|
58
|
+
import { SelectionArray } from "../selection/SelectionArray"
|
|
59
|
+
import {
|
|
60
|
+
MarimekkoChartManager,
|
|
61
|
+
Item,
|
|
62
|
+
PlacedItem,
|
|
63
|
+
EntityWithSize,
|
|
64
|
+
LabelCandidate,
|
|
65
|
+
LabelWithPlacement,
|
|
66
|
+
LabelCandidateWithElement,
|
|
67
|
+
Bar,
|
|
68
|
+
} from "./MarimekkoChartConstants"
|
|
69
|
+
import { MarimekkoChartState } from "./MarimekkoChartState"
|
|
70
|
+
import { ChartComponentProps } from "../chart/ChartTypeMap.js"
|
|
71
|
+
import { MarimekkoBars } from "./MarimekkoBars"
|
|
72
|
+
import { toPlacedMarimekkoItems } from "./MarimekkoChartHelpers"
|
|
73
|
+
|
|
74
|
+
const MARKER_MARGIN: number = 4
|
|
75
|
+
const MARKER_AREA_HEIGHT: number = 25
|
|
76
|
+
const MAX_LABEL_COUNT: number = 20
|
|
77
|
+
|
|
78
|
+
export type MarimekkoChartProps = ChartComponentProps<MarimekkoChartState>
|
|
79
|
+
|
|
80
|
+
@observer
|
|
81
|
+
export class MarimekkoChart
|
|
82
|
+
extends React.Component<MarimekkoChartProps>
|
|
83
|
+
implements ChartInterface, HorizontalColorLegendManager, AxisManager
|
|
84
|
+
{
|
|
85
|
+
base = React.createRef<SVGGElement>()
|
|
86
|
+
|
|
87
|
+
constructor(props: MarimekkoChartProps) {
|
|
88
|
+
super(props)
|
|
89
|
+
|
|
90
|
+
makeObservable(this, {
|
|
91
|
+
focusColorBin: observable,
|
|
92
|
+
tooltipState: observable,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
labelAngleInDegrees = -45 // 0 is horizontal, -90 is vertical from bottom to top, ...
|
|
97
|
+
|
|
98
|
+
// currently hovered legend color
|
|
99
|
+
focusColorBin: ColorScaleBin | undefined = undefined
|
|
100
|
+
|
|
101
|
+
// current tooltip target & position
|
|
102
|
+
tooltipState = new TooltipState<{
|
|
103
|
+
entityName: string
|
|
104
|
+
}>()
|
|
105
|
+
|
|
106
|
+
@computed get chartState(): MarimekkoChartState {
|
|
107
|
+
return this.props.chartState
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@computed private get manager(): MarimekkoChartManager {
|
|
111
|
+
return this.chartState.manager
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@computed private get inputTable(): OwidTable {
|
|
115
|
+
return this.chartState.inputTable
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@computed private get series(): readonly StackedSeries<EntityName>[] {
|
|
119
|
+
return this.chartState.series
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@computed private get yColumnSlugs(): string[] {
|
|
123
|
+
return this.chartState.yColumnSlugs
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@computed private get xColumnSlug(): string | undefined {
|
|
127
|
+
return this.chartState.xColumnSlug
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@computed private get xColumn(): CoreColumn | undefined {
|
|
131
|
+
return this.chartState.xColumn
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@computed private get colorColumn(): CoreColumn | undefined {
|
|
135
|
+
return this.chartState.colorColumn
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@computed private get latestTime(): number | undefined {
|
|
139
|
+
const times =
|
|
140
|
+
this.manager.tableAfterAuthorTimelineAndActiveChartTransform?.getTimesUniqSortedAscForColumns(
|
|
141
|
+
this.yColumnSlugs
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return times ? R.last(times) : undefined
|
|
145
|
+
}
|
|
146
|
+
@computed private get tableAtLatestTimelineTimepoint():
|
|
147
|
+
| OwidTable
|
|
148
|
+
| undefined {
|
|
149
|
+
if (this.latestTime)
|
|
150
|
+
return this.manager.tableAfterAuthorTimelineAndActiveChartTransform?.filterByTargetTimes(
|
|
151
|
+
[this.latestTime],
|
|
152
|
+
0
|
|
153
|
+
)
|
|
154
|
+
else return undefined
|
|
155
|
+
}
|
|
156
|
+
@computed private get xColumnAtLastTimePoint(): CoreColumn | undefined {
|
|
157
|
+
if (this.xColumnSlug === undefined) return undefined
|
|
158
|
+
const columnSlug = [this.xColumnSlug]
|
|
159
|
+
if (this.tableAtLatestTimelineTimepoint)
|
|
160
|
+
return this.tableAtLatestTimelineTimepoint.getColumns(columnSlug)[0]
|
|
161
|
+
else return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@computed private get yColumnsAtLastTimePoint(): CoreColumn[] {
|
|
165
|
+
const columnSlugs = this.yColumnSlugs
|
|
166
|
+
return (
|
|
167
|
+
this.tableAtLatestTimelineTimepoint?.getColumns(columnSlugs) ?? []
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@computed private get yColumns(): CoreColumn[] {
|
|
172
|
+
return this.chartState.yColumns
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@computed private get colorColumnSlug(): string | undefined {
|
|
176
|
+
return this.chartState.colorColumnSlug
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@computed private get colorScale(): ColorScale {
|
|
180
|
+
return this.chartState.colorScale
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@computed private get colorScaleConfig():
|
|
184
|
+
| ColorScaleConfigInterface
|
|
185
|
+
| undefined {
|
|
186
|
+
return this.chartState.colorScaleConfig
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@computed private get sortConfig(): SortConfig {
|
|
190
|
+
return this.manager.sortConfig ?? {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@computed private get bounds(): Bounds {
|
|
194
|
+
return (this.props.bounds ?? DEFAULT_GRAPHER_BOUNDS).padRight(10)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@computed private get innerBounds(): Bounds {
|
|
198
|
+
// This is a workaround to get the actual width of the vertical axis - dualAxis does this
|
|
199
|
+
// internally but we can't access this.dualAxis here due to a dependency cycle
|
|
200
|
+
const axis = this.verticalAxisPart.clone()
|
|
201
|
+
axis.range = [0, this.bounds.height]
|
|
202
|
+
const verticalAxisTrueWidth = axis.width
|
|
203
|
+
|
|
204
|
+
const whiteSpaceOnLeft = this.bounds.left + verticalAxisTrueWidth
|
|
205
|
+
const labelLinesHeight = MARKER_AREA_HEIGHT
|
|
206
|
+
// only pad left by the amount the longest label would exceed whatever space the
|
|
207
|
+
// vertical axis needs anyhow for label and tickmarks
|
|
208
|
+
const marginToEnsureWidestEntityLabelFitsEvenIfAtX0 =
|
|
209
|
+
Math.max(whiteSpaceOnLeft, this.longestLabelWidth) -
|
|
210
|
+
whiteSpaceOnLeft
|
|
211
|
+
return this.bounds
|
|
212
|
+
.padBottom(this.longestLabelHeight + 2)
|
|
213
|
+
.padBottom(labelLinesHeight)
|
|
214
|
+
.padTop(
|
|
215
|
+
this.showLegend ? this.legend.height + this.legendPaddingTop : 0
|
|
216
|
+
)
|
|
217
|
+
.padLeft(marginToEnsureWidestEntityLabelFitsEvenIfAtX0)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@computed get isStatic(): boolean {
|
|
221
|
+
return this.manager.isStatic ?? false
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@computed get fontSize(): number {
|
|
225
|
+
return this.manager.fontSize ?? BASE_FONT_SIZE
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@computed private get xRange(): [number, number] {
|
|
229
|
+
return [this.bounds.left, this.bounds.right]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@computed private get yAxisConfig(): AxisConfig {
|
|
233
|
+
return new AxisConfig(this.manager.yAxisConfig, this)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@computed private get xAxisConfig(): AxisConfig {
|
|
237
|
+
const { xColumnSlug } = this
|
|
238
|
+
return new AxisConfig(
|
|
239
|
+
{
|
|
240
|
+
...this.manager.xAxisConfig,
|
|
241
|
+
orient: Position.top,
|
|
242
|
+
hideAxis: xColumnSlug === undefined,
|
|
243
|
+
hideGridlines: xColumnSlug === undefined,
|
|
244
|
+
},
|
|
245
|
+
this
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@computed private get verticalAxisPart(): VerticalAxis {
|
|
250
|
+
return this.chartState.toVerticalAxis(this.yAxisConfig)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@computed private get horizontalAxisPart(): HorizontalAxis {
|
|
254
|
+
return this.chartState.toHorizontalAxis(this.xAxisConfig)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@computed private get dualAxis(): DualAxis {
|
|
258
|
+
return new DualAxis({
|
|
259
|
+
bounds: this.innerBounds,
|
|
260
|
+
verticalAxis: this.verticalAxisPart,
|
|
261
|
+
horizontalAxis: this.horizontalAxisPart,
|
|
262
|
+
comparisonLines: this.manager.comparisonLines,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@computed private get selectionArray(): SelectionArray {
|
|
267
|
+
return this.chartState.selectionArray
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@computed private get items(): Item[] {
|
|
271
|
+
return this.chartState.items
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@computed get placedItems(): PlacedItem[] {
|
|
275
|
+
return toPlacedMarimekkoItems(this.chartState, {
|
|
276
|
+
dualAxis: this.dualAxis,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@computed private get placedItemsMap(): Map<string, PlacedItem> {
|
|
281
|
+
return new Map(this.placedItems.map((item) => [item.entityName, item]))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// legend props
|
|
285
|
+
|
|
286
|
+
@computed private get legendPaddingTop(): number {
|
|
287
|
+
return this.legend.height > 0 ? this.fontSize : 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@computed get legendX(): number {
|
|
291
|
+
return this.bounds.x
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@computed get categoryLegendY(): number {
|
|
295
|
+
return this.bounds.top
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@computed get legendWidth(): number {
|
|
299
|
+
return this.bounds.width
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@computed get legendAlign(): HorizontalAlign {
|
|
303
|
+
return HorizontalAlign.left
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@computed get detailsOrderedByReference(): string[] {
|
|
307
|
+
return this.manager.detailsOrderedByReference ?? []
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@computed get categoricalLegendData(): CategoricalBin[] {
|
|
311
|
+
const { colorColumnSlug, colorScale, series } = this
|
|
312
|
+
if (colorColumnSlug) {
|
|
313
|
+
return colorScale.categoricalLegendBins
|
|
314
|
+
} else if (series.length > 0) {
|
|
315
|
+
const customHiddenCategories =
|
|
316
|
+
this.colorScaleConfig?.customHiddenCategories
|
|
317
|
+
return series.map((series, index) => {
|
|
318
|
+
return new CategoricalBin({
|
|
319
|
+
index,
|
|
320
|
+
value: series.seriesName,
|
|
321
|
+
label: series.seriesName,
|
|
322
|
+
color: series.color,
|
|
323
|
+
isHidden: !!customHiddenCategories?.[series.seriesName],
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
return []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getLegendBinState(bin: ColorScaleBin): LegendInteractionState {
|
|
331
|
+
const { focusColorBin } = this
|
|
332
|
+
|
|
333
|
+
// If nothing is focused, all items are active
|
|
334
|
+
if (!focusColorBin && this.hoverColors.length === 0)
|
|
335
|
+
return LegendInteractionState.Default
|
|
336
|
+
|
|
337
|
+
const isHovered = this.hoverColors?.includes(bin.color)
|
|
338
|
+
if (isHovered) return LegendInteractionState.Focused
|
|
339
|
+
|
|
340
|
+
// Check if this bin matches the focused color bin
|
|
341
|
+
const isFocused = focusColorBin && bin.equals(focusColorBin)
|
|
342
|
+
return isFocused
|
|
343
|
+
? LegendInteractionState.Focused
|
|
344
|
+
: LegendInteractionState.Muted
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
legendStyleConfig: LegendStyleConfig = LEGEND_STYLE_FOR_STACKED_CHARTS
|
|
348
|
+
|
|
349
|
+
@computed get hoverColors(): string[] {
|
|
350
|
+
if (this.focusColorBin) return [this.focusColorBin.color]
|
|
351
|
+
if (this.tooltipItem?.entityColor)
|
|
352
|
+
return [this.tooltipItem.entityColor.color]
|
|
353
|
+
if (this.selectionArray.hasSelection) {
|
|
354
|
+
const selectedItems = this.items.filter((item) =>
|
|
355
|
+
this.selectionArray.selectedSet.has(item.entityName)
|
|
356
|
+
)
|
|
357
|
+
const uniqueSelectedColors = new Set(
|
|
358
|
+
selectedItems.map((item) => item.entityColor?.color)
|
|
359
|
+
)
|
|
360
|
+
return this.categoricalLegendData
|
|
361
|
+
.filter((bin) => uniqueSelectedColors.has(bin.color as any))
|
|
362
|
+
.map((bin) => bin.color)
|
|
363
|
+
}
|
|
364
|
+
return []
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@computed private get showLegend(): boolean {
|
|
368
|
+
return (
|
|
369
|
+
(!!this.colorColumnSlug || this.categoricalLegendData.length > 1) &&
|
|
370
|
+
!this.manager.isDisplayedAlongsideComplementaryTable
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@action.bound onLegendMouseOver(bin: ColorScaleBin): void {
|
|
375
|
+
this.focusColorBin = bin
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@action.bound onLegendMouseLeave(): void {
|
|
379
|
+
this.focusColorBin = undefined
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@computed private get legend(): HorizontalCategoricalColorLegend {
|
|
383
|
+
return new HorizontalCategoricalColorLegend({ manager: this })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@action.bound private onEntityMouseOver(entityName: string): void {
|
|
387
|
+
this.tooltipState.target = { entityName }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@action.bound private onMouseMove(ev: React.MouseEvent): void {
|
|
391
|
+
const ref = this.manager.base?.current
|
|
392
|
+
if (ref) {
|
|
393
|
+
this.tooltipState.position = getRelativeMouse(ref, ev)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@action.bound private dismissTooltip(): void {
|
|
398
|
+
this.tooltipState.target = null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@action.bound private onEntityClick(entityName: string): void {
|
|
402
|
+
this.onSelectEntity(entityName)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@action.bound private onSelectEntity(entityName: string): void {
|
|
406
|
+
if (this.canAddCountry) this.selectionArray.toggleSelection(entityName)
|
|
407
|
+
}
|
|
408
|
+
@computed private get canAddCountry(): boolean {
|
|
409
|
+
const { addCountryMode } = this.manager
|
|
410
|
+
return (addCountryMode &&
|
|
411
|
+
addCountryMode !== EntitySelectionMode.Disabled) as boolean
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@computed private get tooltipItem(): Item | undefined {
|
|
415
|
+
const { target } = this.tooltipState
|
|
416
|
+
return (
|
|
417
|
+
target &&
|
|
418
|
+
this.items.find(
|
|
419
|
+
({ entityName }) => entityName === target.entityName
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
override componentDidMount(): void {
|
|
425
|
+
exposeInstanceOnWindow(this)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
override render(): React.ReactElement {
|
|
429
|
+
if (this.chartState.errorInfo.reason)
|
|
430
|
+
return (
|
|
431
|
+
<NoDataModal
|
|
432
|
+
manager={this.manager}
|
|
433
|
+
bounds={this.bounds}
|
|
434
|
+
message={this.chartState.errorInfo.reason}
|
|
435
|
+
/>
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const {
|
|
439
|
+
manager,
|
|
440
|
+
bounds,
|
|
441
|
+
dualAxis,
|
|
442
|
+
tooltipItem,
|
|
443
|
+
xColumn,
|
|
444
|
+
yColumns,
|
|
445
|
+
colorColumn,
|
|
446
|
+
colorScale,
|
|
447
|
+
manager: { endTime, xOverrideTime },
|
|
448
|
+
inputTable: { timeColumn },
|
|
449
|
+
tooltipState: { target, position, fading },
|
|
450
|
+
} = this
|
|
451
|
+
|
|
452
|
+
const { entityName, xPoint, bars } = tooltipItem ?? {}
|
|
453
|
+
|
|
454
|
+
const yValues =
|
|
455
|
+
bars?.map((bar: Bar) => {
|
|
456
|
+
const column = this.chartState.transformedTable.get(
|
|
457
|
+
bar.columnSlug
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
const shouldShowYTimeNotice =
|
|
461
|
+
bar.yPoint.value !== undefined &&
|
|
462
|
+
bar.yPoint.time !== endTime
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
name: bar.seriesName,
|
|
466
|
+
value: bar.yPoint.value,
|
|
467
|
+
column,
|
|
468
|
+
originalTime: shouldShowYTimeNotice
|
|
469
|
+
? column.formatTime(bar.yPoint.time)
|
|
470
|
+
: undefined,
|
|
471
|
+
}
|
|
472
|
+
}) ?? []
|
|
473
|
+
|
|
474
|
+
// TODO: when we have proper time support to work across date/year variables then
|
|
475
|
+
// this should be set properly and the x axis time be passed in on it's own.
|
|
476
|
+
// For now we disable x axis notices when the xOverrideTime is set which is
|
|
477
|
+
// usually the case when matching day and year variables
|
|
478
|
+
const shouldShowXTimeNotice =
|
|
479
|
+
xPoint && xPoint.time !== endTime && xOverrideTime === undefined
|
|
480
|
+
const xOriginalTime = shouldShowXTimeNotice ? xPoint?.time : undefined
|
|
481
|
+
const xOriginalTimeFormatted = xOriginalTime
|
|
482
|
+
? xColumn?.formatTime(xOriginalTime)
|
|
483
|
+
: undefined
|
|
484
|
+
const targetNotice =
|
|
485
|
+
xOriginalTime || yValues.some(({ originalTime }) => !!originalTime)
|
|
486
|
+
? timeColumn.formatValue(endTime)
|
|
487
|
+
: undefined
|
|
488
|
+
const toleranceNotice = targetNotice
|
|
489
|
+
? {
|
|
490
|
+
icon: TooltipFooterIcon.Notice,
|
|
491
|
+
text: makeTooltipToleranceNotice(targetNotice),
|
|
492
|
+
}
|
|
493
|
+
: undefined
|
|
494
|
+
|
|
495
|
+
const columns = excludeUndefined([xColumn, ...yColumns])
|
|
496
|
+
const allRoundedToSigFigs = columns.every(
|
|
497
|
+
(column) => column.roundsToSignificantFigures
|
|
498
|
+
)
|
|
499
|
+
const anyRoundedToSigFigs = columns.some(
|
|
500
|
+
(column) => column.roundsToSignificantFigures
|
|
501
|
+
)
|
|
502
|
+
const sigFigs = excludeUndefined(
|
|
503
|
+
columns.map((column) =>
|
|
504
|
+
column.roundsToSignificantFigures
|
|
505
|
+
? column.numSignificantFigures
|
|
506
|
+
: undefined
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
const roundingNotice = anyRoundedToSigFigs
|
|
510
|
+
? {
|
|
511
|
+
icon: allRoundedToSigFigs
|
|
512
|
+
? TooltipFooterIcon.None
|
|
513
|
+
: TooltipFooterIcon.Significance,
|
|
514
|
+
text: makeTooltipRoundingNotice(sigFigs, {
|
|
515
|
+
plural: sigFigs.length > 1,
|
|
516
|
+
}),
|
|
517
|
+
}
|
|
518
|
+
: undefined
|
|
519
|
+
const superscript =
|
|
520
|
+
!!roundingNotice && roundingNotice.icon !== TooltipFooterIcon.None
|
|
521
|
+
|
|
522
|
+
const footer = excludeUndefined([toleranceNotice, roundingNotice])
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<g
|
|
526
|
+
ref={this.base}
|
|
527
|
+
id={makeIdForHumanConsumption("marimekko-chart")}
|
|
528
|
+
className="MarimekkoChart"
|
|
529
|
+
onMouseMove={(ev): void => this.onMouseMove(ev)}
|
|
530
|
+
onMouseLeave={(): void => this.dismissTooltip()}
|
|
531
|
+
>
|
|
532
|
+
<rect
|
|
533
|
+
x={bounds.left}
|
|
534
|
+
y={bounds.top}
|
|
535
|
+
width={bounds.width}
|
|
536
|
+
height={bounds.height}
|
|
537
|
+
opacity={0}
|
|
538
|
+
fill="rgba(255,255,255,0)"
|
|
539
|
+
/>
|
|
540
|
+
<DualAxisComponent
|
|
541
|
+
dualAxis={dualAxis}
|
|
542
|
+
showTickMarks={true}
|
|
543
|
+
detailsMarker={manager.detailsMarkerInSvg}
|
|
544
|
+
backgroundColor={manager.backgroundColor}
|
|
545
|
+
/>
|
|
546
|
+
{this.showLegend && (
|
|
547
|
+
<HorizontalCategoricalColorLegend manager={this} />
|
|
548
|
+
)}
|
|
549
|
+
{this.renderBars()}
|
|
550
|
+
{this.labelLines}
|
|
551
|
+
{this.placedLabels}
|
|
552
|
+
{target && (
|
|
553
|
+
<Tooltip
|
|
554
|
+
id="marimekkoTooltip"
|
|
555
|
+
tooltipManager={this.manager}
|
|
556
|
+
x={position.x}
|
|
557
|
+
y={position.y}
|
|
558
|
+
style={{ maxWidth: "250px" }}
|
|
559
|
+
offsetX={20}
|
|
560
|
+
offsetY={-16}
|
|
561
|
+
title={entityName}
|
|
562
|
+
subtitle={timeColumn.formatValue(endTime)}
|
|
563
|
+
footer={footer}
|
|
564
|
+
dissolve={fading}
|
|
565
|
+
dismiss={() => (this.tooltipState.target = null)}
|
|
566
|
+
>
|
|
567
|
+
{yValues.map(
|
|
568
|
+
({ name, value, column, originalTime }) => (
|
|
569
|
+
<TooltipValue
|
|
570
|
+
key={name}
|
|
571
|
+
label={column.displayName}
|
|
572
|
+
unit={column.displayUnit}
|
|
573
|
+
value={column.formatValueShort(value)}
|
|
574
|
+
originalTime={originalTime}
|
|
575
|
+
isRoundedToSignificantFigures={
|
|
576
|
+
column.roundsToSignificantFigures
|
|
577
|
+
}
|
|
578
|
+
showSignificanceSuperscript={superscript}
|
|
579
|
+
/>
|
|
580
|
+
)
|
|
581
|
+
)}
|
|
582
|
+
{xColumn && !xColumn.isMissing && (
|
|
583
|
+
<TooltipValue
|
|
584
|
+
label={xColumn.displayName}
|
|
585
|
+
unit={xColumn.displayUnit}
|
|
586
|
+
value={xColumn.formatValueShort(xPoint?.value)}
|
|
587
|
+
originalTime={xOriginalTimeFormatted}
|
|
588
|
+
isRoundedToSignificantFigures={
|
|
589
|
+
xColumn.roundsToSignificantFigures
|
|
590
|
+
}
|
|
591
|
+
showSignificanceSuperscript={superscript}
|
|
592
|
+
/>
|
|
593
|
+
)}
|
|
594
|
+
{colorColumn &&
|
|
595
|
+
!colorColumn.isMissing &&
|
|
596
|
+
tooltipItem?.entityColor && (
|
|
597
|
+
<TooltipValue
|
|
598
|
+
label={
|
|
599
|
+
colorScale.legendDescription ??
|
|
600
|
+
colorColumn.displayName
|
|
601
|
+
}
|
|
602
|
+
value={
|
|
603
|
+
colorScale.getBinForValue(
|
|
604
|
+
tooltipItem.entityColor
|
|
605
|
+
.colorDomainValue
|
|
606
|
+
)?.label ??
|
|
607
|
+
tooltipItem.entityColor.colorDomainValue
|
|
608
|
+
}
|
|
609
|
+
/>
|
|
610
|
+
)}
|
|
611
|
+
</Tooltip>
|
|
612
|
+
)}
|
|
613
|
+
</g>
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private renderBars(): React.ReactElement {
|
|
618
|
+
return (
|
|
619
|
+
<MarimekkoBars
|
|
620
|
+
dualAxis={this.dualAxis}
|
|
621
|
+
focusColorBin={this.focusColorBin}
|
|
622
|
+
placedItems={this.placedItems}
|
|
623
|
+
tooltipState={this.tooltipState}
|
|
624
|
+
fontSize={this.fontSize}
|
|
625
|
+
x0={this.chartState.x0}
|
|
626
|
+
y0={this.chartState.y0}
|
|
627
|
+
selectionArray={this.selectionArray}
|
|
628
|
+
selectedItems={this.chartState.selectedItems}
|
|
629
|
+
onEntityClick={this.onEntityClick}
|
|
630
|
+
onEntityMouseLeave={this.dismissTooltip}
|
|
631
|
+
onEntityMouseOver={this.onEntityMouseOver}
|
|
632
|
+
/>
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private paddingInPixels = 5
|
|
637
|
+
|
|
638
|
+
private static labelCandidateFromItem(
|
|
639
|
+
item: EntityWithSize,
|
|
640
|
+
fontSize: number,
|
|
641
|
+
isSelected: boolean
|
|
642
|
+
): LabelCandidate {
|
|
643
|
+
const label = item.shortEntityName ?? item.entityName
|
|
644
|
+
const labelBounds = Bounds.forText(label, { fontSize })
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
item: item,
|
|
648
|
+
label,
|
|
649
|
+
bounds: labelBounds,
|
|
650
|
+
isPicked: isSelected,
|
|
651
|
+
isSelected,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** This function splits label candidates into N groups so that each group has approximately
|
|
656
|
+
the same sum of x value metric. This is useful for picking labels because we want to have e.g.
|
|
657
|
+
20 labels relatively evenly spaced (in x domain space) and this function gives us 20 groups that
|
|
658
|
+
are roughly of equal size and then we can pick the largest of each group */
|
|
659
|
+
private static splitIntoEqualDomainSizeChunks(
|
|
660
|
+
items: Item[],
|
|
661
|
+
candidates: LabelCandidate[],
|
|
662
|
+
numChunks: number
|
|
663
|
+
): LabelCandidate[][] {
|
|
664
|
+
// candidates contains all entities available in the chart for some time
|
|
665
|
+
// items is just the entities for the currently selected time, so can be a way smaller subset
|
|
666
|
+
const validItemNames = items.map(({ entityName }) => entityName)
|
|
667
|
+
|
|
668
|
+
// filter the list to remove any candidates that are not currently visible
|
|
669
|
+
// all further calculations are then done only with validCandidates
|
|
670
|
+
const validCandidates = candidates.filter((candidate) =>
|
|
671
|
+
validItemNames.includes(candidate.item.entityName)
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
const chunks: LabelCandidate[][] = []
|
|
675
|
+
let currentChunk: LabelCandidate[] = []
|
|
676
|
+
let domainSizeOfChunk = 0
|
|
677
|
+
const domainSizeThreshold = Math.ceil(
|
|
678
|
+
_.sumBy(validCandidates, (candidate) => candidate.item.xValue) /
|
|
679
|
+
numChunks
|
|
680
|
+
)
|
|
681
|
+
for (const candidate of validCandidates) {
|
|
682
|
+
while (domainSizeOfChunk > domainSizeThreshold) {
|
|
683
|
+
chunks.push(currentChunk)
|
|
684
|
+
currentChunk = []
|
|
685
|
+
domainSizeOfChunk -= domainSizeThreshold
|
|
686
|
+
}
|
|
687
|
+
domainSizeOfChunk += candidate.item.xValue
|
|
688
|
+
currentChunk.push(candidate)
|
|
689
|
+
}
|
|
690
|
+
chunks.push(currentChunk)
|
|
691
|
+
|
|
692
|
+
return chunks.filter((chunk) => chunk.length > 0)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@computed private get pickedLabelCandidates(): LabelCandidate[] {
|
|
696
|
+
const {
|
|
697
|
+
xColumnAtLastTimePoint,
|
|
698
|
+
yColumnsAtLastTimePoint,
|
|
699
|
+
xRange,
|
|
700
|
+
sortConfig,
|
|
701
|
+
paddingInPixels,
|
|
702
|
+
items,
|
|
703
|
+
} = this
|
|
704
|
+
const { selectedItems } = this.chartState
|
|
705
|
+
|
|
706
|
+
if (yColumnsAtLastTimePoint.length === 0) return []
|
|
707
|
+
|
|
708
|
+
// Measure the labels (before any rotation, just normal horizontal labels)
|
|
709
|
+
const selectedItemsSet = new Set(
|
|
710
|
+
selectedItems.map((item) => item.entityName)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
// This is similar to what we would get with .sortedItems but
|
|
714
|
+
// we want this for the last year to pick all labels there - sortedItems
|
|
715
|
+
// changes with the time point the user selects
|
|
716
|
+
const ySizeMap: Map<string, number> = new Map(
|
|
717
|
+
yColumnsAtLastTimePoint[0].owidRows.map((row) => [
|
|
718
|
+
row.entityName,
|
|
719
|
+
row.value,
|
|
720
|
+
])
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
// We want labels to be chosen according to the latest time point available in the chart.
|
|
724
|
+
// The reason for this is that it makes it so the labels are pretty consistent across time,
|
|
725
|
+
// and not very jumpy when the user drags across the timeline.
|
|
726
|
+
const labelCandidateSource = xColumnAtLastTimePoint
|
|
727
|
+
? xColumnAtLastTimePoint
|
|
728
|
+
: yColumnsAtLastTimePoint[0]
|
|
729
|
+
|
|
730
|
+
let labelCandidates: LabelCandidate[] =
|
|
731
|
+
labelCandidateSource.owidRows.map((row) =>
|
|
732
|
+
MarimekkoChart.labelCandidateFromItem(
|
|
733
|
+
{
|
|
734
|
+
entityName: row.entityName,
|
|
735
|
+
shortEntityName: getShortNameForEntity(row.entityName),
|
|
736
|
+
xValue:
|
|
737
|
+
xColumnAtLastTimePoint !== undefined
|
|
738
|
+
? row.value
|
|
739
|
+
: 1,
|
|
740
|
+
ySortValue: ySizeMap.get(row.entityName),
|
|
741
|
+
},
|
|
742
|
+
this.entityLabelFontSize,
|
|
743
|
+
selectedItemsSet.has(row.entityName)
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
// If focus mode is active, only label focused series
|
|
748
|
+
if (this.chartState.focusArray.hasFocusedSeries) {
|
|
749
|
+
labelCandidates = labelCandidates.filter((candidate) =>
|
|
750
|
+
this.chartState.focusArray.has(candidate.item.entityName)
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (labelCandidates.length === 0) return []
|
|
755
|
+
|
|
756
|
+
labelCandidates.sort((a, b) => {
|
|
757
|
+
const yRowsForA = a.item.ySortValue
|
|
758
|
+
const yRowsForB = b.item.ySortValue
|
|
759
|
+
|
|
760
|
+
if (yRowsForA !== undefined && yRowsForB !== undefined) {
|
|
761
|
+
const diff = yRowsForB - yRowsForA
|
|
762
|
+
if (diff !== 0) return diff
|
|
763
|
+
else return b.item.entityName.localeCompare(a.item.entityName)
|
|
764
|
+
} else if (yRowsForA === undefined && yRowsForB !== undefined)
|
|
765
|
+
return -1
|
|
766
|
+
else if (yRowsForA !== undefined && yRowsForB === undefined)
|
|
767
|
+
return 1
|
|
768
|
+
// (yRowsForA === undefined && yRowsForB === undefined)
|
|
769
|
+
else return 0
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
if (sortConfig.sortOrder === SortOrder.desc) {
|
|
773
|
+
labelCandidates.reverse()
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const [sortedLabelsWithValues, sortedLabelsWithoutValues] = _.partition(
|
|
777
|
+
labelCandidates,
|
|
778
|
+
(item) =>
|
|
779
|
+
item.item.ySortValue !== 0 && item.item.ySortValue !== undefined
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
if (sortedLabelsWithValues.length) {
|
|
783
|
+
R.first(sortedLabelsWithValues)!.isPicked = true
|
|
784
|
+
R.last(sortedLabelsWithValues)!.isPicked = true
|
|
785
|
+
}
|
|
786
|
+
if (sortedLabelsWithoutValues.length) {
|
|
787
|
+
if (sortConfig.sortOrder === SortOrder.desc)
|
|
788
|
+
R.first(sortedLabelsWithoutValues)!.isPicked = true
|
|
789
|
+
else R.last(sortedLabelsWithoutValues)!.isPicked = true
|
|
790
|
+
}
|
|
791
|
+
const availablePixels = xRange[1] - xRange[0]
|
|
792
|
+
|
|
793
|
+
const labelHeight = labelCandidates[0].bounds.height
|
|
794
|
+
|
|
795
|
+
const numLabelsToAdd = Math.floor(
|
|
796
|
+
Math.min(
|
|
797
|
+
availablePixels / (labelHeight + paddingInPixels) / 3, // factor 3 is arbitrary to taste
|
|
798
|
+
MAX_LABEL_COUNT
|
|
799
|
+
)
|
|
800
|
+
)
|
|
801
|
+
const chunks = MarimekkoChart.splitIntoEqualDomainSizeChunks(
|
|
802
|
+
items,
|
|
803
|
+
labelCandidates,
|
|
804
|
+
numLabelsToAdd
|
|
805
|
+
)
|
|
806
|
+
const picks = chunks.flatMap((chunk) => {
|
|
807
|
+
const picked = chunk.filter((candidate) => candidate.isPicked)
|
|
808
|
+
if (picked.length > 0) return picked
|
|
809
|
+
else {
|
|
810
|
+
return _.maxBy(chunk, (candidate) => candidate.item.xValue)
|
|
811
|
+
}
|
|
812
|
+
})
|
|
813
|
+
for (const max of picks) {
|
|
814
|
+
if (max) max.isPicked = true
|
|
815
|
+
}
|
|
816
|
+
const picked = labelCandidates.filter((candidate) => candidate.isPicked)
|
|
817
|
+
|
|
818
|
+
return picked
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@computed private get labelsWithPlacementInfo(): LabelWithPlacement[] {
|
|
822
|
+
const {
|
|
823
|
+
dualAxis,
|
|
824
|
+
placedItemsMap,
|
|
825
|
+
labels,
|
|
826
|
+
unrotatedLongestLabelWidth,
|
|
827
|
+
unrotatedHighestLabelHeight,
|
|
828
|
+
labelAngleInDegrees,
|
|
829
|
+
} = this
|
|
830
|
+
const { x0 } = this.chartState
|
|
831
|
+
const labelsYPosition = dualAxis.verticalAxis.place(0)
|
|
832
|
+
|
|
833
|
+
const labelsWithPlacements: LabelWithPlacement[] = labels
|
|
834
|
+
.map(({ candidate, labelElement }) => {
|
|
835
|
+
const item = placedItemsMap.get(candidate.item.entityName)
|
|
836
|
+
if (!item)
|
|
837
|
+
console.error("Could not find item in placedItemsMap")
|
|
838
|
+
const xPoint = item?.xPoint?.value ?? 1
|
|
839
|
+
const barWidth =
|
|
840
|
+
dualAxis.horizontalAxis.place(xPoint) -
|
|
841
|
+
dualAxis.horizontalAxis.place(x0)
|
|
842
|
+
|
|
843
|
+
const labelId = candidate.item.entityName
|
|
844
|
+
if (!item) {
|
|
845
|
+
console.error(
|
|
846
|
+
"Could not find item",
|
|
847
|
+
candidate.item.entityName
|
|
848
|
+
)
|
|
849
|
+
return null
|
|
850
|
+
} else {
|
|
851
|
+
const currentX =
|
|
852
|
+
dualAxis.horizontalAxis.place(x0) + item.xPosition
|
|
853
|
+
const labelWithPlacement = {
|
|
854
|
+
label: (
|
|
855
|
+
<g
|
|
856
|
+
transform={`translate(${0}, ${labelsYPosition})`}
|
|
857
|
+
>
|
|
858
|
+
{labelElement}
|
|
859
|
+
</g>
|
|
860
|
+
),
|
|
861
|
+
preferredPlacement: currentX + barWidth / 2,
|
|
862
|
+
correctedPlacement: currentX + barWidth / 2,
|
|
863
|
+
labelKey: labelId,
|
|
864
|
+
}
|
|
865
|
+
return labelWithPlacement
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
.filter(
|
|
869
|
+
(item: LabelWithPlacement | null): item is LabelWithPlacement =>
|
|
870
|
+
item !== null
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
// This collision detection code is optimized for the particular
|
|
874
|
+
// case of distributing items in 1D, knowing that we picked a low
|
|
875
|
+
// enough number of labels that we will be able to fit all labels.
|
|
876
|
+
// The algorithm iterates the list twice, i.e. works in linear time
|
|
877
|
+
// with the number of labels to show
|
|
878
|
+
// The logic in pseudo code:
|
|
879
|
+
// for current, next in iterate-left-to-right-pairs:
|
|
880
|
+
// if next.x < current.x + label-width:
|
|
881
|
+
// next.x = current.x + label-width
|
|
882
|
+
// last.x = Math.min(last.x, max-x)
|
|
883
|
+
// for current, prev in iterate-right-to-left-pairs:
|
|
884
|
+
// if prev.x > current.x - label-width:
|
|
885
|
+
// prev.x = current.x - label-width
|
|
886
|
+
|
|
887
|
+
// The label width is uniform for now and starts with
|
|
888
|
+
// the height of a label when printed in normal horizontal layout
|
|
889
|
+
// Since labels are rotated we need to make a bit more space so that they
|
|
890
|
+
// stack correctly. Consider:
|
|
891
|
+
// ╱---╱ ╱---╱
|
|
892
|
+
// ╱ ╱ ╱ ╱
|
|
893
|
+
// ╱ ╱ ╱ ╱
|
|
894
|
+
// ╱---╱ ╱---╱
|
|
895
|
+
// If we would just use exactly the label width then the flatter the angle
|
|
896
|
+
// the more they would actually overlap so we need a correction factor. It turns
|
|
897
|
+
// out than tan(angle) is the correction factor we want, although for horizontal
|
|
898
|
+
// labels we don't want to use +infinity :) so we Math.min it with the longest label width
|
|
899
|
+
if (labelsWithPlacements.length === 0) return []
|
|
900
|
+
|
|
901
|
+
labelsWithPlacements.sort((a, b) => {
|
|
902
|
+
const diff = a.preferredPlacement - b.preferredPlacement
|
|
903
|
+
if (diff !== 0) return diff
|
|
904
|
+
else return a.labelKey.localeCompare(b.labelKey)
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
const labelWidth = unrotatedHighestLabelHeight
|
|
908
|
+
const correctionFactor =
|
|
909
|
+
1 +
|
|
910
|
+
Math.min(
|
|
911
|
+
unrotatedLongestLabelWidth / labelWidth,
|
|
912
|
+
Math.abs(Math.tan(labelAngleInDegrees))
|
|
913
|
+
)
|
|
914
|
+
const correctedLabelWidth = labelWidth * correctionFactor
|
|
915
|
+
|
|
916
|
+
for (let i = 0; i < labelsWithPlacements.length - 1; i++) {
|
|
917
|
+
const current = labelsWithPlacements[i]
|
|
918
|
+
const next = labelsWithPlacements[i + 1]
|
|
919
|
+
const minNextX = current.correctedPlacement + correctedLabelWidth
|
|
920
|
+
if (next.correctedPlacement < minNextX)
|
|
921
|
+
next.correctedPlacement = minNextX
|
|
922
|
+
}
|
|
923
|
+
labelsWithPlacements[
|
|
924
|
+
labelsWithPlacements.length - 1
|
|
925
|
+
].correctedPlacement = Math.min(
|
|
926
|
+
labelsWithPlacements[labelsWithPlacements.length - 1]
|
|
927
|
+
.correctedPlacement,
|
|
928
|
+
dualAxis.horizontalAxis.rangeSize +
|
|
929
|
+
dualAxis.horizontalAxis.place(x0)
|
|
930
|
+
)
|
|
931
|
+
for (let i = labelsWithPlacements.length - 1; i > 0; i--) {
|
|
932
|
+
const current = labelsWithPlacements[i]
|
|
933
|
+
const previous = labelsWithPlacements[i - 1]
|
|
934
|
+
const maxPreviousX =
|
|
935
|
+
current.correctedPlacement - correctedLabelWidth
|
|
936
|
+
if (previous.correctedPlacement > maxPreviousX)
|
|
937
|
+
previous.correctedPlacement = maxPreviousX
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return labelsWithPlacements
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
@computed private get labelLines(): React.ReactElement[] {
|
|
944
|
+
const { labelsWithPlacementInfo, dualAxis } = this
|
|
945
|
+
const { selectedItems } = this.chartState
|
|
946
|
+
const shiftedGroups: LabelWithPlacement[][] = []
|
|
947
|
+
const unshiftedElements: LabelWithPlacement[] = []
|
|
948
|
+
const selectedItemsKeys = new Set(
|
|
949
|
+
selectedItems.map((item) => item.entityName)
|
|
950
|
+
)
|
|
951
|
+
let startNewGroup = true
|
|
952
|
+
|
|
953
|
+
const barEndpointY = dualAxis.verticalAxis.place(0)
|
|
954
|
+
|
|
955
|
+
for (const labelWithPlacement of labelsWithPlacementInfo) {
|
|
956
|
+
if (
|
|
957
|
+
labelWithPlacement.preferredPlacement ===
|
|
958
|
+
labelWithPlacement.correctedPlacement
|
|
959
|
+
) {
|
|
960
|
+
unshiftedElements.push(labelWithPlacement)
|
|
961
|
+
startNewGroup = true
|
|
962
|
+
} else {
|
|
963
|
+
if (startNewGroup) {
|
|
964
|
+
shiftedGroups.push([labelWithPlacement])
|
|
965
|
+
startNewGroup = false
|
|
966
|
+
} else {
|
|
967
|
+
shiftedGroups[shiftedGroups.length - 1].push(
|
|
968
|
+
labelWithPlacement
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// If we wanted to hide the label lines if all lines are straight
|
|
974
|
+
// then we could do this but this makes it jumpy over time
|
|
975
|
+
// if (shiftedGroups.length === 0) return []
|
|
976
|
+
// else {
|
|
977
|
+
const labelLines: React.ReactElement[] = []
|
|
978
|
+
for (const group of shiftedGroups) {
|
|
979
|
+
let indexInGroup = 0
|
|
980
|
+
for (const item of group) {
|
|
981
|
+
const lineColor = selectedItemsKeys.has(item.labelKey)
|
|
982
|
+
? "#999"
|
|
983
|
+
: "#bbb"
|
|
984
|
+
const markerBarEndpointX = item.preferredPlacement
|
|
985
|
+
const markerTextEndpointX = item.correctedPlacement
|
|
986
|
+
const markerBarEndpointY = barEndpointY + MARKER_MARGIN
|
|
987
|
+
const markerTextEndpointY =
|
|
988
|
+
barEndpointY + MARKER_AREA_HEIGHT - MARKER_MARGIN
|
|
989
|
+
const markerNetHeight = MARKER_AREA_HEIGHT - 2 * MARKER_MARGIN
|
|
990
|
+
const markerStepSize = markerNetHeight / (group.length + 1)
|
|
991
|
+
const directionUnawareMakerYMid =
|
|
992
|
+
(indexInGroup + 1) * markerStepSize
|
|
993
|
+
const markerYMid =
|
|
994
|
+
markerBarEndpointX > markerTextEndpointX
|
|
995
|
+
? directionUnawareMakerYMid
|
|
996
|
+
: markerNetHeight - directionUnawareMakerYMid
|
|
997
|
+
labelLines.push(
|
|
998
|
+
<g
|
|
999
|
+
id={makeIdForHumanConsumption(
|
|
1000
|
+
"label-line",
|
|
1001
|
+
item.labelKey
|
|
1002
|
+
)}
|
|
1003
|
+
className="indicator"
|
|
1004
|
+
key={`labelline-${item.labelKey}`}
|
|
1005
|
+
>
|
|
1006
|
+
<path
|
|
1007
|
+
d={`M${markerBarEndpointX},${markerBarEndpointY} v${markerYMid} H${markerTextEndpointX} V${markerTextEndpointY}`}
|
|
1008
|
+
stroke={lineColor}
|
|
1009
|
+
strokeWidth={1}
|
|
1010
|
+
fill="none"
|
|
1011
|
+
/>
|
|
1012
|
+
</g>
|
|
1013
|
+
)
|
|
1014
|
+
indexInGroup++
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const item of unshiftedElements) {
|
|
1018
|
+
const lineColor = selectedItemsKeys.has(item.labelKey)
|
|
1019
|
+
? "#555"
|
|
1020
|
+
: "#bbb"
|
|
1021
|
+
const markerBarEndpointX = item.preferredPlacement
|
|
1022
|
+
const markerBarEndpointY = barEndpointY + MARKER_MARGIN
|
|
1023
|
+
const markerTextEndpointY =
|
|
1024
|
+
barEndpointY + MARKER_AREA_HEIGHT - MARKER_MARGIN
|
|
1025
|
+
|
|
1026
|
+
labelLines.push(
|
|
1027
|
+
<g
|
|
1028
|
+
id={makeIdForHumanConsumption("label-line", item.labelKey)}
|
|
1029
|
+
className="indicator"
|
|
1030
|
+
key={`labelline-${item.labelKey}`}
|
|
1031
|
+
>
|
|
1032
|
+
<path
|
|
1033
|
+
d={`M${markerBarEndpointX},${markerBarEndpointY} V${markerTextEndpointY}`}
|
|
1034
|
+
stroke={lineColor}
|
|
1035
|
+
strokeWidth={1}
|
|
1036
|
+
fill="none"
|
|
1037
|
+
/>
|
|
1038
|
+
</g>
|
|
1039
|
+
)
|
|
1040
|
+
}
|
|
1041
|
+
return labelLines
|
|
1042
|
+
//}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
@computed private get placedLabels(): React.ReactElement[] {
|
|
1046
|
+
const labelOffset = MARKER_AREA_HEIGHT
|
|
1047
|
+
// old logic tried to hide labellines but that is too jumpy
|
|
1048
|
+
// labelLines.length
|
|
1049
|
+
// ? MARKER_AREA_HEIGHT
|
|
1050
|
+
// : this.baseFontSize / 2
|
|
1051
|
+
const placedLabels = this.labelsWithPlacementInfo.map((item) => (
|
|
1052
|
+
<g
|
|
1053
|
+
key={`label-${item.labelKey}`}
|
|
1054
|
+
id={makeIdForHumanConsumption("label", item.labelKey)}
|
|
1055
|
+
className="bar-label"
|
|
1056
|
+
transform={`translate(${item.correctedPlacement}, ${labelOffset})`}
|
|
1057
|
+
>
|
|
1058
|
+
{item.label}
|
|
1059
|
+
</g>
|
|
1060
|
+
))
|
|
1061
|
+
|
|
1062
|
+
return placedLabels
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
@computed private get unrotatedLongestLabelWidth(): number {
|
|
1066
|
+
const widths = this.pickedLabelCandidates.map(
|
|
1067
|
+
(candidate) => candidate.bounds.width
|
|
1068
|
+
)
|
|
1069
|
+
const maxWidth = Math.max(...widths)
|
|
1070
|
+
return maxWidth
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
@computed private get unrotatedHighestLabelHeight(): number {
|
|
1074
|
+
const heights = this.pickedLabelCandidates.map(
|
|
1075
|
+
(candidate) => candidate.bounds.height
|
|
1076
|
+
)
|
|
1077
|
+
return Math.max(...heights)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
@computed private get longestLabelHeight(): number {
|
|
1081
|
+
// This takes the angle of rotation of the entity labels into account
|
|
1082
|
+
// This is somewhat simplified as we treat this as a one-dimensional
|
|
1083
|
+
// entity whereas in reality the textbox if of course 2D. To account
|
|
1084
|
+
// for that we do max(fontSize, rotatedLabelHeight) in the end
|
|
1085
|
+
// as a rough proxy
|
|
1086
|
+
const rotatedLabelHeight =
|
|
1087
|
+
this.unrotatedLongestLabelWidth *
|
|
1088
|
+
Math.abs(Math.sin((this.labelAngleInDegrees * Math.PI) / 180))
|
|
1089
|
+
return Math.max(this.fontSize, rotatedLabelHeight)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@computed private get longestLabelWidth(): number {
|
|
1093
|
+
// This takes the angle of rotation of the entity labels into account
|
|
1094
|
+
// This is somewhat simplified as we treat this as a one-dimensional
|
|
1095
|
+
// entity whereas in reality the textbox if of course 2D. To account
|
|
1096
|
+
// for that we do max(fontSize, rotatedLabelHeight) in the end
|
|
1097
|
+
// as a rough proxy
|
|
1098
|
+
const rotatedLabelWidth =
|
|
1099
|
+
this.unrotatedLongestLabelWidth *
|
|
1100
|
+
Math.abs(Math.cos((this.labelAngleInDegrees * Math.PI) / 180))
|
|
1101
|
+
return Math.max(this.fontSize, rotatedLabelWidth)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
@computed private get entityLabelFontSize(): number {
|
|
1105
|
+
return GRAPHER_FONT_SCALE_12 * this.fontSize
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
@computed private get labels(): LabelCandidateWithElement[] {
|
|
1109
|
+
const { labelAngleInDegrees, series } = this
|
|
1110
|
+
const { domainColorForEntityMap } = this.chartState
|
|
1111
|
+
return this.pickedLabelCandidates.map((candidate) => {
|
|
1112
|
+
const domainColor = domainColorForEntityMap.get(
|
|
1113
|
+
candidate.item.entityName
|
|
1114
|
+
)
|
|
1115
|
+
const seriesColor = series[0].color
|
|
1116
|
+
const color = domainColor?.color ?? seriesColor ?? "#000"
|
|
1117
|
+
return {
|
|
1118
|
+
candidate,
|
|
1119
|
+
labelElement: (
|
|
1120
|
+
<text
|
|
1121
|
+
key={`${candidate.item.entityName}-label`}
|
|
1122
|
+
y={0}
|
|
1123
|
+
fontWeight={candidate.isSelected ? 700 : 400}
|
|
1124
|
+
fill={color}
|
|
1125
|
+
transform={`rotate(${labelAngleInDegrees}, 0, 0)`}
|
|
1126
|
+
opacity={1}
|
|
1127
|
+
fontSize={this.entityLabelFontSize}
|
|
1128
|
+
textAnchor="end"
|
|
1129
|
+
dy={dyFromAlign(VerticalAlign.middle)}
|
|
1130
|
+
onMouseOver={(): void =>
|
|
1131
|
+
this.onEntityMouseOver(candidate.item.entityName)
|
|
1132
|
+
}
|
|
1133
|
+
onMouseLeave={(): void => this.dismissTooltip()}
|
|
1134
|
+
onClick={(): void =>
|
|
1135
|
+
this.onEntityClick(candidate.item.entityName)
|
|
1136
|
+
}
|
|
1137
|
+
>
|
|
1138
|
+
{candidate.label}
|
|
1139
|
+
</text>
|
|
1140
|
+
),
|
|
1141
|
+
}
|
|
1142
|
+
})
|
|
1143
|
+
}
|
|
1144
|
+
}
|