@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,1152 @@
|
|
|
1
|
+
import * as _ from "lodash-es"
|
|
2
|
+
import * as R from "remeda"
|
|
3
|
+
import React from "react"
|
|
4
|
+
import {
|
|
5
|
+
Bounds,
|
|
6
|
+
exposeInstanceOnWindow,
|
|
7
|
+
PointVector,
|
|
8
|
+
makeIdForHumanConsumption,
|
|
9
|
+
guid,
|
|
10
|
+
excludeUndefined,
|
|
11
|
+
getRelativeMouse,
|
|
12
|
+
dyFromAlign,
|
|
13
|
+
isTouchDevice,
|
|
14
|
+
domainExtent,
|
|
15
|
+
calculateTrendDirection,
|
|
16
|
+
} from "../../utils/index.js"
|
|
17
|
+
import { observable, computed, action, makeObservable } from "mobx"
|
|
18
|
+
import { observer } from "mobx-react"
|
|
19
|
+
import { NoDataModal } from "../noDataModal/NoDataModal"
|
|
20
|
+
import {
|
|
21
|
+
BASE_FONT_SIZE,
|
|
22
|
+
DEFAULT_GRAPHER_BOUNDS,
|
|
23
|
+
GRAPHER_FONT_SCALE_11,
|
|
24
|
+
GRAPHER_FONT_SCALE_12,
|
|
25
|
+
GRAPHER_TEXT_OUTLINE_FACTOR,
|
|
26
|
+
} from "../core/GrapherConstants"
|
|
27
|
+
import {
|
|
28
|
+
SeriesName,
|
|
29
|
+
MissingDataStrategy,
|
|
30
|
+
Time,
|
|
31
|
+
SeriesStrategy,
|
|
32
|
+
VerticalAlign,
|
|
33
|
+
HorizontalAlign,
|
|
34
|
+
} from "../../types/index.js"
|
|
35
|
+
import { ChartInterface } from "../chart/ChartInterface"
|
|
36
|
+
import { scaleLinear, ScaleLinear } from "d3-scale"
|
|
37
|
+
import { select, type BaseType, type Selection } from "d3-selection"
|
|
38
|
+
import {
|
|
39
|
+
PlacedSlopeChartSeries,
|
|
40
|
+
RawSlopeChartSeries,
|
|
41
|
+
RenderSlopeChartSeries,
|
|
42
|
+
SlopeChartSeries,
|
|
43
|
+
SlopeChartManager,
|
|
44
|
+
} from "./SlopeChartConstants"
|
|
45
|
+
import { CoreColumn } from "../../core-table/index.js"
|
|
46
|
+
import { getHoverStateForSeries } from "../chart/ChartUtils"
|
|
47
|
+
import { VerticalAxis } from "../axis/Axis"
|
|
48
|
+
import { VerticalAxisZeroLine } from "../axis/AxisViews"
|
|
49
|
+
import { NoDataSection } from "../scatterCharts/NoDataSection"
|
|
50
|
+
|
|
51
|
+
import { LineLegend, LineLegendProps } from "../lineLegend/LineLegend"
|
|
52
|
+
import {
|
|
53
|
+
formatTooltipRangeValues,
|
|
54
|
+
makeTooltipRoundingNotice,
|
|
55
|
+
makeTooltipToleranceNotice,
|
|
56
|
+
Tooltip,
|
|
57
|
+
TooltipState,
|
|
58
|
+
TooltipValueRange,
|
|
59
|
+
} from "../tooltip/Tooltip"
|
|
60
|
+
import { TooltipFooterIcon } from "../tooltip/TooltipProps"
|
|
61
|
+
|
|
62
|
+
import { Halo } from "../../components/index.js"
|
|
63
|
+
import { HorizontalColorLegendManager } from "../legend/HorizontalColorLegends"
|
|
64
|
+
import { CategoricalBin } from "../color/ColorScaleBin"
|
|
65
|
+
import {
|
|
66
|
+
GRAPHER_BACKGROUND_DEFAULT,
|
|
67
|
+
GRAPHER_DARK_TEXT,
|
|
68
|
+
} from "../color/ColorConstants"
|
|
69
|
+
import { FocusArray } from "../focus/FocusArray"
|
|
70
|
+
import { LineLabelSeries } from "../lineLegend/LineLegendTypes"
|
|
71
|
+
import { SlopeChartState } from "./SlopeChartState"
|
|
72
|
+
import { AxisConfig, AxisManager } from "../axis/AxisConfig"
|
|
73
|
+
import { ChartComponentProps } from "../chart/ChartTypeMap.js"
|
|
74
|
+
import { InteractionState } from "../interaction/InteractionState"
|
|
75
|
+
import {
|
|
76
|
+
getYAxisConfigDefaults,
|
|
77
|
+
toPlacedSlopeChartSeries,
|
|
78
|
+
toRenderSlopeChartSeries,
|
|
79
|
+
} from "./SlopeChartHelpers"
|
|
80
|
+
import { Slope } from "./Slope"
|
|
81
|
+
import { MarkX } from "./MarkX"
|
|
82
|
+
import { CATEGORICAL_LEGEND_STYLE } from "../lineCharts/LineChartConstants"
|
|
83
|
+
|
|
84
|
+
type SVGMouseOrTouchEvent =
|
|
85
|
+
| React.MouseEvent<SVGGElement>
|
|
86
|
+
| React.TouchEvent<SVGGElement>
|
|
87
|
+
|
|
88
|
+
const LINE_LEGEND_PADDING = 4
|
|
89
|
+
|
|
90
|
+
export type SlopeChartProps = ChartComponentProps<SlopeChartState>
|
|
91
|
+
|
|
92
|
+
@observer
|
|
93
|
+
export class SlopeChart
|
|
94
|
+
extends React.Component<SlopeChartProps>
|
|
95
|
+
implements ChartInterface, AxisManager
|
|
96
|
+
{
|
|
97
|
+
constructor(props: SlopeChartProps) {
|
|
98
|
+
super(props)
|
|
99
|
+
|
|
100
|
+
makeObservable<SlopeChart, "hoveredSeriesName" | "tooltipState">(this, {
|
|
101
|
+
hoveredSeriesName: observable,
|
|
102
|
+
tooltipState: observable,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private slopeAreaRef = React.createRef<SVGGElement>()
|
|
107
|
+
|
|
108
|
+
private sidebarMargin = 10
|
|
109
|
+
|
|
110
|
+
private hoveredSeriesName: string | undefined = undefined
|
|
111
|
+
private tooltipState = new TooltipState<{
|
|
112
|
+
series: SlopeChartSeries
|
|
113
|
+
}>({ fade: "immediate" })
|
|
114
|
+
|
|
115
|
+
@computed get chartState(): SlopeChartState {
|
|
116
|
+
return this.props.chartState
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@computed private get manager(): SlopeChartManager {
|
|
120
|
+
return this.chartState.manager
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@computed private get bounds(): Bounds {
|
|
124
|
+
return this.props.bounds ?? DEFAULT_GRAPHER_BOUNDS
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@computed private get innerBounds(): Bounds {
|
|
128
|
+
return this.bounds
|
|
129
|
+
.padTop(6) // Leave room for overflowing dots
|
|
130
|
+
.padBottom(this.bottomPadding)
|
|
131
|
+
.padRight(this.sidebarWidth + this.sidebarMargin)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@computed get fontSize(): number {
|
|
135
|
+
return this.manager.fontSize ?? BASE_FONT_SIZE
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@computed private get missingDataStrategy(): MissingDataStrategy {
|
|
139
|
+
return this.chartState.missingDataStrategy
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@computed private get focusArray(): FocusArray {
|
|
143
|
+
return this.chartState.focusArray
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@computed private get formatColumn(): CoreColumn {
|
|
147
|
+
return this.chartState.formatColumn
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@computed private get lineStrokeWidth(): number {
|
|
151
|
+
return this.manager.isStaticAndSmall ? 3 : 1.5
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@computed private get backgroundColor(): string {
|
|
155
|
+
return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@computed private get isHoverModeActive(): boolean {
|
|
159
|
+
return (
|
|
160
|
+
this.hoveredSeriesNames.length > 0 ||
|
|
161
|
+
// if the external legend is hovered, we want to mute
|
|
162
|
+
// all non-hovered series even if the chart doesn't plot
|
|
163
|
+
// the currently hovered series
|
|
164
|
+
!!this.manager.externalLegendHoverBin
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@computed private get canToggleFocusMode(): boolean {
|
|
169
|
+
return !isTouchDevice() && this.series.length > 1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@computed private get startTime(): Time {
|
|
173
|
+
return this.chartState.startTime
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@computed private get endTime(): Time {
|
|
177
|
+
return this.chartState.endTime
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@computed private get startX(): number {
|
|
181
|
+
return this.xScale(this.startTime)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@computed private get endX(): number {
|
|
185
|
+
return this.xScale(this.endTime)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@computed private get seriesStrategy(): SeriesStrategy {
|
|
189
|
+
return this.chartState.seriesStrategy
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@computed get series(): SlopeChartSeries[] {
|
|
193
|
+
return this.chartState.series
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@computed private get placedSeries(): PlacedSlopeChartSeries[] {
|
|
197
|
+
return toPlacedSlopeChartSeries(this.series, {
|
|
198
|
+
yAxis: this.yAxis,
|
|
199
|
+
startX: this.startX,
|
|
200
|
+
endX: this.endX,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private hoverStateForSeries(series: SlopeChartSeries): InteractionState {
|
|
205
|
+
return getHoverStateForSeries(series, {
|
|
206
|
+
isHoverModeActive: this.isHoverModeActive,
|
|
207
|
+
hoveredSeriesNames: this.hoveredSeriesNames,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@computed private get renderSeries(): RenderSlopeChartSeries[] {
|
|
212
|
+
return toRenderSlopeChartSeries(this.placedSeries, {
|
|
213
|
+
isFocusModeActive: this.chartState.isFocusModeActive,
|
|
214
|
+
isHoverModeActive: this.isHoverModeActive,
|
|
215
|
+
hoveredSeriesNames: this.hoveredSeriesNames,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@computed get noDataSeries(): RawSlopeChartSeries[] {
|
|
220
|
+
return this.chartState.rawSeries.filter(
|
|
221
|
+
(series) => !this.chartState.isSeriesValid(series)
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@computed private get showNoDataSection(): boolean {
|
|
226
|
+
if (this.manager.hideNoDataSection) return false
|
|
227
|
+
|
|
228
|
+
// nothing to show if there are no series with missing data
|
|
229
|
+
if (this.noDataSeries.length === 0) return false
|
|
230
|
+
|
|
231
|
+
// the No Data section is HTML and won't show up in the SVG export
|
|
232
|
+
if (this.manager.isStatic) return false
|
|
233
|
+
|
|
234
|
+
// we usually don't show the no data section if columns are plotted
|
|
235
|
+
// (since columns don't appear in the entity selector there is no need
|
|
236
|
+
// to explain that a column is missing – it just adds noise). but if
|
|
237
|
+
// the missing data strategy is set to hide, then we do want to give
|
|
238
|
+
// feedback as to why a slope is currently not rendered
|
|
239
|
+
return (
|
|
240
|
+
this.seriesStrategy === SeriesStrategy.entity ||
|
|
241
|
+
this.missingDataStrategy === MissingDataStrategy.hide
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@computed private get bottomPadding(): number {
|
|
246
|
+
return 1.5 * GRAPHER_FONT_SCALE_12 * this.fontSize
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@computed private get xLabelPadding(): number {
|
|
250
|
+
return this.useCompactLayout ? 4 : 8
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@computed get yAxisConfig(): AxisConfig {
|
|
254
|
+
const { yAxisConfig } = this.manager
|
|
255
|
+
const defaults = getYAxisConfigDefaults(yAxisConfig)
|
|
256
|
+
const custom = { hideAxis: true }
|
|
257
|
+
return new AxisConfig({ ...defaults, ...yAxisConfig, ...custom }, this)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@computed get yDomainDefault(): [number, number] {
|
|
261
|
+
const defaultDomain: [number, number] = [Infinity, -Infinity]
|
|
262
|
+
return (
|
|
263
|
+
domainExtent(
|
|
264
|
+
this.chartState.allYValues,
|
|
265
|
+
this.chartState.yScaleType
|
|
266
|
+
) ?? defaultDomain
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@computed get yDomain(): [number, number] {
|
|
271
|
+
const domain = this.yAxisConfig.domain || [Infinity, -Infinity]
|
|
272
|
+
const domainDefault = this.yDomainDefault
|
|
273
|
+
return [
|
|
274
|
+
Math.min(domain[0], domainDefault[0]),
|
|
275
|
+
Math.max(domain[1], domainDefault[1]),
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@computed private get yRange(): [number, number] {
|
|
280
|
+
return this.innerBounds.yRange()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@computed get yAxis(): VerticalAxis {
|
|
284
|
+
return this.chartState.toVerticalAxis(this.yAxisConfig, {
|
|
285
|
+
yDomain: this.yDomain,
|
|
286
|
+
yRange: this.yRange,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@computed private get yAxisWidth(): number {
|
|
291
|
+
return this.yAxis.width
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@computed private get xScale(): ScaleLinear<number, number> {
|
|
295
|
+
const { xRange } = this
|
|
296
|
+
return scaleLinear().domain(this.chartState.xDomain).range(xRange)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@computed private get sidebarWidth(): number {
|
|
300
|
+
return this.showNoDataSection
|
|
301
|
+
? R.clamp(this.bounds.width * 0.125, { min: 60, max: 140 })
|
|
302
|
+
: 0
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@computed private get formattedStartTime(): string {
|
|
306
|
+
return this.formatColumn.formatTime(this.chartState.xDomain[0])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@computed private get formattedEndTime(): string {
|
|
310
|
+
return this.formatColumn.formatTime(this.chartState.xDomain[1])
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@computed private get xMarkFontSize(): number {
|
|
314
|
+
return this.yAxis.tickFontSize
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@computed private get xStartMarkWidth(): number {
|
|
318
|
+
return Bounds.forText(this.formattedStartTime, {
|
|
319
|
+
fontSize: this.xMarkFontSize,
|
|
320
|
+
}).width
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@computed private get xEndMarkWidth(): number {
|
|
324
|
+
return Bounds.forText(this.formattedEndTime, {
|
|
325
|
+
fontSize: this.xMarkFontSize,
|
|
326
|
+
}).width
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@computed get externalLegend(): HorizontalColorLegendManager | undefined {
|
|
330
|
+
if (!this.manager.showLegend) {
|
|
331
|
+
const categoricalLegendData = this.series.map(
|
|
332
|
+
(series, index) =>
|
|
333
|
+
new CategoricalBin({
|
|
334
|
+
index,
|
|
335
|
+
value: series.seriesName,
|
|
336
|
+
label: series.displayName,
|
|
337
|
+
color: series.color,
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
return {
|
|
341
|
+
categoricalLegendData,
|
|
342
|
+
categoricalLegendStyleConfig: CATEGORICAL_LEGEND_STYLE,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@computed private get maxLineLegendWidth(): number {
|
|
349
|
+
return 0.25 * this.innerBounds.width
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@computed private get lineLegendFontSize(): number {
|
|
353
|
+
return LineLegend.fontSize({ fontSize: this.fontSize })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@computed private get lineLegendYRange(): [number, number] {
|
|
357
|
+
const top = this.bounds.top
|
|
358
|
+
|
|
359
|
+
const bottom =
|
|
360
|
+
this.bounds.bottom -
|
|
361
|
+
// leave space for the x-axis labels
|
|
362
|
+
this.bottomPadding +
|
|
363
|
+
// but allow for a little extra space
|
|
364
|
+
this.lineLegendFontSize / 2
|
|
365
|
+
|
|
366
|
+
return [top, bottom]
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@computed private get lineLegendPropsCommon(): Partial<LineLegendProps> {
|
|
370
|
+
return {
|
|
371
|
+
yAxis: this.yAxis,
|
|
372
|
+
maxWidth: this.maxLineLegendWidth,
|
|
373
|
+
fontSize: this.fontSize,
|
|
374
|
+
isStatic: this.manager.isStatic,
|
|
375
|
+
yRange: this.lineLegendYRange,
|
|
376
|
+
verticalAlign: VerticalAlign.top,
|
|
377
|
+
showTextOutlines: true,
|
|
378
|
+
textOutlineColor: this.backgroundColor,
|
|
379
|
+
onMouseOver: this.onLineLegendMouseOver,
|
|
380
|
+
onMouseLeave: this.onLineLegendMouseLeave,
|
|
381
|
+
onClick: this.canToggleFocusMode
|
|
382
|
+
? this.onLineLegendClick
|
|
383
|
+
: undefined,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@computed private get lineLegendPropsRight(): Partial<LineLegendProps> {
|
|
388
|
+
return { xAnchor: "start" }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@computed private get lineLegendPropsLeft(): Partial<LineLegendProps> {
|
|
392
|
+
return {
|
|
393
|
+
xAnchor: "end",
|
|
394
|
+
seriesNamesSortedByImportance:
|
|
395
|
+
this.seriesSortedByImportanceForLineLegendLeft,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private formatValue(value: number): string {
|
|
400
|
+
return this.formatColumn.formatValueShortWithAbbreviations(value)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@computed private get lineLegendMaxLevelLeft(): number {
|
|
404
|
+
if (!this.manager.showLegend) return 0
|
|
405
|
+
|
|
406
|
+
// can't use `lineLegendSeriesLeft` due to a circular dependency
|
|
407
|
+
const series = this.series.map((series) =>
|
|
408
|
+
this.constructSingleLineLegendSeries(
|
|
409
|
+
series,
|
|
410
|
+
(series) => series.start.value,
|
|
411
|
+
{ showSeriesName: false }
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return LineLegend.maxLevel({
|
|
416
|
+
series,
|
|
417
|
+
...this.lineLegendPropsCommon,
|
|
418
|
+
seriesNamesSortedByImportance:
|
|
419
|
+
this.seriesSortedByImportanceForLineLegendLeft,
|
|
420
|
+
// not including `lineLegendPropsLeft` due to a circular dependency
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@computed private get lineLegendWidthLeft(): number {
|
|
425
|
+
const props: LineLegendProps = {
|
|
426
|
+
series: this.lineLegendSeriesLeft,
|
|
427
|
+
...this.lineLegendPropsCommon,
|
|
428
|
+
...this.lineLegendPropsLeft,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// We usually use the "stable" width of the line legend, which might be
|
|
432
|
+
// a bit too wide because the connector line width is always added, even
|
|
433
|
+
// it no connector lines are drawn. Using the stable width prevents
|
|
434
|
+
// layout shifts when the connector lines are toggled on and off.
|
|
435
|
+
// However, if the chart area is very narrow (like when it's faceted),
|
|
436
|
+
// the stable width of the line legend takes too much space, so we use the
|
|
437
|
+
// actual width instead.
|
|
438
|
+
return this.isNarrow
|
|
439
|
+
? LineLegend.width(props)
|
|
440
|
+
: LineLegend.stableWidth(props)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@computed private get lineLegendRight(): LineLegend {
|
|
444
|
+
return new LineLegend({
|
|
445
|
+
series: this.lineLegendSeriesRight,
|
|
446
|
+
...this.lineLegendPropsCommon,
|
|
447
|
+
...this.lineLegendPropsRight,
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@computed private get lineLegendWidthRight(): number {
|
|
452
|
+
// We usually use the "stable" width of the line legend, which might be
|
|
453
|
+
// a bit too wide because the connector line width is always added, even
|
|
454
|
+
// it no connector lines are drawn. Using the stable width prevents
|
|
455
|
+
// layout shifts when the connector lines are toggled on and off.
|
|
456
|
+
// However, if the chart area is very narrow (like when it's faceted),
|
|
457
|
+
// the stable width of the line legend takes too much space, so we use the
|
|
458
|
+
// actual width instead.
|
|
459
|
+
return this.isNarrow
|
|
460
|
+
? this.lineLegendRight.width
|
|
461
|
+
: this.lineLegendRight.stableWidth
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@computed private get visibleLineLegendLabelsRight(): Set<SeriesName> {
|
|
465
|
+
return new Set(this.lineLegendRight?.visibleSeriesNames ?? [])
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
@computed
|
|
469
|
+
private get seriesSortedByImportanceForLineLegendLeft(): SeriesName[] {
|
|
470
|
+
return this.series
|
|
471
|
+
.map((s) => s.seriesName)
|
|
472
|
+
.sort((s1: SeriesName, s2: SeriesName): number => {
|
|
473
|
+
const PREFER_S1 = -1
|
|
474
|
+
const PREFER_S2 = 1
|
|
475
|
+
|
|
476
|
+
const s1_isLabelled = this.visibleLineLegendLabelsRight.has(s1)
|
|
477
|
+
const s2_isLabelled = this.visibleLineLegendLabelsRight.has(s2)
|
|
478
|
+
|
|
479
|
+
// prefer to show value labels for series that are already labelled
|
|
480
|
+
if (s1_isLabelled && !s2_isLabelled) return PREFER_S1
|
|
481
|
+
if (s2_isLabelled && !s1_isLabelled) return PREFER_S2
|
|
482
|
+
|
|
483
|
+
return 0
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@computed private get xRange(): [number, number] {
|
|
488
|
+
const lineLegendWidthLeft =
|
|
489
|
+
this.lineLegendWidthLeft + LINE_LEGEND_PADDING
|
|
490
|
+
const lineLegendWidthRight =
|
|
491
|
+
this.lineLegendWidthRight + LINE_LEGEND_PADDING
|
|
492
|
+
const chartAreaWidth = this.innerBounds.width
|
|
493
|
+
|
|
494
|
+
// start and end value when the slopes are as wide as possible
|
|
495
|
+
const minStartX =
|
|
496
|
+
this.innerBounds.x + this.yAxisWidth + lineLegendWidthLeft
|
|
497
|
+
const maxEndX = this.innerBounds.right - lineLegendWidthRight
|
|
498
|
+
|
|
499
|
+
// use all available space if the chart is narrow
|
|
500
|
+
if (this.manager.isNarrow || this.isNarrow) {
|
|
501
|
+
return [minStartX, maxEndX]
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const offset = 0.25
|
|
505
|
+
let startX = this.innerBounds.x + offset * chartAreaWidth
|
|
506
|
+
let endX = this.innerBounds.right - offset * chartAreaWidth
|
|
507
|
+
|
|
508
|
+
// make sure the start and end values are within the bounds
|
|
509
|
+
startX = Math.max(startX, minStartX)
|
|
510
|
+
endX = Math.min(endX, maxEndX)
|
|
511
|
+
|
|
512
|
+
// pick a reasonable max width based on an ideal aspect ratio
|
|
513
|
+
const idealAspectRatio = 0.9
|
|
514
|
+
const availableWidth =
|
|
515
|
+
chartAreaWidth -
|
|
516
|
+
this.yAxisWidth -
|
|
517
|
+
lineLegendWidthLeft -
|
|
518
|
+
lineLegendWidthRight
|
|
519
|
+
const idealWidth = idealAspectRatio * this.bounds.height
|
|
520
|
+
const maxSlopeWidth = Math.min(idealWidth, availableWidth)
|
|
521
|
+
|
|
522
|
+
const currentSlopeWidth = endX - startX
|
|
523
|
+
if (currentSlopeWidth > maxSlopeWidth) {
|
|
524
|
+
const padding = currentSlopeWidth - maxSlopeWidth
|
|
525
|
+
startX += padding / 2
|
|
526
|
+
endX -= padding / 2
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return [startX, endX]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@computed private get isNarrow(): boolean {
|
|
533
|
+
return this.bounds.width < 320
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@computed private get useCompactLayout(): boolean {
|
|
537
|
+
return !!this.manager.isSemiNarrow || this.isNarrow
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
@computed private get hoveredSeriesNames(): SeriesName[] {
|
|
541
|
+
const hoveredSeriesNames: SeriesName[] = []
|
|
542
|
+
|
|
543
|
+
// hovered series name (either by hovering over a slope or a line legend label)
|
|
544
|
+
if (this.hoveredSeriesName)
|
|
545
|
+
hoveredSeriesNames.push(this.hoveredSeriesName)
|
|
546
|
+
|
|
547
|
+
// hovered legend item in the external facet legend
|
|
548
|
+
if (this.manager.externalLegendHoverBin) {
|
|
549
|
+
hoveredSeriesNames.push(
|
|
550
|
+
...this.series
|
|
551
|
+
.map((s) => s.seriesName)
|
|
552
|
+
.filter((name) =>
|
|
553
|
+
this.manager.externalLegendHoverBin?.contains(name)
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return hoveredSeriesNames
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private constructSingleLineLegendSeries(
|
|
562
|
+
series: SlopeChartSeries,
|
|
563
|
+
getValue: (series: SlopeChartSeries) => number,
|
|
564
|
+
{
|
|
565
|
+
showSeriesName,
|
|
566
|
+
showAnnotation,
|
|
567
|
+
}: {
|
|
568
|
+
showSeriesName?: boolean
|
|
569
|
+
showAnnotation?: boolean
|
|
570
|
+
}
|
|
571
|
+
): LineLabelSeries {
|
|
572
|
+
const { seriesName, displayName, color, annotation } = series
|
|
573
|
+
const value = getValue(series)
|
|
574
|
+
const formattedValue = this.formatValue(value)
|
|
575
|
+
return {
|
|
576
|
+
color,
|
|
577
|
+
seriesName,
|
|
578
|
+
annotation: showAnnotation ? annotation : undefined,
|
|
579
|
+
label: showSeriesName ? displayName : formattedValue,
|
|
580
|
+
formattedValue: showSeriesName ? formattedValue : undefined,
|
|
581
|
+
placeFormattedValueInNewLine: this.useCompactLayout,
|
|
582
|
+
yValue: value,
|
|
583
|
+
focus: series.focus,
|
|
584
|
+
hover: this.hoverStateForSeries(series),
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@computed private get lineLegendSeriesLeft(): LineLabelSeries[] {
|
|
589
|
+
const { showSeriesNamesInLineLegendLeft: showSeriesName } = this
|
|
590
|
+
return this.series.map((series) =>
|
|
591
|
+
this.constructSingleLineLegendSeries(
|
|
592
|
+
series,
|
|
593
|
+
(series) => series.start.value,
|
|
594
|
+
{ showSeriesName }
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@computed private get lineLegendSeriesRight(): LineLabelSeries[] {
|
|
600
|
+
return this.series.map((series) =>
|
|
601
|
+
this.constructSingleLineLegendSeries(
|
|
602
|
+
series,
|
|
603
|
+
(series) => series.end.value,
|
|
604
|
+
{
|
|
605
|
+
showSeriesName: this.manager.showLegend,
|
|
606
|
+
showAnnotation: !this.useCompactLayout,
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private animSelection?: Selection<
|
|
613
|
+
BaseType,
|
|
614
|
+
unknown,
|
|
615
|
+
SVGGElement | null,
|
|
616
|
+
unknown
|
|
617
|
+
>
|
|
618
|
+
private playIntroAnimation() {
|
|
619
|
+
// Nice little intro animation
|
|
620
|
+
this.animSelection = select(this.slopeAreaRef.current)
|
|
621
|
+
.selectAll(".slope")
|
|
622
|
+
.attr("stroke-dasharray", "100%")
|
|
623
|
+
.attr("stroke-dashoffset", "100%")
|
|
624
|
+
|
|
625
|
+
this.animSelection
|
|
626
|
+
.transition()
|
|
627
|
+
.duration(600)
|
|
628
|
+
.attr("stroke-dashoffset", "0%")
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
override componentDidMount() {
|
|
632
|
+
exposeInstanceOnWindow(this)
|
|
633
|
+
|
|
634
|
+
if (!this.manager.disableIntroAnimation) {
|
|
635
|
+
this.playIntroAnimation()
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
override componentWillUnmount(): void {
|
|
640
|
+
if (this.animSelection) this.animSelection.interrupt()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
@computed private get showSeriesNamesInLineLegendLeft(): boolean {
|
|
644
|
+
return this.lineLegendMaxLevelLeft >= 4 && !!this.manager.showLegend
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private updateTooltipPosition(event: SVGMouseOrTouchEvent): void {
|
|
648
|
+
const ref = this.manager.base?.current
|
|
649
|
+
if (ref) this.tooltipState.position = getRelativeMouse(ref, event)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private detectHoveredSlope(event: SVGMouseOrTouchEvent): void {
|
|
653
|
+
const ref = this.slopeAreaRef.current
|
|
654
|
+
if (!ref) return
|
|
655
|
+
|
|
656
|
+
const mouse = getRelativeMouse(ref, event)
|
|
657
|
+
this.mouseFrame = requestAnimationFrame(() => {
|
|
658
|
+
if (this.placedSeries.length === 0) return
|
|
659
|
+
|
|
660
|
+
const distanceMap = new Map<PlacedSlopeChartSeries, number>()
|
|
661
|
+
for (const series of this.placedSeries) {
|
|
662
|
+
distanceMap.set(
|
|
663
|
+
series,
|
|
664
|
+
PointVector.distanceFromPointToLineSegmentSq(
|
|
665
|
+
mouse,
|
|
666
|
+
series.startPoint,
|
|
667
|
+
series.endPoint
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const closestSlope = _.minBy(this.placedSeries, (s) =>
|
|
673
|
+
distanceMap.get(s)
|
|
674
|
+
)!
|
|
675
|
+
const distanceSq = distanceMap.get(closestSlope)!
|
|
676
|
+
const tolerance = 10
|
|
677
|
+
const toleranceSq = tolerance * tolerance
|
|
678
|
+
|
|
679
|
+
if (closestSlope && distanceSq < toleranceSq) {
|
|
680
|
+
this.onSlopeMouseOver(closestSlope)
|
|
681
|
+
} else {
|
|
682
|
+
this.onSlopeMouseLeave()
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private hoverTimer?: number
|
|
688
|
+
@action.bound onLineLegendMouseOver(seriesName: SeriesName): void {
|
|
689
|
+
clearTimeout(this.hoverTimer)
|
|
690
|
+
this.hoveredSeriesName = seriesName
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@action.bound private clearHoveredSeries(): void {
|
|
694
|
+
this.hoveredSeriesName = undefined
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@action.bound onLineLegendMouseLeave(): void {
|
|
698
|
+
clearTimeout(this.hoverTimer)
|
|
699
|
+
|
|
700
|
+
// Wait before clearing selection in case the mouse is moving
|
|
701
|
+
// quickly over neighboring labels
|
|
702
|
+
this.hoverTimer = window.setTimeout(() => {
|
|
703
|
+
this.clearHoveredSeries()
|
|
704
|
+
}, 200)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
@action.bound onLineLegendClick(seriesName: SeriesName): void {
|
|
708
|
+
this.focusArray.toggle(seriesName)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
@action.bound onSlopeMouseOver(series: SlopeChartSeries): void {
|
|
712
|
+
this.hoveredSeriesName = series.seriesName
|
|
713
|
+
this.tooltipState.target = { series }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
@action.bound onSlopeMouseLeave(): void {
|
|
717
|
+
this.clearHoveredSeries()
|
|
718
|
+
this.tooltipState.target = null
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
mouseFrame?: number
|
|
722
|
+
@action.bound onMouseMove(event: SVGMouseOrTouchEvent): void {
|
|
723
|
+
this.updateTooltipPosition(event)
|
|
724
|
+
this.detectHoveredSlope(event)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
@action.bound onMouseLeave(): void {
|
|
728
|
+
if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame)
|
|
729
|
+
|
|
730
|
+
this.onSlopeMouseLeave()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
@computed private get renderUid(): number {
|
|
734
|
+
return guid()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
@computed private get tooltip(): React.ReactElement | undefined {
|
|
738
|
+
const {
|
|
739
|
+
manager: { isRelativeMode },
|
|
740
|
+
tooltipState: { target, position, fading },
|
|
741
|
+
formatColumn,
|
|
742
|
+
startTime,
|
|
743
|
+
endTime,
|
|
744
|
+
} = this
|
|
745
|
+
|
|
746
|
+
const { series } = target || {}
|
|
747
|
+
if (!series) return
|
|
748
|
+
|
|
749
|
+
const formatTime = (time: Time) => formatColumn.formatTime(time)
|
|
750
|
+
|
|
751
|
+
const title = series.displayName
|
|
752
|
+
const titleAnnotation = series.annotation
|
|
753
|
+
|
|
754
|
+
const actualStartTime = series.start.originalTime
|
|
755
|
+
const actualEndTime = series.end.originalTime
|
|
756
|
+
const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}`
|
|
757
|
+
const timeLabel = isRelativeMode
|
|
758
|
+
? `% change between ${formatColumn.formatTime(actualStartTime)} and ${formatColumn.formatTime(actualEndTime)}`
|
|
759
|
+
: timeRange
|
|
760
|
+
|
|
761
|
+
const constructTargetYearForToleranceNotice = () => {
|
|
762
|
+
const isStartValueOriginal = series.start.originalTime === startTime
|
|
763
|
+
const isEndValueOriginal = series.end.originalTime === endTime
|
|
764
|
+
|
|
765
|
+
if (!isStartValueOriginal && !isEndValueOriginal) {
|
|
766
|
+
return `${formatTime(startTime)} and ${formatTime(endTime)}`
|
|
767
|
+
} else if (!isStartValueOriginal) {
|
|
768
|
+
return formatTime(startTime)
|
|
769
|
+
} else if (!isEndValueOriginal) {
|
|
770
|
+
return formatTime(endTime)
|
|
771
|
+
} else {
|
|
772
|
+
return undefined
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const targetYear = constructTargetYearForToleranceNotice()
|
|
777
|
+
const toleranceNotice = targetYear
|
|
778
|
+
? {
|
|
779
|
+
icon: TooltipFooterIcon.Notice,
|
|
780
|
+
text: makeTooltipToleranceNotice(targetYear),
|
|
781
|
+
}
|
|
782
|
+
: undefined
|
|
783
|
+
const roundingNotice = series.column.roundsToSignificantFigures
|
|
784
|
+
? {
|
|
785
|
+
icon: TooltipFooterIcon.None,
|
|
786
|
+
text: makeTooltipRoundingNotice(
|
|
787
|
+
[series.column.numSignificantFigures],
|
|
788
|
+
{ plural: !isRelativeMode }
|
|
789
|
+
),
|
|
790
|
+
}
|
|
791
|
+
: undefined
|
|
792
|
+
const footer = excludeUndefined([toleranceNotice, roundingNotice])
|
|
793
|
+
|
|
794
|
+
const values = isRelativeMode
|
|
795
|
+
? [series.end.value]
|
|
796
|
+
: [series.start.value, series.end.value]
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<Tooltip
|
|
800
|
+
id={this.renderUid}
|
|
801
|
+
tooltipManager={this.props.chartState.manager}
|
|
802
|
+
x={position.x}
|
|
803
|
+
y={position.y}
|
|
804
|
+
offsetX={20}
|
|
805
|
+
offsetY={-16}
|
|
806
|
+
style={{ maxWidth: "250px" }}
|
|
807
|
+
title={title}
|
|
808
|
+
titleAnnotation={titleAnnotation}
|
|
809
|
+
subtitle={timeLabel}
|
|
810
|
+
subtitleFormat={targetYear ? "notice" : undefined}
|
|
811
|
+
dissolve={fading}
|
|
812
|
+
footer={footer}
|
|
813
|
+
dismiss={() => (this.tooltipState.target = null)}
|
|
814
|
+
>
|
|
815
|
+
<TooltipValueRange
|
|
816
|
+
label={series.column.displayName}
|
|
817
|
+
unit={series.column.displayUnit}
|
|
818
|
+
values={formatTooltipRangeValues(values, series.column)}
|
|
819
|
+
trend={calculateTrendDirection(...values)}
|
|
820
|
+
isRoundedToSignificantFigures={
|
|
821
|
+
series.column.roundsToSignificantFigures
|
|
822
|
+
}
|
|
823
|
+
labelVariant="unit-only"
|
|
824
|
+
/>
|
|
825
|
+
</Tooltip>
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private makeMissingDataLabel(series: RawSlopeChartSeries): string {
|
|
830
|
+
const { displayName, start, end } = series
|
|
831
|
+
|
|
832
|
+
const startTime = this.formatColumn.formatTime(this.startTime)
|
|
833
|
+
const endTime = this.formatColumn.formatTime(this.endTime)
|
|
834
|
+
|
|
835
|
+
// mention the start or end value if they're missing
|
|
836
|
+
if (start?.value === undefined && end?.value === undefined) {
|
|
837
|
+
return `${displayName} (${startTime} & ${endTime})`
|
|
838
|
+
} else if (start?.value === undefined) {
|
|
839
|
+
return `${displayName} (${startTime})`
|
|
840
|
+
} else if (end?.value === undefined) {
|
|
841
|
+
return `${displayName} (${endTime})`
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// if both values are given but the series shows up in the No Data
|
|
845
|
+
// section, then tolerance has been applied to one of the values
|
|
846
|
+
// in such a way that we decided not to render the slope after all
|
|
847
|
+
// (e.g. when the original times are too close to each other)
|
|
848
|
+
const isToleranceAppliedToStartValue =
|
|
849
|
+
start.originalTime !== this.startTime
|
|
850
|
+
const isToleranceAppliedToEndValue = end.originalTime !== this.endTime
|
|
851
|
+
if (isToleranceAppliedToStartValue && isToleranceAppliedToEndValue) {
|
|
852
|
+
return `${displayName} (${startTime} & ${endTime})`
|
|
853
|
+
} else if (isToleranceAppliedToStartValue) {
|
|
854
|
+
return `${displayName} (${startTime})`
|
|
855
|
+
} else if (isToleranceAppliedToEndValue) {
|
|
856
|
+
return `${displayName} (${endTime})`
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return displayName
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private renderNoDataSection(): React.ReactElement | undefined {
|
|
863
|
+
if (!this.showNoDataSection) return
|
|
864
|
+
|
|
865
|
+
const bounds = new Bounds(
|
|
866
|
+
this.innerBounds.right + this.sidebarMargin,
|
|
867
|
+
this.bounds.top,
|
|
868
|
+
this.sidebarWidth,
|
|
869
|
+
this.bounds.height
|
|
870
|
+
)
|
|
871
|
+
const seriesNames = this.noDataSeries.map((series) =>
|
|
872
|
+
this.makeMissingDataLabel(series)
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
return (
|
|
876
|
+
<NoDataSection
|
|
877
|
+
seriesNames={seriesNames}
|
|
878
|
+
bounds={bounds}
|
|
879
|
+
align={HorizontalAlign.right}
|
|
880
|
+
baseFontSize={this.fontSize}
|
|
881
|
+
/>
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private renderSlopes() {
|
|
886
|
+
return (
|
|
887
|
+
<g id={makeIdForHumanConsumption("slopes")}>
|
|
888
|
+
{this.renderSeries.map((series) => (
|
|
889
|
+
<Slope
|
|
890
|
+
key={series.seriesName}
|
|
891
|
+
series={series}
|
|
892
|
+
strokeWidth={this.lineStrokeWidth}
|
|
893
|
+
outlineWidth={0.5}
|
|
894
|
+
outlineStroke={this.backgroundColor}
|
|
895
|
+
/>
|
|
896
|
+
))}
|
|
897
|
+
</g>
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private renderInteractiveSlopes(): React.ReactElement {
|
|
902
|
+
return (
|
|
903
|
+
<g
|
|
904
|
+
ref={this.slopeAreaRef}
|
|
905
|
+
onMouseMove={this.onMouseMove}
|
|
906
|
+
onTouchMove={this.onMouseMove}
|
|
907
|
+
onTouchStart={this.onMouseMove}
|
|
908
|
+
onMouseLeave={this.onMouseLeave}
|
|
909
|
+
>
|
|
910
|
+
<rect
|
|
911
|
+
x={this.startX}
|
|
912
|
+
y={this.bounds.y}
|
|
913
|
+
width={this.endX - this.startX}
|
|
914
|
+
height={this.bounds.height}
|
|
915
|
+
fillOpacity={0}
|
|
916
|
+
/>
|
|
917
|
+
{this.renderSlopes()}
|
|
918
|
+
</g>
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private renderZeroLine(): React.ReactElement | null {
|
|
923
|
+
// Don't draw a zero line if all start values are zero,
|
|
924
|
+
// which is trivially true in relative mode
|
|
925
|
+
if (this.chartState.isRelativeMode) return null
|
|
926
|
+
|
|
927
|
+
// Don't draw a zero line if all start values are zero
|
|
928
|
+
const areAllStartValuesZero = !this.chartState.series.some(
|
|
929
|
+
(series) => series.start.value !== 0
|
|
930
|
+
)
|
|
931
|
+
if (areAllStartValuesZero) return null
|
|
932
|
+
|
|
933
|
+
// Don't show a zero line if it's not in the domain
|
|
934
|
+
const isZeroInDomain =
|
|
935
|
+
this.yAxis.domain[0] <= 0 && this.yAxis.domain[1] >= 0
|
|
936
|
+
if (!isZeroInDomain) return null
|
|
937
|
+
|
|
938
|
+
const fontSize = GRAPHER_FONT_SCALE_12 * this.fontSize
|
|
939
|
+
const tickLabelOffset = 5
|
|
940
|
+
|
|
941
|
+
const tickLabel = this.yAxis.formatTick(0)
|
|
942
|
+
const tickLabelLength = Bounds.forText(tickLabel, { fontSize }).width
|
|
943
|
+
const bounds = this.innerBounds.padLeft(
|
|
944
|
+
tickLabelLength + tickLabelOffset
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
return (
|
|
948
|
+
<>
|
|
949
|
+
{!this.yAxis.hideGridlines && (
|
|
950
|
+
<VerticalAxisZeroLine
|
|
951
|
+
bounds={bounds}
|
|
952
|
+
verticalAxis={this.yAxis}
|
|
953
|
+
stroke="#ddd"
|
|
954
|
+
strokeDasharray="3,2"
|
|
955
|
+
/>
|
|
956
|
+
)}
|
|
957
|
+
<text
|
|
958
|
+
x={this.innerBounds.left}
|
|
959
|
+
y={this.yAxis.place(0).toFixed(2)}
|
|
960
|
+
dy={dyFromAlign(VerticalAlign.middle)}
|
|
961
|
+
fontSize={fontSize}
|
|
962
|
+
fill={GRAPHER_DARK_TEXT}
|
|
963
|
+
>
|
|
964
|
+
{tickLabel}
|
|
965
|
+
</text>
|
|
966
|
+
</>
|
|
967
|
+
)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private renderLogNotice(): React.ReactElement | null {
|
|
971
|
+
if (!this.chartState.isLogScale) return null
|
|
972
|
+
|
|
973
|
+
const fontSize = GRAPHER_FONT_SCALE_11 * this.fontSize
|
|
974
|
+
|
|
975
|
+
const longText = "plotted on a logarithmic axis"
|
|
976
|
+
const shortText = "log axis"
|
|
977
|
+
|
|
978
|
+
const longTextWidth = Bounds.forText(longText, { fontSize }).width
|
|
979
|
+
const shortTextWidth = Bounds.forText(shortText, { fontSize }).width
|
|
980
|
+
|
|
981
|
+
// Determine how much space is available for the log notice
|
|
982
|
+
const xDist = this.endX - this.startX
|
|
983
|
+
const rightPadding = 0.5 * this.xEndMarkWidth
|
|
984
|
+
const leftPadding = 0.5 * this.xStartMarkWidth
|
|
985
|
+
const maxWidth = xDist - rightPadding - leftPadding - 24
|
|
986
|
+
|
|
987
|
+
// Prefer the long text if it fits. If both texts are too long,
|
|
988
|
+
// don't display a notice
|
|
989
|
+
const renderedText =
|
|
990
|
+
longTextWidth <= maxWidth
|
|
991
|
+
? longText
|
|
992
|
+
: shortTextWidth <= maxWidth
|
|
993
|
+
? shortText
|
|
994
|
+
: null
|
|
995
|
+
|
|
996
|
+
if (!renderedText) return null
|
|
997
|
+
|
|
998
|
+
// Placed in between the start and end x marks
|
|
999
|
+
const midX = (this.startX + this.endX) / 2
|
|
1000
|
+
const y = this.innerBounds.bottom + this.xLabelPadding
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<text
|
|
1004
|
+
x={midX}
|
|
1005
|
+
y={y}
|
|
1006
|
+
fontSize={fontSize}
|
|
1007
|
+
textAnchor="middle"
|
|
1008
|
+
dy={dyFromAlign(VerticalAlign.bottom)}
|
|
1009
|
+
fill={GRAPHER_DARK_TEXT}
|
|
1010
|
+
fontStyle="italic"
|
|
1011
|
+
>
|
|
1012
|
+
{renderedText}
|
|
1013
|
+
</text>
|
|
1014
|
+
)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private renderYAxis(): React.ReactElement | null {
|
|
1018
|
+
return (
|
|
1019
|
+
<>
|
|
1020
|
+
{this.renderZeroLine()}
|
|
1021
|
+
{this.renderLogNotice()}
|
|
1022
|
+
</>
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private renderXAxis() {
|
|
1027
|
+
const { startX, endX } = this
|
|
1028
|
+
|
|
1029
|
+
return (
|
|
1030
|
+
<g id={makeIdForHumanConsumption("horizontal-axis")}>
|
|
1031
|
+
<MarkX
|
|
1032
|
+
label={this.formattedStartTime}
|
|
1033
|
+
x={startX}
|
|
1034
|
+
top={this.innerBounds.top}
|
|
1035
|
+
bottom={this.innerBounds.bottom}
|
|
1036
|
+
labelPadding={this.xLabelPadding}
|
|
1037
|
+
fontSize={this.xMarkFontSize}
|
|
1038
|
+
/>
|
|
1039
|
+
<MarkX
|
|
1040
|
+
label={this.formattedEndTime}
|
|
1041
|
+
x={endX}
|
|
1042
|
+
top={this.innerBounds.top}
|
|
1043
|
+
bottom={this.innerBounds.bottom}
|
|
1044
|
+
labelPadding={this.xLabelPadding}
|
|
1045
|
+
fontSize={this.xMarkFontSize}
|
|
1046
|
+
/>
|
|
1047
|
+
</g>
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private renderLineLegendRight(): React.ReactElement {
|
|
1052
|
+
return (
|
|
1053
|
+
<LineLegend
|
|
1054
|
+
series={this.lineLegendSeriesRight}
|
|
1055
|
+
x={this.xRange[1] + LINE_LEGEND_PADDING}
|
|
1056
|
+
{...this.lineLegendPropsCommon}
|
|
1057
|
+
{...this.lineLegendPropsRight}
|
|
1058
|
+
/>
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private renderLineLegendLeft(): React.ReactElement | null {
|
|
1063
|
+
// don't show labels for the start values in relative mode since they're all trivially zero
|
|
1064
|
+
if (this.manager.isRelativeMode) return null
|
|
1065
|
+
|
|
1066
|
+
const uniqYValues = _.uniq(
|
|
1067
|
+
this.lineLegendSeriesLeft.map((series) => series.yValue)
|
|
1068
|
+
)
|
|
1069
|
+
const allSlopesStartFromZero =
|
|
1070
|
+
uniqYValues.length === 1 && uniqYValues[0] === 0
|
|
1071
|
+
|
|
1072
|
+
// if all values have a start value of 0, show the 0-label only once
|
|
1073
|
+
if (allSlopesStartFromZero)
|
|
1074
|
+
return (
|
|
1075
|
+
<Halo
|
|
1076
|
+
id="x-axis-zero-label"
|
|
1077
|
+
outlineWidth={
|
|
1078
|
+
GRAPHER_TEXT_OUTLINE_FACTOR * this.lineLegendFontSize
|
|
1079
|
+
}
|
|
1080
|
+
outlineColor={this.backgroundColor}
|
|
1081
|
+
>
|
|
1082
|
+
<text
|
|
1083
|
+
x={this.startX}
|
|
1084
|
+
y={this.yAxis.place(0)}
|
|
1085
|
+
textAnchor="end"
|
|
1086
|
+
dx={-LINE_LEGEND_PADDING - 4}
|
|
1087
|
+
dy={dyFromAlign(VerticalAlign.middle)}
|
|
1088
|
+
fontSize={this.lineLegendFontSize}
|
|
1089
|
+
>
|
|
1090
|
+
{this.formatValue(0)}
|
|
1091
|
+
</text>
|
|
1092
|
+
</Halo>
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
return (
|
|
1096
|
+
<LineLegend
|
|
1097
|
+
series={this.lineLegendSeriesLeft}
|
|
1098
|
+
x={this.xRange[0] - LINE_LEGEND_PADDING}
|
|
1099
|
+
{...this.lineLegendPropsCommon}
|
|
1100
|
+
{...this.lineLegendPropsLeft}
|
|
1101
|
+
/>
|
|
1102
|
+
)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private renderLineLegends(): React.ReactElement | undefined {
|
|
1106
|
+
return (
|
|
1107
|
+
<>
|
|
1108
|
+
{this.renderLineLegendLeft()}
|
|
1109
|
+
{this.renderLineLegendRight()}
|
|
1110
|
+
</>
|
|
1111
|
+
)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private renderInteractive(): React.ReactElement {
|
|
1115
|
+
return (
|
|
1116
|
+
<>
|
|
1117
|
+
{this.renderYAxis()}
|
|
1118
|
+
{this.renderXAxis()}
|
|
1119
|
+
{this.renderInteractiveSlopes()}
|
|
1120
|
+
{this.renderLineLegends()}
|
|
1121
|
+
{this.renderNoDataSection()}
|
|
1122
|
+
{this.tooltip}
|
|
1123
|
+
</>
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private renderStatic(): React.ReactElement {
|
|
1128
|
+
return (
|
|
1129
|
+
<>
|
|
1130
|
+
{this.renderYAxis()}
|
|
1131
|
+
{this.renderXAxis()}
|
|
1132
|
+
{this.renderSlopes()}
|
|
1133
|
+
{this.renderLineLegends()}
|
|
1134
|
+
</>
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
override render() {
|
|
1139
|
+
if (this.chartState.errorInfo.reason)
|
|
1140
|
+
return (
|
|
1141
|
+
<NoDataModal
|
|
1142
|
+
manager={this.manager}
|
|
1143
|
+
bounds={this.props.bounds}
|
|
1144
|
+
message={this.chartState.errorInfo.reason}
|
|
1145
|
+
/>
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
return this.manager.isStatic
|
|
1149
|
+
? this.renderStatic()
|
|
1150
|
+
: this.renderInteractive()
|
|
1151
|
+
}
|
|
1152
|
+
}
|