From c4f254591a6bfc79efedf055d5bd8cfe684a8734 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 09:00:25 +0100 Subject: [PATCH 01/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20show=20selected=20e?= =?UTF-8?q?ntities=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorBasicTab.tsx | 2 +- baker/updateChartEntities.ts | 2 +- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/adminSiteClient/EditorBasicTab.tsx b/adminSiteClient/EditorBasicTab.tsx index 04f05a9cead..9e3464f4d9f 100644 --- a/adminSiteClient/EditorBasicTab.tsx +++ b/adminSiteClient/EditorBasicTab.tsx @@ -109,7 +109,7 @@ class DimensionSlotView< const { selection } = grapher const { availableEntityNames, availableEntityNameSet } = selection - if (grapher.isScatter || grapher.isSlopeChart || grapher.isMarimekko) { + if (grapher.isScatter || grapher.isMarimekko) { // chart types that display all entities by default shouldn't select any by default selection.clearSelection() } else if ( diff --git a/baker/updateChartEntities.ts b/baker/updateChartEntities.ts index 49e582a71d8..cbaf6a74107 100644 --- a/baker/updateChartEntities.ts +++ b/baker/updateChartEntities.ts @@ -106,7 +106,7 @@ const obtainAvailableEntitiesForGrapherConfig = async ( // In these chart types, an unselected entity is still shown const chartTypeShowsUnselectedEntities = - grapher.isScatter || grapher.isSlopeChart || grapher.isMarimekko + grapher.isScatter || grapher.isMarimekko if (canChangeEntities || chartTypeShowsUnselectedEntities) return grapher.tableForSelection.availableEntityNames as string[] diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 8b865b803d6..4254f7cee3c 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -100,6 +100,10 @@ export class SlopeChart transformTable(table: OwidTable) { if (!table.has(this.yColumnSlug)) return table + table = table.filterByEntityNames( + this.selectionArray.selectedEntityNames + ) + // TODO: remove this filter once we don't have mixed type columns in datasets table = table.replaceNonNumericCellsWithErrorValues([this.yColumnSlug]) From a681846a9496d430c730dd39cfa300f0bff0586a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 09:34:42 +0100 Subject: [PATCH 02/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20support=20relative?= =?UTF-8?q?=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/controls/SettingsMenu.tsx | 2 ++ .../grapher/src/slopeCharts/SlopeChart.tsx | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index 30cbd370147..fb7f4351ce7 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -51,6 +51,7 @@ const { StackedDiscreteBar, StackedBar, Marimekko, + SlopeChart, } = GRAPHER_CHART_TYPES export interface SettingsMenuManager @@ -170,6 +171,7 @@ export class SettingsMenu extends React.Component<{ ScatterPlot, LineChart, Marimekko, + SlopeChart, ].includes(this.chartType as any) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 4254f7cee3c..e83ffb961bf 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -471,13 +471,27 @@ export class SlopeChart return this.inputTable.get(this.manager.colorColumnSlug) } - @computed get transformedTable() { + @computed get transformedTableFromGrapher(): OwidTable { return ( this.manager.transformedTable ?? this.transformTable(this.inputTable) ) } + @computed get transformedTable(): OwidTable { + let table = this.transformedTableFromGrapher + // The % growth transform cannot be applied in transformTable() because it will filter out + // any rows before startHandleTimeBound and change the timeline bounds. + const { isRelativeMode, startHandleTimeBound } = this.manager + if (isRelativeMode && startHandleTimeBound !== undefined) { + table = table.toTotalGrowthForEachColumnComparedToStartTime( + startHandleTimeBound, + this.yColumnSlug ? [this.yColumnSlug] : [] + ) + } + return table + } + @computed get inputTable() { return this.manager.table } From 018e13220ee9490a4315469bb0647188f32acb74 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 10:22:10 +0100 Subject: [PATCH 03/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20only=20allow=20sele?= =?UTF-8?q?ction=20using=20the=20entity=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 80 +------------------ .../src/slopeCharts/SlopeChartConstants.ts | 2 +- 2 files changed, 2 insertions(+), 80 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index e83ffb961bf..9adc4b2f7bf 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -3,7 +3,6 @@ import { Bounds, DEFAULT_BOUNDS, intersection, - without, uniq, isEmpty, last, @@ -53,11 +52,7 @@ import { SlopeEntryProps, } from "./SlopeChartConstants" import { OwidTable } from "@ourworldindata/core-table" -import { - autoDetectYColumnSlugs, - makeSelectionArray, - isElementInteractive, -} from "../chart/ChartUtils" +import { autoDetectYColumnSlugs, makeSelectionArray } from "../chart/ChartUtils" import { AxisConfig, AxisManager } from "../axis/AxisConfig" import { VerticalAxis } from "../axis/Axis" import { VerticalAxisComponent } from "../axis/AxisViews" @@ -95,8 +90,6 @@ export class SlopeChart // currently hovered legend color @observable hoverColor?: string - private hasInteractedWithChart = false - transformTable(table: OwidTable) { if (!table.has(this.yColumnSlug)) return table @@ -169,15 +162,6 @@ export class SlopeChart this.hoverKey = undefined } - @action.bound onSlopeClick() { - const { hoverKey, isEntitySelectionEnabled } = this - if (!isEntitySelectionEnabled || hoverKey === undefined) { - return - } - this.hasInteractedWithChart = true - this.selectionArray.toggleSelection(hoverKey) - } - // Both legend managers accept a `onLegendMouseOver` property, but define different signatures. // The component expects a string, // the component expects a ColorScaleBin. @@ -206,29 +190,6 @@ export class SlopeChart ) } - // When the color legend is clicked, toggle selection fo all associated keys - @action.bound onLegendClick() { - const { hoverColor, isEntitySelectionEnabled } = this - if (!isEntitySelectionEnabled || hoverColor === undefined) return - - this.hasInteractedWithChart = true - - const seriesNamesToToggle = this.series - .filter((g) => g.color === hoverColor) - .map((g) => g.seriesName) - const areAllSeriesActive = - intersection(seriesNamesToToggle, this.selectedEntityNames) - .length === seriesNamesToToggle.length - if (areAllSeriesActive) - this.selectionArray.setSelectedEntities( - without(this.selectedEntityNames, ...seriesNamesToToggle) - ) - else - this.selectionArray.setSelectedEntities( - this.selectedEntityNames.concat(seriesNamesToToggle) - ) - } - // Colors on the legend for which every matching group is focused @computed get focusColors() { const { colorsInUse } = this @@ -407,7 +368,6 @@ export class SlopeChart hoverKeys={hoverKeys} onMouseOver={this.onSlopeMouseOver} onMouseLeave={this.onSlopeMouseLeave} - onClick={this.onSlopeClick} isPortrait={this.isPortrait} /> {showLegend && legend} @@ -519,48 +479,10 @@ export class SlopeChart return colorByEntity } - // click anywhere inside the Grapher frame to dismiss the current selection - @action.bound onGrapherClick(e: Event): void { - const target = e.target as HTMLElement - const isTargetInteractive = isElementInteractive(target) - if ( - this.isEntitySelectionEnabled && - this.hasInteractedWithChart && - !this.hoverKey && - !this.hoverColor && - !this.manager.isModalOpen && - !isTargetInteractive - ) { - this.selectionArray.clearSelection() - } - } - - @computed private get grapherElement() { - return this.manager.base?.current - } - componentDidMount() { - if (this.grapherElement) { - // listening to "mousedown" instead of "click" fixes a bug - // where the current selection was incorrectly dismissed - // when the user drags the slider but releases the drag outside of the timeline - this.grapherElement.addEventListener( - "mousedown", - this.onGrapherClick - ) - } exposeInstanceOnWindow(this) } - componentWillUnmount(): void { - if (this.grapherElement) { - this.grapherElement.removeEventListener( - "mousedown", - this.onGrapherClick - ) - } - } - @computed get series() { const column = this.yColumn if (!column) return [] diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index bb52f727212..91eda0418be 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -48,7 +48,7 @@ export interface LabelledSlopesProps { hoverKeys: string[] onMouseOver: (slopeProps: SlopeEntryProps) => void onMouseLeave: () => void - onClick: () => void + onClick?: () => void isPortrait: boolean } From 33c00ccb68d2a6c614fd8f650d452412289e3f51 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 14:38:55 +0100 Subject: [PATCH 04/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20drop=20support=20fo?= =?UTF-8?q?r=20color=20dim=20and=20focus=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/core/Grapher.tsx | 2 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 420 ++++-------------- .../src/slopeCharts/SlopeChartConstants.ts | 5 +- 3 files changed, 97 insertions(+), 330 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 99080b5f556..7a2f0de6d8a 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1410,7 +1410,7 @@ export class Grapher if (this.isLineChart || this.isDiscreteBar) return [yAxis, color] else if (this.isScatter) return [yAxis, xAxis, size, color] else if (this.isMarimekko) return [yAxis, xAxis, color] - else if (this.isSlopeChart) return [yAxis, color] + else if (this.isSlopeChart) return [yAxis] return [yAxis] } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 9adc4b2f7bf..3ae9f2c5e67 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -2,10 +2,7 @@ import React from "react" import { Bounds, DEFAULT_BOUNDS, - intersection, - uniq, isEmpty, - last, sortBy, max, getRelativeMouse, @@ -14,7 +11,6 @@ import { exposeInstanceOnWindow, PointVector, clamp, - HorizontalAlign, difference, makeIdForHumanConsumption, } from "@ourworldindata/utils" @@ -22,11 +18,7 @@ import { TextWrap } from "@ourworldindata/components" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" import { NoDataModal } from "../noDataModal/NoDataModal" -import { - VerticalColorLegend, - VerticalColorLegendManager, -} from "../verticalColorLegend/VerticalColorLegend" -import { ColorScale, ColorScaleManager } from "../color/ColorScale" +import { ColorScaleManager } from "../color/ColorScale" import { BASE_FONT_SIZE, GRAPHER_DARK_TEXT, @@ -36,32 +28,34 @@ import { import { ScaleType, EntitySelectionMode, - Color, SeriesName, ColorSchemeName, + SeriesStrategy, + EntityName, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" import { scaleLinear, ScaleLinear } from "d3-scale" import { select } from "d3-selection" import { - DEFAULT_SLOPE_CHART_COLOR, LabelledSlopesProps, SlopeChartSeries, SlopeChartValue, SlopeEntryProps, } from "./SlopeChartConstants" import { OwidTable } from "@ourworldindata/core-table" -import { autoDetectYColumnSlugs, makeSelectionArray } from "../chart/ChartUtils" +import { + autoDetectSeriesStrategy, + autoDetectYColumnSlugs, + makeSelectionArray, +} from "../chart/ChartUtils" import { AxisConfig, AxisManager } from "../axis/AxisConfig" import { VerticalAxis } from "../axis/Axis" import { VerticalAxisComponent } from "../axis/AxisViews" -import { - HorizontalCategoricalColorLegend, - HorizontalColorLegendManager, -} from "../horizontalColorLegend/HorizontalColorLegends" -import { CategoricalBin, ColorScaleBin } from "../color/ColorScaleBin" import { NoDataSection } from "../scatterCharts/NoDataSection" +import { CategoricalColorAssigner } from "../color/CategoricalColorAssigner" +import { ColorScheme } from "../color/ColorScheme" +import { ColorSchemes } from "../color/ColorSchemes" export interface SlopeChartManager extends ChartManager { isModalOpen?: boolean @@ -79,16 +73,10 @@ export class SlopeChart bounds?: Bounds manager: SlopeChartManager }> - implements - ChartInterface, - VerticalColorLegendManager, - HorizontalColorLegendManager, - ColorScaleManager + implements ChartInterface, ColorScaleManager { // currently hovered individual series key @observable hoverKey?: string - // currently hovered legend color - @observable hoverColor?: string transformTable(table: OwidTable) { if (!table.has(this.yColumnSlug)) return table @@ -125,35 +113,6 @@ export class SlopeChart return !!(this.manager.isNarrow || this.manager.isStaticAndSmall) } - @computed private get showHorizontalLegend(): boolean { - return !!(this.manager.isSemiNarrow || this.manager.isStaticAndSmall) - } - - // used by the component - @computed get legendItems() { - return this.colorScale.legendBins - .filter((bin) => this.colorsInUse.includes(bin.color)) - .map((bin) => { - return { - key: bin.label ?? "", - label: bin.label ?? "", - color: bin.color, - } - }) - } - - // used by the component - @computed get categoricalLegendData(): CategoricalBin[] { - return this.legendItems.map( - (legendItem, index) => - new CategoricalBin({ - ...legendItem, - index, - value: legendItem.label, - }) - ) - } - @action.bound onSlopeMouseOver(slopeProps: SlopeEntryProps) { this.hoverKey = slopeProps.seriesName } @@ -162,18 +121,6 @@ export class SlopeChart this.hoverKey = undefined } - // Both legend managers accept a `onLegendMouseOver` property, but define different signatures. - // The component expects a string, - // the component expects a ColorScaleBin. - @action.bound onLegendMouseOver(binOrColor: string | ColorScaleBin) { - this.hoverColor = - typeof binOrColor === "string" ? binOrColor : binOrColor.color - } - - @action.bound onLegendMouseLeave() { - this.hoverColor = undefined - } - @computed private get selectionArray() { return makeSelectionArray(this.manager.selection) } @@ -182,127 +129,19 @@ export class SlopeChart return this.selectionArray.selectedEntityNames } - @computed get isEntitySelectionEnabled(): boolean { - const { manager } = this - return !!( - manager.addCountryMode !== EntitySelectionMode.Disabled && - manager.addCountryMode - ) - } - - // Colors on the legend for which every matching group is focused - @computed get focusColors() { - const { colorsInUse } = this - return colorsInUse.filter((color) => { - const matchingSeriesNames = this.series - .filter((g) => g.color === color) - .map((g) => g.seriesName) - return ( - intersection(matchingSeriesNames, this.selectedEntityNames) - .length === matchingSeriesNames.length - ) - }) - } - - @computed get focusKeys() { - return this.selectedEntityNames - } - - // All currently hovered group keys, combining the legend and the main UI - @computed.struct get hoverKeys() { - const { hoverColor, hoverKey } = this - - const hoverKeys = - hoverColor === undefined - ? [] - : uniq( - this.series - .filter((g) => g.color === hoverColor) - .map((g) => g.seriesName) - ) - - if (hoverKey !== undefined) hoverKeys.push(hoverKey) - - return hoverKeys - } - - // Colors currently on the chart and not greyed out - @computed get activeColors() { - const { hoverKeys, focusKeys } = this - const activeKeys = hoverKeys.concat(focusKeys) - - if (activeKeys.length === 0) - // No hover or focus means they're all active by default - return uniq(this.series.map((g) => g.color)) - - return uniq( - this.series - .filter((g) => activeKeys.indexOf(g.seriesName) !== -1) - .map((g) => g.color) - ) - } - - // Only show colors on legend that are actually in use - @computed private get colorsInUse() { - return uniq(this.series.map((series) => series.color)) - } - - @computed get legendAlign(): HorizontalAlign { - return HorizontalAlign.left - } - - @computed get verticalColorLegend(): VerticalColorLegend { - return new VerticalColorLegend({ manager: this }) - } - - @computed get horizontalColorLegend(): HorizontalCategoricalColorLegend { - return new HorizontalCategoricalColorLegend({ manager: this }) - } - - @computed get legendHeight(): number { - return this.showHorizontalLegend - ? this.horizontalColorLegend.height - : this.verticalColorLegend.height - } - - @computed get legendWidth(): number { - return this.showHorizontalLegend - ? this.bounds.width - : this.verticalColorLegend.width - } - - @computed get maxLegendWidth(): number { - return this.showHorizontalLegend - ? this.bounds.width - : this.bounds.width * 0.5 - } - @computed private get sidebarWidth(): number { - // the min width is set to prevent the "No data" title from line breaking - return clamp(this.legendWidth, 51, this.maxLegendWidth) + return Math.min(120, 0.15 * this.bounds.width) } - // correction is to account for the space taken by the legend @computed private get innerBounds() { - const { sidebarWidth, showLegend, legendHeight } = this + const { sidebarWidth } = this let bounds = this.bounds - if (showLegend) { - bounds = this.showHorizontalLegend - ? bounds.padTop(legendHeight + 8) - : bounds.padRight(sidebarWidth + 16) + if (this.showNoDataSection) { + bounds = bounds.padRight(sidebarWidth + 16) } return bounds } - // verify the validity of data used to show legend - // this is for backwards compatibility with charts that were added without legend - // eg: https://ourworldindata.org/grapher/mortality-rate-improvement-by-cohort - @computed private get showLegend() { - const { colorsInUse } = this - const { legendBins } = this.colorScale - return legendBins.some((bin) => colorsInUse.includes(bin.color)) - } - @computed private get selectedEntitiesWithoutData(): string[] { return difference( @@ -311,12 +150,16 @@ export class SlopeChart ) } + @computed private get showNoDataSection(): boolean { + return this.selectedEntitiesWithoutData.length > 0 + } + @computed private get noDataSection(): React.ReactElement { const bounds = new Bounds( - this.legendX, - this.legendY + this.legendHeight + 12, + this.bounds.right - this.sidebarWidth, + this.bounds.top, this.sidebarWidth, - this.bounds.height - this.legendHeight - 12 + this.bounds.height ) return ( - ) : ( - - ) + const { series, hoverKey, innerBounds } = this return ( - {showLegend && legend} - {/* only show the "No data" section if there is space */} - {showLegend && - !showHorizontalLegend && - selectedEntitiesWithoutData.length > 0 && - this.noDataSection} + {this.showNoDataSection && this.noDataSection} ) } - @computed get categoryLegendY(): number { - return this.bounds.top - } - - @computed get legendY() { - return this.bounds.top - } - - @computed get legendX(): number { - return this.showHorizontalLegend - ? this.bounds.left - : this.bounds.right - this.sidebarWidth - } - @computed get failMessage() { if (this.yColumn.isMissing) return "Missing Y column" else if (isEmpty(this.series)) return "No matching data" return "" } - colorScale = this.props.manager.colorScaleOverride ?? new ColorScale(this) - - @computed get colorScaleConfig() { - return this.manager.colorScale - } - - @computed get colorScaleColumn() { - return ( - // For faceted charts, we have to get the values of inputTable before it's filtered by - // the faceting logic. - this.manager.colorScaleColumnOverride ?? this.colorColumn - ) - } - - defaultBaseColorScheme = ColorSchemeName.continents + defaultBaseColorScheme = ColorSchemeName.OwidDistinctLines @computed private get yColumn() { return this.transformedTable.get(this.yColumnSlug) @@ -424,13 +219,6 @@ export class SlopeChart return autoDetectYColumnSlugs(this.manager)[0] } - @computed private get colorColumn() { - // NB: This is tricky. Often it seems we use the Continent variable (123) for colors, but we only have 1 year for that variable, which - // would likely get filtered by any time filtering. So we need to jump up to the root table to get the color values we want. - // We should probably refactor this as part of a bigger color refactoring. - return this.inputTable.get(this.manager.colorColumnSlug) - } - @computed get transformedTableFromGrapher(): OwidTable { return ( this.manager.transformedTable ?? @@ -456,48 +244,66 @@ export class SlopeChart return this.manager.table } - // helper method to directly get the associated color value given an Entity - // dimension data saves color a level deeper. eg: { Afghanistan => { 2015: Asia|Color }} - // this returns that data in the form { Afghanistan => Asia } - @computed private get colorBySeriesName(): Map< - SeriesName, - Color | undefined - > { - const { colorScale, colorColumn } = this - if (colorColumn.isMissing) return new Map() - - const colorByEntity = new Map() + componentDidMount() { + exposeInstanceOnWindow(this) + } - colorColumn.valueByEntityNameAndOriginalTime.forEach( - (timeToColorMap, seriesName) => { - const values = Array.from(timeToColorMap.values()) - const key = last(values) - colorByEntity.set(seriesName, colorScale.getColor(key)) - } + @computed private get colorScheme(): ColorScheme { + return ( + (this.manager.baseColorScheme + ? ColorSchemes.get(this.manager.baseColorScheme) + : null) ?? ColorSchemes.get(this.defaultBaseColorScheme) ) + } - return colorByEntity + @computed get seriesStrategy(): SeriesStrategy { + return autoDetectSeriesStrategy(this.manager, true) } - componentDidMount() { - exposeInstanceOnWindow(this) + @computed private get categoricalColorAssigner(): CategoricalColorAssigner { + return new CategoricalColorAssigner({ + colorScheme: this.colorScheme, + invertColorScheme: this.manager.invertColorScheme, + colorMap: + this.seriesStrategy === SeriesStrategy.entity + ? this.inputTable.entityNameColorIndex + : this.inputTable.columnDisplayNameToColorMap, + autoColorMapCache: this.manager.seriesColorMap, + }) + } + + private getColorKey( + entityName: EntityName, + columnName: string, + entityCount: number + ): SeriesName { + if (this.seriesStrategy === SeriesStrategy.entity) { + return entityName + } + // If only one entity is plotted, we want to use the column colors. + // Unlike in `getSeriesName`, we don't care whether the user can select + // multiple entities, only whether more than one is plotted. + if (entityCount > 1) { + return `${entityName} - ${columnName}` + } else { + return columnName + } } @computed get series() { const column = this.yColumn if (!column) return [] - const { colorBySeriesName } = this const { minTime, maxTime } = column - const table = this.inputTable - + const totalEntityCount = + this.transformedTable.availableEntityNames.length return column.uniqEntityNames - .map((seriesName) => { + .map((entityName) => { const values: SlopeChartValue[] = [] const yValues = - column.valueByEntityNameAndOriginalTime.get(seriesName)! || + column.valueByEntityNameAndOriginalTime.get(entityName)! || [] yValues.forEach((value, time) => { @@ -512,13 +318,16 @@ export class SlopeChart // sort values by time const sortedValues = sortBy(values, (v) => v.x) - const color = - table.getColorForEntityName(seriesName) ?? - colorBySeriesName.get(seriesName) ?? - DEFAULT_SLOPE_CHART_COLOR + const color = this.categoricalColorAssigner.assign( + this.getColorKey( + entityName, + column.displayName, + totalEntityCount + ) + ) return { - seriesName, + seriesName: entityName, color, values: sortedValues, } as SlopeChartSeries @@ -532,11 +341,11 @@ class SlopeEntry extends React.Component { line: SVGElement | null = null @computed get isInBackground() { - const { isLayerMode, isHovered, isFocused } = this.props + const { isLayerMode, isHovered } = this.props if (!isLayerMode) return false - return !(isHovered || isFocused) + return !isHovered } render() { @@ -554,22 +363,19 @@ class SlopeEntry extends React.Component { rightEntityLabel, leftEntityLabelBounds, rightEntityLabelBounds, - isFocused, isHovered, - isMultiHoverMode, seriesName, } = this.props const { isInBackground } = this const lineColor = isInBackground ? "#e2e2e2" : color const labelColor = isInBackground ? "#ccc" : GRAPHER_DARK_TEXT - const opacity = isHovered ? 1 : isFocused ? 0.7 : 0.5 - const lineStrokeWidth = - isHovered && !isMultiHoverMode ? 4 : isFocused ? 3 : 2 + const opacity = isHovered ? 1 : 0.5 + const lineStrokeWidth = isHovered ? 4 : 2 - const showDots = isFocused || isHovered - const showValueLabels = isFocused || isHovered - const showLeftEntityLabel = isFocused || (isHovered && isMultiHoverMode) + const showDots = isHovered + const showValueLabels = isHovered + const showLeftEntityLabel = isHovered const sharedLabelProps = { fill: labelColor, @@ -656,8 +462,7 @@ class SlopeEntry extends React.Component { { textProps: { ...sharedLabelProps, - fontWeight: - isFocused || isHovered ? "bold" : undefined, + fontWeight: isHovered ? "bold" : undefined, }, } )} @@ -693,35 +498,14 @@ class LabelledSlopes return this.manager.fontSize ?? BASE_FONT_SIZE } - @computed private get focusedSeriesNames() { - return intersection( - this.props.focusKeys || [], - this.data.map((g) => g.seriesName) - ) - } - - @computed private get hoveredSeriesNames() { - return intersection( - this.props.hoverKeys || [], - this.data.map((g) => g.seriesName) - ) + @computed private get hoveredSeriesName() { + return this.props.hoverKey } // Layered mode occurs when any entity on the chart is hovered or focused // Then, a special "foreground" set of entities is rendered over the background @computed private get isLayerMode() { - return ( - this.hoveredSeriesNames.length > 0 || - this.focusedSeriesNames.length > 0 || - // if the user has selected entities that are not in the chart, - // we want to move all entities into the background - (this.props.focusKeys?.length > 0 && - this.focusedSeriesNames.length === 0) - ) - } - - @computed private get isMultiHoverMode() { - return this.hoveredSeriesNames.length > 1 + return this.hoveredSeriesName !== undefined } @computed get isPortrait(): boolean { @@ -917,7 +701,6 @@ class LabelledSlopes y2, color: series.color, seriesName: series.seriesName, - isFocused: false, isHovered: false, hasLeftLabel: true, hasRightLabel: true, @@ -928,20 +711,16 @@ class LabelledSlopes } @computed get backgroundGroups() { - return this.slopeData.filter( - (group) => !(group.isHovered || group.isFocused) - ) + return this.slopeData.filter((group) => !group.isHovered) } @computed get foregroundGroups() { - return this.slopeData.filter( - (group) => !!(group.isHovered || group.isFocused) - ) + return this.slopeData.filter((group) => !!group.isHovered) } // Get the final slope data with hover focusing and collision detection @computed get slopeData(): SlopeEntryProps[] { - const { focusedSeriesNames, hoveredSeriesNames } = this + const { hoveredSeriesName } = this let slopeData = this.initialSlopeData @@ -966,14 +745,12 @@ class LabelledSlopes ) // used to determine priority for labelling conflicts - const isFocused = focusedSeriesNames.includes(slope.seriesName) - const isHovered = hoveredSeriesNames.includes(slope.seriesName) + const isHovered = hoveredSeriesName === slope.seriesName return { ...slope, leftEntityLabelBounds, rightEntityLabelBounds, - isFocused, isHovered, } }) @@ -984,10 +761,6 @@ class LabelledSlopes // Hovered slopes always have priority return s1 else if (!s1.isHovered && s2.isHovered) return s2 - else if (s1.isFocused && !s2.isFocused) - // Focused slopes are next in priority - return s1 - else if (!s1.isFocused && s2.isFocused) return s2 else if (s1.hasRightLabel && !s2.hasRightLabel) // Slopes which already have one label are prioritized for the other side return s1 @@ -1033,9 +806,7 @@ class LabelledSlopes }) // Order by focus/hover for draw order - slopeData = sortBy(slopeData, (slope) => - slope.isFocused || slope.isHovered ? 1 : 0 - ) + slopeData = sortBy(slopeData, (slope) => (slope.isHovered ? 1 : 0)) return slopeData } @@ -1148,14 +919,13 @@ class LabelledSlopes } renderGroups(groups: SlopeEntryProps[]) { - const { isLayerMode, isMultiHoverMode } = this + const { isLayerMode } = this return groups.map((slope) => ( )) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 91eda0418be..0d8eaf0464b 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -19,7 +19,6 @@ export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" export interface SlopeEntryProps extends ChartSeries { isLayerMode: boolean - isMultiHoverMode: boolean x1: number y1: number x2: number @@ -35,7 +34,6 @@ export interface SlopeEntryProps extends ChartSeries { rightEntityLabelBounds: Bounds rightValueLabel: TextWrap - isFocused: boolean isHovered: boolean } @@ -44,8 +42,7 @@ export interface LabelledSlopesProps { yColumn: CoreColumn bounds: Bounds seriesArr: SlopeChartSeries[] - focusKeys: string[] - hoverKeys: string[] + hoverKey?: string onMouseOver: (slopeProps: SlopeEntryProps) => void onMouseLeave: () => void onClick?: () => void From a9d26c8840674153811e025f90e5c01c2ff9c410 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 14:55:33 +0100 Subject: [PATCH 05/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20support=20multiple?= =?UTF-8?q?=20y-dimensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core-table/src/OwidTable.ts | 3 +- .../grapher/src/core/Grapher.tsx | 2 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 177 ++++++++++++------ .../src/slopeCharts/SlopeChartConstants.ts | 2 +- 4 files changed, 126 insertions(+), 58 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/OwidTable.ts b/packages/@ourworldindata/core-table/src/OwidTable.ts index c17d1e7b540..cf7dc20a1aa 100644 --- a/packages/@ourworldindata/core-table/src/OwidTable.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.ts @@ -124,7 +124,8 @@ export class OwidTable extends CoreTable { return min(this.allTimes) as Time } - @imemo get maxTime(): number | undefined { + // TODO: remove undefined? + @imemo get maxTime(): Time | undefined { return max(this.allTimes) } diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 7a2f0de6d8a..137e4f0ffe5 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -2027,7 +2027,7 @@ export class Grapher } @computed get supportsMultipleYColumns(): boolean { - return !(this.isScatter || this.isSlopeChart) + return !this.isScatter } @computed private get xDimension(): ChartDimension | undefined { diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 3ae9f2c5e67..2846f2515db 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -27,9 +27,11 @@ import { } from "../core/GrapherConstants" import { ScaleType, - EntitySelectionMode, SeriesName, ColorSchemeName, + ColumnSlug, + MissingDataStrategy, + Time, SeriesStrategy, EntityName, } from "@ourworldindata/types" @@ -43,10 +45,11 @@ import { SlopeChartValue, SlopeEntryProps, } from "./SlopeChartConstants" -import { OwidTable } from "@ourworldindata/core-table" +import { CoreColumn, OwidTable } from "@ourworldindata/core-table" import { autoDetectSeriesStrategy, autoDetectYColumnSlugs, + getDefaultFailMessage, makeSelectionArray, } from "../chart/ChartUtils" import { AxisConfig, AxisManager } from "../axis/AxisConfig" @@ -59,6 +62,7 @@ import { ColorSchemes } from "../color/ColorSchemes" export interface SlopeChartManager extends ChartManager { isModalOpen?: boolean + canSelectMultipleEntities?: boolean } const LABEL_SLOPE_PADDING = 8 @@ -79,18 +83,44 @@ export class SlopeChart @observable hoverKey?: string transformTable(table: OwidTable) { - if (!table.has(this.yColumnSlug)) return table - table = table.filterByEntityNames( this.selectionArray.selectedEntityNames ) // TODO: remove this filter once we don't have mixed type columns in datasets - table = table.replaceNonNumericCellsWithErrorValues([this.yColumnSlug]) + table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + + // drop all data when the author chose to hide entities with missing data and + // at least one of the variables has no data for the current entity + if ( + this.missingDataStrategy === MissingDataStrategy.hide && + table.hasAnyColumnNoValidValue(this.yColumnSlugs) + ) { + table = table.dropAllRows() + } + + return table + + // TODO: re-enable? + // return table + // .dropRowsWithErrorValuesForColumn(this.yColumnSlug) + // .interpolateColumnWithTolerance(this.yColumnSlug) + } + + transformTableForSelection(table: OwidTable): OwidTable { + // if entities with partial data are not plotted, + // make sure they don't show up in the entity selector + if (this.missingDataStrategy === MissingDataStrategy.hide) { + table = table.replaceNonNumericCellsWithErrorValues( + this.yColumnSlugs + ) + + table = table.dropEntitiesThatHaveNoDataInSomeColumn( + this.yColumnSlugs + ) + } return table - .dropRowsWithErrorValuesForColumn(this.yColumnSlug) - .interpolateColumnWithTolerance(this.yColumnSlug) } @computed get manager() { @@ -113,6 +143,10 @@ export class SlopeChart return !!(this.manager.isNarrow || this.manager.isStaticAndSmall) } + @computed private get missingDataStrategy(): MissingDataStrategy { + return this.manager.missingDataStrategy || MissingDataStrategy.auto + } + @action.bound onSlopeMouseOver(slopeProps: SlopeEntryProps) { this.hoverKey = slopeProps.seriesName } @@ -151,7 +185,11 @@ export class SlopeChart } @computed private get showNoDataSection(): boolean { - return this.selectedEntitiesWithoutData.length > 0 + // TODO: for now, only show missing data section for entities + return ( + this.seriesStrategy === SeriesStrategy.entity && + this.selectedEntitiesWithoutData.length > 0 + ) } @computed private get noDataSection(): React.ReactElement { @@ -191,7 +229,7 @@ export class SlopeChart this.transformedTable.get(slug)) } - @computed protected get yColumnSlug() { - return autoDetectYColumnSlugs(this.manager)[0] + @computed protected get yColumnSlugs(): ColumnSlug[] { + return autoDetectYColumnSlugs(this.manager) } @computed get transformedTableFromGrapher(): OwidTable { @@ -234,7 +273,7 @@ export class SlopeChart if (isRelativeMode && startHandleTimeBound !== undefined) { table = table.toTotalGrowthForEachColumnComparedToStartTime( startHandleTimeBound, - this.yColumnSlug ? [this.yColumnSlug] : [] + this.yColumnSlugs ?? [] ) } return table @@ -256,6 +295,14 @@ export class SlopeChart ) } + @computed private get startTime(): Time { + return this.transformedTable.minTime + } + + @computed private get endTime(): Time { + return this.transformedTable.maxTime! // TODO: remove the ! when we have a better way to handle missing maxTime + } + @computed get seriesStrategy(): SeriesStrategy { return autoDetectSeriesStrategy(this.manager, true) } @@ -272,6 +319,21 @@ export class SlopeChart }) } + private getSeriesName( + entityName: EntityName, + columnName: string, + entityCount: number + ): SeriesName { + if (this.seriesStrategy === SeriesStrategy.entity) { + return entityName + } + if (entityCount > 1 || this.manager.canSelectMultipleEntities) { + return `${entityName} - ${columnName}` + } else { + return columnName + } + } + private getColorKey( entityName: EntityName, columnName: string, @@ -291,48 +353,53 @@ export class SlopeChart } @computed get series() { - const column = this.yColumn - if (!column) return [] - - const { minTime, maxTime } = column - + const { startTime, endTime } = this const totalEntityCount = this.transformedTable.availableEntityNames.length - return column.uniqEntityNames - .map((entityName) => { - const values: SlopeChartValue[] = [] + return this.yColumns.flatMap((column) => + column.uniqEntityNames + .map((entityName) => { + const seriesName = this.getSeriesName( + entityName, + column.displayName || "Missing name", + totalEntityCount + ) + + const values: SlopeChartValue[] = [] - const yValues = - column.valueByEntityNameAndOriginalTime.get(entityName)! || - [] + const yValues = + column.valueByEntityNameAndOriginalTime.get( + entityName + )! || [] - yValues.forEach((value, time) => { - if (time !== minTime && time !== maxTime) return + yValues.forEach((value, time) => { + if (time !== startTime && time !== endTime) return - values.push({ - x: time, - y: value, + values.push({ + x: time, + y: value, + }) }) - }) - // sort values by time - const sortedValues = sortBy(values, (v) => v.x) + // sort values by time + const sortedValues = sortBy(values, (v) => v.x) - const color = this.categoricalColorAssigner.assign( - this.getColorKey( - entityName, - column.displayName, - totalEntityCount + const color = this.categoricalColorAssigner.assign( + this.getColorKey( + entityName, + column.displayName, + totalEntityCount + ) ) - ) - return { - seriesName: entityName, - color, - values: sortedValues, - } as SlopeChartSeries - }) - .filter((series) => series.values.length >= 2) + return { + seriesName, + color, + values: sortedValues, + } as SlopeChartSeries + }) + .filter((series) => series.values.length >= 2) + ) } } @@ -482,8 +549,8 @@ class LabelledSlopes return this.props.seriesArr } - @computed private get yColumn() { - return this.props.yColumn + @computed private get formatColumn() { + return this.props.formatColumn } @computed private get manager() { @@ -531,7 +598,7 @@ class LabelledSlopes const axis = this.yAxisConfig.toVerticalAxis() axis.domain = this.yDomain axis.range = this.yRange - axis.formatColumn = this.yColumn + axis.formatColumn = this.formatColumn axis.label = "" return axis } @@ -621,7 +688,7 @@ class LabelledSlopes } @computed private get slopeLabels() { - const { isPortrait, yColumn, allowedLabelWidth: maxWidth } = this + const { isPortrait, formatColumn, allowedLabelWidth: maxWidth } = this return this.data.map((series) => { const text = series.seriesName @@ -630,8 +697,8 @@ class LabelledSlopes (isPortrait ? GRAPHER_FONT_SCALE_9_6 : GRAPHER_FONT_SCALE_10_5) * this.fontSize - const leftValueStr = yColumn.formatValueShort(v1.y) - const rightValueStr = yColumn.formatValueShort(v2.y) + const leftValueStr = formatColumn.formatValueShort(v1.y) + const rightValueStr = formatColumn.formatValueShort(v2.y) // value labels const valueLabelProps = { @@ -1008,7 +1075,7 @@ class LabelledSlopes fill={GRAPHER_DARK_TEXT} fontSize={this.yAxis.tickFontSize} > - {this.yColumn.formatTime(xDomain[0])} + {this.formatColumn.formatTime(xDomain[0])} - {this.yColumn.formatTime(xDomain[1])} + {this.formatColumn.formatTime(xDomain[1])} {this.renderGroups(this.backgroundGroups)} diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 0d8eaf0464b..1869e0a89aa 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -39,7 +39,7 @@ export interface SlopeEntryProps extends ChartSeries { export interface LabelledSlopesProps { manager: ChartManager - yColumn: CoreColumn + formatColumn: CoreColumn bounds: Bounds seriesArr: SlopeChartSeries[] hoverKey?: string From 9e61bc348d77ebc181cc2a0b270329688d3a6e02 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 15:10:18 +0100 Subject: [PATCH 06/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20drop=20non-positive?= =?UTF-8?q?=20values=20in=20log=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 2846f2515db..fc391493fff 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -90,6 +90,9 @@ export class SlopeChart // TODO: remove this filter once we don't have mixed type columns in datasets table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + if (this.isLogScale) + table = table.replaceNonPositiveCellsForLogScale(this.yColumnSlugs) + // drop all data when the author chose to hide entities with missing data and // at least one of the variables has no data for the current entity if ( @@ -143,6 +146,10 @@ export class SlopeChart return !!(this.manager.isNarrow || this.manager.isStaticAndSmall) } + @computed get isLogScale(): boolean { + return this.props.manager.yAxisConfig?.scaleType === ScaleType.log + } + @computed private get missingDataStrategy(): MissingDataStrategy { return this.manager.missingDataStrategy || MissingDataStrategy.auto } From 7d228b9d746f4c768e428d5c1e03ba61d1c85e04 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 16:09:27 +0100 Subject: [PATCH 07/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20use=20line=20legend?= =?UTF-8?q?=20instead=20of=20custom=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 997 +++++++----------- .../src/slopeCharts/SlopeChartConstants.ts | 13 +- 2 files changed, 380 insertions(+), 630 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index fc391493fff..87e5c7ef1b6 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -4,7 +4,6 @@ import { DEFAULT_BOUNDS, isEmpty, sortBy, - max, getRelativeMouse, domainExtent, minBy, @@ -14,17 +13,11 @@ import { difference, makeIdForHumanConsumption, } from "@ourworldindata/utils" -import { TextWrap } from "@ourworldindata/components" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" import { NoDataModal } from "../noDataModal/NoDataModal" import { ColorScaleManager } from "../color/ColorScale" -import { - BASE_FONT_SIZE, - GRAPHER_DARK_TEXT, - GRAPHER_FONT_SCALE_9_6, - GRAPHER_FONT_SCALE_10_5, -} from "../core/GrapherConstants" +import { BASE_FONT_SIZE, GRAPHER_DARK_TEXT } from "../core/GrapherConstants" import { ScaleType, SeriesName, @@ -40,7 +33,6 @@ import { ChartManager } from "../chart/ChartManager" import { scaleLinear, ScaleLinear } from "d3-scale" import { select } from "d3-selection" import { - LabelledSlopesProps, SlopeChartSeries, SlopeChartValue, SlopeEntryProps, @@ -52,22 +44,20 @@ import { getDefaultFailMessage, makeSelectionArray, } from "../chart/ChartUtils" -import { AxisConfig, AxisManager } from "../axis/AxisConfig" +import { AxisConfig } from "../axis/AxisConfig" import { VerticalAxis } from "../axis/Axis" import { VerticalAxisComponent } from "../axis/AxisViews" import { NoDataSection } from "../scatterCharts/NoDataSection" import { CategoricalColorAssigner } from "../color/CategoricalColorAssigner" import { ColorScheme } from "../color/ColorScheme" import { ColorSchemes } from "../color/ColorSchemes" +import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend" export interface SlopeChartManager extends ChartManager { isModalOpen?: boolean canSelectMultipleEntities?: boolean } -const LABEL_SLOPE_PADDING = 8 -const LABEL_LABEL_PADDING = 2 - const TOP_PADDING = 6 const BOTTOM_PADDING = 20 @@ -79,6 +69,8 @@ export class SlopeChart }> implements ChartInterface, ColorScaleManager { + base: React.RefObject = React.createRef() + // currently hovered individual series key @observable hoverKey?: string @@ -215,6 +207,291 @@ export class SlopeChart ) } + // used by LineLegend + @computed get focusedSeriesNames(): string[] { + return this.hoverKey ? [this.hoverKey] : [] + } + + // Layered mode occurs when any entity on the chart is hovered or focused + // Then, a special "foreground" set of entities is rendered over the background + @computed private get isLayerMode() { + return this.hoverKey !== undefined + } + + @computed private get formatColumn() { + return this.yColumns[0] + } + + @computed get allowedLabelWidth() { + return this.bounds.width * 0.2 + } + + @computed get maxLabelWidth(): number { + // const maxLabelWidths = this.series.map((slope) => { + // const entityLabelWidth = slope.leftEntityLabel.width + // const maxValueLabelWidth = Math.max( + // slope.leftValueLabel.width, + // slope.rightValueLabel.width + // ) + // return ( + // entityLabelWidth + + // maxValueLabelWidth + + // LABEL_SLOPE_PADDING + + // LABEL_LABEL_PADDING + // ) + // }) + // return max(maxLabelWidths) ?? 0 + return 100 // TODO: remove? + } + + @computed private get initialSlopeData() { + const { series, xScale, yAxis, yDomain } = this + + const slopeData: SlopeEntryProps[] = [] + + series.forEach((series) => { + // Ensure values fit inside the chart + if ( + !series.values.every( + (d) => d.y >= yDomain[0] && d.y <= yDomain[1] + ) + ) + return + + const [v1, v2] = series.values + const [x1, x2] = [xScale(v1.x), xScale(v2.x)] + const [y1, y2] = [yAxis.place(v1.y), yAxis.place(v2.y)] + + slopeData.push({ + x1, + y1, + x2, + y2, + color: series.color, + seriesName: series.seriesName, + isHovered: false, + } as SlopeEntryProps) + }) + + return slopeData + } + + mouseFrame?: number + @action.bound onMouseLeave() { + if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame) + + this.onSlopeMouseLeave() + } + + @action.bound onMouseMove( + ev: React.MouseEvent | React.TouchEvent + ) { + if (this.base.current) { + const mouse = getRelativeMouse(this.base.current, ev.nativeEvent) + + this.mouseFrame = requestAnimationFrame(() => { + if (this.innerBounds.contains(mouse)) { + if (this.slopeData.length === 0) return + + const { x1: startX, x2: endX } = this.slopeData[0] + + // whether the mouse is over the chart area, + // the left label area, or the right label area + const mousePosition = + mouse.x < startX + ? "left" + : mouse.x > endX + ? "right" + : "chart" + + // don't track mouse movements when hovering over labels on the left or right + if (mousePosition === "left" || mousePosition === "right") { + this.onSlopeMouseLeave() + return + } + + const distToSlopeOrLabel = new Map< + SlopeEntryProps, + number + >() + for (const s of this.slopeData) { + // start and end point of a line + const p1 = new PointVector(s.x1, s.y1) + const p2 = new PointVector(s.x2, s.y2) + + // calculate the distance to the slope or label + const dist = + PointVector.distanceFromPointToLineSegmentSq( + mouse, + p1, + p2 + ) + distToSlopeOrLabel.set(s, dist) + } + + const closestSlope = minBy(this.slopeData, (s) => + distToSlopeOrLabel.get(s) + ) + const distanceSq = distToSlopeOrLabel.get(closestSlope!)! + const tolerance = mousePosition === "chart" ? 20 : 10 + const toleranceSq = tolerance * tolerance + + if (closestSlope && distanceSq < toleranceSq) { + this.onSlopeMouseOver(closestSlope) + } else { + this.onSlopeMouseLeave() + } + } + }) + } + } + + // Get the final slope data with hover focusing and collision detection + @computed get slopeData(): SlopeEntryProps[] { + let slopeData = this.initialSlopeData + + slopeData = slopeData.map((slope) => { + // used to determine priority for labelling conflicts + const isHovered = this.hoverKey === slope.seriesName + + return { + ...slope, + isHovered, + } + }) + + // Order by focus/hover for draw order + slopeData = sortBy(slopeData, (slope) => (slope.isHovered ? 1 : 0)) + + return slopeData + } + + private renderGroups(groups: SlopeEntryProps[]) { + const { isLayerMode } = this + + return groups.map((slope) => ( + + )) + } + + private renderLabelledSlopes() { + const { bounds, slopeData, xDomain, yAxis, yRange, onMouseMove } = this + + if (isEmpty(slopeData)) + return + + const { x1, x2 } = slopeData[0] + const [y1, y2] = yRange + + return ( + + + + {this.yAxis.tickLabels.map((tick) => { + const y = yAxis.place(tick.value) + return ( + + {/* grid lines connecting the chart area to the axis */} + + {/* grid lines within the chart area */} + + + ) + })} + + + + + + {this.formatColumn.formatTime(xDomain[0])} + + + {this.formatColumn.formatTime(xDomain[1])} + + + {this.renderGroups(this.backgroundGroups)} + {this.renderGroups(this.foregroundGroups)} + + + ) + } + + @computed get backgroundGroups() { + return this.slopeData.filter((group) => !group.isHovered) + } + + @computed get foregroundGroups() { + return this.slopeData.filter((group) => !!group.isHovered) + } + + private playIntroAnimation() { + // Nice little intro animation + select(this.base.current) + .select(".slopes") + .attr("stroke-dasharray", "100%") + .attr("stroke-dashoffset", "100%") + .transition() + .attr("stroke-dashoffset", "0%") + } + render() { if (this.failMessage) return ( @@ -226,23 +503,14 @@ export class SlopeChart ) const { manager } = this.props - const { series, hoverKey, innerBounds } = this return ( - + {this.renderLabelledSlopes()} + {manager.showLegend && } {this.showNoDataSection && this.noDataSection} ) @@ -292,7 +560,11 @@ export class SlopeChart componentDidMount() { exposeInstanceOnWindow(this) - } + + if (!this.manager.disableIntroAnimation) { + this.playIntroAnimation() + } + } @computed private get colorScheme(): ColorScheme { return ( @@ -408,206 +680,32 @@ export class SlopeChart .filter((series) => series.values.length >= 2) ) } -} - -@observer -class SlopeEntry extends React.Component { - line: SVGElement | null = null - - @computed get isInBackground() { - const { isLayerMode, isHovered } = this.props - - if (!isLayerMode) return false - - return !isHovered - } - - render() { - const { - x1, - y1, - x2, - y2, - color, - hasLeftLabel, - hasRightLabel, - leftValueLabel, - leftEntityLabel, - rightValueLabel, - rightEntityLabel, - leftEntityLabelBounds, - rightEntityLabelBounds, - isHovered, - seriesName, - } = this.props - const { isInBackground } = this - - const lineColor = isInBackground ? "#e2e2e2" : color - const labelColor = isInBackground ? "#ccc" : GRAPHER_DARK_TEXT - const opacity = isHovered ? 1 : 0.5 - const lineStrokeWidth = isHovered ? 4 : 2 - - const showDots = isHovered - const showValueLabels = isHovered - const showLeftEntityLabel = isHovered - - const sharedLabelProps = { - fill: labelColor, - style: { cursor: "default" }, - } - - return ( - - (this.line = el)} - x1={x1} - y1={y1} - x2={x2} - y2={y2} - stroke={lineColor} - strokeWidth={lineStrokeWidth} - opacity={opacity} - /> - {showDots && ( - <> - - - - )} - {/* value label on the left */} - {hasLeftLabel && - showValueLabels && - leftValueLabel.render( - x1 - LABEL_SLOPE_PADDING, - leftEntityLabelBounds.y, - { - textProps: { - ...sharedLabelProps, - textAnchor: "end", - }, - } - )} - {/* entity label on the left */} - {hasLeftLabel && - showLeftEntityLabel && - leftEntityLabel.render( - // -2px is a minor visual correction - leftEntityLabelBounds.x - 2, - leftEntityLabelBounds.y, - { - textProps: { - ...sharedLabelProps, - textAnchor: "end", - }, - } - )} - {/* value label on the right */} - {hasRightLabel && - showValueLabels && - rightValueLabel.render( - rightEntityLabelBounds.x + - rightEntityLabel.width + - LABEL_LABEL_PADDING, - rightEntityLabelBounds.y, - { - textProps: sharedLabelProps, - } - )} - {/* entity label on the right */} - {hasRightLabel && - rightEntityLabel.render( - rightEntityLabelBounds.x, - rightEntityLabelBounds.y, - { - textProps: { - ...sharedLabelProps, - fontWeight: isHovered ? "bold" : undefined, - }, - } - )} - - ) - } -} -@observer -class LabelledSlopes - extends React.Component - implements AxisManager -{ - base: React.RefObject = React.createRef() + @observable private hoverTimer?: NodeJS.Timeout - @computed private get data() { - return this.props.seriesArr + @action.bound onLineLegendMouseOver(seriesName: SeriesName): void { + clearTimeout(this.hoverTimer) + this.hoverKey = seriesName } - @computed private get formatColumn() { - return this.props.formatColumn - } - - @computed private get manager() { - return this.props.manager - } - - @computed private get bounds() { - return this.props.bounds - } - - @computed get fontSize() { - return this.manager.fontSize ?? BASE_FONT_SIZE + @action.bound clearHighlightedSeries(): void { + clearTimeout(this.hoverTimer) + this.hoverTimer = setTimeout(() => { + // wait before clearing selection in case the mouse is moving quickly over neighboring labels + this.hoverKey = undefined + }, 200) } - @computed private get hoveredSeriesName() { - return this.props.hoverKey - } - - // Layered mode occurs when any entity on the chart is hovered or focused - // Then, a special "foreground" set of entities is rendered over the background - @computed private get isLayerMode() { - return this.hoveredSeriesName !== undefined - } - - @computed get isPortrait(): boolean { - return this.props.isPortrait - } - - @computed private get allValues() { - return this.props.seriesArr.flatMap((g) => g.values) - } - - @computed private get xDomainDefault(): [number, number] { - return domainExtent( - this.allValues.map((v) => v.x), - ScaleType.linear - ) + @action.bound onLineLegendMouseLeave(): void { + this.clearHighlightedSeries() } @computed private get yAxisConfig(): AxisConfig { return new AxisConfig(this.manager.yAxisConfig, this) } - @computed get yAxis(): VerticalAxis { - const axis = this.yAxisConfig.toVerticalAxis() - axis.domain = this.yDomain - axis.range = this.yRange - axis.formatColumn = this.formatColumn - axis.label = "" - return axis + @computed private get allValues() { + return this.series.flatMap((g) => g.values) } @computed private get yScaleType() { @@ -621,10 +719,6 @@ class LabelledSlopes ) } - @computed private get xDomain(): [number, number] { - return this.xDomainDefault - } - @computed private get yDomain(): [number, number] { const domain = this.yAxisConfig.domain || [Infinity, -Infinity] const domainDefault = this.yDomainDefault @@ -641,6 +735,15 @@ class LabelledSlopes .yRange() } + @computed get yAxis(): VerticalAxis { + const axis = this.yAxisConfig.toVerticalAxis() + axis.domain = this.yDomain + axis.range = this.yRange + axis.formatColumn = this.yColumns[0] + axis.label = "" + return axis + } + @computed get yAxisWidth(): number { return this.yAxis.width + 5 // 5px account for the tick marks } @@ -672,431 +775,89 @@ class LabelledSlopes return scaleLinear().domain(xDomain).range(xRange) } - @computed get maxLabelWidth(): number { - const { slopeLabels } = this - const maxLabelWidths = slopeLabels.map((slope) => { - const entityLabelWidth = slope.leftEntityLabel.width - const maxValueLabelWidth = Math.max( - slope.leftValueLabel.width, - slope.rightValueLabel.width - ) - return ( - entityLabelWidth + - maxValueLabelWidth + - LABEL_SLOPE_PADDING + - LABEL_LABEL_PADDING - ) - }) - return max(maxLabelWidths) ?? 0 - } - - @computed get allowedLabelWidth() { - return this.bounds.width * 0.2 - } - - @computed private get slopeLabels() { - const { isPortrait, formatColumn, allowedLabelWidth: maxWidth } = this - - return this.data.map((series) => { - const text = series.seriesName - const [v1, v2] = series.values - const fontSize = - (isPortrait - ? GRAPHER_FONT_SCALE_9_6 - : GRAPHER_FONT_SCALE_10_5) * this.fontSize - const leftValueStr = formatColumn.formatValueShort(v1.y) - const rightValueStr = formatColumn.formatValueShort(v2.y) - - // value labels - const valueLabelProps = { - maxWidth: Infinity, // no line break - fontSize, - lineHeight: 1, - } - const leftValueLabel = new TextWrap({ - text: leftValueStr, - ...valueLabelProps, - }) - const rightValueLabel = new TextWrap({ - text: rightValueStr, - ...valueLabelProps, - }) - - // entity labels - const entityLabelProps = { - ...valueLabelProps, - maxWidth, - fontWeight: 700, - separators: [" ", "-"], - } - const leftEntityLabel = new TextWrap({ - text, - ...entityLabelProps, - }) - const rightEntityLabel = new TextWrap({ - text, - ...entityLabelProps, - }) - - return { - seriesName: series.seriesName, - leftValueLabel, - leftEntityLabel, - rightValueLabel, - rightEntityLabel, - } - }) - } - - @computed private get initialSlopeData() { - const { data, slopeLabels, xScale, yAxis, yDomain } = this - - const slopeData: SlopeEntryProps[] = [] - - data.forEach((series, i) => { - // Ensure values fit inside the chart - if ( - !series.values.every( - (d) => d.y >= yDomain[0] && d.y <= yDomain[1] - ) - ) - return - - const labels = slopeLabels[i] - const [v1, v2] = series.values - const [x1, x2] = [xScale(v1.x), xScale(v2.x)] - const [y1, y2] = [yAxis.place(v1.y), yAxis.place(v2.y)] - - slopeData.push({ - ...labels, - x1, - y1, - x2, - y2, - color: series.color, - seriesName: series.seriesName, - isHovered: false, - hasLeftLabel: true, - hasRightLabel: true, - } as SlopeEntryProps) - }) - - return slopeData + @computed private get xDomain(): [number, number] { + return this.xDomainDefault } - @computed get backgroundGroups() { - return this.slopeData.filter((group) => !group.isHovered) + @computed private get xDomainDefault(): [number, number] { + return domainExtent( + this.allValues.map((v) => v.x), + ScaleType.linear + ) } - @computed get foregroundGroups() { - return this.slopeData.filter((group) => !!group.isHovered) + @computed get lineLegendX(): number { + return this.bounds.right - 240 } - // Get the final slope data with hover focusing and collision detection - @computed get slopeData(): SlopeEntryProps[] { - const { hoveredSeriesName } = this - - let slopeData = this.initialSlopeData - - slopeData = slopeData.map((slope) => { - // used for collision detection - const leftEntityLabelBounds = new Bounds( - // labels on the left are placed like this: | - slope.x1 - - LABEL_SLOPE_PADDING - - slope.leftValueLabel.width - - LABEL_LABEL_PADDING, - slope.y1 - slope.leftEntityLabel.lines[0].height / 2, - slope.leftEntityLabel.width, - slope.leftEntityLabel.height - ) - const rightEntityLabelBounds = new Bounds( - // labels on the left are placed like this: | - slope.x2 + LABEL_SLOPE_PADDING, - slope.y2 - slope.rightEntityLabel.height / 2, - slope.rightEntityLabel.width, - slope.rightEntityLabel.height - ) - - // used to determine priority for labelling conflicts - const isHovered = hoveredSeriesName === slope.seriesName - + @computed get labelSeries(): LineLabelSeries[] { + return this.series.map((series) => { + const { seriesName, color, values } = series return { - ...slope, - leftEntityLabelBounds, - rightEntityLabelBounds, - isHovered, + color, + seriesName, + label: seriesName, + yValue: values[1].y, } }) - - // How to work out which of two slopes to prioritize for labelling conflicts - function chooseLabel(s1: SlopeEntryProps, s2: SlopeEntryProps) { - if (s1.isHovered && !s2.isHovered) - // Hovered slopes always have priority - return s1 - else if (!s1.isHovered && s2.isHovered) return s2 - else if (s1.hasRightLabel && !s2.hasRightLabel) - // Slopes which already have one label are prioritized for the other side - return s1 - else if (!s1.hasRightLabel && s2.hasRightLabel) return s2 - else return s1 // Equal priority, just do the first one - } - - // Eliminate overlapping labels, one pass for each side - slopeData.forEach((s1) => { - slopeData.forEach((s2) => { - if ( - s1 !== s2 && - s1.hasRightLabel && - s2.hasRightLabel && - // entity labels don't necessarily share the same x position. - // that's why we check for vertical intersection only - s1.rightEntityLabelBounds.hasVerticalOverlap( - s2.rightEntityLabelBounds - ) - ) { - if (chooseLabel(s1, s2) === s1) s2.hasRightLabel = false - else s1.hasRightLabel = false - } - }) - }) - - slopeData.forEach((s1) => { - slopeData.forEach((s2) => { - if ( - s1 !== s2 && - s1.hasLeftLabel && - s2.hasLeftLabel && - // entity labels don't necessarily share the same x position. - // that's why we check for vertical intersection only - s1.leftEntityLabelBounds.hasVerticalOverlap( - s2.leftEntityLabelBounds - ) - ) { - if (chooseLabel(s1, s2) === s1) s2.hasLeftLabel = false - else s1.hasLeftLabel = false - } - }) - }) - - // Order by focus/hover for draw order - slopeData = sortBy(slopeData, (slope) => (slope.isHovered ? 1 : 0)) - - return slopeData - } - - mouseFrame?: number - @action.bound onMouseLeave() { - if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame) - - if (this.props.onMouseLeave) this.props.onMouseLeave() - } - - @action.bound onMouseMove( - ev: React.MouseEvent | React.TouchEvent - ) { - if (this.base.current) { - const mouse = getRelativeMouse(this.base.current, ev.nativeEvent) - - this.mouseFrame = requestAnimationFrame(() => { - if (this.props.bounds.contains(mouse)) { - if (this.slopeData.length === 0) return - - const { x1: startX, x2: endX } = this.slopeData[0] - - // whether the mouse is over the chart area, - // the left label area, or the right label area - const mousePosition = - mouse.x < startX - ? "left" - : mouse.x > endX - ? "right" - : "chart" - - // don't track mouse movements when hovering over labels on the left - if (mousePosition === "left") { - this.props.onMouseLeave() - return - } - - const distToSlopeOrLabel = new Map< - SlopeEntryProps, - number - >() - for (const s of this.slopeData) { - // start and end point of a line - let p1: PointVector - let p2: PointVector - - if (mousePosition === "chart") { - // points define the slope line - p1 = new PointVector(s.x1, s.y1) - p2 = new PointVector(s.x2, s.y2) - } else { - const labelBox = s.rightEntityLabelBounds.toProps() - // points define a "strike-through" line that stretches from - // the end point of the slopes to the right side of the right label - const y = labelBox.y + labelBox.height / 2 - p1 = new PointVector(endX, y) - p2 = new PointVector(labelBox.x + labelBox.width, y) - } - - // calculate the distance to the slope or label - const dist = - PointVector.distanceFromPointToLineSegmentSq( - mouse, - p1, - p2 - ) - distToSlopeOrLabel.set(s, dist) - } - - const closestSlope = minBy(this.slopeData, (s) => - distToSlopeOrLabel.get(s) - ) - const distanceSq = distToSlopeOrLabel.get(closestSlope!)! - const tolerance = mousePosition === "chart" ? 20 : 10 - const toleranceSq = tolerance * tolerance - - if ( - closestSlope && - distanceSq < toleranceSq && - this.props.onMouseOver - ) { - this.props.onMouseOver(closestSlope) - } else { - this.props.onMouseLeave() - } - } - }) - } - } - - @action.bound onClick() { - if (this.props.onClick) this.props.onClick() } +} - componentDidMount() { - if (!this.manager.disableIntroAnimation) { - this.playIntroAnimation() - } - } +@observer +class SlopeEntry extends React.Component { + line: SVGElement | null = null - private playIntroAnimation() { - // Nice little intro animation - select(this.base.current) - .select(".slopes") - .attr("stroke-dasharray", "100%") - .attr("stroke-dashoffset", "100%") - .transition() - .attr("stroke-dashoffset", "0%") - } + @computed get isInBackground() { + const { isLayerMode, isHovered } = this.props - renderGroups(groups: SlopeEntryProps[]) { - const { isLayerMode } = this + if (!isLayerMode) return false - return groups.map((slope) => ( - - )) + return !isHovered } render() { - const { bounds, slopeData, xDomain, yAxis, yRange, onMouseMove } = this + const { x1, y1, x2, y2, color, isHovered, seriesName } = this.props + const { isInBackground } = this - if (isEmpty(slopeData)) - return + const lineColor = isInBackground ? "#e2e2e2" : color + const opacity = isHovered ? 1 : 0.5 + const lineStrokeWidth = isHovered ? 4 : 2 - const { x1, x2 } = slopeData[0] - const [y1, y2] = yRange + const showDots = isHovered return ( - - - {this.yAxis.tickLabels.map((tick) => { - const y = yAxis.place(tick.value) - return ( - - {/* grid lines connecting the chart area to the axis */} - - {/* grid lines within the chart area */} - - - ) - })} - - (this.line = el)} + x1={x1} + y1={y1} + x2={x2} + y2={y2} + stroke={lineColor} + strokeWidth={lineStrokeWidth} + opacity={opacity} /> - - - - {this.formatColumn.formatTime(xDomain[0])} - - - {this.formatColumn.formatTime(xDomain[1])} - - - {this.renderGroups(this.backgroundGroups)} - {this.renderGroups(this.foregroundGroups)} - + {showDots && ( + <> + + + + )} ) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 1869e0a89aa..6c271390a20 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -3,7 +3,6 @@ import { ChartSeries } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" import { ScaleType } from "@ourworldindata/types" import { Bounds } from "@ourworldindata/utils" -import { TextWrap } from "@ourworldindata/components" export interface SlopeChartValue { x: number @@ -18,22 +17,12 @@ export interface SlopeChartSeries extends ChartSeries { export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" export interface SlopeEntryProps extends ChartSeries { - isLayerMode: boolean x1: number y1: number x2: number y2: number - hasLeftLabel: boolean - leftEntityLabel: TextWrap - leftValueLabel: TextWrap - leftEntityLabelBounds: Bounds - - hasRightLabel: boolean - rightEntityLabel: TextWrap - rightEntityLabelBounds: Bounds - rightValueLabel: TextWrap - + isLayerMode: boolean isHovered: boolean } From 198a26ff8f620aeca5017f05f78bc42c7964c7c5 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 16:14:14 +0100 Subject: [PATCH 08/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20add=20support=20for?= =?UTF-8?q?=20entity=20name=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 29 ++++++++++++++++++- .../src/slopeCharts/SlopeChartConstants.ts | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 87e5c7ef1b6..677edd06bf0 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -27,6 +27,7 @@ import { Time, SeriesStrategy, EntityName, + PrimitiveType, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" @@ -613,6 +614,28 @@ export class SlopeChart } } + // todo: for now just works with 1 y column + @computed private get annotationsMap(): Map< + PrimitiveType, + Set + > { + return this.inputTable + .getAnnotationColumnForColumn(this.yColumnSlugs[0]) + ?.getUniqueValuesGroupedBy(this.inputTable.entityNameSlug) + } + + private getAnnotationsForSeries( + seriesName: SeriesName + ): string | undefined { + const annotationsMap = this.annotationsMap + const annos = annotationsMap?.get(seriesName) + return annos + ? Array.from(annos.values()) + .filter((anno) => anno) + .join(" & ") + : undefined + } + private getColorKey( entityName: EntityName, columnName: string, @@ -671,10 +694,13 @@ export class SlopeChart ) ) + const annotation = this.getAnnotationsForSeries(seriesName) + return { seriesName, color, values: sortedValues, + annotation, } as SlopeChartSeries }) .filter((series) => series.values.length >= 2) @@ -792,11 +818,12 @@ export class SlopeChart @computed get labelSeries(): LineLabelSeries[] { return this.series.map((series) => { - const { seriesName, color, values } = series + const { seriesName, color, values, annotation } = series return { color, seriesName, label: seriesName, + annotation, yValue: values[1].y, } }) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 6c271390a20..16d767bd113 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -12,6 +12,7 @@ export interface SlopeChartValue { export interface SlopeChartSeries extends ChartSeries { size: number values: SlopeChartValue[] + annotation?: string } export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" From d33eebda913738cc950d3ebe6355181359d594e0 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 16:32:54 +0100 Subject: [PATCH 09/91] =?UTF-8?q?=E2=9C=A8=20(grapher)=20disallow=20switch?= =?UTF-8?q?ing=20to=20a=20slope=20chart=20for=20projections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartUtils.tsx | 15 ++++++++++- .../grapher/src/core/Grapher.tsx | 27 ++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 375e9816c7a..501d646456d 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -1,5 +1,5 @@ import React from "react" -import { Box, getCountryByName } from "@ourworldindata/utils" +import { areSetsEqual, Box, getCountryByName } from "@ourworldindata/utils" import { SeriesStrategy, EntityName, @@ -15,6 +15,7 @@ import { GRAPHER_SIDE_PANEL_CLASS, GRAPHER_TIMELINE_CLASS, GRAPHER_SETTINGS_CLASS, + validChartTypeCombinations, } from "../core/GrapherConstants" export const autoDetectYColumnSlugs = (manager: ChartManager): string[] => { @@ -175,3 +176,15 @@ export function mapChartTypeNameToQueryParam( return GRAPHER_TAB_QUERY_PARAMS.marimekko } } + +export function findValidChartTypeCombination( + chartTypes: GrapherChartType[] +): GrapherChartType[] | undefined { + const chartTypeSet = new Set(chartTypes) + for (const validCombination of validChartTypeCombinations) { + const validCombinationSet = new Set(validCombination) + if (areSetsEqual(chartTypeSet, validCombinationSet)) + return validCombination + } + return undefined +} diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 137e4f0ffe5..e350b0d7e9b 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -66,7 +66,6 @@ import { extractDetailsFromSyntax, omit, isTouchDevice, - areSetsEqual, } from "@ourworldindata/utils" import { MarkdownTextWrap, @@ -138,7 +137,6 @@ import { GRAPHER_FRAME_PADDING_HORIZONTAL, GRAPHER_FRAME_PADDING_VERTICAL, latestGrapherConfigSchema, - validChartTypeCombinations, GRAPHER_SQUARE_SIZE, } from "../core/GrapherConstants" import { loadVariableDataAndMetadata } from "./loadVariable" @@ -201,6 +199,7 @@ import { ScatterPlotManager } from "../scatterCharts/ScatterPlotChartConstants" import { autoDetectSeriesStrategy, autoDetectYColumnSlugs, + findValidChartTypeCombination, mapChartTypeNameToQueryParam, mapQueryParamToChartTypeName, } from "../chart/ChartUtils" @@ -1526,21 +1525,31 @@ export class Grapher }) } + @computed get hasProjectedData(): boolean { + return this.inputTable.numericColumnSlugs.some( + (slug) => this.inputTable.get(slug).isProjection + ) + } + @computed get validChartTypes(): GrapherChartType[] { const { chartTypes } = this // all single-chart Graphers are valid if (chartTypes.length <= 1) return chartTypes - const chartTypeSet = new Set(chartTypes) - for (const validCombination of validChartTypeCombinations) { - const validCombinationSet = new Set(validCombination) - if (areSetsEqual(chartTypeSet, validCombinationSet)) - return validCombination - } + // find valid combination in a pre-defined list + const validChartTypes = findValidChartTypeCombination(chartTypes) // if the given combination is not valid, then ignore all but the first chart type - return chartTypes.slice(0, 1) + if (!validChartTypes) return chartTypes.slice(0, 1) + + // projected data is only supported for line charts + const isLineChart = validChartTypes[0] === GRAPHER_CHART_TYPES.LineChart + if (isLineChart && this.hasProjectedData) { + return [GRAPHER_CHART_TYPES.LineChart] + } + + return validChartTypes } @computed get validChartTypeSet(): Set { From 5c9727e27806cc0139cd8298298afb77afab78c8 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 20 Nov 2024 17:17:49 +0100 Subject: [PATCH 10/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20add=20tooltips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 92 +++++++++++++++++++ .../src/slopeCharts/SlopeChartConstants.ts | 2 + 2 files changed, 94 insertions(+) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 677edd06bf0..563cfbaa568 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -12,6 +12,8 @@ import { clamp, difference, makeIdForHumanConsumption, + guid, + excludeUndefined, } from "@ourworldindata/utils" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" @@ -53,6 +55,13 @@ import { CategoricalColorAssigner } from "../color/CategoricalColorAssigner" import { ColorScheme } from "../color/ColorScheme" import { ColorSchemes } from "../color/ColorSchemes" import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend" +import { + makeTooltipRoundingNotice, + Tooltip, + TooltipState, + TooltipValueRange, +} from "../tooltip/Tooltip" +import { TooltipFooterIcon } from "../tooltip/TooltipProps" export interface SlopeChartManager extends ChartManager { isModalOpen?: boolean @@ -75,6 +84,10 @@ export class SlopeChart // currently hovered individual series key @observable hoverKey?: string + @observable tooltipState = new TooltipState<{ + series: SlopeChartSeries + }>() + transformTable(table: OwidTable) { table = table.filterByEntityNames( this.selectionArray.selectedEntityNames @@ -149,10 +162,14 @@ export class SlopeChart @action.bound onSlopeMouseOver(slopeProps: SlopeEntryProps) { this.hoverKey = slopeProps.seriesName + this.tooltipState.target = { + series: slopeProps.series, + } } @action.bound onSlopeMouseLeave() { this.hoverKey = undefined + this.tooltipState.target = null } @computed private get selectionArray() { @@ -269,6 +286,7 @@ export class SlopeChart x2, y2, color: series.color, + series: series, seriesName: series.seriesName, isHovered: false, } as SlopeEntryProps) @@ -287,6 +305,11 @@ export class SlopeChart @action.bound onMouseMove( ev: React.MouseEvent | React.TouchEvent ) { + const ref = this.manager.base?.current + if (ref) { + this.tooltipState.position = getRelativeMouse(ref, ev) + } + if (this.base.current) { const mouse = getRelativeMouse(this.base.current, ev.nativeEvent) @@ -493,6 +516,74 @@ export class SlopeChart .attr("stroke-dashoffset", "0%") } + @computed get renderUid(): number { + return guid() + } + + @computed get tooltip(): React.ReactElement | undefined { + const { + tooltipState: { target, position, fading }, + } = this + + const { series } = target || {} + if (!series) return + + const { isRelativeMode } = this.manager, + timeRange = [this.startTime, this.endTime] + .map((t) => this.formatColumn.formatTime(t)) + .join(" to "), + timeLabel = timeRange + (isRelativeMode ? " (relative change)" : "") + + const columns = this.yColumns + const allRoundedToSigFigs = columns.every( + (column) => column.roundsToSignificantFigures + ) + const anyRoundedToSigFigs = columns.some( + (column) => column.roundsToSignificantFigures + ) + const sigFigs = excludeUndefined( + columns.map((column) => + column.roundsToSignificantFigures + ? column.numSignificantFigures + : undefined + ) + ) + + const roundingNotice = anyRoundedToSigFigs + ? { + icon: allRoundedToSigFigs + ? TooltipFooterIcon.none + : TooltipFooterIcon.significance, + text: makeTooltipRoundingNotice(sigFigs, { + plural: sigFigs.length > 1, + }), + } + : undefined + const footer = excludeUndefined([roundingNotice]) + + return ( + (this.tooltipState.target = null)} + > + v.y)} + /> + + ) + } + render() { if (this.failMessage) return ( @@ -513,6 +604,7 @@ export class SlopeChart {this.renderLabelledSlopes()} {manager.showLegend && } {this.showNoDataSection && this.noDataSection} + {this.tooltip} ) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 16d767bd113..9b27bf3110b 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -18,6 +18,8 @@ export interface SlopeChartSeries extends ChartSeries { export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" export interface SlopeEntryProps extends ChartSeries { + series: SlopeChartSeries + x1: number y1: number x2: number From f82ca07f143f47545218764ee992590bfe7bc2a8 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 21 Nov 2024 12:21:34 +0100 Subject: [PATCH 11/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core-table/src/CoreTableColumns.ts | 12 +- .../core-table/src/OwidTable.ts | 5 +- .../grapher/src/lineCharts/LineChart.tsx | 108 +- .../src/lineCharts/lineChartHelpers.ts | 75 + .../src/scatterCharts/NoDataSection.tsx | 22 +- .../src/scatterCharts/ScatterPlotChart.tsx | 2 +- .../src/slopeCharts/SlopeChart.test.ts | 6 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 1305 ++++++++--------- .../src/slopeCharts/SlopeChartConstants.ts | 53 +- .../src/stackedCharts/StackedAreaChart.tsx | 44 +- .../types/src/grapherTypes/GrapherTypes.ts | 7 + packages/@ourworldindata/types/src/index.ts | 1 + 12 files changed, 794 insertions(+), 846 deletions(-) create mode 100644 packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts diff --git a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts index 40da7cba3dc..390dfb88bcd 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts @@ -305,12 +305,22 @@ export abstract class AbstractCoreColumn { @imemo get displayName(): string { return ( this.display?.name ?? - this.def.presentation?.titlePublic ?? // this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name + // this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name + this.def.presentation?.titlePublic ?? this.name ?? "" ) } + @imemo get nonEmptyDisplayName(): string { + return ( + this.display?.name || + // this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name + this.def.presentation?.titlePublic || + this.nonEmptyName + ) + } + @imemo get titlePublicOrDisplayName(): IndicatorTitleWithFragments { return this.def.presentation?.titlePublic ? { diff --git a/packages/@ourworldindata/core-table/src/OwidTable.ts b/packages/@ourworldindata/core-table/src/OwidTable.ts index cf7dc20a1aa..34dc42551cb 100644 --- a/packages/@ourworldindata/core-table/src/OwidTable.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.ts @@ -124,9 +124,8 @@ export class OwidTable extends CoreTable { return min(this.allTimes) as Time } - // TODO: remove undefined? - @imemo get maxTime(): Time | undefined { - return max(this.allTimes) + @imemo get maxTime(): Time { + return max(this.allTimes) as Time } @imemo private get allTimes(): Time[] { diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 876a49b57bf..d48ddcdd868 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -102,6 +102,13 @@ import { HorizontalColorLegendManager, HorizontalNumericColorLegend, } from "../horizontalColorLegend/HorizontalColorLegends" +import { + AnnotationsMap, + getAnnotationsForSeries, + getAnnotationsMap, + getColorKey, + getSeriesName, +} from "./lineChartHelpers" const LINE_CHART_CLASS_NAME = "LineChart" @@ -707,7 +714,10 @@ export class LineChart rows={sortedData.map((series) => { const { seriesName: name, isProjection: striped } = series - const annotation = this.getAnnotationsForSeries(name) + const annotation = getAnnotationsForSeries( + this.annotationsMap, + name + ) const point = series.points.find( (point) => point.x === target.x @@ -1148,24 +1158,8 @@ export class LineChart // End of color legend props - // todo: for now just works with 1 y column - @computed private get annotationsMap(): Map< - PrimitiveType, - Set - > { - return this.inputTable - .getAnnotationColumnForColumn(this.yColumnSlugs[0]) - ?.getUniqueValuesGroupedBy(this.inputTable.entityNameSlug) - } - - getAnnotationsForSeries(seriesName: SeriesName): string | undefined { - const annotationsMap = this.annotationsMap - const annos = annotationsMap?.get(seriesName) - return annos - ? Array.from(annos.values()) - .filter((anno) => anno) - .join(" & ") - : undefined + @computed private get annotationsMap(): AnnotationsMap | undefined { + return getAnnotationsMap(this.inputTable, this.yColumnSlugs[0]) } @computed private get colorScheme(): ColorScheme { @@ -1196,39 +1190,6 @@ export class LineChart }) } - private getSeriesName( - entityName: EntityName, - columnName: string, - entityCount: number - ): SeriesName { - if (this.seriesStrategy === SeriesStrategy.entity) { - return entityName - } - if (entityCount > 1 || this.manager.canSelectMultipleEntities) { - return `${entityName} - ${columnName}` - } else { - return columnName - } - } - - private getColorKey( - entityName: EntityName, - columnName: string, - entityCount: number - ): SeriesName { - if (this.seriesStrategy === SeriesStrategy.entity) { - return entityName - } - // If only one entity is plotted, we want to use the column colors. - // Unlike in `getSeriesName`, we don't care whether the user can select - // multiple entities, only whether more than one is plotted. - if (entityCount > 1) { - return `${entityName} - ${columnName}` - } else { - return columnName - } - } - // cache value for performance @computed private get rowIndicesByEntityName(): Map { return this.transformedTable.rowIndex([ @@ -1237,14 +1198,20 @@ export class LineChart } private constructSingleSeries( - entityName: string, - col: CoreColumn + entityName: EntityName, + column: CoreColumn ): LineChartSeries { - const { hasColorScale, transformedTable, colorColumn } = this + const { + manager: { canSelectMultipleEntities = false }, + transformedTable: { availableEntityNames }, + seriesStrategy, + hasColorScale, + colorColumn, + } = this // Construct the points - const timeValues = col.originalTimeColumn.valuesIncludingErrorValues - const values = col.valuesIncludingErrorValues + const timeValues = column.originalTimeColumn.valuesIncludingErrorValues + const values = column.valuesIncludingErrorValues const colorValues = colorColumn.valuesIncludingErrorValues // If Y and Color are the same column, we need to get rid of any duplicate rows. // Duplicates occur because Y doesn't have tolerance applied, but Color does. @@ -1269,26 +1236,34 @@ export class LineChart }) // Construct series properties - const totalEntityCount = transformedTable.availableEntityNames.length - const seriesName = this.getSeriesName( + const columnName = column.nonEmptyDisplayName + const seriesName = getSeriesName({ entityName, - col.displayName, - totalEntityCount - ) + columnName, + seriesStrategy, + availableEntityNames, + canSelectMultipleEntities, + }) + let seriesColor: Color if (hasColorScale) { const colorValue = last(points)?.colorValue seriesColor = this.getColorScaleColor(colorValue) } else { seriesColor = this.categoricalColorAssigner.assign( - this.getColorKey(entityName, col.displayName, totalEntityCount) + getColorKey({ + entityName, + columnName, + seriesStrategy, + availableEntityNames, + }) ) } return { points, seriesName, - isProjection: col.isProjection, + isProjection: column.isProjection, color: seriesColor, } } @@ -1350,7 +1325,10 @@ export class LineChart seriesName, // E.g. https://ourworldindata.org/grapher/size-poverty-gap-world label: !this.manager.showLegend ? "" : `${seriesName}`, - annotation: this.getAnnotationsForSeries(seriesName), + annotation: getAnnotationsForSeries( + this.annotationsMap, + seriesName + ), yValue: lastValue, } }) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts new file mode 100644 index 00000000000..5296a3a3eca --- /dev/null +++ b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts @@ -0,0 +1,75 @@ +import { OwidTable } from "@ourworldindata/core-table" +import { + ColumnSlug, + EntityName, + PrimitiveType, + SeriesName, + SeriesStrategy, +} from "@ourworldindata/types" + +export type AnnotationsMap = Map> + +export function getSeriesName({ + entityName, + columnName, + seriesStrategy, + availableEntityNames, + canSelectMultipleEntities, +}: { + entityName: EntityName + columnName: string + seriesStrategy: SeriesStrategy + availableEntityNames: EntityName[] + canSelectMultipleEntities: boolean +}): SeriesName { + // if entities are plotted, use the entity name + if (seriesStrategy === SeriesStrategy.entity) return entityName + + // if columns are plotted, use the column name + // and prepend the entity name if multiple entities can be selected + return availableEntityNames.length > 1 && canSelectMultipleEntities + ? `${entityName} - ${columnName}` + : columnName +} + +export function getColorKey({ + entityName, + columnName, + seriesStrategy, + availableEntityNames, +}: { + entityName: EntityName + columnName: string + seriesStrategy: SeriesStrategy + availableEntityNames: EntityName[] +}): SeriesName { + // if entities are plotted, use the entity name + if (seriesStrategy === SeriesStrategy.entity) return entityName + + // If only one entity is plotted, we want to use the column colors. + // Unlike in `getSeriesName`, we don't care whether the user can select + // multiple entities, only whether more than one is plotted. + return availableEntityNames.length > 1 + ? `${entityName} - ${columnName}` + : columnName +} + +export function getAnnotationsMap( + table: OwidTable, + slug: ColumnSlug +): AnnotationsMap | undefined { + return table + .getAnnotationColumnForColumn(slug) + ?.getUniqueValuesGroupedBy(table.entityNameSlug) +} + +export function getAnnotationsForSeries( + annotationsMap: AnnotationsMap | undefined, + seriesName: SeriesName +): string | undefined { + const annotations = annotationsMap?.get(seriesName) + if (!annotations) return undefined + return Array.from(annotations.values()) + .filter((anno) => anno) + .join(" & ") +} diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/NoDataSection.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/NoDataSection.tsx index 6ccac9dd108..bf9dfb192f3 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/NoDataSection.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/NoDataSection.tsx @@ -6,19 +6,19 @@ import { } from "../core/GrapherConstants" export function NoDataSection({ - entityNames, + seriesNames, bounds, baseFontSize = 16, }: { - entityNames: string[] + seriesNames: string[] bounds: Bounds baseFontSize?: number }): React.ReactElement { { - const displayedEntities = entityNames.slice(0, 5) - const numRemainingEntities = Math.max( + const displayedNames = seriesNames.slice(0, 5) + const remaining = Math.max( 0, - entityNames.length - displayedEntities.length + seriesNames.length - displayedNames.length ) return ( @@ -40,7 +40,7 @@ export function NoDataSection({ No data
    - {displayedEntities.map((entityName) => ( + {displayedNames.map((entityName) => (
  • ))}
- {numRemainingEntities > 0 && ( -
- &{" "} - {numRemainingEntities === 1 - ? "one" - : numRemainingEntities}{" "} - more -
+ {remaining > 0 && ( +
& {remaining === 1 ? "one" : remaining} more
)} ) diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index e4688cad8ee..fc505bb6459 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -871,7 +871,7 @@ export class ScatterPlotChart {!this.manager.isStatic && separatorLine(noDataSectionBounds.top)} diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts index 7d7e4ae8d80..9a22d9c08a6 100755 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts @@ -48,10 +48,8 @@ it("filters non-numeric values", () => { const chart = new SlopeChart({ manager }) expect(chart.series.length).toEqual(1) expect( - chart.series.every((series) => - series.values.every( - (value) => isNumber(value.x) && isNumber(value.y) - ) + chart.series.every( + (series) => isNumber(series.startValue) && isNumber(series.endValue) ) ).toBeTruthy() }) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 563cfbaa568..2a54bcf579a 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -3,23 +3,27 @@ import { Bounds, DEFAULT_BOUNDS, isEmpty, - sortBy, - getRelativeMouse, domainExtent, - minBy, exposeInstanceOnWindow, PointVector, clamp, - difference, makeIdForHumanConsumption, guid, excludeUndefined, + partition, + max, + getRelativeMouse, + minBy, } from "@ourworldindata/utils" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" import { NoDataModal } from "../noDataModal/NoDataModal" import { ColorScaleManager } from "../color/ColorScale" -import { BASE_FONT_SIZE, GRAPHER_DARK_TEXT } from "../core/GrapherConstants" +import { + BASE_FONT_SIZE, + GRAPHER_DARK_TEXT, + GRAPHER_FONT_SCALE_12, +} from "../core/GrapherConstants" import { ScaleType, SeriesName, @@ -30,15 +34,16 @@ import { SeriesStrategy, EntityName, PrimitiveType, + RenderMode, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" import { scaleLinear, ScaleLinear } from "d3-scale" import { select } from "d3-selection" import { + PlacedSlopeChartSeries, + RawSlopeChartSeries, SlopeChartSeries, - SlopeChartValue, - SlopeEntryProps, } from "./SlopeChartConstants" import { CoreColumn, OwidTable } from "@ourworldindata/core-table" import { @@ -62,15 +67,25 @@ import { TooltipValueRange, } from "../tooltip/Tooltip" import { TooltipFooterIcon } from "../tooltip/TooltipProps" +import { + AnnotationsMap, + getAnnotationsForSeries, + getAnnotationsMap, + getColorKey, + getSeriesName, +} from "../lineCharts/lineChartHelpers" export interface SlopeChartManager extends ChartManager { isModalOpen?: boolean + canChangeEntity?: boolean canSelectMultipleEntities?: boolean } const TOP_PADDING = 6 const BOTTOM_PADDING = 20 +const LINE_LEGEND_PADDING = 4 + @observer export class SlopeChart extends React.Component<{ @@ -80,13 +95,12 @@ export class SlopeChart implements ChartInterface, ColorScaleManager { base: React.RefObject = React.createRef() + defaultBaseColorScheme = ColorSchemeName.OwidDistinctLines - // currently hovered individual series key - @observable hoverKey?: string - + @observable hoveredSeriesName?: string @observable tooltipState = new TooltipState<{ series: SlopeChartSeries - }>() + }>({ fade: "immediate" }) transformTable(table: OwidTable) { table = table.filterByEntityNames( @@ -109,50 +123,58 @@ export class SlopeChart } return table - - // TODO: re-enable? - // return table - // .dropRowsWithErrorValuesForColumn(this.yColumnSlug) - // .interpolateColumnWithTolerance(this.yColumnSlug) } transformTableForSelection(table: OwidTable): OwidTable { // if entities with partial data are not plotted, // make sure they don't show up in the entity selector if (this.missingDataStrategy === MissingDataStrategy.hide) { - table = table.replaceNonNumericCellsWithErrorValues( - this.yColumnSlugs - ) + table = table + .replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + .dropEntitiesThatHaveNoDataInSomeColumn(this.yColumnSlugs) + } + + return table + } + + @computed get transformedTableFromGrapher(): OwidTable { + return ( + this.manager.transformedTable ?? + this.transformTable(this.inputTable) + ) + } - table = table.dropEntitiesThatHaveNoDataInSomeColumn( - this.yColumnSlugs + @computed get transformedTable(): OwidTable { + let table = this.transformedTableFromGrapher + // The % growth transform cannot be applied in transformTable() because it will filter out + // any rows before startHandleTimeBound and change the timeline bounds. + const { isRelativeMode, startHandleTimeBound } = this.manager + if (isRelativeMode && startHandleTimeBound !== undefined) { + table = table.toTotalGrowthForEachColumnComparedToStartTime( + startHandleTimeBound, + this.yColumnSlugs ?? [] ) } - return table } - @computed get manager() { + @computed private get manager(): SlopeChartManager { return this.props.manager } - @computed.struct get bounds() { - return this.props.bounds ?? DEFAULT_BOUNDS + @computed get inputTable(): OwidTable { + return this.manager.table } - @computed get isStatic(): boolean { - return this.manager.isStatic ?? false + @computed private get bounds(): Bounds { + return this.props.bounds ?? DEFAULT_BOUNDS } @computed get fontSize() { return this.manager.fontSize ?? BASE_FONT_SIZE } - @computed private get isPortrait(): boolean { - return !!(this.manager.isNarrow || this.manager.isStaticAndSmall) - } - - @computed get isLogScale(): boolean { + @computed private get isLogScale(): boolean { return this.props.manager.yAxisConfig?.scaleType === ScaleType.log } @@ -160,350 +182,338 @@ export class SlopeChart return this.manager.missingDataStrategy || MissingDataStrategy.auto } - @action.bound onSlopeMouseOver(slopeProps: SlopeEntryProps) { - this.hoverKey = slopeProps.seriesName - this.tooltipState.target = { - series: slopeProps.series, - } + @computed private get selectionArray() { + return makeSelectionArray(this.manager.selection) } - @action.bound onSlopeMouseLeave() { - this.hoverKey = undefined - this.tooltipState.target = null + @computed private get formatColumn() { + return this.yColumns[0] } - @computed private get selectionArray() { - return makeSelectionArray(this.manager.selection) + @computed private get sidebarWidth(): number { + return this.showNoDataSection + ? clamp(this.bounds.width * 0.125, 60, 140) + : 0 } - @computed private get selectedEntityNames() { - return this.selectionArray.selectedEntityNames + // used by LineLegend + @computed get focusedSeriesNames(): SeriesName[] { + return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] } - @computed private get sidebarWidth(): number { - return Math.min(120, 0.15 * this.bounds.width) + @computed private get isFocusModeActive(): boolean { + return this.hoveredSeriesName !== undefined } - @computed private get innerBounds() { - const { sidebarWidth } = this - let bounds = this.bounds - if (this.showNoDataSection) { - bounds = bounds.padRight(sidebarWidth + 16) - } - return bounds + @computed private get startX(): number { + return this.xScale(this.startTime) } - @computed - private get selectedEntitiesWithoutData(): string[] { - return difference( - this.selectedEntityNames, - this.series.map((s) => s.seriesName) - ) + @computed private get endX(): number { + return this.xScale(this.endTime) } - @computed private get showNoDataSection(): boolean { - // TODO: for now, only show missing data section for entities - return ( - this.seriesStrategy === SeriesStrategy.entity && - this.selectedEntitiesWithoutData.length > 0 - ) + private updateTooltipPosition( + event: React.MouseEvent | React.TouchEvent + ) { + const ref = this.manager.base?.current + if (ref) this.tooltipState.position = getRelativeMouse(ref, event) } - @computed private get noDataSection(): React.ReactElement { - const bounds = new Bounds( - this.bounds.right - this.sidebarWidth, - this.bounds.top, - this.sidebarWidth, - this.bounds.height - ) + private detectHoveredSlope( + event: React.MouseEvent | React.TouchEvent + ) { + const ref = this.base.current + if (!ref) return + + const mouse = getRelativeMouse(ref, event) + this.mouseFrame = requestAnimationFrame(() => { + if (this.placedSeries.length === 0) return + + const distToSlope = new Map() + for (const series of this.placedSeries) { + distToSlope.set( + series, + PointVector.distanceFromPointToLineSegmentSq( + mouse, + series.startPoint, + series.endPoint + ) + ) + } + + const closestSlope = minBy(this.placedSeries, (s) => + distToSlope.get(s) + ) + const distanceSq = distToSlope.get(closestSlope!)! + const tolerance = 10 + const toleranceSq = tolerance * tolerance + + if (closestSlope && distanceSq < toleranceSq) { + this.onSlopeMouseOver(closestSlope) + } else { + this.onSlopeMouseLeave() + } + }) + } + + @computed get failMessage() { + const message = getDefaultFailMessage(this.manager) + if (message) return message + else if (isEmpty(this.series)) return "No matching data" + return "" + } + + @computed private get yColumns(): CoreColumn[] { + return this.yColumnSlugs.map((slug) => this.transformedTable.get(slug)) + } + + @computed protected get yColumnSlugs(): ColumnSlug[] { + return autoDetectYColumnSlugs(this.manager) + } + + @computed private get colorScheme(): ColorScheme { return ( - + (this.manager.baseColorScheme + ? ColorSchemes.get(this.manager.baseColorScheme) + : null) ?? ColorSchemes.get(this.defaultBaseColorScheme) ) } - // used by LineLegend - @computed get focusedSeriesNames(): string[] { - return this.hoverKey ? [this.hoverKey] : [] + @computed private get startTime(): Time { + return this.transformedTable.minTime } - // Layered mode occurs when any entity on the chart is hovered or focused - // Then, a special "foreground" set of entities is rendered over the background - @computed private get isLayerMode() { - return this.hoverKey !== undefined + @computed private get endTime(): Time { + return this.transformedTable.maxTime } - @computed private get formatColumn() { - return this.yColumns[0] + @computed get seriesStrategy(): SeriesStrategy { + return autoDetectSeriesStrategy(this.manager, true) } - @computed get allowedLabelWidth() { - return this.bounds.width * 0.2 + @computed private get categoricalColorAssigner(): CategoricalColorAssigner { + return new CategoricalColorAssigner({ + colorScheme: this.colorScheme, + invertColorScheme: this.manager.invertColorScheme, + colorMap: + this.seriesStrategy === SeriesStrategy.entity + ? this.inputTable.entityNameColorIndex + : this.inputTable.columnDisplayNameToColorMap, + autoColorMapCache: this.manager.seriesColorMap, + }) } - @computed get maxLabelWidth(): number { - // const maxLabelWidths = this.series.map((slope) => { - // const entityLabelWidth = slope.leftEntityLabel.width - // const maxValueLabelWidth = Math.max( - // slope.leftValueLabel.width, - // slope.rightValueLabel.width - // ) - // return ( - // entityLabelWidth + - // maxValueLabelWidth + - // LABEL_SLOPE_PADDING + - // LABEL_LABEL_PADDING - // ) - // }) - // return max(maxLabelWidths) ?? 0 - return 100 // TODO: remove? + @computed private get annotationsMap(): AnnotationsMap | undefined { + return getAnnotationsMap(this.inputTable, this.yColumnSlugs[0]) } - @computed private get initialSlopeData() { - const { series, xScale, yAxis, yDomain } = this + private constructSingleSeries( + entityName: EntityName, + column: CoreColumn + ): RawSlopeChartSeries | undefined { + const { startTime, endTime, seriesStrategy } = this + const { canSelectMultipleEntities = false } = this.manager + const { availableEntityNames } = this.transformedTable + + const columnName = column.nonEmptyDisplayName + const seriesName = getSeriesName({ + entityName, + columnName, + seriesStrategy, + availableEntityNames, + canSelectMultipleEntities, + }) + + const valueByTime = + column.valueByEntityNameAndOriginalTime.get(entityName) + const startValue = valueByTime?.get(startTime) + const endValue = valueByTime?.get(endTime) + + const colorKey = getColorKey({ + entityName, + columnName, + seriesStrategy, + availableEntityNames, + }) + const color = this.categoricalColorAssigner.assign(colorKey) + + const annotation = getAnnotationsForSeries( + this.annotationsMap, + seriesName + ) + + return { + seriesName, + color, + startValue, + endValue, + annotation, + } + } - const slopeData: SlopeEntryProps[] = [] + private isSeriesValid( + series: RawSlopeChartSeries + ): series is SlopeChartSeries { + return series.startValue !== undefined && series.endValue !== undefined + } - series.forEach((series) => { - // Ensure values fit inside the chart - if ( - !series.values.every( - (d) => d.y >= yDomain[0] && d.y <= yDomain[1] + @computed get rawSeries(): RawSlopeChartSeries[] { + return excludeUndefined( + this.yColumns.flatMap((column) => + column.uniqEntityNames.map((entityName) => + this.constructSingleSeries(entityName, column) ) ) - return - - const [v1, v2] = series.values - const [x1, x2] = [xScale(v1.x), xScale(v2.x)] - const [y1, y2] = [yAxis.place(v1.y), yAxis.place(v2.y)] - - slopeData.push({ - x1, - y1, - x2, - y2, - color: series.color, - series: series, - seriesName: series.seriesName, - isHovered: false, - } as SlopeEntryProps) + ) + } + + @computed get series(): SlopeChartSeries[] { + return this.rawSeries.filter(this.isSeriesValid) + } + + @computed private get placedSeries(): PlacedSlopeChartSeries[] { + const { yAxis, startX, endX } = this + + return this.series.map((series) => { + const startPoint = new PointVector( + startX, + yAxis.place(series.startValue) + ) + const endPoint = new PointVector(endX, yAxis.place(series.endValue)) + return { ...series, startPoint, endPoint } }) + } - return slopeData + @computed + private get noDataSeries(): RawSlopeChartSeries[] { + return this.rawSeries.filter((series) => !this.isSeriesValid(series)) } - mouseFrame?: number - @action.bound onMouseLeave() { - if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame) + @computed private get showNoDataSection(): boolean { + return this.noDataSeries.length > 0 + } - this.onSlopeMouseLeave() + @computed private get yAxisConfig(): AxisConfig { + return new AxisConfig(this.manager.yAxisConfig, this) } - @action.bound onMouseMove( - ev: React.MouseEvent | React.TouchEvent - ) { - const ref = this.manager.base?.current - if (ref) { - this.tooltipState.position = getRelativeMouse(ref, ev) - } + @computed private get allValues(): number[] { + return this.series.flatMap((series) => [ + series.startValue, + series.endValue, + ]) + } - if (this.base.current) { - const mouse = getRelativeMouse(this.base.current, ev.nativeEvent) - - this.mouseFrame = requestAnimationFrame(() => { - if (this.innerBounds.contains(mouse)) { - if (this.slopeData.length === 0) return - - const { x1: startX, x2: endX } = this.slopeData[0] - - // whether the mouse is over the chart area, - // the left label area, or the right label area - const mousePosition = - mouse.x < startX - ? "left" - : mouse.x > endX - ? "right" - : "chart" - - // don't track mouse movements when hovering over labels on the left or right - if (mousePosition === "left" || mousePosition === "right") { - this.onSlopeMouseLeave() - return - } - - const distToSlopeOrLabel = new Map< - SlopeEntryProps, - number - >() - for (const s of this.slopeData) { - // start and end point of a line - const p1 = new PointVector(s.x1, s.y1) - const p2 = new PointVector(s.x2, s.y2) - - // calculate the distance to the slope or label - const dist = - PointVector.distanceFromPointToLineSegmentSq( - mouse, - p1, - p2 - ) - distToSlopeOrLabel.set(s, dist) - } - - const closestSlope = minBy(this.slopeData, (s) => - distToSlopeOrLabel.get(s) - ) - const distanceSq = distToSlopeOrLabel.get(closestSlope!)! - const tolerance = mousePosition === "chart" ? 20 : 10 - const toleranceSq = tolerance * tolerance - - if (closestSlope && distanceSq < toleranceSq) { - this.onSlopeMouseOver(closestSlope) - } else { - this.onSlopeMouseLeave() - } - } - }) - } + @computed private get yScaleType(): ScaleType { + return this.yAxisConfig.scaleType ?? ScaleType.linear } - // Get the final slope data with hover focusing and collision detection - @computed get slopeData(): SlopeEntryProps[] { - let slopeData = this.initialSlopeData + @computed private get yDomainDefault(): [number, number] { + return domainExtent(this.allValues, this.yScaleType) + } - slopeData = slopeData.map((slope) => { - // used to determine priority for labelling conflicts - const isHovered = this.hoverKey === slope.seriesName + @computed private get yDomain(): [number, number] { + const domain = this.yAxisConfig.domain || [Infinity, -Infinity] + const domainDefault = this.yDomainDefault + return [ + Math.min(domain[0], domainDefault[0]), + Math.max(domain[1], domainDefault[1]), + ] + } - return { - ...slope, - isHovered, - } - }) + @computed get yRange(): [number, number] { + return this.bounds + .padTop(TOP_PADDING) + .padBottom(BOTTOM_PADDING) + .yRange() + } - // Order by focus/hover for draw order - slopeData = sortBy(slopeData, (slope) => (slope.isHovered ? 1 : 0)) + @computed get yAxis(): VerticalAxis { + const axis = this.yAxisConfig.toVerticalAxis() + axis.domain = this.yDomain + axis.range = this.yRange + axis.formatColumn = this.yColumns[0] + axis.label = "" + return axis + } - return slopeData + @computed get yAxisWidth(): number { + return this.yAxis.width + 5 // 5px account for the tick marks } - private renderGroups(groups: SlopeEntryProps[]) { - const { isLayerMode } = this + @computed private get xScale(): ScaleLinear { + const { xDomain, xRange } = this + return scaleLinear().domain(xDomain).range(xRange) + } - return groups.map((slope) => ( - - )) + @computed private get xDomain(): [number, number] { + return [this.startTime, this.endTime] } - private renderLabelledSlopes() { - const { bounds, slopeData, xDomain, yAxis, yRange, onMouseMove } = this + @computed private get maxLabelWidth(): number { + // TODO: copied from line legend + const fontSize = + GRAPHER_FONT_SCALE_12 * (this.manager.fontSize ?? BASE_FONT_SIZE) + return max( + this.series.map( + (series) => + Bounds.forText(series.seriesName, { fontSize }).width + ) + )! + } - if (isEmpty(slopeData)) - return + @computed get maxLineLegendWidth(): number { + // todo: copied from line legend (left padding, marker margin) + return Math.min(this.maxLabelWidth + 35 + 4, this.bounds.width / 3) + } - const { x1, x2 } = slopeData[0] - const [y1, y2] = yRange + @computed get xRange(): [number, number] { + const lineLegendWidth = this.maxLineLegendWidth + LINE_LEGEND_PADDING - return ( - - - - {this.yAxis.tickLabels.map((tick) => { - const y = yAxis.place(tick.value) - return ( - - {/* grid lines connecting the chart area to the axis */} - - {/* grid lines within the chart area */} - - - ) - })} - - - - - - {this.formatColumn.formatTime(xDomain[0])} - - - {this.formatColumn.formatTime(xDomain[1])} - - - {this.renderGroups(this.backgroundGroups)} - {this.renderGroups(this.foregroundGroups)} - - - ) + // pick a reasonable width based on an ideal aspect ratio + const idealAspectRatio = 0.6 + const chartAreaWidth = this.bounds.width - this.sidebarWidth + const availableWidth = + chartAreaWidth - this.yAxisWidth - lineLegendWidth + const idealWidth = idealAspectRatio * this.bounds.height + const maxSlopeWidth = Math.min(idealWidth, availableWidth) + + let startX = + this.bounds.x + Math.max(0.25 * chartAreaWidth, this.yAxisWidth + 4) + let endX = + this.bounds.x + + Math.min( + chartAreaWidth - 0.25 * chartAreaWidth, + chartAreaWidth - lineLegendWidth + ) + + const currentSlopeWidth = endX - startX + if (currentSlopeWidth > maxSlopeWidth) { + const padding = currentSlopeWidth - maxSlopeWidth + startX += padding / 2 + endX -= padding / 2 + } + + return [startX, endX] } - @computed get backgroundGroups() { - return this.slopeData.filter((group) => !group.isHovered) + @computed get lineLegendX(): number { + return this.xRange[1] + LINE_LEGEND_PADDING } - @computed get foregroundGroups() { - return this.slopeData.filter((group) => !!group.isHovered) + // used in LineLegend + @computed get labelSeries(): LineLabelSeries[] { + return this.series.map((series) => { + const { seriesName, color, endValue, annotation } = series + return { + color, + seriesName, + label: seriesName, + annotation, + yValue: endValue, + } + }) } private playIntroAnimation() { @@ -516,22 +526,68 @@ export class SlopeChart .attr("stroke-dashoffset", "0%") } - @computed get renderUid(): number { - return guid() + componentDidMount() { + exposeInstanceOnWindow(this) + + if (!this.manager.disableIntroAnimation) { + this.playIntroAnimation() + } } - @computed get tooltip(): React.ReactElement | undefined { - const { - tooltipState: { target, position, fading }, - } = this + private hoverTimer?: NodeJS.Timeout + @action.bound onLineLegendMouseOver(seriesName: SeriesName): void { + clearTimeout(this.hoverTimer) + this.hoveredSeriesName = seriesName + } - const { series } = target || {} - if (!series) return + @action.bound onLineLegendMouseLeave(): void { + clearTimeout(this.hoverTimer) + this.hoverTimer = setTimeout(() => { + // wait before clearing selection in case the mouse is moving quickly over neighboring labels + this.hoveredSeriesName = undefined + }, 200) + } - const { isRelativeMode } = this.manager, - timeRange = [this.startTime, this.endTime] - .map((t) => this.formatColumn.formatTime(t)) - .join(" to "), + @action.bound onSlopeMouseOver(series: SlopeChartSeries) { + this.hoveredSeriesName = series.seriesName + this.tooltipState.target = { series } + } + + @action.bound onSlopeMouseLeave() { + this.hoveredSeriesName = undefined + this.tooltipState.target = null + } + + mouseFrame?: number + @action.bound onMouseMove( + ev: React.MouseEvent | React.TouchEvent + ) { + this.updateTooltipPosition(ev) + this.detectHoveredSlope(ev) + } + + @action.bound onMouseLeave() { + if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame) + + this.onSlopeMouseLeave() + } + + @computed get renderUid(): number { + return guid() + } + + @computed get tooltip(): React.ReactElement | undefined { + const { + tooltipState: { target, position, fading }, + } = this + + const { series } = target || {} + if (!series) return + + const { isRelativeMode } = this.manager, + timeRange = [this.startTime, this.endTime] + .map((t) => this.formatColumn.formatTime(t)) + .join(" to "), timeLabel = timeRange + (isRelativeMode ? " (relative change)" : "") const columns = this.yColumns @@ -578,406 +634,267 @@ export class SlopeChart > v.y)} + values={[series.startValue, series.endValue]} /> ) } - render() { - if (this.failMessage) - return ( - - ) - - const { manager } = this.props - - return ( - - {this.renderLabelledSlopes()} - {manager.showLegend && } - {this.showNoDataSection && this.noDataSection} - {this.tooltip} - + private renderNoDataSection(): React.ReactElement { + const seriesNames = this.noDataSeries.map((series) => series.seriesName) + const bounds = new Bounds( + this.bounds.right - this.sidebarWidth, + this.bounds.top, + this.sidebarWidth, + this.bounds.height ) - } - @computed get failMessage() { - const message = getDefaultFailMessage(this.manager) - if (message) return message - else if (isEmpty(this.series)) return "No matching data" - return "" - } - - defaultBaseColorScheme = ColorSchemeName.OwidDistinctLines - - @computed private get yColumns(): CoreColumn[] { - return this.yColumnSlugs.map((slug) => this.transformedTable.get(slug)) - } - - @computed protected get yColumnSlugs(): ColumnSlug[] { - return autoDetectYColumnSlugs(this.manager) - } - - @computed get transformedTableFromGrapher(): OwidTable { return ( - this.manager.transformedTable ?? - this.transformTable(this.inputTable) + ) } - @computed get transformedTable(): OwidTable { - let table = this.transformedTableFromGrapher - // The % growth transform cannot be applied in transformTable() because it will filter out - // any rows before startHandleTimeBound and change the timeline bounds. - const { isRelativeMode, startHandleTimeBound } = this.manager - if (isRelativeMode && startHandleTimeBound !== undefined) { - table = table.toTotalGrowthForEachColumnComparedToStartTime( - startHandleTimeBound, - this.yColumnSlugs ?? [] - ) - } - return table - } - - @computed get inputTable() { - return this.manager.table - } - - componentDidMount() { - exposeInstanceOnWindow(this) - - if (!this.manager.disableIntroAnimation) { - this.playIntroAnimation() - } - } - - @computed private get colorScheme(): ColorScheme { + private renderSlope( + series: PlacedSlopeChartSeries, + mode?: RenderMode + ): React.ReactElement { return ( - (this.manager.baseColorScheme - ? ColorSchemes.get(this.manager.baseColorScheme) - : null) ?? ColorSchemes.get(this.defaultBaseColorScheme) + ) } - @computed private get startTime(): Time { - return this.transformedTable.minTime - } - - @computed private get endTime(): Time { - return this.transformedTable.maxTime! // TODO: remove the ! when we have a better way to handle missing maxTime - } - - @computed get seriesStrategy(): SeriesStrategy { - return autoDetectSeriesStrategy(this.manager, true) - } - - @computed private get categoricalColorAssigner(): CategoricalColorAssigner { - return new CategoricalColorAssigner({ - colorScheme: this.colorScheme, - invertColorScheme: this.manager.invertColorScheme, - colorMap: - this.seriesStrategy === SeriesStrategy.entity - ? this.inputTable.entityNameColorIndex - : this.inputTable.columnDisplayNameToColorMap, - autoColorMapCache: this.manager.seriesColorMap, - }) - } - - private getSeriesName( - entityName: EntityName, - columnName: string, - entityCount: number - ): SeriesName { - if (this.seriesStrategy === SeriesStrategy.entity) { - return entityName - } - if (entityCount > 1 || this.manager.canSelectMultipleEntities) { - return `${entityName} - ${columnName}` - } else { - return columnName - } - } - - // todo: for now just works with 1 y column - @computed private get annotationsMap(): Map< - PrimitiveType, - Set - > { - return this.inputTable - .getAnnotationColumnForColumn(this.yColumnSlugs[0]) - ?.getUniqueValuesGroupedBy(this.inputTable.entityNameSlug) - } - - private getAnnotationsForSeries( - seriesName: SeriesName - ): string | undefined { - const annotationsMap = this.annotationsMap - const annos = annotationsMap?.get(seriesName) - return annos - ? Array.from(annos.values()) - .filter((anno) => anno) - .join(" & ") - : undefined - } - - private getColorKey( - entityName: EntityName, - columnName: string, - entityCount: number - ): SeriesName { - if (this.seriesStrategy === SeriesStrategy.entity) { - return entityName + private renderSlopes() { + if (!this.isFocusModeActive) { + return this.placedSeries.map((series) => this.renderSlope(series)) } - // If only one entity is plotted, we want to use the column colors. - // Unlike in `getSeriesName`, we don't care whether the user can select - // multiple entities, only whether more than one is plotted. - if (entityCount > 1) { - return `${entityName} - ${columnName}` - } else { - return columnName - } - } - - @computed get series() { - const { startTime, endTime } = this - const totalEntityCount = - this.transformedTable.availableEntityNames.length - return this.yColumns.flatMap((column) => - column.uniqEntityNames - .map((entityName) => { - const seriesName = this.getSeriesName( - entityName, - column.displayName || "Missing name", - totalEntityCount - ) - const values: SlopeChartValue[] = [] - - const yValues = - column.valueByEntityNameAndOriginalTime.get( - entityName - )! || [] - - yValues.forEach((value, time) => { - if (time !== startTime && time !== endTime) return - - values.push({ - x: time, - y: value, - }) - }) - - // sort values by time - const sortedValues = sortBy(values, (v) => v.x) - - const color = this.categoricalColorAssigner.assign( - this.getColorKey( - entityName, - column.displayName, - totalEntityCount - ) - ) - - const annotation = this.getAnnotationsForSeries(seriesName) - - return { - seriesName, - color, - values: sortedValues, - annotation, - } as SlopeChartSeries - }) - .filter((series) => series.values.length >= 2) + const [focusedSeries, backgroundSeries] = partition( + this.placedSeries, + (series) => series.seriesName === this.hoveredSeriesName ) - } - - @observable private hoverTimer?: NodeJS.Timeout - @action.bound onLineLegendMouseOver(seriesName: SeriesName): void { - clearTimeout(this.hoverTimer) - this.hoverKey = seriesName - } - - @action.bound clearHighlightedSeries(): void { - clearTimeout(this.hoverTimer) - this.hoverTimer = setTimeout(() => { - // wait before clearing selection in case the mouse is moving quickly over neighboring labels - this.hoverKey = undefined - }, 200) - } - - @action.bound onLineLegendMouseLeave(): void { - this.clearHighlightedSeries() - } - - @computed private get yAxisConfig(): AxisConfig { - return new AxisConfig(this.manager.yAxisConfig, this) - } - - @computed private get allValues() { - return this.series.flatMap((g) => g.values) - } - - @computed private get yScaleType() { - return this.yAxisConfig.scaleType || ScaleType.linear - } - - @computed private get yDomainDefault(): [number, number] { - return domainExtent( - this.allValues.map((v) => v.y), - this.yScaleType || ScaleType.linear + return ( + <> + {backgroundSeries.map((series) => + this.renderSlope(series, RenderMode.mute) + )} + {focusedSeries.map((series) => + this.renderSlope(series, RenderMode.focus) + )} + ) } - @computed private get yDomain(): [number, number] { - const domain = this.yAxisConfig.domain || [Infinity, -Infinity] - const domainDefault = this.yDomainDefault - return [ - Math.min(domain[0], domainDefault[0]), - Math.max(domain[1], domainDefault[1]), - ] - } - - @computed get yRange(): [number, number] { - return this.bounds - .padTop(TOP_PADDING) - .padBottom(BOTTOM_PADDING) - .yRange() - } - - @computed get yAxis(): VerticalAxis { - const axis = this.yAxisConfig.toVerticalAxis() - axis.domain = this.yDomain - axis.range = this.yRange - axis.formatColumn = this.yColumns[0] - axis.label = "" - return axis - } - - @computed get yAxisWidth(): number { - return this.yAxis.width + 5 // 5px account for the tick marks - } - - @computed get xRange(): [number, number] { - // take into account the space taken by the yAxis and slope labels - const bounds = this.bounds - .padLeft(this.yAxisWidth + 4) - .padLeft(this.maxLabelWidth) - .padRight(this.maxLabelWidth) - - // pick a reasonable width based on an ideal aspect ratio - const idealAspectRatio = 0.9 - const availableWidth = bounds.width - const idealWidth = idealAspectRatio * bounds.height - const slopeWidth = this.isPortrait - ? availableWidth - : clamp(idealWidth, 220, availableWidth) - - const leftRightPadding = (availableWidth - slopeWidth) / 2 - return bounds - .padLeft(leftRightPadding) - .padRight(leftRightPadding) - .xRange() - } + private renderChartArea() { + const { bounds, xDomain, yRange, startX, endX } = this - @computed private get xScale(): ScaleLinear { - const { xDomain, xRange } = this - return scaleLinear().domain(xDomain).range(xRange) - } + const [bottom, top] = yRange - @computed private get xDomain(): [number, number] { - return this.xDomainDefault - } - - @computed private get xDomainDefault(): [number, number] { - return domainExtent( - this.allValues.map((v) => v.x), - ScaleType.linear + return ( + + + + + + + + + {this.renderSlopes()} + + ) } - @computed get lineLegendX(): number { - return this.bounds.right - 240 - } + render() { + if (this.failMessage) + return ( + + ) - @computed get labelSeries(): LineLabelSeries[] { - return this.series.map((series) => { - const { seriesName, color, values, annotation } = series - return { - color, - seriesName, - label: seriesName, - annotation, - yValue: values[1].y, - } - }) + return ( + + {this.renderChartArea()} + {this.manager.showLegend && } + {this.showNoDataSection && this.renderNoDataSection()} + {this.tooltip} + + ) } } -@observer -class SlopeEntry extends React.Component { - line: SVGElement | null = null - - @computed get isInBackground() { - const { isLayerMode, isHovered } = this.props - - if (!isLayerMode) return false - - return !isHovered - } - - render() { - const { x1, y1, x2, y2, color, isHovered, seriesName } = this.props - const { isInBackground } = this +interface SlopeProps { + series: PlacedSlopeChartSeries + color: string + mode?: RenderMode + onMouseOver?: (series: SlopeChartSeries) => void + onMouseLeave?: () => void +} - const lineColor = isInBackground ? "#e2e2e2" : color - const opacity = isHovered ? 1 : 0.5 - const lineStrokeWidth = isHovered ? 4 : 2 +function Slope({ + series, + color, + mode = RenderMode.default, + onMouseOver, + onMouseLeave, +}: SlopeProps) { + const { seriesName, startPoint, endPoint } = series + + const usedColor = { + [RenderMode.default]: color, + [RenderMode.focus]: color, + [RenderMode.mute]: "#e2e2e2", + [RenderMode.background]: "#e2e2e2", + }[mode] + + return ( + onMouseOver?.(series)} + onMouseLeave={() => onMouseLeave?.()} + > + + + + + ) +} - const showDots = isHovered +interface GridLinesProps { + bounds: Bounds + yAxis: VerticalAxis + startX: number + endX: number +} - return ( - - (this.line = el)} - x1={x1} - y1={y1} - x2={x2} - y2={y2} - stroke={lineColor} - strokeWidth={lineStrokeWidth} - opacity={opacity} - /> - {showDots && ( - <> - + {yAxis.tickLabels.map((tick) => { + const y = yAxis.place(tick.value) + return ( + + {/* grid lines connecting the chart area to the axis */} + - - - )} - - ) - } + + ) + })} + + ) +} + +function MarkX({ + label, + x, + top, + bottom, + fontSize, +}: { + label: string + x: number + top: number + bottom: number + fontSize: number +}) { + return ( + <> + + + {label} + + + ) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 9b27bf3110b..290259fd0f0 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -1,51 +1,20 @@ -import { CoreColumn } from "@ourworldindata/core-table" +import { PartialBy, PointVector } from "@ourworldindata/utils" import { ChartSeries } from "../chart/ChartInterface" -import { ChartManager } from "../chart/ChartManager" -import { ScaleType } from "@ourworldindata/types" -import { Bounds } from "@ourworldindata/utils" - -export interface SlopeChartValue { - x: number - y: number -} export interface SlopeChartSeries extends ChartSeries { - size: number - values: SlopeChartValue[] + startValue: number + endValue: number annotation?: string } -export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" - -export interface SlopeEntryProps extends ChartSeries { - series: SlopeChartSeries - - x1: number - y1: number - x2: number - y2: number +export type RawSlopeChartSeries = PartialBy< + SlopeChartSeries, + "startValue" | "endValue" +> - isLayerMode: boolean - isHovered: boolean +export interface PlacedSlopeChartSeries extends SlopeChartSeries { + startPoint: PointVector + endPoint: PointVector } -export interface LabelledSlopesProps { - manager: ChartManager - formatColumn: CoreColumn - bounds: Bounds - seriesArr: SlopeChartSeries[] - hoverKey?: string - onMouseOver: (slopeProps: SlopeEntryProps) => void - onMouseLeave: () => void - onClick?: () => void - isPortrait: boolean -} - -export interface SlopeAxisProps { - bounds: Bounds - orient: "left" | "right" - column: CoreColumn - scale: any - scaleType: ScaleType - fontSize: number -} +export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index e218c1950c9..58eb7020266 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -18,7 +18,7 @@ import { max, } from "@ourworldindata/utils" import { computed, action, observable } from "mobx" -import { SeriesName } from "@ourworldindata/types" +import { RenderMode, SeriesName } from "@ourworldindata/types" import { GRAPHER_AREA_OPACITY_DEFAULT, GRAPHER_AREA_OPACITY_MUTE, @@ -68,21 +68,21 @@ interface AreasProps extends React.SVGAttributes { const STACKED_AREA_CHART_CLASS_NAME = "StackedArea" -const AREA_OPACITY = { - DEFAULT: GRAPHER_AREA_OPACITY_DEFAULT, - FOCUS: GRAPHER_AREA_OPACITY_FOCUS, - MUTE: GRAPHER_AREA_OPACITY_MUTE, +const AREA_OPACITY: Partial> = { + default: GRAPHER_AREA_OPACITY_DEFAULT, + focus: GRAPHER_AREA_OPACITY_FOCUS, + mute: GRAPHER_AREA_OPACITY_MUTE, } -const BORDER_OPACITY = { - DEFAULT: 0.7, - HOVER: 1, - MUTE: 0.3, +const BORDER_OPACITY: Partial> = { + default: 0.7, + focus: 1, + mute: 0.3, } -const BORDER_WIDTH = { - DEFAULT: 0.5, - HOVER: 1.5, +const BORDER_WIDTH: Partial> = { + default: 0.5, + mute: 1.5, } @observer @@ -183,10 +183,10 @@ class Areas extends React.Component { } const points = [...placedPoints, ...reverse(clone(prevPoints))] const opacity = !this.isFocusModeActive - ? AREA_OPACITY.DEFAULT // normal opacity + ? AREA_OPACITY.default // normal opacity : focusedSeriesName === series.seriesName - ? AREA_OPACITY.FOCUS // hovered - : AREA_OPACITY.MUTE // non-hovered + ? AREA_OPACITY.focus // hovered + : AREA_OPACITY.mute // non-hovered return ( { return placedSeriesArr.map((placedSeries) => { const opacity = !this.isFocusModeActive - ? BORDER_OPACITY.DEFAULT // normal opacity + ? BORDER_OPACITY.default // normal opacity : focusedSeriesName === placedSeries.seriesName - ? BORDER_OPACITY.HOVER // hovered - : BORDER_OPACITY.MUTE // non-hovered + ? BORDER_OPACITY.focus // hovered + : BORDER_OPACITY.mute // non-hovered const strokeWidth = focusedSeriesName === placedSeries.seriesName - ? BORDER_WIDTH.HOVER - : BORDER_WIDTH.DEFAULT + ? BORDER_WIDTH.focus + : BORDER_WIDTH.default return ( Date: Thu, 21 Nov 2024 14:36:34 +0100 Subject: [PATCH 12/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20remove=20color?= =?UTF-8?q?=20dimension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorBasicTab.tsx | 7 ++--- ...407-RemoveColorDimensionFromSlopeCharts.ts | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts diff --git a/adminSiteClient/EditorBasicTab.tsx b/adminSiteClient/EditorBasicTab.tsx index 9e3464f4d9f..fa60a83d82d 100644 --- a/adminSiteClient/EditorBasicTab.tsx +++ b/adminSiteClient/EditorBasicTab.tsx @@ -372,8 +372,8 @@ export class EditorBasicTab< grapher.stackMode = StackMode.relative } - // Give scatterplots and slope charts a default color dimension if they don't have one - if (grapher.isScatter || grapher.isSlopeChart) { + // Give scatterplots a default color and size dimensions + if (grapher.isScatter) { const hasColor = grapher.dimensions.find( (d) => d.property === DimensionProperty.color ) @@ -382,10 +382,7 @@ export class EditorBasicTab< variableId: CONTINENTS_INDICATOR_ID, property: DimensionProperty.color, }) - } - // Give scatterplots a default size dimension if they don't have one - if (grapher.isScatter) { const hasSize = grapher.dimensions.find( (d) => d.property === DimensionProperty.size ) diff --git a/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts b/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts new file mode 100644 index 00000000000..bbd12966c7d --- /dev/null +++ b/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class RemoveColorDimensionFromSlopeCharts1732195571407 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // update dimensions field in chart configs + await queryRunner.query(` + -- sql + UPDATE chart_configs + SET + patch = JSON_REPLACE(patch, '$.dimensions', JSON_ARRAY(patch -> '$.dimensions[0]')), + full = JSON_REPLACE(full, '$.dimensions', JSON_ARRAY(full -> '$.dimensions[0]')) + WHERE + chartType = 'SlopeChart' + `) + + // remove from chart_dimensions table + await queryRunner.query(` + -- sql + DELETE cd FROM chart_dimensions cd + JOIN charts c ON c.id = cd.chartId + JOIN chart_configs cc ON c.configId = cc.id + WHERE cc.chartType = 'SlopeChart' AND cd.property = 'color' + `) + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async down(): Promise {} +} From e1491a3af703edcbf56d74d2d211d462e486a72d Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 21 Nov 2024 15:38:37 +0100 Subject: [PATCH 13/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20allow=20to=20hide?= =?UTF-8?q?=20legend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorFeatures.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/adminSiteClient/EditorFeatures.tsx b/adminSiteClient/EditorFeatures.tsx index 2d9761add1c..d7e16563dee 100644 --- a/adminSiteClient/EditorFeatures.tsx +++ b/adminSiteClient/EditorFeatures.tsx @@ -62,6 +62,7 @@ export class EditorFeatures { @computed get hideLegend() { return ( this.grapher.isLineChart || + this.grapher.isSlopeChart || this.grapher.isStackedArea || this.grapher.isStackedDiscreteBar ) From 67744109a4fc628dc9b365e40faa32b7a93fbe9d Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 21 Nov 2024 15:39:12 +0100 Subject: [PATCH 14/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20fix=20start/end?= =?UTF-8?q?=20time=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/core/Grapher.tsx | 8 ++++---- .../grapher/src/slopeCharts/SlopeChart.tsx | 14 ++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index e350b0d7e9b..7f9673c9208 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1645,7 +1645,7 @@ export class Grapher if (this.shouldAddChangeInPrefixToTitle) text = "Change in " + lowerCaseFirstLetterUnlessAbbreviation(text) - if (this.shouldAddTimeSuffixToTitle) + if (this.shouldAddTimeSuffixToTitle && this.timeTitleSuffix) text = appendAnnotationField(text, this.timeTitleSuffix) return text.trim() @@ -1748,11 +1748,11 @@ export class Grapher return this.xAxis.scaleType } - @computed private get timeTitleSuffix(): string { + @computed private get timeTitleSuffix(): string | undefined { const timeColumn = this.table.timeColumn - if (timeColumn.isMissing) return "" // Do not show year until data is loaded + if (timeColumn.isMissing) return undefined // Do not show year until data is loaded const { startTime, endTime } = this - if (startTime === undefined || endTime === undefined) return "" + if (startTime === undefined || endTime === undefined) return undefined const time = startTime === endTime diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 2a54bcf579a..bdd4b3efabc 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -281,11 +281,11 @@ export class SlopeChart } @computed private get startTime(): Time { - return this.transformedTable.minTime + return this.manager.startTime! } @computed private get endTime(): Time { - return this.transformedTable.maxTime + return this.manager.endTime! } @computed get seriesStrategy(): SeriesStrategy { @@ -311,7 +311,7 @@ export class SlopeChart private constructSingleSeries( entityName: EntityName, column: CoreColumn - ): RawSlopeChartSeries | undefined { + ): RawSlopeChartSeries { const { startTime, endTime, seriesStrategy } = this const { canSelectMultipleEntities = false } = this.manager const { availableEntityNames } = this.transformedTable @@ -359,11 +359,9 @@ export class SlopeChart } @computed get rawSeries(): RawSlopeChartSeries[] { - return excludeUndefined( - this.yColumns.flatMap((column) => - column.uniqEntityNames.map((entityName) => - this.constructSingleSeries(entityName, column) - ) + return this.yColumns.flatMap((column) => + this.selectionArray.selectedEntityNames.map((entityName) => + this.constructSingleSeries(entityName, column) ) ) } From 3fc555ac0df1193adf6d0f5f8147ae9ddfbe3e55 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 21 Nov 2024 15:56:32 +0100 Subject: [PATCH 15/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20error=20out=20wh?= =?UTF-8?q?en=20start=20and=20end=20time=20are=20the=20same?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index bdd4b3efabc..7dc29b809e7 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -260,6 +260,7 @@ export class SlopeChart @computed get failMessage() { const message = getDefaultFailMessage(this.manager) if (message) return message + else if (this.startTime === this.endTime) return "No matching data" else if (isEmpty(this.series)) return "No matching data" return "" } From 9714ebe9582716be7ff6f97e3ef9c9e7c23ca556 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 13:34:19 +0100 Subject: [PATCH 16/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20fix=20No=20Data?= =?UTF-8?q?=20section=20when=20missing=20data=20strategy=20is=20hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorFeatures.tsx | 4 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 72 ++++++++++++++----- .../src/slopeCharts/SlopeChartConstants.ts | 3 +- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/adminSiteClient/EditorFeatures.tsx b/adminSiteClient/EditorFeatures.tsx index d7e16563dee..70adfd194bf 100644 --- a/adminSiteClient/EditorFeatures.tsx +++ b/adminSiteClient/EditorFeatures.tsx @@ -119,9 +119,9 @@ export class EditorFeatures { return true } - // for line charts, specifying a missing data strategy only makes sense + // for line and slope charts, specifying a missing data strategy only makes sense // if there are multiple entities - if (this.grapher.isLineChart) { + if (this.grapher.isLineChart || this.grapher.isSlopeChart) { return ( this.grapher.canChangeEntity || this.grapher.canSelectMultipleEntities diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 7dc29b809e7..b2f3cf0f243 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -113,15 +113,6 @@ export class SlopeChart if (this.isLogScale) table = table.replaceNonPositiveCellsForLogScale(this.yColumnSlugs) - // drop all data when the author chose to hide entities with missing data and - // at least one of the variables has no data for the current entity - if ( - this.missingDataStrategy === MissingDataStrategy.hide && - table.hasAnyColumnNoValidValue(this.yColumnSlugs) - ) { - table = table.dropAllRows() - } - return table } @@ -315,8 +306,8 @@ export class SlopeChart ): RawSlopeChartSeries { const { startTime, endTime, seriesStrategy } = this const { canSelectMultipleEntities = false } = this.manager - const { availableEntityNames } = this.transformedTable + const { availableEntityNames } = this.selectionArray const columnName = column.nonEmptyDisplayName const seriesName = getSeriesName({ entityName, @@ -346,6 +337,7 @@ export class SlopeChart return { seriesName, + entityName, color, startValue, endValue, @@ -359,7 +351,35 @@ export class SlopeChart return series.startValue !== undefined && series.endValue !== undefined } - @computed get rawSeries(): RawSlopeChartSeries[] { + /** + * Usually we drop rows with missing data in the transformTable function. + * But slope charts have a "No data" section. If slopes that have data + * but shouldn't be plotted because a "sibling" slope of the same entity + * doesn't have data are dropped from the transformed table, then we + * would have no way of knowing whether a slope has been dropped because + * it actually had no data or a sibling slope had no data. That's why we + * filter out slopes that are valid but shouldn't be plotted here, so + * that the noDataSeries is populated correctly. + */ + private shouldSeriesBePlotted( + series: RawSlopeChartSeries + ): series is SlopeChartSeries { + if (!this.isSeriesValid(series)) return false + + if ( + this.seriesStrategy === SeriesStrategy.column && + this.missingDataStrategy === MissingDataStrategy.hide + ) { + const entitySeries = this.rawSeriesByEntityName.get( + series.entityName + ) + return !!entitySeries?.every((series) => this.isSeriesValid(series)) + } + + return true + } + + @computed private get rawSeries(): RawSlopeChartSeries[] { return this.yColumns.flatMap((column) => this.selectionArray.selectedEntityNames.map((entityName) => this.constructSingleSeries(entityName, column) @@ -367,19 +387,35 @@ export class SlopeChart ) } - @computed get series(): SlopeChartSeries[] { - return this.rawSeries.filter(this.isSeriesValid) + @computed private get rawSeriesByEntityName(): Map< + SeriesName, + RawSlopeChartSeries[] + > { + const map = new Map() + this.rawSeries.forEach((series) => { + const { entityName } = series + if (!map.has(entityName)) map.set(entityName, []) + map.get(entityName)!.push(series) + }) + return map + } + + @computed private get series(): SlopeChartSeries[] { + return this.rawSeries.filter((series) => + this.shouldSeriesBePlotted(series) + ) } @computed private get placedSeries(): PlacedSlopeChartSeries[] { const { yAxis, startX, endX } = this return this.series.map((series) => { - const startPoint = new PointVector( - startX, - yAxis.place(series.startValue) - ) - const endPoint = new PointVector(endX, yAxis.place(series.endValue)) + const startY = yAxis.place(series.startValue) + const endY = yAxis.place(series.endValue) + + const startPoint = new PointVector(startX, startY) + const endPoint = new PointVector(endX, endY) + return { ...series, startPoint, endPoint } }) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 290259fd0f0..544410718b7 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -1,7 +1,8 @@ -import { PartialBy, PointVector } from "@ourworldindata/utils" +import { EntityName, PartialBy, PointVector } from "@ourworldindata/utils" import { ChartSeries } from "../chart/ChartInterface" export interface SlopeChartSeries extends ChartSeries { + entityName: EntityName startValue: number endValue: number annotation?: string From 932cc9d04ee853af73256ad27cc9f3e3969f9869 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 13:43:17 +0100 Subject: [PATCH 17/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20fix=20tabs=20for?= =?UTF-8?q?=20line=20chart=20that=20turned=20into=20discrete=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/captionedChart/CaptionedChart.tsx | 4 ++-- .../grapher/src/controls/ContentSwitchers.tsx | 16 ++++++++++++---- .../@ourworldindata/grapher/src/core/Grapher.tsx | 8 +++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx index 2238dc78e8f..23fabd65955 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx @@ -83,7 +83,7 @@ export interface CaptionedChartManager isOnMapTab?: boolean isOnTableTab?: boolean activeChartType?: GrapherChartType - isLineChartThatTurnedIntoDiscreteBar?: boolean + isLineChartThatTurnedIntoDiscreteBarActive?: boolean showEntitySelectionToggle?: boolean isExportingForSocialMedia?: boolean @@ -197,7 +197,7 @@ export class CaptionedChart extends React.Component { if (manager.isOnTableTab) return undefined if (manager.isOnMapTab) return GRAPHER_MAP_TYPE if (manager.isOnChartTab) { - return manager.isLineChartThatTurnedIntoDiscreteBar + return manager.isLineChartThatTurnedIntoDiscreteBarActive ? GRAPHER_CHART_TYPES.DiscreteBar : manager.activeChartType } diff --git a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx index 6c0eaa289a5..8d947e5bb62 100644 --- a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx +++ b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx @@ -175,9 +175,11 @@ function TabIcon({ case GRAPHER_TAB_NAMES.WorldMap: return default: - const chartIcon = isLineChartThatTurnedIntoDiscreteBar - ? chartIcons[GRAPHER_CHART_TYPES.DiscreteBar] - : chartIcons[tab] + const chartIcon = + tab === GRAPHER_TAB_NAMES.LineChart && + isLineChartThatTurnedIntoDiscreteBar + ? chartIcons[GRAPHER_CHART_TYPES.DiscreteBar] + : chartIcons[tab] return chartIcon } } @@ -193,9 +195,15 @@ function makeTabLabelText( if (tab === GRAPHER_TAB_NAMES.WorldMap) return "Map" if (!options.hasMultipleChartTypes) return "Chart" + if ( + tab === GRAPHER_TAB_NAMES.LineChart && + options.isLineChartThatTurnedIntoDiscreteBar + ) + return "Bar" + switch (tab) { case GRAPHER_TAB_NAMES.LineChart: - return options.isLineChartThatTurnedIntoDiscreteBar ? "Bar" : "Line" + return "Line" case GRAPHER_TAB_NAMES.SlopeChart: return "Slope" diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 7f9673c9208..3b81f3ef2ec 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1944,7 +1944,7 @@ export class Grapher @computed get typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart(): GrapherChartType { - return this.isLineChartThatTurnedIntoDiscreteBar + return this.isLineChartThatTurnedIntoDiscreteBarActive ? GRAPHER_CHART_TYPES.DiscreteBar : (this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart) } @@ -2003,6 +2003,12 @@ export class Grapher return closestMinTime !== undefined && closestMinTime === closestMaxTime } + @computed get isLineChartThatTurnedIntoDiscreteBarActive(): boolean { + return ( + this.isOnLineChartTab && this.isLineChartThatTurnedIntoDiscreteBar + ) + } + @computed get isOnLineChartTab(): boolean { return this.activeChartType === GRAPHER_CHART_TYPES.LineChart } From 940355b37f2c0388157b4e9012a090db947a2228 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 13:43:52 +0100 Subject: [PATCH 18/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20fix=20entity=20s?= =?UTF-8?q?elector=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 3b81f3ef2ec..c514aedcd2e 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -3558,6 +3558,7 @@ export class Grapher this.hasChartTab && this.canSelectMultipleEntities && (this.isOnLineChartTab || + this.isOnSlopeChartTab || this.isOnStackedAreaTab || this.isOnStackedBarTab || this.isOnDiscreteBarTab || From 5193febe65acd78ca8cdf2a38b652bb9026c9f80 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 13:44:23 +0100 Subject: [PATCH 19/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...407-RemoveColorDimensionFromSlopeCharts.ts | 6 ++- .../core-table/src/OwidTable.ts | 4 +- .../src/barCharts/DiscreteBarChart.tsx | 14 ++++-- .../grapher/src/lineCharts/LineChart.tsx | 1 - .../grapher/src/slopeCharts/SlopeChart.tsx | 48 ++++++++----------- 5 files changed, 36 insertions(+), 37 deletions(-) diff --git a/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts b/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts index bbd12966c7d..1750d7e2576 100644 --- a/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts +++ b/db/migration/1732195571407-RemoveColorDimensionFromSlopeCharts.ts @@ -4,7 +4,9 @@ export class RemoveColorDimensionFromSlopeCharts1732195571407 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - // update dimensions field in chart configs + // remove color dimension for all slope charts + // the y-dimension always comes first and the color dimension second, + // so it's safe to keep the first dimension only await queryRunner.query(` -- sql UPDATE chart_configs @@ -15,7 +17,7 @@ export class RemoveColorDimensionFromSlopeCharts1732195571407 chartType = 'SlopeChart' `) - // remove from chart_dimensions table + // remove the color dimension for slope charts from the chart_dimensions table await queryRunner.query(` -- sql DELETE cd FROM chart_dimensions cd diff --git a/packages/@ourworldindata/core-table/src/OwidTable.ts b/packages/@ourworldindata/core-table/src/OwidTable.ts index 34dc42551cb..c17d1e7b540 100644 --- a/packages/@ourworldindata/core-table/src/OwidTable.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.ts @@ -124,8 +124,8 @@ export class OwidTable extends CoreTable { return min(this.allTimes) as Time } - @imemo get maxTime(): Time { - return max(this.allTimes) as Time + @imemo get maxTime(): number | undefined { + return max(this.allTimes) } @imemo private get allTimes(): Time[] { diff --git a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx index 144ed286209..242ddc08248 100644 --- a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx @@ -409,11 +409,14 @@ export class DiscreteBarChart {this.placedSeries.map((series) => { return ( - series.label && - series.label.render( - series.entityLabelX, - series.barY - series.label.height / 2, - { textProps: style } + series.label && ( + + {series.label.render( + series.entityLabelX, + series.barY - series.label.height / 2, + { textProps: style } + )} + ) ) })} @@ -990,6 +993,7 @@ function makeProjectedDataPattern(color: string): React.ReactElement { const size = 7 return ( + | React.TouchEvent + export interface SlopeChartManager extends ChartManager { - isModalOpen?: boolean - canChangeEntity?: boolean canSelectMultipleEntities?: boolean } @@ -92,9 +92,9 @@ export class SlopeChart bounds?: Bounds manager: SlopeChartManager }> - implements ChartInterface, ColorScaleManager + implements ChartInterface { - base: React.RefObject = React.createRef() + slopeAreaRef: React.RefObject = React.createRef() defaultBaseColorScheme = ColorSchemeName.OwidDistinctLines @observable hoveredSeriesName?: string @@ -166,7 +166,7 @@ export class SlopeChart } @computed private get isLogScale(): boolean { - return this.props.manager.yAxisConfig?.scaleType === ScaleType.log + return this.yScaleType === ScaleType.log } @computed private get missingDataStrategy(): MissingDataStrategy { @@ -204,26 +204,22 @@ export class SlopeChart return this.xScale(this.endTime) } - private updateTooltipPosition( - event: React.MouseEvent | React.TouchEvent - ) { + private updateTooltipPosition(event: SVGMouseOrTouchEvent) { const ref = this.manager.base?.current if (ref) this.tooltipState.position = getRelativeMouse(ref, event) } - private detectHoveredSlope( - event: React.MouseEvent | React.TouchEvent - ) { - const ref = this.base.current + private detectHoveredSlope(event: SVGMouseOrTouchEvent) { + const ref = this.slopeAreaRef.current if (!ref) return const mouse = getRelativeMouse(ref, event) this.mouseFrame = requestAnimationFrame(() => { if (this.placedSeries.length === 0) return - const distToSlope = new Map() + const distanceMap = new Map() for (const series of this.placedSeries) { - distToSlope.set( + distanceMap.set( series, PointVector.distanceFromPointToLineSegmentSq( mouse, @@ -234,9 +230,9 @@ export class SlopeChart } const closestSlope = minBy(this.placedSeries, (s) => - distToSlope.get(s) - ) - const distanceSq = distToSlope.get(closestSlope!)! + distanceMap.get(s) + )! + const distanceSq = distanceMap.get(closestSlope)! const tolerance = 10 const toleranceSq = tolerance * tolerance @@ -506,7 +502,7 @@ export class SlopeChart @computed get xRange(): [number, number] { const lineLegendWidth = this.maxLineLegendWidth + LINE_LEGEND_PADDING - // pick a reasonable width based on an ideal aspect ratio + // pick a reasonable max width based on an ideal aspect ratio const idealAspectRatio = 0.6 const chartAreaWidth = this.bounds.width - this.sidebarWidth const availableWidth = @@ -553,7 +549,7 @@ export class SlopeChart private playIntroAnimation() { // Nice little intro animation - select(this.base.current) + select(this.slopeAreaRef.current) .select(".slopes") .attr("stroke-dasharray", "100%") .attr("stroke-dashoffset", "100%") @@ -594,11 +590,9 @@ export class SlopeChart } mouseFrame?: number - @action.bound onMouseMove( - ev: React.MouseEvent | React.TouchEvent - ) { - this.updateTooltipPosition(ev) - this.detectHoveredSlope(ev) + @action.bound onMouseMove(event: SVGMouseOrTouchEvent) { + this.updateTooltipPosition(event) + this.detectHoveredSlope(event) } @action.bound onMouseLeave() { @@ -771,7 +765,7 @@ export class SlopeChart /> Date: Fri, 22 Nov 2024 14:06:19 +0100 Subject: [PATCH 20/91] =?UTF-8?q?=F0=9F=A7=AA=20(slope)=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/lineChartHelpers.ts | 2 +- .../grapher/src/slopeCharts/SlopeChart.test.ts | 14 ++------------ .../grapher/src/slopeCharts/SlopeChart.tsx | 6 +++--- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts index 5296a3a3eca..74fdccc4c52 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts +++ b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts @@ -27,7 +27,7 @@ export function getSeriesName({ // if columns are plotted, use the column name // and prepend the entity name if multiple entities can be selected - return availableEntityNames.length > 1 && canSelectMultipleEntities + return availableEntityNames.length > 1 || canSelectMultipleEntities ? `${entityName} - ${columnName}` : columnName } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts index 9a22d9c08a6..660f64c2953 100755 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts @@ -7,13 +7,13 @@ import { SynthesizeGDPTable, } from "@ourworldindata/core-table" import { ChartManager } from "../chart/ChartManager" -import { DEFAULT_SLOPE_CHART_COLOR } from "./SlopeChartConstants" -import { isNumber, OwidTableSlugs } from "@ourworldindata/utils" +import { isNumber } from "@ourworldindata/utils" const table = SynthesizeGDPTable({ timeRange: [2000, 2010] }) const manager: ChartManager = { table, yColumnSlug: SampleColumnSlugs.Population, + selection: table.availableEntityNames, } it("can create a new slope chart", () => { @@ -21,16 +21,6 @@ it("can create a new slope chart", () => { expect(chart.series.length).toEqual(2) }) -it("slope charts can have different colors", () => { - const manager: ChartManager = { - table, - yColumnSlug: SampleColumnSlugs.Population, - colorColumnSlug: OwidTableSlugs.entityName, - } - const chart = new SlopeChart({ manager }) - expect(chart.series[0].color).not.toEqual(DEFAULT_SLOPE_CHART_COLOR) -}) - it("filters non-numeric values", () => { const table = SynthesizeFruitTableWithStringValues( { diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 44720736c34..57f0e0f2fcb 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -269,11 +269,11 @@ export class SlopeChart } @computed private get startTime(): Time { - return this.manager.startTime! + return this.transformedTable.minTime ?? 0 } @computed private get endTime(): Time { - return this.manager.endTime! + return this.transformedTable.maxTime ?? 0 } @computed get seriesStrategy(): SeriesStrategy { @@ -396,7 +396,7 @@ export class SlopeChart return map } - @computed private get series(): SlopeChartSeries[] { + @computed get series(): SlopeChartSeries[] { return this.rawSeries.filter((series) => this.shouldSeriesBePlotted(series) ) From 02b2b60ec6d0ecebc0d191a166767b2288cb5a0b Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 16:01:57 +0100 Subject: [PATCH 21/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20visual=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 102 +++++++++++++++--- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 57f0e0f2fcb..a44eb151e31 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { SVGProps } from "react" import { Bounds, DEFAULT_BOUNDS, @@ -20,6 +20,7 @@ import { observer } from "mobx-react" import { NoDataModal } from "../noDataModal/NoDataModal" import { BASE_FONT_SIZE, + GRAPHER_BACKGROUND_DEFAULT, GRAPHER_DARK_TEXT, GRAPHER_FONT_SCALE_12, } from "../core/GrapherConstants" @@ -601,6 +602,15 @@ export class SlopeChart this.onSlopeMouseLeave() } + @computed private get lineStrokeWidth(): number { + const factor = this.manager.isStaticAndSmall ? 2 : 1 + return factor * 2 + } + + @computed private get backgroundColor(): string { + return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT + } + @computed get renderUid(): number { return guid() } @@ -697,6 +707,9 @@ export class SlopeChart series={series} color={series.color} mode={mode} + strokeWidth={this.lineStrokeWidth} + outlineWidth={0.25} + outlineStroke={this.backgroundColor} /> ) } @@ -809,6 +822,10 @@ interface SlopeProps { series: PlacedSlopeChartSeries color: string mode?: RenderMode + dotRadius?: number + strokeWidth?: number + outlineWidth?: number + outlineStroke?: string onMouseOver?: (series: SlopeChartSeries) => void onMouseLeave?: () => void } @@ -817,16 +834,20 @@ function Slope({ series, color, mode = RenderMode.default, + dotRadius = 3.5, + strokeWidth = 2, + outlineWidth = 0.5, + outlineStroke = "#fff", onMouseOver, onMouseLeave, }: SlopeProps) { const { seriesName, startPoint, endPoint } = series - const usedColor = { - [RenderMode.default]: color, - [RenderMode.focus]: color, - [RenderMode.mute]: "#e2e2e2", - [RenderMode.background]: "#e2e2e2", + const opacity = { + [RenderMode.default]: 1, + [RenderMode.focus]: 1, + [RenderMode.mute]: 0.3, + [RenderMode.background]: 0.3, }[mode] return ( @@ -835,22 +856,72 @@ function Slope({ onMouseOver={() => onMouseOver?.(series)} onMouseLeave={() => onMouseLeave?.()} > + + + + + ) +} + +interface HaloLineProps extends SVGProps { + startPoint: PointVector + endPoint: PointVector + strokeWidth?: number + outlineWidth?: number + outlineStroke?: string +} + +function HaloLine(props: HaloLineProps): React.ReactElement { + const { + startPoint, + endPoint, + outlineWidth = 0.5, + outlineStroke = "#fff", + ...styleProps + } = props + return ( + <> - - - + ) } @@ -921,6 +992,7 @@ function MarkX({ textAnchor="middle" fill={GRAPHER_DARK_TEXT} fontSize={fontSize} + fontWeight={600} > {label} From c489683f02dd5455c966b88efdcf5fe71b657304 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 11:40:07 +0100 Subject: [PATCH 22/91] =?UTF-8?q?=F0=9F=94=A8=20always=20apply=20transform?= =?UTF-8?q?s=20of=20the=20main=20chart=20type=20for=20the=20entity=20selec?= =?UTF-8?q?tor=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/core/Grapher.tsx | 18 +++++++++++++++--- .../grapher/src/lineCharts/LineChart.tsx | 10 +++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index c514aedcd2e..7936c06d778 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -791,13 +791,15 @@ export class Grapher ? this.tableAfterAuthorTimelineAndActiveChartTransform : this.inputTable - if (!this.isReady) return table + if (!this.isReady || !this.mainChartInstance) return table // Some chart types (e.g. stacked area charts) choose not to show an entity // with incomplete data. Such chart types define a custom transform function // to ensure that the entity selector only offers entities that are actually plotted. - if (this.chartInstance.transformTableForSelection) { - table = this.chartInstance.transformTableForSelection(table) + // We apply the `tranformTableForSelection` method of the main chart type, + // so that the entity selector doesn't update when switching between chart types. + if (this.mainChartInstance.transformTableForSelection) { + table = this.mainChartInstance.transformTableForSelection(table) } return table @@ -838,6 +840,16 @@ export class Grapher return transformedTable } + // Chart instance of the "main" chart type, which is the first chart type + // in the list of valid chart types. Doesn't take into account that line + // charts might turn into discrete bar charts. Undefined for map-only Graphers. + @computed private get mainChartInstance(): ChartInterface | undefined { + if (!this.chartType) return undefined + const ChartClass = + ChartComponentClassMap.get(this.chartType) ?? DefaultChartClass + return new ChartClass({ manager: this }) + } + @computed get chartInstance(): ChartInterface { // Note: when timeline handles on a LineChart are collapsed into a single handle, the // LineChart turns into a DiscreteBar. diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 6def600bb85..a6674903be0 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -408,13 +408,9 @@ export class LineChart // if entities with partial data are not plotted, // make sure they don't show up in the entity selector if (this.missingDataStrategy === MissingDataStrategy.hide) { - table = table.replaceNonNumericCellsWithErrorValues( - this.yColumnSlugs - ) - - table = table.dropEntitiesThatHaveNoDataInSomeColumn( - this.yColumnSlugs - ) + table = table + .replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + .dropEntitiesThatHaveNoDataInSomeColumn(this.yColumnSlugs) } return table From 187203de566c205c5ad1cdf61f198f261fc8aa3c Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 12:06:48 +0100 Subject: [PATCH 23/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20add=20Change=20in?= =?UTF-8?q?=20to=20title=20in=20relative=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorFeatures.tsx | 2 +- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adminSiteClient/EditorFeatures.tsx b/adminSiteClient/EditorFeatures.tsx index 70adfd194bf..5d0f91f5301 100644 --- a/adminSiteClient/EditorFeatures.tsx +++ b/adminSiteClient/EditorFeatures.tsx @@ -133,7 +133,7 @@ export class EditorFeatures { @computed get showChangeInPrefixToggle() { return ( - this.grapher.isLineChart && + (this.grapher.isLineChart || this.grapher.isSlopeChart) && (this.grapher.isRelativeMode || this.grapher.canToggleRelativeMode) ) } diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 7936c06d778..e5166936d08 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1628,7 +1628,7 @@ export class Grapher !this.hideAnnotationFieldsInTitle?.changeInPrefix return ( !this.forceHideAnnotationFieldsInTitle?.changeInPrefix && - this.isOnLineChartTab && + (this.isOnLineChartTab || this.isOnSlopeChartTab) && this.isRelativeMode && showChangeInPrefix ) From 4bdfcc1ba0fcebcf5d2bcf737802f0ee3493337e Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 13:54:49 +0100 Subject: [PATCH 24/91] =?UTF-8?q?=E2=9C=A8=20(admin)=20show=20relative=20t?= =?UTF-8?q?oggle=20for=20slope=20charts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorFeatures.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/adminSiteClient/EditorFeatures.tsx b/adminSiteClient/EditorFeatures.tsx index 5d0f91f5301..0fefaa9c693 100644 --- a/adminSiteClient/EditorFeatures.tsx +++ b/adminSiteClient/EditorFeatures.tsx @@ -78,6 +78,7 @@ export class EditorFeatures { this.grapher.isStackedBar || this.grapher.isStackedDiscreteBar || this.grapher.isLineChart || + this.grapher.isSlopeChart || this.grapher.isScatter || this.grapher.isMarimekko ) From 864e6f6adea3df5d49454dcfb11ad50899c36a45 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 25 Nov 2024 12:21:15 +0100 Subject: [PATCH 25/91] =?UTF-8?q?=F0=9F=94=A8=20migrate=20slope=20charts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1732291572062-MigrateSlopeCharts.ts | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 db/migration/1732291572062-MigrateSlopeCharts.ts diff --git a/db/migration/1732291572062-MigrateSlopeCharts.ts b/db/migration/1732291572062-MigrateSlopeCharts.ts new file mode 100644 index 00000000000..aed3e83855c --- /dev/null +++ b/db/migration/1732291572062-MigrateSlopeCharts.ts @@ -0,0 +1,413 @@ +import { GrapherInterface } from "@ourworldindata/types" +import { MigrationInterface, QueryRunner } from "typeorm" + +export class MigrateSlopeCharts1732291572062 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const slopeCharts = await queryRunner.query(` + -- sql + SELECT c.id, cc.id AS configId, cc.patch, cc.full + FROM charts c + JOIN chart_configs cc ON cc.id = c.configId + WHERE cc.chartType = 'SlopeChart' + `) + + const configUpdatesById = new Map( + configUpdates.map(({ id, config }) => [id, config]) + ) + + for (const chart of slopeCharts) { + const migrationConfig = configUpdatesById.get(chart.id) + if (!migrationConfig) continue + + const patchConfig = JSON.parse(chart.patch) + const fullConfig = JSON.parse(chart.full) + + const newPatchConfig = { + ...patchConfig, + ...migrationConfig, + } + const newFullConfig = { + ...fullConfig, + ...migrationConfig, + } + + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + patch = ?, + full = ? + WHERE id = ? + `, + [ + JSON.stringify(newPatchConfig), + JSON.stringify(newFullConfig), + chart.configId, + ] + ) + } + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async down(): Promise {} +} + +const configUpdates: { id: number; config: GrapherInterface }[] = [ + { + id: 414, + config: { + title: "Top marginal income tax rate", + selectedEntityNames: [ + "Colombia", + "Guatemala", + "Indonesia", + "Iran", + "Jamaica", + "Pakistan", + "Trinidad and Tobago", + "Botswana", + "Bolivia", + "Japan", + "United States", + "Sweden", + "Germany", + "Netherlands", + "Belgium", + "France", + "Ireland", + "United Kingdom", + ], + hideRelativeToggle: true, + }, + }, + { + id: 415, + config: { + selectedEntityNames: [ + "Cirrhosis and other chronic liver diseases", + "Neonatal disorders", + "Congenital heart anomalies", + "Diphtheria", + ], + entityType: "cause", + entityTypePlural: "causes", + hideRelativeToggle: true, + }, + }, + { + id: 679, + config: { + selectedEntityNames: [ + "Chad", + "Iraq", + "Benin", + "Kenya", + "Brazil", + "Hungary", + ], + hideRelativeToggle: true, + }, + }, + { + id: 874, + config: { + selectedEntityNames: [ + "North America (WB)", + "South Asia (WB)", + "Europe and Central Asia (WB)", + "Latin America and Caribbean (WB)", + ], + hideRelativeToggle: true, + }, + }, + { + id: 875, + config: { + selectedEntityNames: [ + "North America (WB)", + "South Asia (WB)", + "Latin America and Caribbean (WB)", + ], + hideRelativeToggle: true, + }, + }, + { + id: 1004, + config: { + selectedEntityNames: [ + "Palau", + "Afghanistan", + "Curacao", + "Wallis and Futuna", + ], + }, + }, + { id: 1459, config: {} }, + { id: 1975, config: { selectedEntityNames: ["World"] } }, + { + id: 2832, + config: { + selectedEntityNames: ["Norway", "Italy", "France", "Finland"], + hideTimeline: true, + }, + }, + { + id: 2833, + config: { + selectedEntityNames: ["Belgium", "Poland", "Italy", "Germany"], + hideTimeline: true, + }, + }, + { + id: 2834, + config: { + selectedEntityNames: ["Belgium", "Italy", "Spain", "Norway"], + }, + }, + { + id: 2835, + config: { + selectedEntityNames: [ + "Estonia", + "Norway", + "Poland", + "United Kingdom", + ], + hideTimeline: true, + }, + }, + { + id: 2975, + config: { + selectedEntityNames: [ + "Germany", + "Poland", + "United Kingdom", + "Finland", + ], + hideTimeline: true, + }, + }, + { + id: 2976, + config: { + selectedEntityNames: ["Poland", "Italy", "Spain", "Belgium"], + hideTimeline: true, + }, + }, + { + id: 2977, + config: { + selectedEntityNames: ["Poland", "Norway", "Estonia", "Finland"], + hideTimeline: true, + }, + }, + { + id: 2978, + config: { + selectedEntityNames: [ + "Poland", + "Norway", + "Belgium", + "Estonia", + "Italy", + ], + hideTimeline: true, + }, + }, + { + id: 2979, + config: { + selectedEntityNames: [ + "United Kingdom", + "Estonia", + "Belgium", + "Italy", + ], + hideTimeline: true, + }, + }, + { + id: 3249, + config: { + selectedEntityNames: [ + "France", + "Italy", + "Japan", + "Portugal", + "Germany", + "Mexico", + "Norway", + "Sweden", + "Taiwan", + "Sri Lanka", + "United Kingdom", + "United States", + ], + }, + }, + { + id: 3359, + config: { + selectedEntityNames: [ + "Mali", + "South Africa", + "Nigeria", + "Niger", + "Chad", + "Ethiopia", + "Kenya", + "Uganda", + "Rwanda", + "Burundi", + "Tanzania", + "Mozambique", + "Madagascar", + "Zambia", + "Congo", + "Democratic Republic of Congo", + "Central African Republic", + "Cameroon", + "Togo", + "Benin", + "Sierra Leone", + "Cote d'Ivoire", + "Burkina Faso", + "Guinea-Bissau", + "Papua New Guinea", + "Senegal", + "Angola", + ], + }, + }, + { + id: 3364, + config: { + selectedEntityNames: [ + "India", + "Indonesia", + "United States", + "Antigua and Barbuda", + ], + }, + }, + { + id: 3433, + config: {}, + }, + { + id: 3434, + config: {}, + }, + { + id: 3580, + config: { + entityTypePlural: "species", + }, + }, + { + id: 3620, + config: { + selectedEntityNames: ["Romania", "Benin", "Libya", "Suriname"], + }, + }, + { + id: 3627, + config: { + selectedEntityNames: [ + "Syrian Arab Republic", + "East Asia & Pacific", + "Costa Rica", + "Malta", + ], + }, + }, + { + id: 4408, + config: { + selectedEntityNames: [ + "East Asia (MPD)", + "Latin America (MPD)", + "Eastern Europe (MPD)", + "Western Europe (MPD)", + "Western offshoots (MPD)", + "Sub Saharan Africa (MPD)", + "South and South East Asia (MPD)", + "Middle East and North Africa (MPD)", + "World", + ], + }, + }, + { + id: 4764, + config: { + entityTypePlural: "species", + }, + }, + { + id: 6219, + config: { + hideRelativeToggle: true, + }, + }, + { + id: 6529, + config: { + selectedEntityNames: ["United States", "Russia", "China"], + }, + }, + { + id: 7150, + config: { + selectedEntityNames: ["Burundi", "Togo", "Ethiopia", "Myanmar"], + }, + }, + { + id: 7206, + config: {}, + }, + { + id: 7220, + config: {}, + }, + { + id: 7221, + config: {}, + }, + { + id: 7226, + config: {}, + }, + { + id: 7344, + config: { + selectedEntityNames: [ + "Suriname", + "Malta", + "Australia", + "Guatemala", + ], + }, + }, + { + id: 7448, + config: { + hideRelativeToggle: true, + }, + }, + { + id: 8157, + config: { + selectedEntityNames: [ + "South Asia (WB)", + "North America (WB)", + "Sub-Saharan Africa (WB)", + "East Asia and Pacific (WB)", + "Europe and Central Asia (WB)", + "Latin America and Caribbean (WB)", + "Middle East and North Africa (WB)", + ], + }, + }, +] From f2010855410c2e40debebb9cd102e7fa4f4c1821 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 14:23:24 +0100 Subject: [PATCH 26/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20improve=20tooltip?= =?UTF-8?q?=20in=20relative=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index a44eb151e31..6cfea0d35ad 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -617,17 +617,26 @@ export class SlopeChart @computed get tooltip(): React.ReactElement | undefined { const { + manager: { isRelativeMode }, tooltipState: { target, position, fading }, + formatColumn, + startTime, + endTime, } = this const { series } = target || {} if (!series) return - const { isRelativeMode } = this.manager, - timeRange = [this.startTime, this.endTime] - .map((t) => this.formatColumn.formatTime(t)) - .join(" to "), - timeLabel = timeRange + (isRelativeMode ? " (relative change)" : "") + const title = isRelativeMode + ? `${series.seriesName}, ${formatColumn.formatTime(endTime)}` + : series.seriesName + + const timeRange = [startTime, endTime] + .map((t) => formatColumn.formatTime(t)) + .join(" to ") + const timeLabel = isRelativeMode + ? `% change since ${formatColumn.formatTime(startTime)}` + : timeRange const columns = this.yColumns const allRoundedToSigFigs = columns.every( @@ -656,6 +665,10 @@ export class SlopeChart : undefined const footer = excludeUndefined([roundingNotice]) + const values = isRelativeMode + ? [series.endValue] + : [series.startValue, series.endValue] + return ( (this.tooltipState.target = null)} > - + ) } From 3167772b9bdc5f33b1033eb1ca3ce3495de5dc93 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 15:18:44 +0100 Subject: [PATCH 27/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20improve=20No=20Data?= =?UTF-8?q?=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/noDataModal/NoDataModal.tsx | 4 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 71 +++++++++++++------ 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/noDataModal/NoDataModal.tsx b/packages/@ourworldindata/grapher/src/noDataModal/NoDataModal.tsx index 8502da6e0fe..a63ea9999b1 100644 --- a/packages/@ourworldindata/grapher/src/noDataModal/NoDataModal.tsx +++ b/packages/@ourworldindata/grapher/src/noDataModal/NoDataModal.tsx @@ -30,6 +30,7 @@ export interface NoDataModalManager { export class NoDataModal extends React.Component<{ bounds?: Bounds message?: string + helpText?: string manager: NoDataModalManager }> { @computed private get bounds(): Bounds { @@ -55,11 +56,12 @@ export class NoDataModal extends React.Component<{ isStatic, } = this.manager - const helpText = canAddEntities + const defaultHelpText = canAddEntities ? `Try adding ${entityTypePlural} to display data.` : canChangeEntity ? `Try choosing ${a(entityType)} to display data.` : undefined + const helpText = this.props.helpText ?? defaultHelpText const center = bounds.centerPos const padding = 0.75 * this.fontSize diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 6cfea0d35ad..5bda0407757 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -245,14 +245,22 @@ export class SlopeChart }) } - @computed get failMessage() { + @computed get failMessage(): string { const message = getDefaultFailMessage(this.manager) if (message) return message - else if (this.startTime === this.endTime) return "No matching data" - else if (isEmpty(this.series)) return "No matching data" + else if (this.startTime === this.endTime) + return "No data to display for the selected time period" return "" } + @computed get helpMessage(): string | undefined { + if ( + this.failMessage === + "No data to display for the selected time period" + ) + return "Try dragging the time slider to display data." + } + @computed private get yColumns(): CoreColumn[] { return this.yColumnSlugs.map((slug) => this.transformedTable.get(slug)) } @@ -348,29 +356,29 @@ export class SlopeChart return series.startValue !== undefined && series.endValue !== undefined } - /** - * Usually we drop rows with missing data in the transformTable function. - * But slope charts have a "No data" section. If slopes that have data - * but shouldn't be plotted because a "sibling" slope of the same entity - * doesn't have data are dropped from the transformed table, then we - * would have no way of knowing whether a slope has been dropped because - * it actually had no data or a sibling slope had no data. That's why we - * filter out slopes that are valid but shouldn't be plotted here, so - * that the noDataSeries is populated correctly. - */ + // Usually we drop rows with missing data in the transformTable function. + // But if we did that for slope charts, we wouldn't know whether a slope + // has been dropped because it actually had no data or a sibling slope had + // no data. But we need that information for the "No data" section. That's + // why the filtering happens here, so that the noDataSeries can be populated + // correctly. private shouldSeriesBePlotted( series: RawSlopeChartSeries ): series is SlopeChartSeries { if (!this.isSeriesValid(series)) return false + // when the missing data strategy is set to "hide", we might + // choose not to plot a valid series if ( this.seriesStrategy === SeriesStrategy.column && this.missingDataStrategy === MissingDataStrategy.hide ) { - const entitySeries = this.rawSeriesByEntityName.get( + const allSeriesForEntity = this.rawSeriesByEntityName.get( series.entityName ) - return !!entitySeries?.every((series) => this.isSeriesValid(series)) + return !!allSeriesForEntity?.every((series) => + this.isSeriesValid(series) + ) } return true @@ -487,12 +495,14 @@ export class SlopeChart // TODO: copied from line legend const fontSize = GRAPHER_FONT_SCALE_12 * (this.manager.fontSize ?? BASE_FONT_SIZE) - return max( - this.series.map( - (series) => - Bounds.forText(series.seriesName, { fontSize }).width - ) - )! + return ( + max( + this.series.map( + (series) => + Bounds.forText(series.seriesName, { fontSize }).width + ) + ) ?? 0 + ) } @computed get maxLineLegendWidth(): number { @@ -689,14 +699,30 @@ export class SlopeChart ) } + private makeMissingDataLabel(series: RawSlopeChartSeries): string { + const { seriesName } = series + const startTime = this.formatColumn.formatTime(this.startTime) + const endTime = this.formatColumn.formatTime(this.endTime) + if (series.startValue === undefined && series.endValue === undefined) { + return `${seriesName} (${startTime} & ${endTime})` + } else if (series.startValue === undefined) { + return `${seriesName} (${startTime})` + } else if (series.endValue === undefined) { + return `${seriesName} (${endTime})` + } + return seriesName + } + private renderNoDataSection(): React.ReactElement { - const seriesNames = this.noDataSeries.map((series) => series.seriesName) const bounds = new Bounds( this.bounds.right - this.sidebarWidth, this.bounds.top, this.sidebarWidth, this.bounds.height ) + const seriesNames = this.noDataSeries.map((series) => + this.makeMissingDataLabel(series) + ) return ( ) From b5f5b08a83b81399e1b97d3657c3754c1c7571bf Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 15:20:04 +0100 Subject: [PATCH 28/91] =?UTF-8?q?=E2=9C=A8=20nicer=20entity/column=20label?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.test.ts | 6 +++--- .../grapher/src/lineCharts/lineChartHelpers.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.test.ts b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.test.ts index c825a821719..3655c9b83e3 100755 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.test.ts +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.test.ts @@ -100,7 +100,7 @@ describe("series naming in multi-column mode", () => { selection: [table.availableEntityNames[0]], } const chart = new LineChart({ manager }) - expect(chart.series[0].seriesName).not.toContain(" - ") + expect(chart.series[0].seriesName).not.toContain(" – ") }) it("combines entity and column name if only one entity is selected and multi entity selection is enabled", () => { @@ -110,7 +110,7 @@ describe("series naming in multi-column mode", () => { selection: [table.availableEntityNames[0]], } const chart = new LineChart({ manager }) - expect(chart.series[0].seriesName).toContain(" - ") + expect(chart.series[0].seriesName).toContain(" – ") }) it("combines entity and column name if multiple entities are selected and multi entity selection is disabled", () => { @@ -120,7 +120,7 @@ describe("series naming in multi-column mode", () => { selection: table.availableEntityNames, } const chart = new LineChart({ manager }) - expect(chart.series[0].seriesName).toContain(" - ") + expect(chart.series[0].seriesName).toContain(" – ") }) }) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts index 74fdccc4c52..49a1f81597a 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts +++ b/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts @@ -28,7 +28,7 @@ export function getSeriesName({ // if columns are plotted, use the column name // and prepend the entity name if multiple entities can be selected return availableEntityNames.length > 1 || canSelectMultipleEntities - ? `${entityName} - ${columnName}` + ? `${entityName} – ${columnName}` : columnName } From 2414e5b686b5c0e44da44951934254eabe2af5a7 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 15:39:00 +0100 Subject: [PATCH 29/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20clean=20up=20cod?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lineCharts/LineChartConstants.ts | 2 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 164 +++++++++--------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts b/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts index e7e9a01cf7e..fe5d5a258bb 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts @@ -46,5 +46,5 @@ export interface LinesProps { export interface LineChartManager extends ChartManager { entityYearHighlight?: EntityYearHighlight lineStrokeWidth?: number - canSelectMultipleEntities?: boolean + canSelectMultipleEntities?: boolean // used to pick an appropriate series name } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 5bda0407757..60e49f80593 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -2,7 +2,6 @@ import React, { SVGProps } from "react" import { Bounds, DEFAULT_BOUNDS, - isEmpty, domainExtent, exposeInstanceOnWindow, PointVector, @@ -79,11 +78,11 @@ type SVGMouseOrTouchEvent = | React.TouchEvent export interface SlopeChartManager extends ChartManager { - canSelectMultipleEntities?: boolean + canSelectMultipleEntities?: boolean // used to pick an appropriate series name } -const TOP_PADDING = 6 -const BOTTOM_PADDING = 20 +const TOP_PADDING = 6 // leave room for overflowing dots +const BOTTOM_PADDING = 20 // leave room for the x-axis const LINE_LEGEND_PADDING = 4 @@ -182,85 +181,19 @@ export class SlopeChart return this.yColumns[0] } - @computed private get sidebarWidth(): number { - return this.showNoDataSection - ? clamp(this.bounds.width * 0.125, 60, 140) - : 0 + @computed private get lineStrokeWidth(): number { + const factor = this.manager.isStaticAndSmall ? 2 : 1 + return factor * 2 } - // used by LineLegend - @computed get focusedSeriesNames(): SeriesName[] { - return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] + @computed private get backgroundColor(): string { + return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT } @computed private get isFocusModeActive(): boolean { return this.hoveredSeriesName !== undefined } - @computed private get startX(): number { - return this.xScale(this.startTime) - } - - @computed private get endX(): number { - return this.xScale(this.endTime) - } - - private updateTooltipPosition(event: SVGMouseOrTouchEvent) { - const ref = this.manager.base?.current - if (ref) this.tooltipState.position = getRelativeMouse(ref, event) - } - - private detectHoveredSlope(event: SVGMouseOrTouchEvent) { - const ref = this.slopeAreaRef.current - if (!ref) return - - const mouse = getRelativeMouse(ref, event) - this.mouseFrame = requestAnimationFrame(() => { - if (this.placedSeries.length === 0) return - - const distanceMap = new Map() - for (const series of this.placedSeries) { - distanceMap.set( - series, - PointVector.distanceFromPointToLineSegmentSq( - mouse, - series.startPoint, - series.endPoint - ) - ) - } - - const closestSlope = minBy(this.placedSeries, (s) => - distanceMap.get(s) - )! - const distanceSq = distanceMap.get(closestSlope)! - const tolerance = 10 - const toleranceSq = tolerance * tolerance - - if (closestSlope && distanceSq < toleranceSq) { - this.onSlopeMouseOver(closestSlope) - } else { - this.onSlopeMouseLeave() - } - }) - } - - @computed get failMessage(): string { - const message = getDefaultFailMessage(this.manager) - if (message) return message - else if (this.startTime === this.endTime) - return "No data to display for the selected time period" - return "" - } - - @computed get helpMessage(): string | undefined { - if ( - this.failMessage === - "No data to display for the selected time period" - ) - return "Try dragging the time slider to display data." - } - @computed private get yColumns(): CoreColumn[] { return this.yColumnSlugs.map((slug) => this.transformedTable.get(slug)) } @@ -285,6 +218,14 @@ export class SlopeChart return this.transformedTable.maxTime ?? 0 } + @computed private get startX(): number { + return this.xScale(this.startTime) + } + + @computed private get endX(): number { + return this.xScale(this.endTime) + } + @computed get seriesStrategy(): SeriesStrategy { return autoDetectSeriesStrategy(this.manager, true) } @@ -462,7 +403,7 @@ export class SlopeChart ] } - @computed get yRange(): [number, number] { + @computed private get yRange(): [number, number] { return this.bounds .padTop(TOP_PADDING) .padBottom(BOTTOM_PADDING) @@ -478,7 +419,7 @@ export class SlopeChart return axis } - @computed get yAxisWidth(): number { + @computed private get yAxisWidth(): number { return this.yAxis.width + 5 // 5px account for the tick marks } @@ -491,6 +432,12 @@ export class SlopeChart return [this.startTime, this.endTime] } + @computed private get sidebarWidth(): number { + return this.showNoDataSection + ? clamp(this.bounds.width * 0.125, 60, 140) + : 0 + } + @computed private get maxLabelWidth(): number { // TODO: copied from line legend const fontSize = @@ -544,6 +491,11 @@ export class SlopeChart return this.xRange[1] + LINE_LEGEND_PADDING } + // used by LineLegend + @computed get focusedSeriesNames(): SeriesName[] { + return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] + } + // used in LineLegend @computed get labelSeries(): LineLabelSeries[] { return this.series.map((series) => { @@ -576,6 +528,46 @@ export class SlopeChart } } + private updateTooltipPosition(event: SVGMouseOrTouchEvent) { + const ref = this.manager.base?.current + if (ref) this.tooltipState.position = getRelativeMouse(ref, event) + } + + private detectHoveredSlope(event: SVGMouseOrTouchEvent) { + const ref = this.slopeAreaRef.current + if (!ref) return + + const mouse = getRelativeMouse(ref, event) + this.mouseFrame = requestAnimationFrame(() => { + if (this.placedSeries.length === 0) return + + const distanceMap = new Map() + for (const series of this.placedSeries) { + distanceMap.set( + series, + PointVector.distanceFromPointToLineSegmentSq( + mouse, + series.startPoint, + series.endPoint + ) + ) + } + + const closestSlope = minBy(this.placedSeries, (s) => + distanceMap.get(s) + )! + const distanceSq = distanceMap.get(closestSlope)! + const tolerance = 10 + const toleranceSq = tolerance * tolerance + + if (closestSlope && distanceSq < toleranceSq) { + this.onSlopeMouseOver(closestSlope) + } else { + this.onSlopeMouseLeave() + } + }) + } + private hoverTimer?: NodeJS.Timeout @action.bound onLineLegendMouseOver(seriesName: SeriesName): void { clearTimeout(this.hoverTimer) @@ -612,13 +604,21 @@ export class SlopeChart this.onSlopeMouseLeave() } - @computed private get lineStrokeWidth(): number { - const factor = this.manager.isStaticAndSmall ? 2 : 1 - return factor * 2 + @computed get failMessage(): string { + const message = getDefaultFailMessage(this.manager) + if (message) return message + else if (this.startTime === this.endTime) + return "No data to display for the selected time period" + return "" } - @computed private get backgroundColor(): string { - return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT + @computed get helpMessage(): string | undefined { + if ( + this.failMessage === + "No data to display for the selected time period" + ) + return "Try dragging the time slider to display data." + return undefined } @computed get renderUid(): number { From b71988d9e14db6c925cfa06b67ec911a63473ba8 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 09:47:27 +0100 Subject: [PATCH 30/91] =?UTF-8?q?=F0=9F=94=A8=20update=20slope=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1732291572062-MigrateSlopeCharts.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/db/migration/1732291572062-MigrateSlopeCharts.ts b/db/migration/1732291572062-MigrateSlopeCharts.ts index aed3e83855c..84e7693269c 100644 --- a/db/migration/1732291572062-MigrateSlopeCharts.ts +++ b/db/migration/1732291572062-MigrateSlopeCharts.ts @@ -8,7 +8,9 @@ export class MigrateSlopeCharts1732291572062 implements MigrationInterface { SELECT c.id, cc.id AS configId, cc.patch, cc.full FROM charts c JOIN chart_configs cc ON cc.id = c.configId - WHERE cc.chartType = 'SlopeChart' + WHERE + cc.chartType = 'SlopeChart' + AND cc.full ->> '$.isPublished' = 'true' `) const configUpdatesById = new Map( @@ -93,6 +95,7 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ entityType: "cause", entityTypePlural: "causes", hideRelativeToggle: true, + hideLegend: false, }, }, { @@ -144,7 +147,17 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ }, }, { id: 1459, config: {} }, - { id: 1975, config: { selectedEntityNames: ["World"] } }, + { + id: 1975, + config: { + selectedEntityNames: [ + "North America", + "South America", + "Europe", + "Asia", + ], + }, + }, { id: 2832, config: { From 9cd9f309c720938e9582d97a66681c38379fbbff Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 10:49:45 +0100 Subject: [PATCH 31/91] =?UTF-8?q?=F0=9F=A7=AA=20(slope)=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/slopeCharts/SlopeChart.test.ts | 221 +++++++++++++++++- .../grapher/src/slopeCharts/SlopeChart.tsx | 2 +- 2 files changed, 220 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts index 660f64c2953..fdc342b3b43 100755 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts @@ -1,13 +1,23 @@ #! /usr/bin/env jest -import { SlopeChart } from "./SlopeChart" +import { SlopeChart, SlopeChartManager } from "./SlopeChart" import { + ErrorValueTypes, + OwidTable, SampleColumnSlugs, + SynthesizeFruitTableWithNonPositives, SynthesizeFruitTableWithStringValues, SynthesizeGDPTable, } from "@ourworldindata/core-table" import { ChartManager } from "../chart/ChartManager" -import { isNumber } from "@ourworldindata/utils" +import { + ColumnTypeNames, + FacetStrategy, + isNumber, + ScaleType, + SeriesStrategy, +} from "@ourworldindata/utils" +import { SelectionArray } from "../selection/SelectionArray" const table = SynthesizeGDPTable({ timeRange: [2000, 2010] }) const manager: ChartManager = { @@ -43,3 +53,210 @@ it("filters non-numeric values", () => { ) ).toBeTruthy() }) + +it("can filter points with negative values when using a log scale", () => { + const table = SynthesizeFruitTableWithNonPositives( + { + entityCount: 2, + timeRange: [2000, 2002], + }, + 1, + 1 + ) + + const manager: ChartManager = { + table, + yColumnSlugs: [SampleColumnSlugs.Fruit], + selection: table.availableEntityNames, + } + const chart = new SlopeChart({ manager }) + // expect(chart.series.length).toEqual(2) + expect(chart.allValues.length).toEqual(4) + + const logScaleManager = { + ...manager, + yAxisConfig: { + scaleType: ScaleType.log, + }, + } + const logChart = new SlopeChart({ manager: logScaleManager }) + expect(logChart.yAxis.domain[0]).toBeGreaterThan(0) + // expect(logChart.series.length).toEqual(2) + expect(logChart.allValues.length).toEqual(2) +}) + +describe("series naming in multi-column mode", () => { + const table = SynthesizeGDPTable() + + it("only displays column name if only one entity is selected and multi entity selection is disabled", () => { + const manager = { + table, + canSelectMultipleEntities: false, + selection: [table.availableEntityNames[0]], + } + const chart = new SlopeChart({ manager }) + expect(chart.series[0].seriesName).not.toContain(" – ") + }) + + it("combines entity and column name if only one entity is selected and multi entity selection is enabled", () => { + const manager = { + table, + canSelectMultipleEntities: true, + selection: [table.availableEntityNames[0]], + } + const chart = new SlopeChart({ manager }) + expect(chart.series[0].seriesName).toContain(" – ") + }) + + it("combines entity and column name if multiple entities are selected and multi entity selection is disabled", () => { + const selection = new SelectionArray( + table.availableEntityNames, + table.availableEntities + ) + const manager = { + table, + canSelectMultipleEntities: false, + selection, + } + const chart = new SlopeChart({ manager }) + expect(chart.series[0].seriesName).toContain(" – ") + }) +}) + +describe("colors", () => { + const table = new OwidTable({ + entityName: ["usa", "canada", "usa", "canada"], + year: [2000, 2000, 2001, 2001], + gdp: [100, 200, 200, 300], + entityColor: ["blue", "red", "blue", "red"], + }) + const selection = ["usa", "canada"] + it("can add custom colors", () => { + const manager = { + yColumnSlugs: ["gdp"], + table, + selection, + } + const chart = new SlopeChart({ manager }) + expect(chart.series.map((series) => series.color)).toEqual([ + "blue", + "red", + ]) + }) + + it("uses column color selections when series strategy is column", () => { + const table = new OwidTable( + { + entityName: ["usa", "usa"], + year: [2000, 2001], + gdp: [100, 200], + entityColor: ["blue", "blue"], + }, + [{ slug: "gdp", color: "green", type: ColumnTypeNames.Numeric }] + ) + + const manager: ChartManager = { + yColumnSlugs: ["gdp"], + table: table, + selection, + seriesStrategy: SeriesStrategy.column, + } + const chart = new SlopeChart({ manager }) + const series = chart.series + + expect(series).toHaveLength(1) + expect(series[0].color).toEqual("green") + }) + + it("can assign colors to selected entities and preserve those colors when selection changes when using a color map", () => { + const selection = new SelectionArray(["usa", "canada"]) + const manager: ChartManager = { + yColumnSlugs: ["gdp"], + table: table.dropColumns(["entityColor"]), + selection, + seriesColorMap: new Map(), + } + const chart = new SlopeChart({ manager }) + const series = chart.series + expect(series).toHaveLength(2) + + selection.deselectEntity("usa") + + const newSeries = chart.series + expect(newSeries).toHaveLength(1) + expect(newSeries[0].color).toEqual(series[1].color) + }) + + it("uses variable colors when only one entity selected (even if multiple can be selected with controls)", () => { + const table = new OwidTable( + { + entityName: ["usa", "usa", "canada"], + year: [2000, 2001, 2000], + gdp: [100, 200, 100], + pop: [100, 200, 100], + }, + [ + { slug: "gdp", color: "green", type: ColumnTypeNames.Numeric }, + { slug: "pop", color: "orange", type: ColumnTypeNames.Numeric }, + ] + ) + + const manager: SlopeChartManager = { + yColumnSlugs: ["gdp", "pop"], + table: table, + selection: ["usa"], + seriesStrategy: SeriesStrategy.column, + facetStrategy: FacetStrategy.entity, + canSelectMultipleEntities: true, + } + const chart = new SlopeChart({ manager }) + const series = chart.series + + expect(series).toHaveLength(2) + expect(series[0].color).toEqual("green") + expect(series[1].color).toEqual("orange") + }) + + it("doesn't use variable colors if 2 variables have single entities which are different", () => { + const table = new OwidTable( + { + entityName: ["usa", "usa", "canada", "canada"], + year: [2000, 2001, 2000, 2001], + gdp: [ + 100, + 200, + ErrorValueTypes.MissingValuePlaceholder, + ErrorValueTypes.MissingValuePlaceholder, + ], + pop: [ + ErrorValueTypes.MissingValuePlaceholder, + ErrorValueTypes.MissingValuePlaceholder, + 100, + 200, + ], + }, + [ + { slug: "gdp", color: "green", type: ColumnTypeNames.Numeric }, + { slug: "pop", color: "orange", type: ColumnTypeNames.Numeric }, + ] + ) + + const selection = new SelectionArray( + ["usa", "canada"], + [{ entityName: "usa" }, { entityName: "canada" }] + ) + const manager: SlopeChartManager = { + yColumnSlugs: ["gdp", "pop"], + table: table, + selection, + seriesStrategy: SeriesStrategy.column, + canSelectMultipleEntities: true, + } + const chart = new SlopeChart({ manager }) + const series = chart.series + + expect(series).toHaveLength(2) + expect(series[0].color).not.toEqual("green") + expect(series[1].color).not.toEqual("orange") + }) +}) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 60e49f80593..8949584092d 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -379,7 +379,7 @@ export class SlopeChart return new AxisConfig(this.manager.yAxisConfig, this) } - @computed private get allValues(): number[] { + @computed get allValues(): number[] { return this.series.flatMap((series) => [ series.startValue, series.endValue, From 26bfa860af1cc26e7853edcaa99c6081ff1de7c4 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 12:38:32 +0100 Subject: [PATCH 32/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20drop=20entities=20f?= =?UTF-8?q?rom=20selector=20if=20time=20selection=20disabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core-table/src/OwidTable.ts | 89 +++++ .../grapher/src/slopeCharts/SlopeChart.tsx | 11 + ....timestamp-1732790803730-c1bc0b6a7ecde.mjs | 351 ++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs diff --git a/packages/@ourworldindata/core-table/src/OwidTable.ts b/packages/@ourworldindata/core-table/src/OwidTable.ts index c17d1e7b540..13f2d52026a 100644 --- a/packages/@ourworldindata/core-table/src/OwidTable.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.ts @@ -325,6 +325,95 @@ export class OwidTable extends CoreTable { ) } + // Drop _all rows_ for an entity if all columns have at least one invalid or missing value for that entity. + dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns( + columnSlugs: ColumnSlug[] + ): this { + const indexesByEntityName = this.rowIndicesByEntityName + const uniqTimes = new Set(this.allTimes) + + // entity names to iterate over + const entityNamesToIterateOver = new Set(indexesByEntityName.keys()) + + // set of entities we want to keep + const entityNamesToKeep = new Set() + + // total number of entities + const entityCount = entityNamesToIterateOver.size + + // helper function to generate operation name + const makeOpName = (entityNamesToKeep: Set): string => { + const entityNamesToDrop = differenceOfSets([ + this.availableEntityNameSet, + entityNamesToKeep, + ]) + const droppedEntitiesStr = + entityNamesToDrop.size > 0 + ? [...entityNamesToDrop].join(", ") + : "(None)" + return `Drop entities that have some missing or error value in all column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}` + } + + // Optimization: if there is a column that has a valid data entry for + // every entity and every time, we are done + for (let i = 0; i <= columnSlugs.length; i++) { + const slug = columnSlugs[i] + const col = this.get(slug) + + if ( + col.numValues === entityCount * uniqTimes.size && + col.numErrorValues === 0 + ) { + const entityNamesToKeep = new Set(indexesByEntityName.keys()) + + return this.columnFilter( + this.entityNameSlug, + (rowEntityName) => + entityNamesToKeep.has(rowEntityName as string), + makeOpName(entityNamesToKeep) + ) + } + } + + for (let i = 0; i <= columnSlugs.length; i++) { + const slug = columnSlugs[i] + const col = this.get(slug) + + for (const entityName of entityNamesToIterateOver) { + const indicesForEntityName = indexesByEntityName.get(entityName) + if (!indicesForEntityName) + throw new Error("Unexpected: entity not found in index map") + + // Optimization: If the column is missing values for the entity, + // we know we can't make a decision yet, so we skip this entity + if (indicesForEntityName.length < uniqTimes.size) continue + + // Optimization: We don't care about the number of valid/error + // values, we just need to know if there is at least one invalid value + const hasSomeInvalidValueForEntityInCol = + indicesForEntityName.some( + (index) => + !isNotErrorValue( + col.valuesIncludingErrorValues[index] + ) + ) + + // Optimization: If all values are valid, we know we want to keep this entity, + // so we remove it from the entities to iterate over + if (!hasSomeInvalidValueForEntityInCol) { + entityNamesToKeep.add(entityName) + entityNamesToIterateOver.delete(entityName) + } + } + } + + return this.columnFilter( + this.entityNameSlug, + (rowEntityName) => entityNamesToKeep.has(rowEntityName as string), + makeOpName(entityNamesToKeep) + ) + } + private sumsByTime(columnSlug: ColumnSlug): Map { const timeValues = this.timeColumn.values const values = this.get(columnSlug).values as number[] diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 8949584092d..74cc8addef9 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -79,6 +79,7 @@ type SVGMouseOrTouchEvent = export interface SlopeChartManager extends ChartManager { canSelectMultipleEntities?: boolean // used to pick an appropriate series name + hasTimeline?: boolean // used to filter the table for the entity selector } const TOP_PADDING = 6 // leave room for overflowing dots @@ -117,6 +118,16 @@ export class SlopeChart } transformTableForSelection(table: OwidTable): OwidTable { + // if time selection is disabled, then filter all entities that + // don't have data for the current time period + if (!this.manager.hasTimeline) { + table = table + .filterByTargetTimes([this.startTime, this.endTime]) + .dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns( + this.yColumnSlugs + ) + } + // if entities with partial data are not plotted, // make sure they don't show up in the entity selector if (this.missingDataStrategy === MissingDataStrategy.hide) { diff --git a/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs b/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs new file mode 100644 index 00000000000..9ef808f81c3 --- /dev/null +++ b/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs @@ -0,0 +1,351 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// site/viteUtils.tsx +import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; + +// settings/findBaseDir.ts +import path from "path"; +import fs from "fs"; +function findProjectBaseDir(from) { + if (!fs.existsSync) return void 0; + let dir = path.dirname(from); + while (dir.length) { + if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; + const parentDir = path.resolve(dir, ".."); + if (parentDir === dir) break; + else dir = parentDir; + } + return void 0; +} + +// site/viteUtils.tsx +import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; + +// settings/serverSettings.ts +import path2 from "path"; +import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import fs2 from "fs"; +import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; +import os from "os"; + +// settings/clientSettings.ts +var clientSettings_exports = {}; +__export(clientSettings_exports, { + ADMIN_BASE_URL: () => ADMIN_BASE_URL, + ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, + ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, + ALGOLIA_ID: () => ALGOLIA_ID, + ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, + ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, + BAKED_BASE_URL: () => BAKED_BASE_URL, + BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, + BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, + BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, + BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, + DATA_API_URL: () => DATA_API_URL, + DONATE_API_URL: () => DONATE_API_URL, + ENV: () => ENV, + ETL_API_URL: () => ETL_API_URL, + ETL_WIZARD_URL: () => ETL_WIZARD_URL, + EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, + FEATURE_FLAGS: () => FEATURE_FLAGS, + FeatureFlagFeature: () => FeatureFlagFeature, + GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, + GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, + GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, + GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, + GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, + IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, + IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, + IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, + MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, + PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, + RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, + SENTRY_DSN: () => SENTRY_DSN, + TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH +}); +import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; +if (typeof __vite_injected_original_dirname2 !== "undefined") { + const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); + if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); +} +var ENV = process.env.ENV === "production" ? "production" : "development"; +var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; +var SENTRY_DSN = process.env.SENTRY_DSN; +var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; +var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; +var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; +var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; +var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; +var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; +var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; +var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; +var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; +var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; +var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; +var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; +var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; +var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; +var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; +var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; +var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; +var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; +var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; +var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( + IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 +); +var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; +var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; +var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; +var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { + FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; + return FeatureFlagFeature2; +})(FeatureFlagFeature || {}); +var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; +var FEATURE_FLAGS = new Set( + Object.keys(FeatureFlagFeature).filter( + (key) => featureFlagsRaw.includes(key) + ) +); + +// settings/serverSettings.ts +import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; +var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); +if (baseDir === void 0) throw new Error("could not locate base package.json"); +dotenv2.config({ path: `${baseDir}/.env` }); +var serverSettings = process.env ?? {}; +var BASE_DIR = baseDir; +var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; +var BAKED_BASE_URL2 = BAKED_BASE_URL; +var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; +var ADMIN_BASE_URL2 = ADMIN_BASE_URL; +var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; +var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; +var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; +var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; +var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; +var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; +var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; +var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; +var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; +var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; +var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; +var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; +var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; +var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; +var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; +var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; +var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; +var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; +var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; +var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); +var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; +var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; +var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; +var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; +var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; +var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; +var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; +var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; +var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; +var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; +var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; +var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; +var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; +var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); +var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; +var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; +var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; +var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var rcloneConfig = {}; +var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); +if (fs2.existsSync(rcloneConfigPath)) { + rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); +} +var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( + IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 +); +var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; +var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; +var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; +var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; +var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; +var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; +var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; +var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; +var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; +var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; +var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; +var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; +var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; +var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( + "http://staging-site" +); + +// site/SiteConstants.ts +import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; +import { + faXTwitter, + faFacebookSquare, + faInstagram, + faThreads, + faLinkedin, + faBluesky +} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; +var polyfillFeatures = [ + "es2021", + // String.replaceAll, Promise.any, ... + "es2022", + // Array.at, String.at, ... + "es2023", + // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... + "IntersectionObserver", + "IntersectionObserverEntry" +]; +var POLYFILL_VERSION = "4.8.0"; +var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( + "," +)}`; +var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; +var DATA_INSIGHT_ATOM_FEED_PROPS = { + title: "Atom feed for Daily Data Insights", + href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` +}; +var RSS_FEEDS = [ + { + title: "Research & Writing RSS Feed", + url: "/atom.xml", + icon: faRss + }, + { + title: "Daily Data Insights RSS Feed", + url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, + icon: faRss + } +]; + +// site/viteUtils.tsx +import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; +var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; +var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; +var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; +var VITE_ENTRYPOINT_INFO = { + ["site" /* Site */]: { + entryPointFile: VITE_ASSET_SITE_ENTRY, + outDir: "assets", + outName: "owid" + }, + ["admin" /* Admin */]: { + entryPointFile: VITE_ASSET_ADMIN_ENTRY, + outDir: "assets-admin", + outName: "admin" + } +}; + +// vite.config-common.mts +import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; +import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; +var defineViteConfigForEntrypoint = (entrypoint) => { + const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; + return defineConfig({ + publicDir: false, + // don't copy public folder to dist + resolve: { + // prettier-ignore + alias: { + "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", + // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work + // we alias to the packages source files in dev and prod: + // this means we get instant dev updates when we change one of them, + // and the prod build builds them all as esm modules, which helps with tree shaking + // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts + "@ourworldindata/components": "@ourworldindata/components/src/index.ts", + "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", + "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", + "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", + "@ourworldindata/types": "@ourworldindata/types/src/index.ts", + "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" + } + }, + css: { + devSourcemap: true + }, + define: { + // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY + // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env + ...Object.fromEntries( + Object.entries(clientSettings_exports).map(([key, value]) => [ + `process.env.${key}`, + JSON.stringify(value) + ]) + ) + }, + build: { + manifest: true, + // creates a manifest.json file, which we use to determine which files to load in prod + emptyOutDir: true, + outDir: `dist/${entrypointInfo.outDir}`, + sourcemap: true, + target: ["chrome80", "firefox78", "safari13.1"], + // see docs/browser-support.md + rollupOptions: { + input: { + [entrypointInfo.outName]: entrypointInfo.entryPointFile + }, + output: { + assetFileNames: `${entrypointInfo.outName}.css`, + entryFileNames: `${entrypointInfo.outName}.mjs` + } + } + }, + plugins: [ + pluginReact({ + babel: { + parserOpts: { + plugins: ["decorators-legacy"] + // needed so mobx decorators work correctly + } + } + }), + pluginChecker({ + typescript: { + buildMode: true, + tsconfigPath: "tsconfig.vite-checker.json" + } + }) + ], + server: { + port: 8090, + warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } + }, + preview: { + port: 8090 + } + }); +}; + +// vite.config-site.mts +var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); +export { + vite_config_site_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== From 322f4eeccda5fc270495fcf494ef7799211a39c6 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 12:39:20 +0100 Subject: [PATCH 33/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20fix=20relative?= =?UTF-8?q?=20value=20calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 74cc8addef9..39d00d6b166 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -149,11 +149,11 @@ export class SlopeChart @computed get transformedTable(): OwidTable { let table = this.transformedTableFromGrapher // The % growth transform cannot be applied in transformTable() because it will filter out - // any rows before startHandleTimeBound and change the timeline bounds. - const { isRelativeMode, startHandleTimeBound } = this.manager - if (isRelativeMode && startHandleTimeBound !== undefined) { + // any rows before startTime and change the timeline bounds. + const { isRelativeMode, startTime } = this.manager + if (isRelativeMode && startTime !== undefined) { table = table.toTotalGrowthForEachColumnComparedToStartTime( - startHandleTimeBound, + startTime, this.yColumnSlugs ?? [] ) } From cf81efec5e4938a42eb7339e8e4f35df0de8d190 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 12:45:56 +0100 Subject: [PATCH 34/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20hide=20relative?= =?UTF-8?q?=20toggle=20for=20log=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index e5166936d08..7d3cd0328b2 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -2197,6 +2197,7 @@ export class Grapher @computed get canToggleRelativeMode(): boolean { const { isOnLineChartTab, + isOnSlopeChartTab, hideRelativeToggle, areHandlesOnSameTime, yScaleType, @@ -2207,7 +2208,7 @@ export class Grapher isStackedChartSplitByMetric, } = this - if (isOnLineChartTab) + if (isOnLineChartTab || isOnSlopeChartTab) return ( !hideRelativeToggle && !areHandlesOnSameTime && From f322692eca8c80b916987c303f048d47cf345d37 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 14:52:24 +0100 Subject: [PATCH 35/91] =?UTF-8?q?=F0=9F=90=9B=20(line)=20use=20startTime?= =?UTF-8?q?=20for=20relative=20mode=20calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/lineCharts/LineChart.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index a6674903be0..87c2cb800c6 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -439,10 +439,10 @@ export class LineChart let table = this.transformedTableFromGrapher // The % growth transform cannot be applied in transformTable() because it will filter out // any rows before startHandleTimeBound and change the timeline bounds. - const { isRelativeMode, startHandleTimeBound } = this.manager - if (isRelativeMode && startHandleTimeBound !== undefined) { + const { isRelativeMode, startTime } = this.manager + if (isRelativeMode && startTime !== undefined) { table = table.toTotalGrowthForEachColumnComparedToStartTime( - startHandleTimeBound, + startTime, this.manager.yColumnSlugs ?? [] ) } From a2a6a89bda56fac3a5bd9aa09f00e54be4d74417 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 16:24:14 +0100 Subject: [PATCH 36/91] =?UTF-8?q?=F0=9F=94=A8=20update=20slope=20migration?= =?UTF-8?q?=20after=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1732291572062-MigrateSlopeCharts.ts | 193 +++++++++++++++--- 1 file changed, 159 insertions(+), 34 deletions(-) diff --git a/db/migration/1732291572062-MigrateSlopeCharts.ts b/db/migration/1732291572062-MigrateSlopeCharts.ts index 84e7693269c..e58500bb004 100644 --- a/db/migration/1732291572062-MigrateSlopeCharts.ts +++ b/db/migration/1732291572062-MigrateSlopeCharts.ts @@ -1,4 +1,8 @@ -import { GrapherInterface } from "@ourworldindata/types" +import { + EntitySelectionMode, + GrapherInterface, + ScaleType, +} from "@ourworldindata/types" import { MigrationInterface, QueryRunner } from "typeorm" export class MigrateSlopeCharts1732291572062 implements MigrationInterface { @@ -87,10 +91,12 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ id: 415, config: { selectedEntityNames: [ - "Cirrhosis and other chronic liver diseases", - "Neonatal disorders", "Congenital heart anomalies", - "Diphtheria", + "Neonatal preterm birth", + "Neonatal encephalopathy due to birth asphyxia and trauma", + "Congenital birth defects", + "Diarrheal diseases", + "Malaria", ], entityType: "cause", entityTypePlural: "causes", @@ -102,14 +108,15 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ id: 679, config: { selectedEntityNames: [ - "Chad", - "Iraq", - "Benin", - "Kenya", - "Brazil", - "Hungary", + "Low-income countries", + "High-income countries", + "Lower-middle-income countries", + "Upper-middle-income countries", ], hideRelativeToggle: true, + yAxis: { + scaleType: ScaleType.linear, + }, }, }, { @@ -128,9 +135,11 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ id: 875, config: { selectedEntityNames: [ - "North America (WB)", - "South Asia (WB)", - "Latin America and Caribbean (WB)", + "India", + "United States", + "Indonesia", + "Pakistan", + "Nigeria", ], hideRelativeToggle: true, }, @@ -139,10 +148,12 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ id: 1004, config: { selectedEntityNames: [ - "Palau", - "Afghanistan", - "Curacao", - "Wallis and Futuna", + "Europe (UN)", + "Asia (UN)", + "Africa (UN)", + "Oceania (UN)", + "Northern America (UN)", + "Latin America and the Caribbean (UN)", ], }, }, @@ -155,27 +166,64 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "South America", "Europe", "Asia", + "Oceania", + "Africa", ], }, }, { id: 2832, config: { - selectedEntityNames: ["Norway", "Italy", "France", "Finland"], + selectedEntityNames: [ + "Italy", + "France", + "Finland", + "Norway", + "Estonia", + "United Kingdom", + "Spain", + "Germany", + "Belgium", + ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { id: 2833, config: { - selectedEntityNames: ["Belgium", "Poland", "Italy", "Germany"], + selectedEntityNames: [ + "Belgium", + "Poland", + "Italy", + "Germany", + "Norway", + "Spain", + "France", + "Finland", + "United Kingdom", + "Estonia", + ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { id: 2834, config: { - selectedEntityNames: ["Belgium", "Italy", "Spain", "Norway"], + selectedEntityNames: [ + "Belgium", + "Italy", + "Spain", + "Norway", + "France", + "Poland", + "Estonia", + "United Kingdom", + "Finland", + "Germany", + ], + addCountryMode: EntitySelectionMode.Disabled, }, }, { @@ -186,8 +234,15 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "Norway", "Poland", "United Kingdom", + "France", + "Finland", + "Germany", + "Belgium", + "Italy", + "Spain", ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { @@ -198,22 +253,52 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "Poland", "United Kingdom", "Finland", + "Estonia", + "Spain", + "Italy", + "Norway", + "France", ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { id: 2976, config: { - selectedEntityNames: ["Poland", "Italy", "Spain", "Belgium"], + selectedEntityNames: [ + "Poland", + "Italy", + "Spain", + "Estonia", + "France", + "Germany", + "Belgium", + "United Kingdom", + "Norway", + "Finland", + ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { id: 2977, config: { - selectedEntityNames: ["Poland", "Norway", "Estonia", "Finland"], + selectedEntityNames: [ + "Poland", + "Norway", + "Estonia", + "Finland", + "Germany", + "Belgium", + "United Kingdom", + "Spain", + "Italy", + "France", + ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { @@ -225,8 +310,14 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "Belgium", "Estonia", "Italy", + "Finland", + "Germany", + "United Kingdom", + "Spain", + "France", ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { @@ -237,8 +328,15 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "Estonia", "Belgium", "Italy", + "France", + "Spain", + "Germany", + "Poland", + "Finland", + "Norway", ], hideTimeline: true, + addCountryMode: EntitySelectionMode.Disabled, }, }, { @@ -301,7 +399,7 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ "India", "Indonesia", "United States", - "Antigua and Barbuda", + "Pakistan", ], }, }, @@ -322,17 +420,26 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ { id: 3620, config: { - selectedEntityNames: ["Romania", "Benin", "Libya", "Suriname"], + selectedEntityNames: [ + "Low income", + "High income", + "Middle income", + "Low & middle income", + "Lower middle income", + "Upper middle income", + ], }, }, { id: 3627, config: { selectedEntityNames: [ - "Syrian Arab Republic", - "East Asia & Pacific", - "Costa Rica", - "Malta", + "Low income", + "High income", + "Middle income", + "Low & middle income", + "Lower middle income", + "Upper middle income", ], }, }, @@ -367,13 +474,28 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ { id: 6529, config: { - selectedEntityNames: ["United States", "Russia", "China"], + selectedEntityNames: [ + "North America", + "Europe", + "Asia", + "South America", + "Oceania", + "Africa", + ], }, }, { id: 7150, config: { - selectedEntityNames: ["Burundi", "Togo", "Ethiopia", "Myanmar"], + selectedEntityNames: [ + "Ethiopia", + "Myanmar", + "Niger", + "Chad", + "Colombia", + "Indonesia", + "Nigeria", + ], }, }, { @@ -396,10 +518,13 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ id: 7344, config: { selectedEntityNames: [ - "Suriname", - "Malta", - "Australia", - "Guatemala", + "United States", + "Romania", + "France", + "United Kingdom", + "Colombia", + "Mexico", + "Japan", ], }, }, From 06ddf6c52bf4dfa009f0f2d16772edea4231d678 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 16:31:29 +0100 Subject: [PATCH 37/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20drop=20non-numer?= =?UTF-8?q?ic=20data=20in=20any=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 39d00d6b166..128a8b398dc 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -118,6 +118,8 @@ export class SlopeChart } transformTableForSelection(table: OwidTable): OwidTable { + table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + // if time selection is disabled, then filter all entities that // don't have data for the current time period if (!this.manager.hasTimeline) { @@ -131,9 +133,9 @@ export class SlopeChart // if entities with partial data are not plotted, // make sure they don't show up in the entity selector if (this.missingDataStrategy === MissingDataStrategy.hide) { - table = table - .replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) - .dropEntitiesThatHaveNoDataInSomeColumn(this.yColumnSlugs) + table = table.dropEntitiesThatHaveNoDataInSomeColumn( + this.yColumnSlugs + ) } return table From 1ed2a96ebe40f7e1e0d7de6e21fb6f37e53d559e Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 28 Nov 2024 16:32:12 +0100 Subject: [PATCH 38/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20show=20correct?= =?UTF-8?q?=20label=20for=20relative=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/migration/1732291572062-MigrateSlopeCharts.ts | 1 - packages/@ourworldindata/grapher/src/core/Grapher.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/db/migration/1732291572062-MigrateSlopeCharts.ts b/db/migration/1732291572062-MigrateSlopeCharts.ts index e58500bb004..92674e2e219 100644 --- a/db/migration/1732291572062-MigrateSlopeCharts.ts +++ b/db/migration/1732291572062-MigrateSlopeCharts.ts @@ -63,7 +63,6 @@ const configUpdates: { id: number; config: GrapherInterface }[] = [ { id: 414, config: { - title: "Top marginal income tax rate", selectedEntityNames: [ "Colombia", "Guatemala", diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 7d3cd0328b2..bbe59358b78 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1421,7 +1421,6 @@ export class Grapher if (this.isLineChart || this.isDiscreteBar) return [yAxis, color] else if (this.isScatter) return [yAxis, xAxis, size, color] else if (this.isMarimekko) return [yAxis, xAxis, color] - else if (this.isSlopeChart) return [yAxis] return [yAxis] } @@ -2177,7 +2176,8 @@ export class Grapher @computed get relativeToggleLabel(): string { if (this.isOnScatterTab) return "Display average annual change" - else if (this.isOnLineChartTab) return "Display relative change" + else if (this.isOnLineChartTab || this.isOnSlopeChartTab) + return "Display relative change" return "Display relative values" } From 8f135540cd43823ca3a9c03fcd074e2b318e51da Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 09:59:31 +0100 Subject: [PATCH 39/91] =?UTF-8?q?=F0=9F=94=A8=20use=20current=20chart=20in?= =?UTF-8?q?stance=20for=20table=20for=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/core/Grapher.tsx | 16 +- ....timestamp-1732870709594-73b494fdbcb7f.mjs | 351 ++++++++++++++++++ 2 files changed, 354 insertions(+), 13 deletions(-) create mode 100644 vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index bbe59358b78..a57879a6b0c 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -791,15 +791,15 @@ export class Grapher ? this.tableAfterAuthorTimelineAndActiveChartTransform : this.inputTable - if (!this.isReady || !this.mainChartInstance) return table + if (!this.isReady) return table // Some chart types (e.g. stacked area charts) choose not to show an entity // with incomplete data. Such chart types define a custom transform function // to ensure that the entity selector only offers entities that are actually plotted. // We apply the `tranformTableForSelection` method of the main chart type, // so that the entity selector doesn't update when switching between chart types. - if (this.mainChartInstance.transformTableForSelection) { - table = this.mainChartInstance.transformTableForSelection(table) + if (this.chartInstance.transformTableForSelection) { + table = this.chartInstance.transformTableForSelection(table) } return table @@ -840,16 +840,6 @@ export class Grapher return transformedTable } - // Chart instance of the "main" chart type, which is the first chart type - // in the list of valid chart types. Doesn't take into account that line - // charts might turn into discrete bar charts. Undefined for map-only Graphers. - @computed private get mainChartInstance(): ChartInterface | undefined { - if (!this.chartType) return undefined - const ChartClass = - ChartComponentClassMap.get(this.chartType) ?? DefaultChartClass - return new ChartClass({ manager: this }) - } - @computed get chartInstance(): ChartInterface { // Note: when timeline handles on a LineChart are collapsed into a single handle, the // LineChart turns into a DiscreteBar. diff --git a/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs b/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs new file mode 100644 index 00000000000..9ef808f81c3 --- /dev/null +++ b/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs @@ -0,0 +1,351 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// site/viteUtils.tsx +import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; + +// settings/findBaseDir.ts +import path from "path"; +import fs from "fs"; +function findProjectBaseDir(from) { + if (!fs.existsSync) return void 0; + let dir = path.dirname(from); + while (dir.length) { + if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; + const parentDir = path.resolve(dir, ".."); + if (parentDir === dir) break; + else dir = parentDir; + } + return void 0; +} + +// site/viteUtils.tsx +import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; + +// settings/serverSettings.ts +import path2 from "path"; +import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import fs2 from "fs"; +import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; +import os from "os"; + +// settings/clientSettings.ts +var clientSettings_exports = {}; +__export(clientSettings_exports, { + ADMIN_BASE_URL: () => ADMIN_BASE_URL, + ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, + ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, + ALGOLIA_ID: () => ALGOLIA_ID, + ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, + ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, + BAKED_BASE_URL: () => BAKED_BASE_URL, + BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, + BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, + BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, + BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, + DATA_API_URL: () => DATA_API_URL, + DONATE_API_URL: () => DONATE_API_URL, + ENV: () => ENV, + ETL_API_URL: () => ETL_API_URL, + ETL_WIZARD_URL: () => ETL_WIZARD_URL, + EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, + FEATURE_FLAGS: () => FEATURE_FLAGS, + FeatureFlagFeature: () => FeatureFlagFeature, + GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, + GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, + GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, + GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, + GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, + IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, + IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, + IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, + MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, + PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, + RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, + SENTRY_DSN: () => SENTRY_DSN, + TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH +}); +import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; +if (typeof __vite_injected_original_dirname2 !== "undefined") { + const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); + if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); +} +var ENV = process.env.ENV === "production" ? "production" : "development"; +var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; +var SENTRY_DSN = process.env.SENTRY_DSN; +var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; +var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; +var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; +var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; +var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; +var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; +var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; +var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; +var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; +var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; +var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; +var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; +var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; +var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; +var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; +var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; +var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; +var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; +var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; +var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( + IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 +); +var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; +var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; +var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; +var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { + FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; + return FeatureFlagFeature2; +})(FeatureFlagFeature || {}); +var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; +var FEATURE_FLAGS = new Set( + Object.keys(FeatureFlagFeature).filter( + (key) => featureFlagsRaw.includes(key) + ) +); + +// settings/serverSettings.ts +import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; +var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); +if (baseDir === void 0) throw new Error("could not locate base package.json"); +dotenv2.config({ path: `${baseDir}/.env` }); +var serverSettings = process.env ?? {}; +var BASE_DIR = baseDir; +var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; +var BAKED_BASE_URL2 = BAKED_BASE_URL; +var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; +var ADMIN_BASE_URL2 = ADMIN_BASE_URL; +var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; +var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; +var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; +var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; +var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; +var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; +var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; +var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; +var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; +var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; +var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; +var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; +var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; +var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; +var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; +var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; +var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; +var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; +var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; +var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); +var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; +var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; +var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; +var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; +var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; +var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; +var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; +var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; +var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; +var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; +var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; +var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; +var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; +var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); +var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; +var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; +var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; +var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var rcloneConfig = {}; +var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); +if (fs2.existsSync(rcloneConfigPath)) { + rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); +} +var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( + IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 +); +var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; +var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; +var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; +var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; +var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; +var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; +var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; +var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; +var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; +var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; +var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; +var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; +var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; +var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( + "http://staging-site" +); + +// site/SiteConstants.ts +import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; +import { + faXTwitter, + faFacebookSquare, + faInstagram, + faThreads, + faLinkedin, + faBluesky +} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; +var polyfillFeatures = [ + "es2021", + // String.replaceAll, Promise.any, ... + "es2022", + // Array.at, String.at, ... + "es2023", + // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... + "IntersectionObserver", + "IntersectionObserverEntry" +]; +var POLYFILL_VERSION = "4.8.0"; +var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( + "," +)}`; +var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; +var DATA_INSIGHT_ATOM_FEED_PROPS = { + title: "Atom feed for Daily Data Insights", + href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` +}; +var RSS_FEEDS = [ + { + title: "Research & Writing RSS Feed", + url: "/atom.xml", + icon: faRss + }, + { + title: "Daily Data Insights RSS Feed", + url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, + icon: faRss + } +]; + +// site/viteUtils.tsx +import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; +var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; +var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; +var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; +var VITE_ENTRYPOINT_INFO = { + ["site" /* Site */]: { + entryPointFile: VITE_ASSET_SITE_ENTRY, + outDir: "assets", + outName: "owid" + }, + ["admin" /* Admin */]: { + entryPointFile: VITE_ASSET_ADMIN_ENTRY, + outDir: "assets-admin", + outName: "admin" + } +}; + +// vite.config-common.mts +import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; +import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; +var defineViteConfigForEntrypoint = (entrypoint) => { + const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; + return defineConfig({ + publicDir: false, + // don't copy public folder to dist + resolve: { + // prettier-ignore + alias: { + "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", + // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work + // we alias to the packages source files in dev and prod: + // this means we get instant dev updates when we change one of them, + // and the prod build builds them all as esm modules, which helps with tree shaking + // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts + "@ourworldindata/components": "@ourworldindata/components/src/index.ts", + "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", + "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", + "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", + "@ourworldindata/types": "@ourworldindata/types/src/index.ts", + "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" + } + }, + css: { + devSourcemap: true + }, + define: { + // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY + // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env + ...Object.fromEntries( + Object.entries(clientSettings_exports).map(([key, value]) => [ + `process.env.${key}`, + JSON.stringify(value) + ]) + ) + }, + build: { + manifest: true, + // creates a manifest.json file, which we use to determine which files to load in prod + emptyOutDir: true, + outDir: `dist/${entrypointInfo.outDir}`, + sourcemap: true, + target: ["chrome80", "firefox78", "safari13.1"], + // see docs/browser-support.md + rollupOptions: { + input: { + [entrypointInfo.outName]: entrypointInfo.entryPointFile + }, + output: { + assetFileNames: `${entrypointInfo.outName}.css`, + entryFileNames: `${entrypointInfo.outName}.mjs` + } + } + }, + plugins: [ + pluginReact({ + babel: { + parserOpts: { + plugins: ["decorators-legacy"] + // needed so mobx decorators work correctly + } + } + }), + pluginChecker({ + typescript: { + buildMode: true, + tsconfigPath: "tsconfig.vite-checker.json" + } + }) + ], + server: { + port: 8090, + warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } + }, + preview: { + port: 8090 + } + }); +}; + +// vite.config-site.mts +var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); +export { + vite_config_site_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== From f52dac556ef8b5a1d5306d93affb77c5e1a82d92 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 14:28:53 +0100 Subject: [PATCH 40/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20only=20drop=20en?= =?UTF-8?q?tities=20if=20start=20and=20end=20time=20are=20different?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 2 +- ....timestamp-1732886743306-ebf767a3e601c.mjs | 351 ++++++++++++++++++ 2 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 128a8b398dc..22b0148f18f 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -122,7 +122,7 @@ export class SlopeChart // if time selection is disabled, then filter all entities that // don't have data for the current time period - if (!this.manager.hasTimeline) { + if (!this.manager.hasTimeline && this.startTime !== this.endTime) { table = table .filterByTargetTimes([this.startTime, this.endTime]) .dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns( diff --git a/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs b/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs new file mode 100644 index 00000000000..9ef808f81c3 --- /dev/null +++ b/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs @@ -0,0 +1,351 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// site/viteUtils.tsx +import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; + +// settings/findBaseDir.ts +import path from "path"; +import fs from "fs"; +function findProjectBaseDir(from) { + if (!fs.existsSync) return void 0; + let dir = path.dirname(from); + while (dir.length) { + if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; + const parentDir = path.resolve(dir, ".."); + if (parentDir === dir) break; + else dir = parentDir; + } + return void 0; +} + +// site/viteUtils.tsx +import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; + +// settings/serverSettings.ts +import path2 from "path"; +import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import fs2 from "fs"; +import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; +import os from "os"; + +// settings/clientSettings.ts +var clientSettings_exports = {}; +__export(clientSettings_exports, { + ADMIN_BASE_URL: () => ADMIN_BASE_URL, + ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, + ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, + ALGOLIA_ID: () => ALGOLIA_ID, + ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, + ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, + BAKED_BASE_URL: () => BAKED_BASE_URL, + BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, + BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, + BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, + BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, + DATA_API_URL: () => DATA_API_URL, + DONATE_API_URL: () => DONATE_API_URL, + ENV: () => ENV, + ETL_API_URL: () => ETL_API_URL, + ETL_WIZARD_URL: () => ETL_WIZARD_URL, + EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, + FEATURE_FLAGS: () => FEATURE_FLAGS, + FeatureFlagFeature: () => FeatureFlagFeature, + GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, + GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, + GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, + GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, + GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, + IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, + IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, + IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, + MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, + PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, + RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, + SENTRY_DSN: () => SENTRY_DSN, + TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH +}); +import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; +import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; +if (typeof __vite_injected_original_dirname2 !== "undefined") { + const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); + if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); +} +var ENV = process.env.ENV === "production" ? "production" : "development"; +var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; +var SENTRY_DSN = process.env.SENTRY_DSN; +var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; +var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; +var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; +var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; +var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; +var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; +var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; +var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; +var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; +var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; +var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; +var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; +var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; +var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; +var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; +var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; +var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; +var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; +var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; +var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; +var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( + IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 +); +var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; +var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; +var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; +var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { + FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; + return FeatureFlagFeature2; +})(FeatureFlagFeature || {}); +var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; +var FEATURE_FLAGS = new Set( + Object.keys(FeatureFlagFeature).filter( + (key) => featureFlagsRaw.includes(key) + ) +); + +// settings/serverSettings.ts +import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; +var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); +if (baseDir === void 0) throw new Error("could not locate base package.json"); +dotenv2.config({ path: `${baseDir}/.env` }); +var serverSettings = process.env ?? {}; +var BASE_DIR = baseDir; +var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; +var BAKED_BASE_URL2 = BAKED_BASE_URL; +var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; +var ADMIN_BASE_URL2 = ADMIN_BASE_URL; +var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; +var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; +var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; +var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; +var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; +var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; +var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; +var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; +var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; +var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; +var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; +var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; +var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; +var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; +var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; +var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; +var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; +var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; +var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; +var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); +var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; +var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; +var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; +var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; +var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; +var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; +var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; +var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; +var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; +var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; +var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; +var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; +var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; +var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); +var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; +var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; +var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; +var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; +var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; +var rcloneConfig = {}; +var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); +if (fs2.existsSync(rcloneConfigPath)) { + rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); +} +var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; +var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; +var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( + IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 +); +var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; +var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; +var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; +var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; +var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; +var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; +var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; +var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; +var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; +var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; +var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; +var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; +var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; +var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( + "http://staging-site" +); + +// site/SiteConstants.ts +import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; +import { + faXTwitter, + faFacebookSquare, + faInstagram, + faThreads, + faLinkedin, + faBluesky +} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; +var polyfillFeatures = [ + "es2021", + // String.replaceAll, Promise.any, ... + "es2022", + // Array.at, String.at, ... + "es2023", + // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... + "IntersectionObserver", + "IntersectionObserverEntry" +]; +var POLYFILL_VERSION = "4.8.0"; +var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( + "," +)}`; +var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; +var DATA_INSIGHT_ATOM_FEED_PROPS = { + title: "Atom feed for Daily Data Insights", + href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` +}; +var RSS_FEEDS = [ + { + title: "Research & Writing RSS Feed", + url: "/atom.xml", + icon: faRss + }, + { + title: "Daily Data Insights RSS Feed", + url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, + icon: faRss + } +]; + +// site/viteUtils.tsx +import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; +import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; +var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; +var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; +var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; +var VITE_ENTRYPOINT_INFO = { + ["site" /* Site */]: { + entryPointFile: VITE_ASSET_SITE_ENTRY, + outDir: "assets", + outName: "owid" + }, + ["admin" /* Admin */]: { + entryPointFile: VITE_ASSET_ADMIN_ENTRY, + outDir: "assets-admin", + outName: "admin" + } +}; + +// vite.config-common.mts +import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; +import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; +var defineViteConfigForEntrypoint = (entrypoint) => { + const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; + return defineConfig({ + publicDir: false, + // don't copy public folder to dist + resolve: { + // prettier-ignore + alias: { + "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", + // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work + // we alias to the packages source files in dev and prod: + // this means we get instant dev updates when we change one of them, + // and the prod build builds them all as esm modules, which helps with tree shaking + // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts + "@ourworldindata/components": "@ourworldindata/components/src/index.ts", + "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", + "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", + "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", + "@ourworldindata/types": "@ourworldindata/types/src/index.ts", + "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" + } + }, + css: { + devSourcemap: true + }, + define: { + // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY + // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env + ...Object.fromEntries( + Object.entries(clientSettings_exports).map(([key, value]) => [ + `process.env.${key}`, + JSON.stringify(value) + ]) + ) + }, + build: { + manifest: true, + // creates a manifest.json file, which we use to determine which files to load in prod + emptyOutDir: true, + outDir: `dist/${entrypointInfo.outDir}`, + sourcemap: true, + target: ["chrome80", "firefox78", "safari13.1"], + // see docs/browser-support.md + rollupOptions: { + input: { + [entrypointInfo.outName]: entrypointInfo.entryPointFile + }, + output: { + assetFileNames: `${entrypointInfo.outName}.css`, + entryFileNames: `${entrypointInfo.outName}.mjs` + } + } + }, + plugins: [ + pluginReact({ + babel: { + parserOpts: { + plugins: ["decorators-legacy"] + // needed so mobx decorators work correctly + } + } + }), + pluginChecker({ + typescript: { + buildMode: true, + tsconfigPath: "tsconfig.vite-checker.json" + } + }) + ], + server: { + port: 8090, + warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } + }, + preview: { + port: 8090 + } + }); +}; + +// vite.config-site.mts +var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); +export { + vite_config_site_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== From d9cdcb402467b27f2fe3446cde18de452fe4d658 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 14:58:36 +0100 Subject: [PATCH 41/91] =?UTF-8?q?=F0=9F=90=9B=20hide=20slope=20chart=20tab?= =?UTF-8?q?=20if=20line=20chart=20really=20is=20a=20bar=20chart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/core/Grapher.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index a57879a6b0c..ec5cea7e1db 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1544,10 +1544,26 @@ export class Grapher // if the given combination is not valid, then ignore all but the first chart type if (!validChartTypes) return chartTypes.slice(0, 1) - // projected data is only supported for line charts + // make sure showing a slope chart tab next to a line chart tab is sensible const isLineChart = validChartTypes[0] === GRAPHER_CHART_TYPES.LineChart - if (isLineChart && this.hasProjectedData) { - return [GRAPHER_CHART_TYPES.LineChart] + if (isLineChart) { + // projected data is only supported for line charts + if (this.hasProjectedData) return [GRAPHER_CHART_TYPES.LineChart] + + // if the line chart really is a bar chart, don't show the slope chart tab + const minTime = minTimeBoundFromJSONOrNegativeInfinity( + this.legacyConfigAsAuthored.minTime + ) + const maxTime = maxTimeBoundFromJSONOrPositiveInfinity( + this.legacyConfigAsAuthored.maxTime + ) + const times = + this.tableAfterAuthorTimelineFilter.timeColumn.uniqValues + const [startTime, endTime] = [minTime, maxTime].map((time) => + findClosestTime(times, time) + ) + const isDiscreteBar = this.hideTimeline && startTime === endTime + if (isDiscreteBar) return [GRAPHER_CHART_TYPES.LineChart] } return validChartTypes From f3b4993c3ab45f0c3e5f9a2c1afe9d27d403cb4f Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 15:14:11 +0100 Subject: [PATCH 42/91] =?UTF-8?q?=E2=9C=A8=20automatically=20adjust=20hand?= =?UTF-8?q?les=20when=20switching=20from=20line=20to=20slope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/controls/ContentSwitchers.tsx | 3 +++ .../grapher/src/core/Grapher.tsx | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx index 8d947e5bb62..848f4340514 100644 --- a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx +++ b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx @@ -18,6 +18,7 @@ export interface ContentSwitchersManager { activeTab?: GrapherTabName hasMultipleChartTypes?: boolean setTab: (tab: GrapherTabName) => void + onTabChange: (oldTab: GrapherTabName, newTab: GrapherTabName) => void isNarrow?: boolean isMedium?: boolean isLineChartThatTurnedIntoDiscreteBar?: boolean @@ -112,8 +113,10 @@ export class ContentSwitchers extends React.Component<{ } @action.bound setTab(tabIndex: number): void { + const oldTab = this.manager.activeTab const newTab = this.availableTabs[tabIndex] this.manager.setTab(newTab) + this.manager.onTabChange?.(oldTab!, newTab) } render(): React.ReactElement { diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index ec5cea7e1db..d3d3d2ff221 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1313,6 +1313,26 @@ export class Grapher } } + @action.bound onTabChange( + oldTab: GrapherTabName, + newTab: GrapherTabName + ): void { + // if switching from a line to a slope chart and the handles are + // on the same time, then automatically adjust the handles so that + // a time period is selected and the slope chart view is meaningful + if ( + oldTab === GRAPHER_TAB_NAMES.LineChart && + newTab === GRAPHER_TAB_NAMES.SlopeChart && + this.areHandlesOnSameTime + ) { + if (this.startHandleTimeBound !== -Infinity) { + this.startHandleTimeBound = -Infinity + } else { + this.endHandleTimeBound = Infinity + } + } + } + // todo: can we remove this? // I believe these states can only occur during editing. @action.bound private ensureValidConfigWhenEditing(): void { From a617f446d03a377e5fc87109083eda5371042e0a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 15:17:12 +0100 Subject: [PATCH 43/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20only=20add=20ent?= =?UTF-8?q?ity=20name=20if=20necessary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 22b0148f18f..fbbed46fd2c 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -266,7 +266,7 @@ export class SlopeChart const { startTime, endTime, seriesStrategy } = this const { canSelectMultipleEntities = false } = this.manager - const { availableEntityNames } = this.selectionArray + const { availableEntityNames } = this.transformedTable const columnName = column.nonEmptyDisplayName const seriesName = getSeriesName({ entityName, From 687c486a9e4c105495806ab46dbe0690ad9c880f Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 15:23:57 +0100 Subject: [PATCH 44/91] =?UTF-8?q?=F0=9F=8E=89=20enable=20timeline=20animat?= =?UTF-8?q?ion=20for=20slope=20charts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index d3d3d2ff221..dcfee91cd07 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -3540,7 +3540,7 @@ export class Grapher } @computed get disablePlay(): boolean { - return this.isOnSlopeChartTab + return false } @computed get animationEndTime(): Time { From 17e4cf901d3e1f8d742c06a54c45af2df98338bf Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 15:58:30 +0100 Subject: [PATCH 45/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20update=20comment?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/core/Grapher.tsx | 4 +--- .../grapher/src/slopeCharts/SlopeChart.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index dcfee91cd07..6f1ad7a6dbe 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -796,8 +796,6 @@ export class Grapher // Some chart types (e.g. stacked area charts) choose not to show an entity // with incomplete data. Such chart types define a custom transform function // to ensure that the entity selector only offers entities that are actually plotted. - // We apply the `tranformTableForSelection` method of the main chart type, - // so that the entity selector doesn't update when switching between chart types. if (this.chartInstance.transformTableForSelection) { table = this.chartInstance.transformTableForSelection(table) } @@ -1319,7 +1317,7 @@ export class Grapher ): void { // if switching from a line to a slope chart and the handles are // on the same time, then automatically adjust the handles so that - // a time period is selected and the slope chart view is meaningful + // the slope chart view is meaningful if ( oldTab === GRAPHER_TAB_NAMES.LineChart && newTab === GRAPHER_TAB_NAMES.SlopeChart && diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index fbbed46fd2c..b81b4e60de2 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -385,7 +385,18 @@ export class SlopeChart } @computed private get showNoDataSection(): boolean { - return this.noDataSeries.length > 0 + // nothing to show if there are no series with missing data + if (this.noDataSeries.length === 0) return false + + // we usually don't show the no data section if columns are plotted + // (since columns don't appear in the entity selector there is no need + // to explain that a column is missing – it just adds noise). but if + // the missing data strategy is set to hide, then we do want to give + // feedback as to why a slope is currently not rendered + return ( + this.seriesStrategy === SeriesStrategy.entity || + this.missingDataStrategy === MissingDataStrategy.hide + ) } @computed private get yAxisConfig(): AxisConfig { From 7e583e0b27cd05153a273bbfc9fe6ae0ad241907 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 16:35:07 +0100 Subject: [PATCH 46/91] =?UTF-8?q?=F0=9F=94=A8=20remove=20accidental=20comm?= =?UTF-8?q?its?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....timestamp-1732790803730-c1bc0b6a7ecde.mjs | 351 ------------------ ....timestamp-1732870709594-73b494fdbcb7f.mjs | 351 ------------------ ....timestamp-1732886743306-ebf767a3e601c.mjs | 351 ------------------ 3 files changed, 1053 deletions(-) delete mode 100644 vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs delete mode 100644 vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs delete mode 100644 vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs diff --git a/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs b/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs deleted file mode 100644 index 9ef808f81c3..00000000000 --- a/vite.config-site.mts.timestamp-1732790803730-c1bc0b6a7ecde.mjs +++ /dev/null @@ -1,351 +0,0 @@ -var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// site/viteUtils.tsx -import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; - -// settings/findBaseDir.ts -import path from "path"; -import fs from "fs"; -function findProjectBaseDir(from) { - if (!fs.existsSync) return void 0; - let dir = path.dirname(from); - while (dir.length) { - if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; - const parentDir = path.resolve(dir, ".."); - if (parentDir === dir) break; - else dir = parentDir; - } - return void 0; -} - -// site/viteUtils.tsx -import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; - -// settings/serverSettings.ts -import path2 from "path"; -import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import fs2 from "fs"; -import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; -import os from "os"; - -// settings/clientSettings.ts -var clientSettings_exports = {}; -__export(clientSettings_exports, { - ADMIN_BASE_URL: () => ADMIN_BASE_URL, - ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, - ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, - ALGOLIA_ID: () => ALGOLIA_ID, - ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, - ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, - BAKED_BASE_URL: () => BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, - BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, - BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, - BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, - DATA_API_URL: () => DATA_API_URL, - DONATE_API_URL: () => DONATE_API_URL, - ENV: () => ENV, - ETL_API_URL: () => ETL_API_URL, - ETL_WIZARD_URL: () => ETL_WIZARD_URL, - EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, - FEATURE_FLAGS: () => FEATURE_FLAGS, - FeatureFlagFeature: () => FeatureFlagFeature, - GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, - GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, - GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, - GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, - GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, - GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, - IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, - IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, - IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, - MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, - PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, - RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, - SENTRY_DSN: () => SENTRY_DSN, - TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH -}); -import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; -if (typeof __vite_injected_original_dirname2 !== "undefined") { - const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); - if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); -} -var ENV = process.env.ENV === "production" ? "production" : "development"; -var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; -var SENTRY_DSN = process.env.SENTRY_DSN; -var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; -var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; -var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; -var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; -var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; -var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; -var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; -var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; -var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; -var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; -var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; -var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; -var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; -var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; -var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; -var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; -var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; -var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; -var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; -var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( - IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 -); -var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; -var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; -var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; -var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { - FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; - return FeatureFlagFeature2; -})(FeatureFlagFeature || {}); -var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; -var FEATURE_FLAGS = new Set( - Object.keys(FeatureFlagFeature).filter( - (key) => featureFlagsRaw.includes(key) - ) -); - -// settings/serverSettings.ts -import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; -var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); -if (baseDir === void 0) throw new Error("could not locate base package.json"); -dotenv2.config({ path: `${baseDir}/.env` }); -var serverSettings = process.env ?? {}; -var BASE_DIR = baseDir; -var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; -var BAKED_BASE_URL2 = BAKED_BASE_URL; -var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; -var ADMIN_BASE_URL2 = ADMIN_BASE_URL; -var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; -var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; -var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; -var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; -var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; -var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; -var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; -var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; -var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; -var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; -var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; -var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; -var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; -var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; -var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; -var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; -var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; -var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; -var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; -var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); -var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; -var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; -var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; -var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; -var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; -var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; -var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; -var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; -var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; -var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; -var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; -var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; -var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; -var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); -var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; -var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; -var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; -var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var rcloneConfig = {}; -var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); -if (fs2.existsSync(rcloneConfigPath)) { - rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); -} -var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( - IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 -); -var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; -var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; -var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; -var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; -var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; -var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; -var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; -var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; -var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; -var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; -var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; -var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; -var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; -var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( - "http://staging-site" -); - -// site/SiteConstants.ts -import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; -import { - faXTwitter, - faFacebookSquare, - faInstagram, - faThreads, - faLinkedin, - faBluesky -} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; -var polyfillFeatures = [ - "es2021", - // String.replaceAll, Promise.any, ... - "es2022", - // Array.at, String.at, ... - "es2023", - // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... - "IntersectionObserver", - "IntersectionObserverEntry" -]; -var POLYFILL_VERSION = "4.8.0"; -var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( - "," -)}`; -var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; -var DATA_INSIGHT_ATOM_FEED_PROPS = { - title: "Atom feed for Daily Data Insights", - href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` -}; -var RSS_FEEDS = [ - { - title: "Research & Writing RSS Feed", - url: "/atom.xml", - icon: faRss - }, - { - title: "Daily Data Insights RSS Feed", - url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, - icon: faRss - } -]; - -// site/viteUtils.tsx -import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; -var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; -var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; -var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; -var VITE_ENTRYPOINT_INFO = { - ["site" /* Site */]: { - entryPointFile: VITE_ASSET_SITE_ENTRY, - outDir: "assets", - outName: "owid" - }, - ["admin" /* Admin */]: { - entryPointFile: VITE_ASSET_ADMIN_ENTRY, - outDir: "assets-admin", - outName: "admin" - } -}; - -// vite.config-common.mts -import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; -import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; -import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; -var defineViteConfigForEntrypoint = (entrypoint) => { - const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; - return defineConfig({ - publicDir: false, - // don't copy public folder to dist - resolve: { - // prettier-ignore - alias: { - "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", - // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work - // we alias to the packages source files in dev and prod: - // this means we get instant dev updates when we change one of them, - // and the prod build builds them all as esm modules, which helps with tree shaking - // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts - "@ourworldindata/components": "@ourworldindata/components/src/index.ts", - "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", - "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", - "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", - "@ourworldindata/types": "@ourworldindata/types/src/index.ts", - "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" - } - }, - css: { - devSourcemap: true - }, - define: { - // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY - // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env - ...Object.fromEntries( - Object.entries(clientSettings_exports).map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value) - ]) - ) - }, - build: { - manifest: true, - // creates a manifest.json file, which we use to determine which files to load in prod - emptyOutDir: true, - outDir: `dist/${entrypointInfo.outDir}`, - sourcemap: true, - target: ["chrome80", "firefox78", "safari13.1"], - // see docs/browser-support.md - rollupOptions: { - input: { - [entrypointInfo.outName]: entrypointInfo.entryPointFile - }, - output: { - assetFileNames: `${entrypointInfo.outName}.css`, - entryFileNames: `${entrypointInfo.outName}.mjs` - } - } - }, - plugins: [ - pluginReact({ - babel: { - parserOpts: { - plugins: ["decorators-legacy"] - // needed so mobx decorators work correctly - } - } - }), - pluginChecker({ - typescript: { - buildMode: true, - tsconfigPath: "tsconfig.vite-checker.json" - } - }) - ], - server: { - port: 8090, - warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } - }, - preview: { - port: 8090 - } - }); -}; - -// vite.config-site.mts -var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); -export { - vite_config_site_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== diff --git a/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs b/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs deleted file mode 100644 index 9ef808f81c3..00000000000 --- a/vite.config-site.mts.timestamp-1732870709594-73b494fdbcb7f.mjs +++ /dev/null @@ -1,351 +0,0 @@ -var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// site/viteUtils.tsx -import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; - -// settings/findBaseDir.ts -import path from "path"; -import fs from "fs"; -function findProjectBaseDir(from) { - if (!fs.existsSync) return void 0; - let dir = path.dirname(from); - while (dir.length) { - if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; - const parentDir = path.resolve(dir, ".."); - if (parentDir === dir) break; - else dir = parentDir; - } - return void 0; -} - -// site/viteUtils.tsx -import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; - -// settings/serverSettings.ts -import path2 from "path"; -import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import fs2 from "fs"; -import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; -import os from "os"; - -// settings/clientSettings.ts -var clientSettings_exports = {}; -__export(clientSettings_exports, { - ADMIN_BASE_URL: () => ADMIN_BASE_URL, - ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, - ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, - ALGOLIA_ID: () => ALGOLIA_ID, - ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, - ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, - BAKED_BASE_URL: () => BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, - BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, - BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, - BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, - DATA_API_URL: () => DATA_API_URL, - DONATE_API_URL: () => DONATE_API_URL, - ENV: () => ENV, - ETL_API_URL: () => ETL_API_URL, - ETL_WIZARD_URL: () => ETL_WIZARD_URL, - EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, - FEATURE_FLAGS: () => FEATURE_FLAGS, - FeatureFlagFeature: () => FeatureFlagFeature, - GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, - GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, - GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, - GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, - GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, - GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, - IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, - IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, - IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, - MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, - PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, - RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, - SENTRY_DSN: () => SENTRY_DSN, - TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH -}); -import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; -if (typeof __vite_injected_original_dirname2 !== "undefined") { - const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); - if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); -} -var ENV = process.env.ENV === "production" ? "production" : "development"; -var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; -var SENTRY_DSN = process.env.SENTRY_DSN; -var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; -var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; -var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; -var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; -var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; -var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; -var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; -var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; -var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; -var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; -var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; -var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; -var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; -var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; -var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; -var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; -var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; -var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; -var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; -var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( - IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 -); -var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; -var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; -var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; -var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { - FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; - return FeatureFlagFeature2; -})(FeatureFlagFeature || {}); -var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; -var FEATURE_FLAGS = new Set( - Object.keys(FeatureFlagFeature).filter( - (key) => featureFlagsRaw.includes(key) - ) -); - -// settings/serverSettings.ts -import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; -var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); -if (baseDir === void 0) throw new Error("could not locate base package.json"); -dotenv2.config({ path: `${baseDir}/.env` }); -var serverSettings = process.env ?? {}; -var BASE_DIR = baseDir; -var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; -var BAKED_BASE_URL2 = BAKED_BASE_URL; -var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; -var ADMIN_BASE_URL2 = ADMIN_BASE_URL; -var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; -var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; -var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; -var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; -var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; -var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; -var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; -var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; -var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; -var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; -var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; -var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; -var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; -var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; -var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; -var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; -var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; -var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; -var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; -var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); -var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; -var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; -var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; -var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; -var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; -var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; -var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; -var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; -var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; -var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; -var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; -var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; -var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; -var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); -var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; -var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; -var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; -var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var rcloneConfig = {}; -var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); -if (fs2.existsSync(rcloneConfigPath)) { - rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); -} -var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( - IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 -); -var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; -var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; -var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; -var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; -var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; -var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; -var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; -var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; -var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; -var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; -var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; -var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; -var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; -var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( - "http://staging-site" -); - -// site/SiteConstants.ts -import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; -import { - faXTwitter, - faFacebookSquare, - faInstagram, - faThreads, - faLinkedin, - faBluesky -} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; -var polyfillFeatures = [ - "es2021", - // String.replaceAll, Promise.any, ... - "es2022", - // Array.at, String.at, ... - "es2023", - // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... - "IntersectionObserver", - "IntersectionObserverEntry" -]; -var POLYFILL_VERSION = "4.8.0"; -var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( - "," -)}`; -var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; -var DATA_INSIGHT_ATOM_FEED_PROPS = { - title: "Atom feed for Daily Data Insights", - href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` -}; -var RSS_FEEDS = [ - { - title: "Research & Writing RSS Feed", - url: "/atom.xml", - icon: faRss - }, - { - title: "Daily Data Insights RSS Feed", - url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, - icon: faRss - } -]; - -// site/viteUtils.tsx -import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; -var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; -var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; -var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; -var VITE_ENTRYPOINT_INFO = { - ["site" /* Site */]: { - entryPointFile: VITE_ASSET_SITE_ENTRY, - outDir: "assets", - outName: "owid" - }, - ["admin" /* Admin */]: { - entryPointFile: VITE_ASSET_ADMIN_ENTRY, - outDir: "assets-admin", - outName: "admin" - } -}; - -// vite.config-common.mts -import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; -import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; -import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; -var defineViteConfigForEntrypoint = (entrypoint) => { - const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; - return defineConfig({ - publicDir: false, - // don't copy public folder to dist - resolve: { - // prettier-ignore - alias: { - "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", - // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work - // we alias to the packages source files in dev and prod: - // this means we get instant dev updates when we change one of them, - // and the prod build builds them all as esm modules, which helps with tree shaking - // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts - "@ourworldindata/components": "@ourworldindata/components/src/index.ts", - "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", - "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", - "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", - "@ourworldindata/types": "@ourworldindata/types/src/index.ts", - "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" - } - }, - css: { - devSourcemap: true - }, - define: { - // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY - // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env - ...Object.fromEntries( - Object.entries(clientSettings_exports).map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value) - ]) - ) - }, - build: { - manifest: true, - // creates a manifest.json file, which we use to determine which files to load in prod - emptyOutDir: true, - outDir: `dist/${entrypointInfo.outDir}`, - sourcemap: true, - target: ["chrome80", "firefox78", "safari13.1"], - // see docs/browser-support.md - rollupOptions: { - input: { - [entrypointInfo.outName]: entrypointInfo.entryPointFile - }, - output: { - assetFileNames: `${entrypointInfo.outName}.css`, - entryFileNames: `${entrypointInfo.outName}.mjs` - } - } - }, - plugins: [ - pluginReact({ - babel: { - parserOpts: { - plugins: ["decorators-legacy"] - // needed so mobx decorators work correctly - } - } - }), - pluginChecker({ - typescript: { - buildMode: true, - tsconfigPath: "tsconfig.vite-checker.json" - } - }) - ], - server: { - port: 8090, - warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } - }, - preview: { - port: 8090 - } - }); -}; - -// vite.config-site.mts -var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); -export { - vite_config_site_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== diff --git a/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs b/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs deleted file mode 100644 index 9ef808f81c3..00000000000 --- a/vite.config-site.mts.timestamp-1732886743306-ebf767a3e601c.mjs +++ /dev/null @@ -1,351 +0,0 @@ -var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// site/viteUtils.tsx -import React from "file:///Users/sophia/code/owid/owid-grapher/node_modules/react/index.js"; - -// settings/findBaseDir.ts -import path from "path"; -import fs from "fs"; -function findProjectBaseDir(from) { - if (!fs.existsSync) return void 0; - let dir = path.dirname(from); - while (dir.length) { - if (fs.existsSync(path.resolve(dir, "package.json"))) return dir; - const parentDir = path.resolve(dir, ".."); - if (parentDir === dir) break; - else dir = parentDir; - } - return void 0; -} - -// site/viteUtils.tsx -import fs3 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/fs-extra/lib/index.js"; - -// settings/serverSettings.ts -import path2 from "path"; -import dotenv2 from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import fs2 from "fs"; -import ini from "file:///Users/sophia/code/owid/owid-grapher/node_modules/ini/lib/ini.js"; -import os from "os"; - -// settings/clientSettings.ts -var clientSettings_exports = {}; -__export(clientSettings_exports, { - ADMIN_BASE_URL: () => ADMIN_BASE_URL, - ADMIN_SERVER_HOST: () => ADMIN_SERVER_HOST, - ADMIN_SERVER_PORT: () => ADMIN_SERVER_PORT, - ALGOLIA_ID: () => ALGOLIA_ID, - ALGOLIA_INDEX_PREFIX: () => ALGOLIA_INDEX_PREFIX, - ALGOLIA_SEARCH_KEY: () => ALGOLIA_SEARCH_KEY, - BAKED_BASE_URL: () => BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL: () => BAKED_GRAPHER_EXPORTS_BASE_URL, - BAKED_GRAPHER_URL: () => BAKED_GRAPHER_URL, - BAKED_SITE_EXPORTS_BASE_URL: () => BAKED_SITE_EXPORTS_BASE_URL, - BUGSNAG_API_KEY: () => BUGSNAG_API_KEY, - DATA_API_URL: () => DATA_API_URL, - DONATE_API_URL: () => DONATE_API_URL, - ENV: () => ENV, - ETL_API_URL: () => ETL_API_URL, - ETL_WIZARD_URL: () => ETL_WIZARD_URL, - EXPLORER_DYNAMIC_THUMBNAIL_URL: () => EXPLORER_DYNAMIC_THUMBNAIL_URL, - FEATURE_FLAGS: () => FEATURE_FLAGS, - FeatureFlagFeature: () => FeatureFlagFeature, - GDOCS_BASIC_ARTICLE_TEMPLATE_URL: () => GDOCS_BASIC_ARTICLE_TEMPLATE_URL, - GDOCS_CLIENT_EMAIL: () => GDOCS_CLIENT_EMAIL, - GDOCS_DETAILS_ON_DEMAND_ID: () => GDOCS_DETAILS_ON_DEMAND_ID, - GOOGLE_TAG_MANAGER_ID: () => GOOGLE_TAG_MANAGER_ID, - GRAPHER_DYNAMIC_CONFIG_URL: () => GRAPHER_DYNAMIC_CONFIG_URL, - GRAPHER_DYNAMIC_THUMBNAIL_URL: () => GRAPHER_DYNAMIC_THUMBNAIL_URL, - IMAGE_HOSTING_R2_BUCKET_PATH: () => IMAGE_HOSTING_R2_BUCKET_PATH, - IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH: () => IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH, - IMAGE_HOSTING_R2_CDN_URL: () => IMAGE_HOSTING_R2_CDN_URL, - MULTI_DIM_DYNAMIC_CONFIG_URL: () => MULTI_DIM_DYNAMIC_CONFIG_URL, - PUBLISHED_AT_FORMAT: () => PUBLISHED_AT_FORMAT, - RECAPTCHA_SITE_KEY: () => RECAPTCHA_SITE_KEY, - SENTRY_DSN: () => SENTRY_DSN, - TOPICS_CONTENT_GRAPH: () => TOPICS_CONTENT_GRAPH -}); -import dotenv from "file:///Users/sophia/code/owid/owid-grapher/node_modules/dotenv/lib/main.js"; -import { parseIntOrUndefined } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname2 = "/Users/sophia/code/owid/owid-grapher/settings"; -if (typeof __vite_injected_original_dirname2 !== "undefined") { - const baseDir2 = findProjectBaseDir(__vite_injected_original_dirname2); - if (baseDir2) dotenv.config({ path: `${baseDir2}/.env` }); -} -var ENV = process.env.ENV === "production" ? "production" : "development"; -var BUGSNAG_API_KEY = process.env.BUGSNAG_API_KEY; -var SENTRY_DSN = process.env.SENTRY_DSN; -var ADMIN_SERVER_PORT = parseIntOrUndefined(process.env.ADMIN_SERVER_PORT) ?? 3030; -var ADMIN_SERVER_HOST = process.env.ADMIN_SERVER_HOST ?? "localhost"; -var BAKED_BASE_URL = process.env.BAKED_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var BAKED_GRAPHER_URL = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher`; -var BAKED_GRAPHER_EXPORTS_BASE_URL = process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports`; -var BAKED_SITE_EXPORTS_BASE_URL = process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports`; -var GRAPHER_DYNAMIC_THUMBNAIL_URL = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}`; -var EXPLORER_DYNAMIC_THUMBNAIL_URL = process.env.EXPLORER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_BASE_URL}/explorers`; -var GRAPHER_DYNAMIC_CONFIG_URL = process.env.GRAPHER_DYNAMIC_CONFIG_URL ?? `${BAKED_GRAPHER_URL}`; -var MULTI_DIM_DYNAMIC_CONFIG_URL = process.env.MULTI_DIM_DYNAMIC_CONFIG_URL ?? `${BAKED_BASE_URL}/multi-dim`; -var ADMIN_BASE_URL = process.env.ADMIN_BASE_URL ?? `http://${ADMIN_SERVER_HOST}:${ADMIN_SERVER_PORT}`; -var DATA_API_URL = process.env.DATA_API_URL ?? "https://api.ourworldindata.org/v1/indicators/"; -var ALGOLIA_ID = process.env.ALGOLIA_ID ?? ""; -var ALGOLIA_SEARCH_KEY = process.env.ALGOLIA_SEARCH_KEY ?? ""; -var ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX ?? ""; -var DONATE_API_URL = process.env.DONATE_API_URL ?? "http://localhost:8788/donation/donate"; -var RECAPTCHA_SITE_KEY = process.env.RECAPTCHA_SITE_KEY ?? "6LcJl5YUAAAAAATQ6F4vl9dAWRZeKPBm15MAZj4Q"; -var GOOGLE_TAG_MANAGER_ID = process.env.GOOGLE_TAG_MANAGER_ID ?? ""; -var TOPICS_CONTENT_GRAPH = process.env.TOPICS_CONTENT_GRAPH === "true"; -var GDOCS_CLIENT_EMAIL = process.env.GDOCS_CLIENT_EMAIL ?? ""; -var GDOCS_BASIC_ARTICLE_TEMPLATE_URL = process.env.GDOCS_BASIC_ARTICLE_TEMPLATE_URL ?? ""; -var IMAGE_HOSTING_R2_CDN_URL = process.env.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH = process.env.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH = IMAGE_HOSTING_R2_BUCKET_PATH.slice( - IMAGE_HOSTING_R2_BUCKET_PATH.indexOf("/") + 1 -); -var ETL_WIZARD_URL = process.env.ETL_WIZARD_URL ?? `http://${ADMIN_SERVER_HOST}:8053/`; -var ETL_API_URL = process.env.ETL_API_URL ?? `http://${ADMIN_SERVER_HOST}:8081/api/v1`; -var GDOCS_DETAILS_ON_DEMAND_ID = process.env.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var PUBLISHED_AT_FORMAT = "ddd, MMM D, YYYY HH:mm"; -var FeatureFlagFeature = /* @__PURE__ */ ((FeatureFlagFeature2) => { - FeatureFlagFeature2["MultiDimDataPage"] = "MultiDimDataPage"; - return FeatureFlagFeature2; -})(FeatureFlagFeature || {}); -var featureFlagsRaw = typeof process.env.FEATURE_FLAGS === "string" && process.env.FEATURE_FLAGS.trim()?.split(",") || []; -var FEATURE_FLAGS = new Set( - Object.keys(FeatureFlagFeature).filter( - (key) => featureFlagsRaw.includes(key) - ) -); - -// settings/serverSettings.ts -import { parseIntOrUndefined as parseIntOrUndefined2 } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -var __vite_injected_original_dirname3 = "/Users/sophia/code/owid/owid-grapher/settings"; -var baseDir = findProjectBaseDir(__vite_injected_original_dirname3); -if (baseDir === void 0) throw new Error("could not locate base package.json"); -dotenv2.config({ path: `${baseDir}/.env` }); -var serverSettings = process.env ?? {}; -var BASE_DIR = baseDir; -var DATA_API_FOR_ADMIN_UI = serverSettings.DATA_API_FOR_ADMIN_UI; -var BAKED_BASE_URL2 = BAKED_BASE_URL; -var VITE_PREVIEW = serverSettings.VITE_PREVIEW === "true"; -var ADMIN_BASE_URL2 = ADMIN_BASE_URL; -var BAKED_GRAPHER_URL2 = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL2}/grapher`; -var OPTIMIZE_SVG_EXPORTS = serverSettings.OPTIMIZE_SVG_EXPORTS === "true"; -var GITHUB_USERNAME = serverSettings.GITHUB_USERNAME ?? "owid-test"; -var GIT_DEFAULT_USERNAME = serverSettings.GIT_DEFAULT_USERNAME ?? "Our World in Data"; -var GIT_DEFAULT_EMAIL = serverSettings.GIT_DEFAULT_EMAIL ?? "info@ourworldindata.org"; -var BUGSNAG_API_KEY2 = serverSettings.BUGSNAG_API_KEY; -var BUGSNAG_NODE_API_KEY = serverSettings.BUGSNAG_NODE_API_KEY; -var BLOG_POSTS_PER_PAGE = parseIntOrUndefined2(serverSettings.BLOG_POSTS_PER_PAGE) ?? 21; -var BLOG_SLUG = serverSettings.BLOG_SLUG ?? "latest"; -var GRAPHER_DB_NAME = serverSettings.GRAPHER_DB_NAME ?? "owid"; -var GRAPHER_DB_USER = serverSettings.GRAPHER_DB_USER ?? "root"; -var GRAPHER_DB_PASS = serverSettings.GRAPHER_DB_PASS ?? ""; -var GRAPHER_DB_HOST = serverSettings.GRAPHER_DB_HOST ?? "localhost"; -var GRAPHER_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_DB_PORT) ?? 3306; -var GRAPHER_TEST_DB_NAME = serverSettings.GRAPHER_TEST_DB_NAME ?? "owid"; -var GRAPHER_TEST_DB_USER = serverSettings.GRAPHER_TEST_DB_USER ?? "root"; -var GRAPHER_TEST_DB_PASS = serverSettings.GRAPHER_TEST_DB_PASS ?? ""; -var GRAPHER_TEST_DB_HOST = serverSettings.GRAPHER_TEST_DB_HOST ?? "localhost"; -var GRAPHER_TEST_DB_PORT = parseIntOrUndefined2(serverSettings.GRAPHER_TEST_DB_PORT) ?? 3306; -var BAKED_SITE_DIR = serverSettings.BAKED_SITE_DIR ?? path2.resolve(BASE_DIR, "bakedSite"); -var SECRET_KEY = serverSettings.SECRET_KEY ?? "fejwiaof jewiafo jeioa fjieowajf isa fjidosajfgj"; -var SESSION_COOKIE_AGE = parseIntOrUndefined2(serverSettings.SESSION_COOKIE_AGE) ?? 1209600; -var ALGOLIA_SECRET_KEY = serverSettings.ALGOLIA_SECRET_KEY ?? ""; -var ALGOLIA_INDEXING = serverSettings.ALGOLIA_INDEXING === "true"; -var HTTPS_ONLY = serverSettings.HTTPS_ONLY !== "false"; -var GIT_DATASETS_DIR = serverSettings.GIT_DATASETS_DIR ?? `${BASE_DIR}/datasetsExport`; -var TMP_DIR = serverSettings.TMP_DIR ?? "/tmp"; -var UNCATEGORIZED_TAG_ID = parseIntOrUndefined2(serverSettings.UNCATEGORIZED_TAG_ID) ?? 375; -var BAKE_ON_CHANGE = serverSettings.BAKE_ON_CHANGE === "true"; -var DEPLOY_QUEUE_FILE_PATH = serverSettings.DEPLOY_QUEUE_FILE_PATH ?? `${BASE_DIR}/.queue`; -var DEPLOY_PENDING_FILE_PATH = serverSettings.DEPLOY_PENDING_FILE_PATH ?? `${BASE_DIR}/.pending`; -var CLOUDFLARE_AUD = serverSettings.CLOUDFLARE_AUD ?? ""; -var CATALOG_PATH = serverSettings.CATALOG_PATH ?? ""; -var GDOCS_PRIVATE_KEY = (serverSettings.GDOCS_PRIVATE_KEY ?? "").replaceAll('"', "").replaceAll("'", ""); -var GDOCS_CLIENT_ID = serverSettings.GDOCS_CLIENT_ID ?? ""; -var GDOCS_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER = serverSettings.GDOCS_IMAGES_BACKPORTING_TARGET_FOLDER ?? ""; -var GDOCS_DONATE_FAQS_DOCUMENT_ID = serverSettings.GDOCS_DONATE_FAQS_DOCUMENT_ID ?? "194PNSFjgSlt9Zm5xYuDOF0l_GLKZbVxH2co3zCok_cE"; -var GDOCS_SHARED_DRIVE_ID = serverSettings.GDOCS_SHARED_DRIVE_ID ?? ""; -var GDOCS_DETAILS_ON_DEMAND_ID2 = serverSettings.GDOCS_DETAILS_ON_DEMAND_ID ?? ""; -var rcloneConfig = {}; -var rcloneConfigPath = path2.join(os.homedir(), ".config/rclone/rclone.conf"); -if (fs2.existsSync(rcloneConfigPath)) { - rcloneConfig = ini.parse(fs2.readFileSync(rcloneConfigPath, "utf-8")); -} -var IMAGE_HOSTING_R2_CDN_URL2 = serverSettings.IMAGE_HOSTING_R2_CDN_URL || ""; -var IMAGE_HOSTING_R2_BUCKET_PATH2 = serverSettings.IMAGE_HOSTING_R2_BUCKET_PATH || ""; -var IMAGE_HOSTING_R2_BUCKET_SUBFOLDER_PATH2 = IMAGE_HOSTING_R2_BUCKET_PATH2.slice( - IMAGE_HOSTING_R2_BUCKET_PATH2.indexOf("/") + 1 -); -var R2_ENDPOINT = serverSettings.R2_ENDPOINT || rcloneConfig["owid-r2"]?.endpoint || "https://078fcdfed9955087315dd86792e71a7e.r2.cloudflarestorage.com"; -var R2_ACCESS_KEY_ID = serverSettings.R2_ACCESS_KEY_ID || rcloneConfig["owid-r2"]?.access_key_id || ""; -var R2_SECRET_ACCESS_KEY = serverSettings.R2_SECRET_ACCESS_KEY || rcloneConfig["owid-r2"]?.secret_access_key || ""; -var R2_REGION = serverSettings.R2_REGION || rcloneConfig["owid-r2"]?.region || "auto"; -var GRAPHER_CONFIG_R2_BUCKET = serverSettings.GRAPHER_CONFIG_R2_BUCKET; -var GRAPHER_CONFIG_R2_BUCKET_PATH = serverSettings.GRAPHER_CONFIG_R2_BUCKET_PATH; -var BUILDKITE_API_ACCESS_TOKEN = serverSettings.BUILDKITE_API_ACCESS_TOKEN ?? ""; -var BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG = serverSettings.BUILDKITE_DEPLOY_CONTENT_PIPELINE_SLUG || "owid-deploy-content-master"; -var BUILDKITE_BRANCH = serverSettings.BUILDKITE_BRANCH || "master"; -var BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL = serverSettings.BUILDKITE_DEPLOY_CONTENT_SLACK_CHANNEL || "C06EWA0DK4H"; -var OPENAI_API_KEY = serverSettings.OPENAI_API_KEY ?? ""; -var SLACK_BOT_OAUTH_TOKEN = serverSettings.SLACK_BOT_OAUTH_TOKEN ?? ""; -var LEGACY_WORDPRESS_IMAGE_URL = serverSettings.LEGACY_WORDPRESS_IMAGE_URL ?? "https://assets.ourworldindata.org/uploads"; -var ENV_IS_STAGING = ADMIN_BASE_URL2.includes( - "http://staging-site" -); - -// site/SiteConstants.ts -import { faRss } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-solid-svg-icons/index.mjs"; -import { - faXTwitter, - faFacebookSquare, - faInstagram, - faThreads, - faLinkedin, - faBluesky -} from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@fortawesome/free-brands-svg-icons/index.mjs"; -var polyfillFeatures = [ - "es2021", - // String.replaceAll, Promise.any, ... - "es2022", - // Array.at, String.at, ... - "es2023", - // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ... - "IntersectionObserver", - "IntersectionObserverEntry" -]; -var POLYFILL_VERSION = "4.8.0"; -var POLYFILL_URL = `https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=${POLYFILL_VERSION}&features=${polyfillFeatures.join( - "," -)}`; -var DATA_INSIGHTS_ATOM_FEED_NAME = "atom-data-insights.xml"; -var DATA_INSIGHT_ATOM_FEED_PROPS = { - title: "Atom feed for Daily Data Insights", - href: `https://ourworldindata.org/${DATA_INSIGHTS_ATOM_FEED_NAME}` -}; -var RSS_FEEDS = [ - { - title: "Research & Writing RSS Feed", - url: "/atom.xml", - icon: faRss - }, - { - title: "Daily Data Insights RSS Feed", - url: `/${DATA_INSIGHTS_ATOM_FEED_NAME}`, - icon: faRss - } -]; - -// site/viteUtils.tsx -import { sortBy } from "file:///Users/sophia/code/owid/owid-grapher/packages/@ourworldindata/utils/dist/index.js"; -import urljoin from "file:///Users/sophia/code/owid/owid-grapher/node_modules/url-join/lib/url-join.js"; -var VITE_DEV_URL = process.env.VITE_DEV_URL ?? "http://localhost:8090"; -var VITE_ASSET_SITE_ENTRY = "site/owid.entry.ts"; -var VITE_ASSET_ADMIN_ENTRY = "adminSiteClient/admin.entry.ts"; -var VITE_ENTRYPOINT_INFO = { - ["site" /* Site */]: { - entryPointFile: VITE_ASSET_SITE_ENTRY, - outDir: "assets", - outName: "owid" - }, - ["admin" /* Admin */]: { - entryPointFile: VITE_ASSET_ADMIN_ENTRY, - outDir: "assets-admin", - outName: "admin" - } -}; - -// vite.config-common.mts -import { defineConfig } from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite/dist/node/index.js"; -import pluginReact from "file:///Users/sophia/code/owid/owid-grapher/node_modules/@vitejs/plugin-react/dist/index.mjs"; -import pluginChecker from "file:///Users/sophia/code/owid/owid-grapher/node_modules/vite-plugin-checker/dist/esm/main.js"; -var defineViteConfigForEntrypoint = (entrypoint) => { - const entrypointInfo = VITE_ENTRYPOINT_INFO[entrypoint]; - return defineConfig({ - publicDir: false, - // don't copy public folder to dist - resolve: { - // prettier-ignore - alias: { - "@ourworldindata/grapher/src": "@ourworldindata/grapher/src", - // need this for imports of @ourworldindata/grapher/src/core/grapher.scss to work - // we alias to the packages source files in dev and prod: - // this means we get instant dev updates when we change one of them, - // and the prod build builds them all as esm modules, which helps with tree shaking - // Idea from https://github.com/LinusBorg/vue-lib-template/blob/3775e49b20a7c3349dd49321cad2ed7f9d575057/packages/playground/vite.config.ts - "@ourworldindata/components": "@ourworldindata/components/src/index.ts", - "@ourworldindata/core-table": "@ourworldindata/core-table/src/index.ts", - "@ourworldindata/explorer": "@ourworldindata/explorer/src/index.ts", - "@ourworldindata/grapher": "@ourworldindata/grapher/src/index.ts", - "@ourworldindata/types": "@ourworldindata/types/src/index.ts", - "@ourworldindata/utils": "@ourworldindata/utils/src/index.ts" - } - }, - css: { - devSourcemap: true - }, - define: { - // Replace all clientSettings with their respective values, i.e. assign e.g. BUGSNAG_API_KEY to process.env.BUGSNAG_API_KEY - // it's important to note that we only expose values that are present in the clientSettings file - not any other things that are stored in .env - ...Object.fromEntries( - Object.entries(clientSettings_exports).map(([key, value]) => [ - `process.env.${key}`, - JSON.stringify(value) - ]) - ) - }, - build: { - manifest: true, - // creates a manifest.json file, which we use to determine which files to load in prod - emptyOutDir: true, - outDir: `dist/${entrypointInfo.outDir}`, - sourcemap: true, - target: ["chrome80", "firefox78", "safari13.1"], - // see docs/browser-support.md - rollupOptions: { - input: { - [entrypointInfo.outName]: entrypointInfo.entryPointFile - }, - output: { - assetFileNames: `${entrypointInfo.outName}.css`, - entryFileNames: `${entrypointInfo.outName}.mjs` - } - } - }, - plugins: [ - pluginReact({ - babel: { - parserOpts: { - plugins: ["decorators-legacy"] - // needed so mobx decorators work correctly - } - } - }), - pluginChecker({ - typescript: { - buildMode: true, - tsconfigPath: "tsconfig.vite-checker.json" - } - }) - ], - server: { - port: 8090, - warmup: { clientFiles: [VITE_ASSET_SITE_ENTRY] } - }, - preview: { - port: 8090 - } - }); -}; - -// vite.config-site.mts -var vite_config_site_default = defineViteConfigForEntrypoint("site" /* Site */); -export { - vite_config_site_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic2l0ZS92aXRlVXRpbHMudHN4IiwgInNldHRpbmdzL2ZpbmRCYXNlRGlyLnRzIiwgInNldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzIiwgInNldHRpbmdzL2NsaWVudFNldHRpbmdzLnRzIiwgInNpdGUvU2l0ZUNvbnN0YW50cy50cyIsICJ2aXRlLmNvbmZpZy1jb21tb24ubXRzIiwgInZpdGUuY29uZmlnLXNpdGUubXRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJpbXBvcnQgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCBmaW5kQmFzZURpciBmcm9tIFwiLi4vc2V0dGluZ3MvZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmcy1leHRyYVwiXG5pbXBvcnQge1xuICAgIEVOVixcbiAgICBCQUtFRF9CQVNFX1VSTCxcbiAgICBWSVRFX1BSRVZJRVcsXG59IGZyb20gXCIuLi9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBQT0xZRklMTF9VUkwgfSBmcm9tIFwiLi9TaXRlQ29uc3RhbnRzLmpzXCJcbmltcG9ydCB0eXBlIHsgTWFuaWZlc3QsIE1hbmlmZXN0Q2h1bmsgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgeyBzb3J0QnkgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcbmltcG9ydCB1cmxqb2luIGZyb20gXCJ1cmwtam9pblwiXG5cbmNvbnN0IFZJVEVfREVWX1VSTCA9IHByb2Nlc3MuZW52LlZJVEVfREVWX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODA5MFwiXG5cbmV4cG9ydCBjb25zdCBWSVRFX0FTU0VUX1NJVEVfRU5UUlkgPSBcInNpdGUvb3dpZC5lbnRyeS50c1wiXG5leHBvcnQgY29uc3QgVklURV9BU1NFVF9BRE1JTl9FTlRSWSA9IFwiYWRtaW5TaXRlQ2xpZW50L2FkbWluLmVudHJ5LnRzXCJcblxuZXhwb3J0IGVudW0gVml0ZUVudHJ5UG9pbnQge1xuICAgIFNpdGUgPSBcInNpdGVcIixcbiAgICBBZG1pbiA9IFwiYWRtaW5cIixcbn1cblxuZXhwb3J0IGNvbnN0IFZJVEVfRU5UUllQT0lOVF9JTkZPID0ge1xuICAgIFtWaXRlRW50cnlQb2ludC5TaXRlXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgICAgICBvdXREaXI6IFwiYXNzZXRzXCIsXG4gICAgICAgIG91dE5hbWU6IFwib3dpZFwiLFxuICAgIH0sXG4gICAgW1ZpdGVFbnRyeVBvaW50LkFkbWluXToge1xuICAgICAgICBlbnRyeVBvaW50RmlsZTogVklURV9BU1NFVF9BRE1JTl9FTlRSWSxcbiAgICAgICAgb3V0RGlyOiBcImFzc2V0cy1hZG1pblwiLFxuICAgICAgICBvdXROYW1lOiBcImFkbWluXCIsXG4gICAgfSxcbn1cblxuLy8gV2UgQUxXQVlTIGxvYWQgcG9seWZpbGxzLlxuXG5jb25zdCBwb2x5ZmlsbFNjcmlwdCA9IDxzY3JpcHQga2V5PVwicG9seWZpbGxcIiBzcmM9e1BPTFlGSUxMX1VSTH0gLz5cbmNvbnN0IHBvbHlmaWxsUHJlbG9hZCA9IChcbiAgICA8bGlua1xuICAgICAgICBrZXk9XCJwb2x5ZmlsbC1wcmVsb2FkXCJcbiAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgIGhyZWY9e1BPTFlGSUxMX1VSTH1cbiAgICAgICAgYXM9XCJzY3JpcHRcIlxuICAgICAgICAvLyBDbG91ZGZsYXJlJ3MgRWFybHkgSGludHMgZ2VuZXJhdGlvbiBmb3IgdGhpcyBVUkwgZnVtYmxlcyB0aGUgYCZhbXA7YCBjb250YWluZWQgaW4gdGhpcyBsaW5rOyBzbyB3ZSBkaXNhYmxlIHRoaXMgZm9yIFwiRWFybHkgSGludHNcIiBmb3Igbm93LlxuICAgICAgICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2Nsb3VkZmxhcmUvd29ya2Vycy1zZGsvaXNzdWVzLzY1MjdcbiAgICAgICAgLy8gQ2xvdWRmbGFyZSBkaXNhYmxlcyBFYXJseSBIaW50cyBnZW5lcmF0aW9uIGZvciBhbnkgPGxpbms+IHRoYXQgZG9lc24ndCBqdXN0IGNvbnRhaW4gYHJlbGAsIGBocmVmYCwgYGFzYCAtIHNvIHRoZSBhY3R1YWwgbmFtZSBvZiB0aGlzXG4gICAgICAgIC8vIGF0dHIgZG9lc24ndCBhY3R1YWxseSBtYXR0ZXIuXG4gICAgICAgIGRhdGEtY2xvdWRmbGFyZS1kaXNhYmxlLWVhcmx5LWhpbnRzXG4gICAgLz5cbilcblxuaW50ZXJmYWNlIEFzc2V0cyB7XG4gICAgZm9ySGVhZGVyOiBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuICAgIGZvckZvb3RlcjogUmVhY3QuUmVhY3RFbGVtZW50W11cbn1cblxuLy8gaW4gZGV2OiB3ZSBuZWVkIHRvIGxvYWQgc2V2ZXJhbCB2aXRlIGNvcmUgc2NyaXB0cyBhbmQgcGx1Z2luczsgb3RoZXIgdGhhbiB0aGF0IHdlIG9ubHkgbmVlZCB0byBsb2FkIHRoZSBlbnRyeSBwb2ludCwgYW5kIHZpdGUgd2lsbCB0YWtlIGNhcmUgb2YgdGhlIHJlc3QuXG5jb25zdCBkZXZBc3NldHMgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQsIGJhc2VVcmw6IHN0cmluZyk6IEFzc2V0cyA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBbcG9seWZpbGxQcmVsb2FkXSxcbiAgICAgICAgZm9yRm9vdGVyOiBbXG4gICAgICAgICAgICBwb2x5ZmlsbFNjcmlwdCxcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9XCJ2aXRlLXJlYWN0LXByZWFtYmxlXCIgLy8gaHR0cHM6Ly92aXRlanMuZGV2L2d1aWRlL2JhY2tlbmQtaW50ZWdyYXRpb24uaHRtbFxuICAgICAgICAgICAgICAgIHR5cGU9XCJtb2R1bGVcIlxuICAgICAgICAgICAgICAgIGRhbmdlcm91c2x5U2V0SW5uZXJIVE1MPXt7XG4gICAgICAgICAgICAgICAgICAgIF9faHRtbDogYGltcG9ydCBSZWZyZXNoUnVudGltZSBmcm9tICcke2Jhc2VVcmx9L0ByZWFjdC1yZWZyZXNoJ1xuICBSZWZyZXNoUnVudGltZS5pbmplY3RJbnRvR2xvYmFsSG9vayh3aW5kb3cpXG4gIHdpbmRvdy4kUmVmcmVzaFJlZyQgPSAoKSA9PiB7fVxuICB3aW5kb3cuJFJlZnJlc2hTaWckID0gKCkgPT4gKHR5cGUpID0+IHR5cGVcbiAgd2luZG93Ll9fdml0ZV9wbHVnaW5fcmVhY3RfcHJlYW1ibGVfaW5zdGFsbGVkX18gPSB0cnVlYCxcbiAgICAgICAgICAgICAgICB9fVxuICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAga2V5PVwidml0ZS1wbHVnaW4tY2hlY2tlclwiXG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS9Adml0ZS1wbHVnaW4tY2hlY2tlci1ydW50aW1lLWVudHJ5YH1cbiAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgPHNjcmlwdFxuICAgICAgICAgICAgICAgIGtleT1cInZpdGUtY2xpZW50XCJcbiAgICAgICAgICAgICAgICB0eXBlPVwibW9kdWxlXCJcbiAgICAgICAgICAgICAgICBzcmM9e2Ake2Jhc2VVcmx9L0B2aXRlL2NsaWVudGB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgICAgIDxzY3JpcHRcbiAgICAgICAgICAgICAgICBrZXk9e2VudHJ5cG9pbnR9XG4gICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgc3JjPXtgJHtiYXNlVXJsfS8ke1ZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdLmVudHJ5UG9pbnRGaWxlfWB9XG4gICAgICAgICAgICAvPixcbiAgICAgICAgXSxcbiAgICB9XG59XG5cbi8vIEdvZXMgdGhyb3VnaCB0aGUgbWFuaWZlc3QuanNvbiBmaWxlcyB0aGF0IHZpdGUgY3JlYXRlcywgZmluZHMgYWxsIHRoZSBhc3NldHMgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBnaXZlbiBlbnRyeSBwb2ludCxcbi8vIGFuZCBjcmVhdGVzIHRoZSBhcHByb3ByaWF0ZSA8bGluaz4gYW5kIDxzY3JpcHQ+IHRhZ3MgZm9yIHRoZW0uXG5leHBvcnQgY29uc3QgY3JlYXRlVGFnc0Zvck1hbmlmZXN0RW50cnkgPSAoXG4gICAgbWFuaWZlc3Q6IE1hbmlmZXN0LFxuICAgIGVudHJ5OiBzdHJpbmcsXG4gICAgYXNzZXRCYXNlVXJsOiBzdHJpbmdcbik6IEFzc2V0cyA9PiB7XG4gICAgY29uc3QgY3JlYXRlVGFncyA9IChlbnRyeTogc3RyaW5nKTogUmVhY3QuUmVhY3RFbGVtZW50W10gPT4ge1xuICAgICAgICBjb25zdCBtYW5pZmVzdEVudHJ5ID1cbiAgICAgICAgICAgIE9iamVjdC52YWx1ZXMobWFuaWZlc3QpLmZpbmQoKGUpID0+IGUuZmlsZSA9PT0gZW50cnkpID8/XG4gICAgICAgICAgICAobWFuaWZlc3RbZW50cnldIGFzIE1hbmlmZXN0Q2h1bmsgfCB1bmRlZmluZWQpXG4gICAgICAgIGxldCBhc3NldHMgPSBbXSBhcyBSZWFjdC5SZWFjdEVsZW1lbnRbXVxuXG4gICAgICAgIGlmICghbWFuaWZlc3RFbnRyeSAmJiAhZW50cnkuZW5kc1dpdGgoXCIuY3NzXCIpKVxuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDb3VsZCBub3QgZmluZCBtYW5pZmVzdCBlbnRyeSBmb3IgJHtlbnRyeX1gKVxuXG4gICAgICAgIGNvbnN0IGFzc2V0VXJsID0gdXJsam9pbihhc3NldEJhc2VVcmwsIG1hbmlmZXN0RW50cnk/LmZpbGUgPz8gZW50cnkpXG5cbiAgICAgICAgaWYgKGVudHJ5LmVuZHNXaXRoKFwiLmNzc1wiKSkge1xuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwicHJlbG9hZFwiXG4gICAgICAgICAgICAgICAgICAgIGhyZWY9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICBhcz1cInN0eWxlXCJcbiAgICAgICAgICAgICAgICAvPixcbiAgICAgICAgICAgICAgICA8bGluayBrZXk9e2VudHJ5fSByZWw9XCJzdHlsZXNoZWV0XCIgaHJlZj17YXNzZXRVcmx9IC8+LFxuICAgICAgICAgICAgXVxuICAgICAgICB9IGVsc2UgaWYgKGVudHJ5Lm1hdGNoKC9cXC5bY21dPyhqc3xqc3h8dHN8dHN4KSQvKSkge1xuICAgICAgICAgICAgLy8gZXhwbGljaXRseSByZWZlcmVuY2UgdGhlIGVudHJ5OyBwcmVsb2FkIGl0IGFuZCBpdHMgZGVwZW5kZW5jaWVzXG4gICAgICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaXNFbnRyeSkge1xuICAgICAgICAgICAgICAgIGFzc2V0cyA9IFtcbiAgICAgICAgICAgICAgICAgICAgLi4uYXNzZXRzLFxuICAgICAgICAgICAgICAgICAgICA8c2NyaXB0XG4gICAgICAgICAgICAgICAgICAgICAgICBrZXk9e2VudHJ5fVxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZT1cIm1vZHVsZVwiXG4gICAgICAgICAgICAgICAgICAgICAgICBzcmM9e2Fzc2V0VXJsfVxuICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS1hdHRhY2gtb3dpZC1lcnJvci1oYW5kbGVyXG4gICAgICAgICAgICAgICAgICAgIC8+LFxuICAgICAgICAgICAgICAgIF1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgYXNzZXRzID0gW1xuICAgICAgICAgICAgICAgIC4uLmFzc2V0cyxcbiAgICAgICAgICAgICAgICA8bGlua1xuICAgICAgICAgICAgICAgICAgICBrZXk9e2Ake2VudHJ5fS1wcmVsb2FkYH1cbiAgICAgICAgICAgICAgICAgICAgcmVsPVwibW9kdWxlcHJlbG9hZFwiIC8vIHNlZSBodHRwczovL2RldmVsb3Blci5jaHJvbWUuY29tL2Jsb2cvbW9kdWxlcHJlbG9hZC9cbiAgICAgICAgICAgICAgICAgICAgaHJlZj17YXNzZXRVcmx9XG4gICAgICAgICAgICAgICAgLz4sXG4gICAgICAgICAgICBdXG4gICAgICAgIH1cblxuICAgICAgICAvLyB3ZSBuZWVkIHRvIHJlY3Vyc2UgaW50byBib3RoIHRoZSBtb2R1bGUgaW1wb3J0cyBhbmQgaW1wb3J0ZWQgY3NzIGZpbGVzLCBhbmQgYWRkIHRhZ3MgZm9yIHRoZW0gYXMgd2VsbFxuICAgICAgICAvLyBhbHNvLCB3ZSBuZWVkIHRvIHRha2UgY2FyZSBvZiB0aGUgb3JkZXIgaGVyZSwgc28gdGhlIGltcG9ydGVkIGZpbGUgaXMgbG9hZGVkIGJlZm9yZSB0aGUgaW1wb3J0aW5nIGZpbGVcbiAgICAgICAgaWYgKG1hbmlmZXN0RW50cnk/LmNzcykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuY3NzLmZsYXRNYXAoY3JlYXRlVGFncyksIC4uLmFzc2V0c11cbiAgICAgICAgfVxuICAgICAgICBpZiAobWFuaWZlc3RFbnRyeT8uaW1wb3J0cykge1xuICAgICAgICAgICAgYXNzZXRzID0gWy4uLm1hbmlmZXN0RW50cnkuaW1wb3J0cy5mbGF0TWFwKGNyZWF0ZVRhZ3MpLCAuLi5hc3NldHNdXG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGFzc2V0c1xuICAgIH1cblxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3MoZW50cnkpXG4gICAgcmV0dXJuIHtcbiAgICAgICAgZm9ySGVhZGVyOiBhc3NldHMuZmlsdGVyKChlbCkgPT4gZWwudHlwZSA9PT0gXCJsaW5rXCIpLFxuICAgICAgICBmb3JGb290ZXI6IGFzc2V0cy5maWx0ZXIoKGVsKSA9PiBlbC50eXBlID09PSBcInNjcmlwdFwiKSxcbiAgICB9XG59XG5cbi8vIGluIHByb2Q6IHdlIG5lZWQgdG8gbWFrZSBzdXJlIHRoYXQgd2UgaW5jbHVkZSA8c2NyaXB0PiBhbmQgPGxpbms+IHRhZ3MgdGhhdCBhcmUgcmVxdWlyZWQgZm9yIHRoZSBlbnRyeSBwb2ludC5cbi8vIHRoaXMgY291bGQgYmUsIGZvciBleGFtcGxlOiBvd2lkLm1qcywgY29tbW9uLm1qcywgb3dpZC5jc3MsIGNvbW1vbi5jc3MuIChwbHVzIEdvb2dsZSBGb250cyBhbmQgcG9seWZpbGxzKVxuY29uc3QgcHJvZEFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgYmFzZVVybDogc3RyaW5nKTogQXNzZXRzID0+IHtcbiAgICBjb25zdCBiYXNlRGlyID0gZmluZEJhc2VEaXIoX19kaXJuYW1lKVxuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cbiAgICBjb25zdCBtYW5pZmVzdFBhdGggPSBgJHtiYXNlRGlyfS9kaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfS8udml0ZS9tYW5pZmVzdC5qc29uYFxuICAgIGxldCBtYW5pZmVzdFxuICAgIHRyeSB7XG4gICAgICAgIG1hbmlmZXN0ID0gZnMucmVhZEpzb25TeW5jKG1hbmlmZXN0UGF0aCkgYXMgTWFuaWZlc3RcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYENvdWxkIG5vdCByZWFkIHRoZSBidWlsZCBtYW5pZmVzdCAoJyR7bWFuaWZlc3RQYXRofScpLCB3aGljaCBpcyByZXF1aXJlZCBmb3IgcHJvZHVjdGlvbi5cbiAgICAgICAgICAgIElmIHlvdSdyZSBydW5uaW5nIGluIFZJVEVfUFJFVklFVyBtb2RlLCB3YWl0IGZvciB0aGUgYnVpbGQgdG8gZmluaXNoIGFuZCB0aGVuIHJlbG9hZCB0aGlzIHBhZ2UuYCxcbiAgICAgICAgICAgIHsgY2F1c2U6IGVyciB9XG4gICAgICAgIClcbiAgICB9XG5cbiAgICBjb25zdCBhc3NldEJhc2VVcmwgPSBgJHtiYXNlVXJsfS8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn0vYFxuICAgIGNvbnN0IGFzc2V0cyA9IGNyZWF0ZVRhZ3NGb3JNYW5pZmVzdEVudHJ5KFxuICAgICAgICBtYW5pZmVzdCxcbiAgICAgICAgZW50cnlwb2ludEluZm8uZW50cnlQb2ludEZpbGUsXG4gICAgICAgIGFzc2V0QmFzZVVybFxuICAgIClcblxuICAgIHJldHVybiB7XG4gICAgICAgIC8vIHNvcnQgZm9yIHNvbWUga2luZCBvZiBjb25zaXN0ZW5jeTogZmlyc3QgbW9kdWxlcHJlbG9hZCwgdGhlbiBwcmVsb2FkLCB0aGVuIHN0eWxlc2hlZXRcbiAgICAgICAgZm9ySGVhZGVyOiBzb3J0QnkoW3BvbHlmaWxsUHJlbG9hZCwgLi4uYXNzZXRzLmZvckhlYWRlcl0sIFwicHJvcHMucmVsXCIpLFxuICAgICAgICBmb3JGb290ZXI6IFtwb2x5ZmlsbFNjcmlwdCwgLi4uYXNzZXRzLmZvckZvb3Rlcl0sXG4gICAgfVxufVxuXG5jb25zdCB1c2VQcm9kdWN0aW9uQXNzZXRzID0gRU5WID09PSBcInByb2R1Y3Rpb25cIiB8fCBWSVRFX1BSRVZJRVdcblxuY29uc3Qgdml0ZUFzc2V0cyA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCwgcHJvZEJhc2VVcmw/OiBzdHJpbmcpID0+XG4gICAgdXNlUHJvZHVjdGlvbkFzc2V0c1xuICAgICAgICA/IHByb2RBc3NldHMoZW50cnlwb2ludCwgcHJvZEJhc2VVcmwgPz8gXCJcIilcbiAgICAgICAgOiBkZXZBc3NldHMoZW50cnlwb2ludCwgVklURV9ERVZfVVJMKVxuXG5leHBvcnQgY29uc3Qgdml0ZUFzc2V0c0ZvckFkbWluID0gKCkgPT4gdml0ZUFzc2V0cyhWaXRlRW50cnlQb2ludC5BZG1pbilcbmV4cG9ydCBjb25zdCB2aXRlQXNzZXRzRm9yU2l0ZSA9ICgpID0+IHZpdGVBc3NldHMoVml0ZUVudHJ5UG9pbnQuU2l0ZSlcblxuZXhwb3J0IGNvbnN0IGdlbmVyYXRlRW1iZWRTbmlwcGV0ID0gKCkgPT4ge1xuICAgIC8vIE1ha2Ugc3VyZSB3ZSdyZSB1c2luZyBhbiBhYnNvbHV0ZSBVUkwgaGVyZSwgc2luY2Ugd2UgZG9uJ3Qga25vdyBpbiB3aGF0IGNvbnRleHQgdGhlIGVtYmVkIHNuaXBwZXQgaXMgdXNlZC5cbiAgICBjb25zdCBhc3NldHMgPSB2aXRlQXNzZXRzKFZpdGVFbnRyeVBvaW50LlNpdGUsIEJBS0VEX0JBU0VfVVJMKVxuXG4gICAgY29uc3Qgc2VyaWFsaXplZEFzc2V0cyA9IFsuLi5hc3NldHMuZm9ySGVhZGVyLCAuLi5hc3NldHMuZm9yRm9vdGVyXS5tYXAoXG4gICAgICAgIChlbCkgPT4gKHtcbiAgICAgICAgICAgIHRhZzogZWwudHlwZSxcbiAgICAgICAgICAgIHByb3BzOiBlbC5wcm9wcyxcbiAgICAgICAgfSlcbiAgICApXG5cbiAgICBjb25zdCBzY3JpcHRDb3VudCA9IHNlcmlhbGl6ZWRBc3NldHMuZmlsdGVyKFxuICAgICAgICAoYXNzZXQpID0+XG4gICAgICAgICAgICBhc3NldC50YWcgPT09IFwic2NyaXB0XCIgJiYgIWFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MIC8vIG9ubG9hZCBkb2Vzbid0IGZpcmUgb24gaW5saW5lIHNjcmlwdHMsIHNvIG5lZWQgdG8gaGFuZGxlIHRoYXQgc2VwYXJhdGVseVxuICAgICkubGVuZ3RoXG5cbiAgICByZXR1cm4gYFxuY29uc3QgYXNzZXRzID0gJHtKU09OLnN0cmluZ2lmeShzZXJpYWxpemVkQXNzZXRzLCB1bmRlZmluZWQsIDIpfTtcbmxldCBsb2FkZWRTY3JpcHRzID0gMDtcblxuY29uc3Qgb25Mb2FkID0gKCkgPT4ge1xuICAgIGxvYWRlZFNjcmlwdHMrKztcbiAgICBpZiAobG9hZGVkU2NyaXB0cyA9PT0gJHtzY3JpcHRDb3VudH0pIHtcbiAgICAgICAgd2luZG93Lk11bHRpRW1iZWRkZXJTaW5nbGV0b24uZW1iZWRBbGwoKTtcbiAgICB9XG59XG5cbmZvciAoY29uc3QgYXNzZXQgb2YgYXNzZXRzKSB7XG4gICAgY29uc3QgZWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KGFzc2V0LnRhZyk7XG4gICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXQucHJvcHMpKSB7XG4gICAgICAgIGVsLnNldEF0dHJpYnV0ZShrZXksIHZhbHVlKTtcbiAgICB9XG4gICAgaWYgKGFzc2V0LnByb3BzICYmIGFzc2V0LnByb3BzLmRhbmdlcm91c2x5U2V0SW5uZXJIVE1MKSB7XG4gICAgICAgIGVsLnRleHQgPSBhc3NldC5wcm9wcy5kYW5nZXJvdXNseVNldElubmVySFRNTC5fX2h0bWxcbiAgICB9IGVsc2UgaWYgKGFzc2V0LnRhZyA9PT0gXCJzY3JpcHRcIikge1xuICAgICAgICBlbC5vbmxvYWQgPSBvbkxvYWQ7XG4gICAgfVxuICAgIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZWwpO1xufWBcbn1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvZmluZEJhc2VEaXIudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL2ZpbmRCYXNlRGlyLnRzXCI7aW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5cbi8qKlxuICogV2l0aCBvdXIgY29kZSByZXNpZGluZyBlaXRoZXIgaW4gc29tZSBzcmMgZm9sZGVyIG9yIGluIHRoZSBgaXRzSnVzdEphdmFzY3JpcHRgIGZvbGRlciwgaXQncyBub3RcbiAqIGFsd2F5cyBzdHJhaWdodGZvcndhcmQgdG8ga25vdyB3aGVyZSB0byBmaW5kIGEgY29uZmlnIGZpbGUgbGlrZSBgLmVudmAuXG4gKiBIZXJlLCB3ZSBqdXN0IHRyYXZlcnNlIHRoZSBkaXJlY3RvcnkgdHJlZSB1cHdhcmRzIHVudGlsIHdlIGZpbmQgYSBgcGFja2FnZS5qc29uYCBmaWxlLCB3aGljaFxuICogc2hvdWxkIGluZGljYXRlIHRoYXQgd2UgaGF2ZSBmb3VuZCB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhlIGBvd2lkLWdyYXBoZXJgIHJlcG8uXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGZpbmRQcm9qZWN0QmFzZURpcihmcm9tOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICAgIGlmICghZnMuZXhpc3RzU3luYykgcmV0dXJuIHVuZGVmaW5lZCAvLyBpZiBmcy5leGlzdHNTeW5jIGRvZXNuJ3QgZXhpc3QsIHdlJ3JlIHByb2JhYmx5IHJ1bm5pbmcgaW4gdGhlIGJyb3dzZXJcblxuICAgIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoZnJvbSlcblxuICAgIHdoaWxlIChkaXIubGVuZ3RoKSB7XG4gICAgICAgIGlmIChmcy5leGlzdHNTeW5jKHBhdGgucmVzb2x2ZShkaXIsIFwicGFja2FnZS5qc29uXCIpKSkgcmV0dXJuIGRpclxuXG4gICAgICAgIGNvbnN0IHBhcmVudERpciA9IHBhdGgucmVzb2x2ZShkaXIsIFwiLi5cIilcbiAgICAgICAgLy8gYnJlYWsgaWYgd2UgaGF2ZSByZWFjaGVkIHRoZSBmaWxlIHN5c3RlbSByb290XG4gICAgICAgIGlmIChwYXJlbnREaXIgPT09IGRpcikgYnJlYWtcbiAgICAgICAgZWxzZSBkaXIgPSBwYXJlbnREaXJcbiAgICB9XG5cbiAgICByZXR1cm4gdW5kZWZpbmVkXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5nc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NldHRpbmdzL3NlcnZlclNldHRpbmdzLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9zZXJ2ZXJTZXR0aW5ncy50c1wiOy8vIFRoaXMgaXMgd2hlcmUgc2VydmVyLXNpZGUgb25seSwgcG90ZW50aWFsbHkgc2Vuc2l0aXZlIHNldHRpbmdzIGVudGVyIGZyb20gdGhlIGVudmlyb25tZW50XG4vLyBETyBOT1Qgc3RvcmUgc2Vuc2l0aXZlIHN0cmluZ3MgaW4gdGhpcyBmaWxlIGl0c2VsZiwgYXMgaXQgaXMgY2hlY2tlZCBpbiB0byBnaXQhXG5cbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCJcbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuaW1wb3J0IGZzIGZyb20gXCJmc1wiXG5pbXBvcnQgaW5pIGZyb20gXCJpbmlcIlxuaW1wb3J0IG9zIGZyb20gXCJvc1wiXG5cbmNvbnN0IGJhc2VEaXIgPSBmaW5kQmFzZURpcihfX2Rpcm5hbWUpXG5pZiAoYmFzZURpciA9PT0gdW5kZWZpbmVkKSB0aHJvdyBuZXcgRXJyb3IoXCJjb3VsZCBub3QgbG9jYXRlIGJhc2UgcGFja2FnZS5qc29uXCIpXG5cbmRvdGVudi5jb25maWcoeyBwYXRoOiBgJHtiYXNlRGlyfS8uZW52YCB9KVxuXG5pbXBvcnQgKiBhcyBjbGllbnRTZXR0aW5ncyBmcm9tIFwiLi9jbGllbnRTZXR0aW5ncy5qc1wiXG5pbXBvcnQgeyBwYXJzZUludE9yVW5kZWZpbmVkIH0gZnJvbSBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiXG5cbmNvbnN0IHNlcnZlclNldHRpbmdzID0gcHJvY2Vzcy5lbnYgPz8ge31cblxuZXhwb3J0IGNvbnN0IEJBU0VfRElSOiBzdHJpbmcgPSBiYXNlRGlyXG5leHBvcnQgY29uc3QgRU5WOiBcImRldmVsb3BtZW50XCIgfCBcInByb2R1Y3Rpb25cIiA9IGNsaWVudFNldHRpbmdzLkVOVlxuXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9QT1JUXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX0hPU1Q6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX1NFUlZFUl9IT1NUXG5leHBvcnQgY29uc3QgREFUQV9BUElfRk9SX0FETUlOX1VJOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkRBVEFfQVBJX0ZPUl9BRE1JTl9VSVxuZXhwb3J0IGNvbnN0IEJBS0VEX0JBU0VfVVJMOiBzdHJpbmcgPSBjbGllbnRTZXR0aW5ncy5CQUtFRF9CQVNFX1VSTFxuXG5leHBvcnQgY29uc3QgVklURV9QUkVWSUVXOiBib29sZWFuID0gc2VydmVyU2V0dGluZ3MuVklURV9QUkVWSUVXID09PSBcInRydWVcIlxuXG5leHBvcnQgY29uc3QgQURNSU5fQkFTRV9VUkw6IHN0cmluZyA9IGNsaWVudFNldHRpbmdzLkFETUlOX0JBU0VfVVJMXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcblxuZXhwb3J0IGNvbnN0IE9QVElNSVpFX1NWR19FWFBPUlRTOiBib29sZWFuID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5PUFRJTUlaRV9TVkdfRVhQT1JUUyA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdJVEhVQl9VU0VSTkFNRTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HSVRIVUJfVVNFUk5BTUUgPz8gXCJvd2lkLXRlc3RcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX1VTRVJOQU1FOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX1VTRVJOQU1FID8/IFwiT3VyIFdvcmxkIGluIERhdGFcIlxuZXhwb3J0IGNvbnN0IEdJVF9ERUZBVUxUX0VNQUlMOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdJVF9ERUZBVUxUX0VNQUlMID8/IFwiaW5mb0BvdXJ3b3JsZGluZGF0YS5vcmdcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfQVBJX0tFWVxuZXhwb3J0IGNvbnN0IEJVR1NOQUdfTk9ERV9BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVR1NOQUdfTk9ERV9BUElfS0VZXG5cbmV4cG9ydCBjb25zdCBCTE9HX1BPU1RTX1BFUl9QQUdFOiBudW1iZXIgPVxuICAgIHBhcnNlSW50T3JVbmRlZmluZWQoc2VydmVyU2V0dGluZ3MuQkxPR19QT1NUU19QRVJfUEFHRSkgPz8gMjFcbmV4cG9ydCBjb25zdCBCTE9HX1NMVUc6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkJMT0dfU0xVRyA/PyBcImxhdGVzdFwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RCX05BTUU6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfTkFNRSA/PyBcIm93aWRcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfVVNFUjogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9EQl9VU0VSID8/IFwicm9vdFwiXG5leHBvcnQgY29uc3QgR1JBUEhFUl9EQl9QQVNTOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX1BBU1MgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX0RCX0hPU1QgPz8gXCJsb2NhbGhvc3RcIlxuLy8gVGhlIE9XSUQgc3RhY2sgdXNlcyAzMzA3LCBidXQgaW5jYXNlIGl0J3MgdW5zZXQsIGFzc3VtZSB1c2VyIGlzIHJ1bm5pbmcgYSBsb2NhbCBzZXR1cFxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfREJfUE9SVDogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLkdSQVBIRVJfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX05BTUU6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9URVNUX0RCX05BTUUgPz8gXCJvd2lkXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfVVNFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfVVNFUiA/PyBcInJvb3RcIlxuZXhwb3J0IGNvbnN0IEdSQVBIRVJfVEVTVF9EQl9QQVNTOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkdSQVBIRVJfVEVTVF9EQl9QQVNTID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHUkFQSEVSX1RFU1RfREJfSE9TVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiXG4vLyBUaGUgT1dJRCBzdGFjayB1c2VzIDMzMDcsIGJ1dCBpbmNhc2UgaXQncyB1bnNldCwgYXNzdW1lIHVzZXIgaXMgcnVubmluZyBhIGxvY2FsIHNldHVwXG5leHBvcnQgY29uc3QgR1JBUEhFUl9URVNUX0RCX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5HUkFQSEVSX1RFU1RfREJfUE9SVCkgPz8gMzMwNlxuXG5leHBvcnQgY29uc3QgQkFLRURfU0lURV9ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQkFLRURfU0lURV9ESVIgPz8gcGF0aC5yZXNvbHZlKEJBU0VfRElSLCBcImJha2VkU2l0ZVwiKSAvLyBXaGVyZSB0aGUgc3RhdGljIGJ1aWxkIG91dHB1dCBnb2VzXG5leHBvcnQgY29uc3QgU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TRUNSRVRfS0VZID8/XG4gICAgXCJmZWp3aWFvZiBqZXdpYWZvIGplaW9hIGZqaWVvd2FqZiBpc2EgZmppZG9zYWpmZ2pcIlxuZXhwb3J0IGNvbnN0IFNFU1NJT05fQ09PS0lFX0FHRTogbnVtYmVyID1cbiAgICBwYXJzZUludE9yVW5kZWZpbmVkKHNlcnZlclNldHRpbmdzLlNFU1NJT05fQ09PS0lFX0FHRSkgPz8gMTIwOTYwMFxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VDUkVUX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5BTEdPTElBX1NFQ1JFVF9LRVkgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSU5ERVhJTkc6IGJvb2xlYW4gPVxuICAgIHNlcnZlclNldHRpbmdzLkFMR09MSUFfSU5ERVhJTkcgPT09IFwidHJ1ZVwiXG5cbi8vIFdvcmRwcmVzcyB0YXJnZXQgc2V0dGluZ1xuZXhwb3J0IGNvbnN0IEhUVFBTX09OTFk6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5IVFRQU19PTkxZICE9PSBcImZhbHNlXCJcblxuZXhwb3J0IGNvbnN0IEdJVF9EQVRBU0VUU19ESVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0lUX0RBVEFTRVRTX0RJUiA/PyBgJHtCQVNFX0RJUn0vZGF0YXNldHNFeHBvcnRgIC8vICBXaGVyZSB0aGUgZ2l0IGV4cG9ydHMgZ29cbmV4cG9ydCBjb25zdCBUTVBfRElSOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5UTVBfRElSID8/IFwiL3RtcFwiXG5leHBvcnQgY29uc3QgVU5DQVRFR09SSVpFRF9UQUdfSUQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChzZXJ2ZXJTZXR0aW5ncy5VTkNBVEVHT1JJWkVEX1RBR19JRCkgPz8gMzc1XG5cbi8vIFNob3VsZCB0aGUgc3RhdGljIHNpdGUgb3V0cHV0IGJlIGJha2VkIHdoZW4gcmVsZXZhbnQgZGF0YWJhc2UgaXRlbXMgY2hhbmdlXG5leHBvcnQgY29uc3QgQkFLRV9PTl9DSEFOR0U6IGJvb2xlYW4gPSBzZXJ2ZXJTZXR0aW5ncy5CQUtFX09OX0NIQU5HRSA9PT0gXCJ0cnVlXCJcbmV4cG9ydCBjb25zdCBERVBMT1lfUVVFVUVfRklMRV9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkRFUExPWV9RVUVVRV9GSUxFX1BBVEggPz8gYCR7QkFTRV9ESVJ9Ly5xdWV1ZWBcbmV4cG9ydCBjb25zdCBERVBMT1lfUEVORElOR19GSUxFX1BBVEg6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuREVQTE9ZX1BFTkRJTkdfRklMRV9QQVRIID8/IGAke0JBU0VfRElSfS8ucGVuZGluZ2BcbmV4cG9ydCBjb25zdCBDTE9VREZMQVJFX0FVRDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0xPVURGTEFSRV9BVUQgPz8gXCJcIlxuXG4vLyBFaXRoZXIgcmVtb3RlIGNhdGFsb2cgYGh0dHBzOi8vb3dpZC1jYXRhbG9nLm55YzMuZGlnaXRhbG9jZWFuc3BhY2VzLmNvbS9gIG9yIGxvY2FsIGNhdGFsb2cgYC4uLi9ldGwvZGF0YS9gXG4vLyBOb3RlIHRoYXQgQ2xvdWRmbGFyZSBwcm94eSBvbiBgaHR0cHM6Ly9jYXRhbG9nLm91cndvcmxkaW5kYXRhLm9yZ2AgZG9lcyBub3Qgc3VwcG9ydCByYW5nZSByZXF1ZXN0cyB5ZXRcbi8vIEl0IGlzIGVtcHR5ICh0dXJuZWQgb2ZmKSBieSBkZWZhdWx0IGZvciBub3csIGluIHRoZSBmdXR1cmUgaXQgc2hvdWxkIGJlXG4vLyBgaHR0cHM6Ly9vd2lkLWNhdGFsb2cubnljMy5kaWdpdGFsb2NlYW5zcGFjZXMuY29tL2AgYnkgZGVmYXVsdFxuZXhwb3J0IGNvbnN0IENBVEFMT0dfUEFUSDogc3RyaW5nID0gc2VydmVyU2V0dGluZ3MuQ0FUQUxPR19QQVRIID8/IFwiXCJcblxuLy8gbWFrZSBhbmQgYmFzaCBoYW5kbGUgc3BhY2VzIGluIGVudiB2YXJpYWJsZXMgZGlmZmVyZW50bHkuXG4vLyBubyBxdW90ZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIGJyZWFrOiBcIlBSSVZBVEU6IGNvbW1hbmQgbm90IGZvdW5kXCJcbi8vIHF1b3RlcyAtIHdhaXQtZm9yLW15c3FsLnNoIHdpbGwgd29yaywgYnV0IHRoZSB2YXJpYWJsZSB3aWxsIGJlIGRvdWJsZS1xdW90ZWQgaW4gbm9kZTogJ1wiLS0tLS1CRUdJTiBQUklWQVRFIGV0Yy4uLlwiJ1xuLy8gZXNjYXBlZCBzcGFjZXMgLSB3YWl0LWZvci1teXNxbC5zaCB3aWxsIHdvcmssIGJ1dCB0aGUgYmFja3NsYXNoZXMgd2lsbCBleGlzdCBpbiBub2RlOiBcIi0tLS0tQkVHSU5cXCBQUklWQVRFXFwgZXRjLi4uXCJcbmV4cG9ydCBjb25zdCBHRE9DU19QUklWQVRFX0tFWTogc3RyaW5nID0gKFxuICAgIHNlcnZlclNldHRpbmdzLkdET0NTX1BSSVZBVEVfS0VZID8/IFwiXCJcbilcbiAgICAucmVwbGFjZUFsbCgnXCInLCBcIlwiKVxuICAgIC5yZXBsYWNlQWxsKFwiJ1wiLCBcIlwiKVxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuR0RPQ1NfQ0xJRU5UX0VNQUlMXG5leHBvcnQgY29uc3QgR0RPQ1NfQ0xJRU5UX0lEOiBzdHJpbmcgPSBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19DTElFTlRfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEdET0NTX0JBQ0tQT1JUSU5HX1RBUkdFVF9GT0xERVI6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuR0RPQ1NfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19JTUFHRVNfQkFDS1BPUlRJTkdfVEFSR0VUX0ZPTERFUiA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBHRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ET05BVEVfRkFRU19ET0NVTUVOVF9JRCA/P1xuICAgIFwiMTk0UE5TRmpnU2x0OVptNXhZdURPRjBsX0dMS1piVnhIMmNvM3pDb2tfY0VcIlxuXG5leHBvcnQgY29uc3QgR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID0gc2VydmVyU2V0dGluZ3MuR0RPQ1NfU0hBUkVEX0RSSVZFX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5HRE9DU19ERVRBSUxTX09OX0RFTUFORF9JRCA/PyBcIlwiXG5cbi8vIExvYWQgUjIgY3JlZGVudGlhbHMgZnJvbSByY2xvbmUgY29uZmlnXG5sZXQgcmNsb25lQ29uZmlnOiBhbnkgPSB7fVxuY29uc3QgcmNsb25lQ29uZmlnUGF0aCA9IHBhdGguam9pbihvcy5ob21lZGlyKCksIFwiLmNvbmZpZy9yY2xvbmUvcmNsb25lLmNvbmZcIilcbmlmIChmcy5leGlzdHNTeW5jKHJjbG9uZUNvbmZpZ1BhdGgpKSB7XG4gICAgcmNsb25lQ29uZmlnID0gaW5pLnBhcnNlKGZzLnJlYWRGaWxlU3luYyhyY2xvbmVDb25maWdQYXRoLCBcInV0Zi04XCIpKVxufVxuXG4vLyBlLmcuIGh0dHBzOi8vaW1hZ2VzLXN0YWdpbmcub3dpZC5pby9cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuLy8gZXh0cmFjdCBSMiBjcmVkZW50aWFscyBmcm9tIHJjbG9uZSBjb25maWcgYXMgZGVmYXVsdHNcbmV4cG9ydCBjb25zdCBSMl9FTkRQT0lOVDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9FTkRQT0lOVCB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LmVuZHBvaW50IHx8XG4gICAgXCJodHRwczovLzA3OGZjZGZlZDk5NTUwODczMTVkZDg2NzkyZTcxYTdlLnIyLmNsb3VkZmxhcmVzdG9yYWdlLmNvbVwiXG5leHBvcnQgY29uc3QgUjJfQUNDRVNTX0tFWV9JRDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9BQ0NFU1NfS0VZX0lEIHx8XG4gICAgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8uYWNjZXNzX2tleV9pZCB8fFxuICAgIFwiXCJcbmV4cG9ydCBjb25zdCBSMl9TRUNSRVRfQUNDRVNTX0tFWTogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9TRUNSRVRfQUNDRVNTX0tFWSB8fFxuICAgIHJjbG9uZUNvbmZpZ1tcIm93aWQtcjJcIl0/LnNlY3JldF9hY2Nlc3Nfa2V5IHx8XG4gICAgXCJcIlxuZXhwb3J0IGNvbnN0IFIyX1JFR0lPTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5SMl9SRUdJT04gfHwgcmNsb25lQ29uZmlnW1wib3dpZC1yMlwiXT8ucmVnaW9uIHx8IFwiYXV0b1wiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0NPTkZJR19SMl9CVUNLRVQ6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUXG5leHBvcnQgY29uc3QgR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEg6IHN0cmluZyB8IHVuZGVmaW5lZCA9XG4gICAgc2VydmVyU2V0dGluZ3MuR1JBUEhFUl9DT05GSUdfUjJfQlVDS0VUX1BBVEhcblxuZXhwb3J0IGNvbnN0IERBVEFfQVBJX1VSTDogc3RyaW5nID0gY2xpZW50U2V0dGluZ3MuREFUQV9BUElfVVJMXG5cbmV4cG9ydCBjb25zdCBGRUFUVVJFX0ZMQUdTID0gY2xpZW50U2V0dGluZ3MuRkVBVFVSRV9GTEFHU1xuXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU46IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuQlVJTERLSVRFX0FQSV9BQ0NFU1NfVE9LRU4gPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHOiBzdHJpbmcgPVxuICAgIHNlcnZlclNldHRpbmdzLkJVSUxES0lURV9ERVBMT1lfQ09OVEVOVF9QSVBFTElORV9TTFVHIHx8XG4gICAgXCJvd2lkLWRlcGxveS1jb250ZW50LW1hc3RlclwiXG5leHBvcnQgY29uc3QgQlVJTERLSVRFX0JSQU5DSDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfQlJBTkNIIHx8IFwibWFzdGVyXCJcbmV4cG9ydCBjb25zdCBCVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTDogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5CVUlMREtJVEVfREVQTE9ZX0NPTlRFTlRfU0xBQ0tfQ0hBTk5FTCB8fCBcIkMwNkVXQTBESzRIXCIgLy8gI2NvbnRlbnQtdXBkYXRlc1xuXG5leHBvcnQgY29uc3QgT1BFTkFJX0FQSV9LRVk6IHN0cmluZyA9IHNlcnZlclNldHRpbmdzLk9QRU5BSV9BUElfS0VZID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFNMQUNLX0JPVF9PQVVUSF9UT0tFTjogc3RyaW5nID1cbiAgICBzZXJ2ZXJTZXR0aW5ncy5TTEFDS19CT1RfT0FVVEhfVE9LRU4gPz8gXCJcIlxuXG5leHBvcnQgY29uc3QgTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkw6IHN0cmluZyA9XG4gICAgc2VydmVyU2V0dGluZ3MuTEVHQUNZX1dPUkRQUkVTU19JTUFHRV9VUkwgPz9cbiAgICBcImh0dHBzOi8vYXNzZXRzLm91cndvcmxkaW5kYXRhLm9yZy91cGxvYWRzXCJcblxuLy8gc2VhcmNoIGV2YWx1YXRpb25cbmV4cG9ydCBjb25zdCBTRUFSQ0hfRVZBTF9VUkw6IHN0cmluZyA9XG4gICAgXCJodHRwczovL3B1Yi1lYzc2MWZlMGRmNTU0YjAyYmM2MDU2MTBmMzI5NjAwMC5yMi5kZXZcIlxuXG4vLyBXZSBjdXJyZW50bHkgdXNlIEVOVj1wcm9kdWN0aW9uIG9uIHN0YWdpbmcgc2VydmVycywgaXQnZCBiZSBiZXR0ZXIgdG8gaGF2ZSBFTlY9c3RhZ2luZ1xuLy8gYnV0IHRoYXQgd291bGQgcmVxdWlyZSBjaGFuZ2luZyBhIGxvdCBvZiBjb2RlXG5leHBvcnQgY29uc3QgRU5WX0lTX1NUQUdJTkc6IGJvb2xlYW4gPSBBRE1JTl9CQVNFX1VSTC5pbmNsdWRlcyhcbiAgICBcImh0dHA6Ly9zdGFnaW5nLXNpdGVcIlxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3NcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci9zZXR0aW5ncy9jbGllbnRTZXR0aW5ncy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MudHNcIjsvLyBBbGwgb2YgdGhpcyBpbmZvcm1hdGlvbiBpcyBhdmFpbGFibGUgdG8gdGhlIGNsaWVudC1zaWRlIGNvZGVcbi8vIERPIE5PVCByZXRyaWV2ZSBzZW5zaXRpdmUgaW5mb3JtYXRpb24gZnJvbSB0aGUgZW52aXJvbm1lbnQgaW4gaGVyZSEgOk9cbi8vIFNldHRpbmdzIGluIGhlcmUgd2lsbCBiZSBtYWRlIGF2YWlsYWJsZSB0byB0aGUgY2xpZW50LXNpZGUgY29kZSB0aGF0IGlzXG4vLyBidW5kbGVkIGFuZCBzaGlwcGVkIG91dCB0byBvdXIgdXNlcnMuXG5cbmltcG9ydCBkb3RlbnYgZnJvbSBcImRvdGVudlwiXG5pbXBvcnQgZmluZEJhc2VEaXIgZnJvbSBcIi4vZmluZEJhc2VEaXIuanNcIlxuXG5pZiAodHlwZW9mIF9fZGlybmFtZSAhPT0gXCJ1bmRlZmluZWRcIikge1xuICAgIC8vIG9ubHkgcnVuIHRoaXMgY29kZSBpbiBub2RlLCBub3QgaW4gdGhlIGJyb3dzZXIuXG4gICAgLy8gaW4gdGhlIGJyb3dzZXIsIHByb2Nlc3MuZW52IGlzIGFscmVhZHkgcG9wdWxhdGVkIGJ5IHZpdGUuXG4gICAgY29uc3QgYmFzZURpciA9IGZpbmRCYXNlRGlyKF9fZGlybmFtZSlcbiAgICBpZiAoYmFzZURpcikgZG90ZW52LmNvbmZpZyh7IHBhdGg6IGAke2Jhc2VEaXJ9Ly5lbnZgIH0pXG59XG5cbmltcG9ydCB7IHBhcnNlSW50T3JVbmRlZmluZWQgfSBmcm9tIFwiQG91cndvcmxkaW5kYXRhL3V0aWxzXCJcblxuZXhwb3J0IGNvbnN0IEVOVjogXCJkZXZlbG9wbWVudFwiIHwgXCJwcm9kdWN0aW9uXCIgPVxuICAgIHByb2Nlc3MuZW52LkVOViA9PT0gXCJwcm9kdWN0aW9uXCIgPyBcInByb2R1Y3Rpb25cIiA6IFwiZGV2ZWxvcG1lbnRcIlxuXG5leHBvcnQgY29uc3QgQlVHU05BR19BUElfS0VZOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbmV4cG9ydCBjb25zdCBTRU5UUllfRFNOOiBzdHJpbmcgfCB1bmRlZmluZWQgPSBwcm9jZXNzLmVudi5TRU5UUllfRFNOXG5leHBvcnQgY29uc3QgQURNSU5fU0VSVkVSX1BPUlQ6IG51bWJlciA9XG4gICAgcGFyc2VJbnRPclVuZGVmaW5lZChwcm9jZXNzLmVudi5BRE1JTl9TRVJWRVJfUE9SVCkgPz8gMzAzMFxuZXhwb3J0IGNvbnN0IEFETUlOX1NFUlZFUl9IT1NUOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX1NFUlZFUl9IT1NUID8/IFwibG9jYWxob3N0XCJcbmV4cG9ydCBjb25zdCBCQUtFRF9CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9CQVNFX1VSTCA/P1xuICAgIGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06JHtBRE1JTl9TRVJWRVJfUE9SVH1gXG5cbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9HUkFQSEVSX1VSTCA/PyBgJHtCQUtFRF9CQVNFX1VSTH0vZ3JhcGhlcmBcbmV4cG9ydCBjb25zdCBCQUtFRF9HUkFQSEVSX0VYUE9SVFNfQkFTRV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuQkFLRURfR1JBUEhFUl9FWFBPUlRTX0JBU0VfVVJMID8/IGAke0JBS0VEX0dSQVBIRVJfVVJMfS9leHBvcnRzYFxuZXhwb3J0IGNvbnN0IEJBS0VEX1NJVEVfRVhQT1JUU19CQVNFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5CQUtFRF9TSVRFX0VYUE9SVFNfQkFTRV9VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L2V4cG9ydHNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfVEhVTUJOQUlMX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBFWFBMT1JFUl9EWU5BTUlDX1RIVU1CTkFJTF9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVhQTE9SRVJfRFlOQU1JQ19USFVNQk5BSUxfVVJMID8/IGAke0JBS0VEX0JBU0VfVVJMfS9leHBsb3JlcnNgXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HUkFQSEVSX0RZTkFNSUNfQ09ORklHX1VSTCA/PyBgJHtCQUtFRF9HUkFQSEVSX1VSTH1gXG5cbmV4cG9ydCBjb25zdCBNVUxUSV9ESU1fRFlOQU1JQ19DT05GSUdfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52Lk1VTFRJX0RJTV9EWU5BTUlDX0NPTkZJR19VUkwgPz8gYCR7QkFLRURfQkFTRV9VUkx9L211bHRpLWRpbWBcblxuZXhwb3J0IGNvbnN0IEFETUlOX0JBU0VfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkFETUlOX0JBU0VfVVJMID8/XG4gICAgYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfToke0FETUlOX1NFUlZFUl9QT1JUfWBcbi8vIGUuZy4gXCJodHRwczovL2FwaS5vdXJ3b3JsZGluZGF0YS5vcmcvdjEvaW5kaWNhdG9ycy9cIiBvciBcImh0dHBzOi8vYXBpLXN0YWdpbmcub3dpZC5pby91c2VyL3YxL2luZGljYXRvcnMvXCJcbmV4cG9ydCBjb25zdCBEQVRBX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuREFUQV9BUElfVVJMID8/IFwiaHR0cHM6Ly9hcGkub3Vyd29ybGRpbmRhdGEub3JnL3YxL2luZGljYXRvcnMvXCJcblxuZXhwb3J0IGNvbnN0IEFMR09MSUFfSUQ6IHN0cmluZyA9IHByb2Nlc3MuZW52LkFMR09MSUFfSUQgPz8gXCJcIlxuZXhwb3J0IGNvbnN0IEFMR09MSUFfU0VBUkNIX0tFWTogc3RyaW5nID0gcHJvY2Vzcy5lbnYuQUxHT0xJQV9TRUFSQ0hfS0VZID8/IFwiXCJcbmV4cG9ydCBjb25zdCBBTEdPTElBX0lOREVYX1BSRUZJWDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5BTEdPTElBX0lOREVYX1BSRUZJWCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBET05BVEVfQVBJX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5ET05BVEVfQVBJX1VSTCA/PyBcImh0dHA6Ly9sb2NhbGhvc3Q6ODc4OC9kb25hdGlvbi9kb25hdGVcIlxuXG5leHBvcnQgY29uc3QgUkVDQVBUQ0hBX1NJVEVfS0VZOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LlJFQ0FQVENIQV9TSVRFX0tFWSA/PyBcIjZMY0psNVlVQUFBQUFBVFE2RjR2bDlkQVdSWmVLUEJtMTVNQVpqNFFcIlxuXG4vLyBlLmcuIFwiR1RNLU4yRDRWOFNcIiAob3VyIHByb2R1Y3Rpb24gR1RNIGNvbnRhaW5lcilcbmV4cG9ydCBjb25zdCBHT09HTEVfVEFHX01BTkFHRVJfSUQ6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuR09PR0xFX1RBR19NQU5BR0VSX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFRPUElDU19DT05URU5UX0dSQVBIOiBib29sZWFuID1cbiAgICBwcm9jZXNzLmVudi5UT1BJQ1NfQ09OVEVOVF9HUkFQSCA9PT0gXCJ0cnVlXCJcblxuZXhwb3J0IGNvbnN0IEdET0NTX0NMSUVOVF9FTUFJTDogc3RyaW5nID0gcHJvY2Vzcy5lbnYuR0RPQ1NfQ0xJRU5UX0VNQUlMID8/IFwiXCJcbmV4cG9ydCBjb25zdCBHRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTDogc3RyaW5nID1cbiAgICBwcm9jZXNzLmVudi5HRE9DU19CQVNJQ19BUlRJQ0xFX1RFTVBMQVRFX1VSTCA/PyBcIlwiXG5cbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0NETl9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIHx8IFwiXCJcbi8vIGUuZy4gb3dpZC1pbWFnZS1ob3N0aW5nLXN0YWdpbmcvZGV2ZWxvcG1lbnRcbmV4cG9ydCBjb25zdCBJTUFHRV9IT1NUSU5HX1IyX0JVQ0tFVF9QQVRIOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEggfHwgXCJcIlxuLy8gZS5nLiBkZXZlbG9wbWVudFxuZXhwb3J0IGNvbnN0IElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1NVQkZPTERFUl9QQVRIOiBzdHJpbmcgPVxuICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguc2xpY2UoXG4gICAgICAgIElNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEguaW5kZXhPZihcIi9cIikgKyAxXG4gICAgKVxuXG4vLyBMaW5rIHRvIHByb2R1Y3Rpb24gd2l6YXJkLiAgWW91IG5lZWQgVGFpbHNjYWxlIHRvIGFjY2VzcyBpdCBpbiBwcm9kdWN0aW9uLlxuZXhwb3J0IGNvbnN0IEVUTF9XSVpBUkRfVVJMOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkVUTF9XSVpBUkRfVVJMID8/IGBodHRwOi8vJHtBRE1JTl9TRVJWRVJfSE9TVH06ODA1My9gXG5cbi8vIFByb2R1Y3Rpb24gRVRMIEFQSSBydW5zIG9uIGh0dHA6Ly9ldGwtcHJvZC0yOjgwODMvdjEgKHlvdSBuZWVkIFRhaWxzY2FsZSB0byBhY2Nlc3MgaXQpXG5leHBvcnQgY29uc3QgRVRMX0FQSV9VUkw6IHN0cmluZyA9XG4gICAgcHJvY2Vzcy5lbnYuRVRMX0FQSV9VUkwgPz8gYGh0dHA6Ly8ke0FETUlOX1NFUlZFUl9IT1NUfTo4MDgxL2FwaS92MWBcblxuZXhwb3J0IGNvbnN0IEdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEOiBzdHJpbmcgPVxuICAgIHByb2Nlc3MuZW52LkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEID8/IFwiXCJcblxuZXhwb3J0IGNvbnN0IFBVQkxJU0hFRF9BVF9GT1JNQVQgPSBcImRkZCwgTU1NIEQsIFlZWVkgSEg6bW1cIlxuXG4vLyBGZWF0dXJlIGZsYWdzOiBGRUFUVVJFX0ZMQUdTIGlzIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZmxhZ3MsIGFuZCB0aGV5IG5lZWQgdG8gYmUgcGFydCBvZiB0aGlzIGVudW0gdG8gYmUgY29uc2lkZXJlZFxuZXhwb3J0IGVudW0gRmVhdHVyZUZsYWdGZWF0dXJlIHtcbiAgICBNdWx0aURpbURhdGFQYWdlID0gXCJNdWx0aURpbURhdGFQYWdlXCIsXG59XG5jb25zdCBmZWF0dXJlRmxhZ3NSYXcgPVxuICAgICh0eXBlb2YgcHJvY2Vzcy5lbnYuRkVBVFVSRV9GTEFHUyA9PT0gXCJzdHJpbmdcIiAmJlxuICAgICAgICBwcm9jZXNzLmVudi5GRUFUVVJFX0ZMQUdTLnRyaW0oKT8uc3BsaXQoXCIsXCIpKSB8fFxuICAgIFtdXG5leHBvcnQgY29uc3QgRkVBVFVSRV9GTEFHUzogU2V0PEZlYXR1cmVGbGFnRmVhdHVyZT4gPSBuZXcgU2V0KFxuICAgIE9iamVjdC5rZXlzKEZlYXR1cmVGbGFnRmVhdHVyZSkuZmlsdGVyKChrZXkpID0+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG5dXG5jb25zdCBQT0xZRklMTF9WRVJTSU9OID0gXCI0LjguMFwiXG5leHBvcnQgY29uc3QgUE9MWUZJTExfVVJMOiBzdHJpbmcgPSBgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC92My9wb2x5ZmlsbC5taW4uanM/dmVyc2lvbj0ke1BPTFlGSUxMX1ZFUlNJT059JmZlYXR1cmVzPSR7cG9seWZpbGxGZWF0dXJlcy5qb2luKFxuICAgIFwiLFwiXG4pfWBcblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTE9DQUxfQkFLRV9ESVIgPSBcImxvY2FsQmFrZVwiXG5cbmV4cG9ydCBjb25zdCBHUkFQSEVSX1BSRVZJRVdfQ0xBU1MgPSBcImdyYXBoZXJQcmV2aWV3XCJcblxuZXhwb3J0IGNvbnN0IFNNQUxMX0JSRUFLUE9JTlRfTUVESUFfUVVFUlkgPSBcIihtYXgtd2lkdGg6IDc2OHB4KVwiXG5cbmV4cG9ydCBjb25zdCBUT1VDSF9ERVZJQ0VfTUVESUFfUVVFUlkgPVxuICAgIFwiKGhvdmVyOiBub25lKSwgKHBvaW50ZXI6IGNvYXJzZSksIChwb2ludGVyOiBub25lKVwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FID0gXCJhdG9tLWRhdGEtaW5zaWdodHMueG1sXCJcblxuZXhwb3J0IGNvbnN0IERBVEFfSU5TSUdIVF9BVE9NX0ZFRURfUFJPUFMgPSB7XG4gICAgdGl0bGU6IFwiQXRvbSBmZWVkIGZvciBEYWlseSBEYXRhIEluc2lnaHRzXCIsXG4gICAgaHJlZjogYGh0dHBzOi8vb3Vyd29ybGRpbmRhdGEub3JnLyR7REFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRX1gLFxufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9UT01CU1RPTkVfUkVBU09OID1cbiAgICBcIk91ciBXb3JsZCBpbiBEYXRhIGlzIGRlc2lnbmVkIHRvIGJlIGFuIGV2ZXJncmVlbiBwdWJsaWNhdGlvbi4gVGhpcyBcIiArXG4gICAgXCJtZWFucyB0aGF0IHdoZW4gYSBwYWdlIGNhbm5vdCBiZSB1cGRhdGVkIGR1ZSB0byBvdXRkYXRlZCBkYXRhIG9yIFwiICtcbiAgICBcIm1pc3NpbmcgaW5mb3JtYXRpb24sIHdlIHByZWZlciB0byByZW1vdmUgaXQgcmF0aGVyIHRoYW4gcHJlc2VudCBcIiArXG4gICAgXCJpbmNvbXBsZXRlIG9yIGluYWNjdXJhdGUgcmVzZWFyY2ggYW5kIGRhdGEgdG8gb3VyIHJlYWRlcnMuXCJcblxuZXhwb3J0IGNvbnN0IFNPQ0lBTFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJYXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3guY29tL291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhWFR3aXR0ZXIsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkluc3RhZ3JhbVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cuaW5zdGFncmFtLmNvbS9vdXJ3b3JsZGluZGF0YS9cIixcbiAgICAgICAgaWNvbjogZmFJbnN0YWdyYW0sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlRocmVhZHNcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LnRocmVhZHMubmV0L0BvdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVRocmVhZHMsXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkZhY2Vib29rXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL2ZhY2Vib29rLmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYUZhY2Vib29rU3F1YXJlLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJMaW5rZWRJblwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly93d3cubGlua2VkaW4uY29tL2NvbXBhbnkvb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFMaW5rZWRpbixcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiQmx1ZXNreVwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9ic2t5LmFwcC9wcm9maWxlL291cndvcmxkaW5kYXRhLm9yZ1wiLFxuICAgICAgICBpY29uOiBmYUJsdWVza3ksXG4gICAgfSxcbl1cblxuZXhwb3J0IGNvbnN0IFJTU19GRUVEUyA9IFtcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIlJlc2VhcmNoICYgV3JpdGluZyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IFwiL2F0b20ueG1sXCIsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJEYWlseSBEYXRhIEluc2lnaHRzIFJTUyBGZWVkXCIsXG4gICAgICAgIHVybDogYC8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbiAgICAgICAgaWNvbjogZmFSc3MsXG4gICAgfSxcbl1cbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctY29tbW9uLm10c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBwbHVnaW5SZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIlxuaW1wb3J0IHBsdWdpbkNoZWNrZXIgZnJvbSBcInZpdGUtcGx1Z2luLWNoZWNrZXJcIlxuaW1wb3J0ICogYXMgY2xpZW50U2V0dGluZ3MgZnJvbSBcIi4vc2V0dGluZ3MvY2xpZW50U2V0dGluZ3MuanNcIlxuaW1wb3J0IHtcbiAgICBWSVRFX0FTU0VUX1NJVEVfRU5UUlksXG4gICAgVklURV9FTlRSWVBPSU5UX0lORk8sXG4gICAgVml0ZUVudHJ5UG9pbnQsXG59IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLmpzXCJcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBjb25zdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCA9IChlbnRyeXBvaW50OiBWaXRlRW50cnlQb2ludCkgPT4ge1xuICAgIGNvbnN0IGVudHJ5cG9pbnRJbmZvID0gVklURV9FTlRSWVBPSU5UX0lORk9bZW50cnlwb2ludF1cblxuICAgIHJldHVybiBkZWZpbmVDb25maWcoe1xuICAgICAgICBwdWJsaWNEaXI6IGZhbHNlLCAvLyBkb24ndCBjb3B5IHB1YmxpYyBmb2xkZXIgdG8gZGlzdFxuICAgICAgICByZXNvbHZlOiB7XG4gICAgICAgICAgICAvLyBwcmV0dGllci1pZ25vcmVcbiAgICAgICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmNcIiwgLy8gbmVlZCB0aGlzIGZvciBpbXBvcnRzIG9mIEBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyL3NyYy9jb3JlL2dyYXBoZXIuc2NzcyB0byB3b3JrXG4gICAgICAgICAgICAgICAgLy8gd2UgYWxpYXMgdG8gdGhlIHBhY2thZ2VzIHNvdXJjZSBmaWxlcyBpbiBkZXYgYW5kIHByb2Q6XG4gICAgICAgICAgICAgICAgLy8gdGhpcyBtZWFucyB3ZSBnZXQgaW5zdGFudCBkZXYgdXBkYXRlcyB3aGVuIHdlIGNoYW5nZSBvbmUgb2YgdGhlbSxcbiAgICAgICAgICAgICAgICAvLyBhbmQgdGhlIHByb2QgYnVpbGQgYnVpbGRzIHRoZW0gYWxsIGFzIGVzbSBtb2R1bGVzLCB3aGljaCBoZWxwcyB3aXRoIHRyZWUgc2hha2luZ1xuICAgICAgICAgICAgICAgIC8vIElkZWEgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vTGludXNCb3JnL3Z1ZS1saWItdGVtcGxhdGUvYmxvYi8zNzc1ZTQ5YjIwYTdjMzM0OWRkNDkzMjFjYWQyZWQ3ZjlkNTc1MDU3L3BhY2thZ2VzL3BsYXlncm91bmQvdml0ZS5jb25maWcudHNcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzXCI6IFwiQG91cndvcmxkaW5kYXRhL2NvbXBvbmVudHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZVwiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb3JlLXRhYmxlL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2V4cGxvcmVyL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXJcIjogXCJAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS90eXBlc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS90eXBlcy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS91dGlsc1wiOiBcIkBvdXJ3b3JsZGluZGF0YS91dGlscy9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGNzczoge1xuICAgICAgICAgICAgZGV2U291cmNlbWFwOiB0cnVlLFxuICAgICAgICB9LFxuICAgICAgICBkZWZpbmU6IHtcbiAgICAgICAgICAgIC8vIFJlcGxhY2UgYWxsIGNsaWVudFNldHRpbmdzIHdpdGggdGhlaXIgcmVzcGVjdGl2ZSB2YWx1ZXMsIGkuZS4gYXNzaWduIGUuZy4gQlVHU05BR19BUElfS0VZIHRvIHByb2Nlc3MuZW52LkJVR1NOQUdfQVBJX0tFWVxuICAgICAgICAgICAgLy8gaXQncyBpbXBvcnRhbnQgdG8gbm90ZSB0aGF0IHdlIG9ubHkgZXhwb3NlIHZhbHVlcyB0aGF0IGFyZSBwcmVzZW50IGluIHRoZSBjbGllbnRTZXR0aW5ncyBmaWxlIC0gbm90IGFueSBvdGhlciB0aGluZ3MgdGhhdCBhcmUgc3RvcmVkIGluIC5lbnZcbiAgICAgICAgICAgIC4uLk9iamVjdC5mcm9tRW50cmllcyhcbiAgICAgICAgICAgICAgICBPYmplY3QuZW50cmllcyhjbGllbnRTZXR0aW5ncykubWFwKChba2V5LCB2YWx1ZV0pID0+IFtcbiAgICAgICAgICAgICAgICAgICAgYHByb2Nlc3MuZW52LiR7a2V5fWAsXG4gICAgICAgICAgICAgICAgICAgIEpTT04uc3RyaW5naWZ5KHZhbHVlKSxcbiAgICAgICAgICAgICAgICBdKVxuICAgICAgICAgICAgKSxcbiAgICAgICAgfSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIG1hbmlmZXN0OiB0cnVlLCAvLyBjcmVhdGVzIGEgbWFuaWZlc3QuanNvbiBmaWxlLCB3aGljaCB3ZSB1c2UgdG8gZGV0ZXJtaW5lIHdoaWNoIGZpbGVzIHRvIGxvYWQgaW4gcHJvZFxuICAgICAgICAgICAgZW1wdHlPdXREaXI6IHRydWUsXG4gICAgICAgICAgICBvdXREaXI6IGBkaXN0LyR7ZW50cnlwb2ludEluZm8ub3V0RGlyfWAsXG4gICAgICAgICAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgICAgICAgICB0YXJnZXQ6IFtcImNocm9tZTgwXCIsIFwiZmlyZWZveDc4XCIsIFwic2FmYXJpMTMuMVwiXSwgLy8gc2VlIGRvY3MvYnJvd3Nlci1zdXBwb3J0Lm1kXG4gICAgICAgICAgICByb2xsdXBPcHRpb25zOiB7XG4gICAgICAgICAgICAgICAgaW5wdXQ6IHtcbiAgICAgICAgICAgICAgICAgICAgW2VudHJ5cG9pbnRJbmZvLm91dE5hbWVdOiBlbnRyeXBvaW50SW5mby5lbnRyeVBvaW50RmlsZSxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIG91dHB1dDoge1xuICAgICAgICAgICAgICAgICAgICBhc3NldEZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0uY3NzYCxcbiAgICAgICAgICAgICAgICAgICAgZW50cnlGaWxlTmFtZXM6IGAke2VudHJ5cG9pbnRJbmZvLm91dE5hbWV9Lm1qc2AsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIHBsdWdpbnM6IFtcbiAgICAgICAgICAgIHBsdWdpblJlYWN0KHtcbiAgICAgICAgICAgICAgICBiYWJlbDoge1xuICAgICAgICAgICAgICAgICAgICBwYXJzZXJPcHRzOiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBwbHVnaW5zOiBbXCJkZWNvcmF0b3JzLWxlZ2FjeVwiXSwgLy8gbmVlZGVkIHNvIG1vYnggZGVjb3JhdG9ycyB3b3JrIGNvcnJlY3RseVxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgICAgIHBsdWdpbkNoZWNrZXIoe1xuICAgICAgICAgICAgICAgIHR5cGVzY3JpcHQ6IHtcbiAgICAgICAgICAgICAgICAgICAgYnVpbGRNb2RlOiB0cnVlLFxuICAgICAgICAgICAgICAgICAgICB0c2NvbmZpZ1BhdGg6IFwidHNjb25maWcudml0ZS1jaGVja2VyLmpzb25cIixcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSksXG4gICAgICAgIF0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgICAgIHdhcm11cDogeyBjbGllbnRGaWxlczogW1ZJVEVfQVNTRVRfU0lURV9FTlRSWV0gfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogODA5MCxcbiAgICAgICAgfSxcbiAgICB9KVxufVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXJcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvdml0ZS5jb25maWctc2l0ZS5tdHNcIjtpbXBvcnQgeyBWaXRlRW50cnlQb2ludCB9IGZyb20gXCIuL3NpdGUvdml0ZVV0aWxzLnRzeFwiXG5pbXBvcnQgeyBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludCB9IGZyb20gXCIuL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIlxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVWaXRlQ29uZmlnRm9yRW50cnlwb2ludChWaXRlRW50cnlQb2ludC5TaXRlKVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7OztBQUFBLE9BQU8sV0FBVzs7O0FDQXVTLE9BQU8sVUFBVTtBQUMxVSxPQUFPLFFBQVE7QUFRQSxTQUFSLG1CQUFvQyxNQUFrQztBQUN6RSxNQUFJLENBQUMsR0FBRyxXQUFZLFFBQU87QUFFM0IsTUFBSSxNQUFNLEtBQUssUUFBUSxJQUFJO0FBRTNCLFNBQU8sSUFBSSxRQUFRO0FBQ2YsUUFBSSxHQUFHLFdBQVcsS0FBSyxRQUFRLEtBQUssY0FBYyxDQUFDLEVBQUcsUUFBTztBQUU3RCxVQUFNLFlBQVksS0FBSyxRQUFRLEtBQUssSUFBSTtBQUV4QyxRQUFJLGNBQWMsSUFBSztBQUFBLFFBQ2xCLE9BQU07QUFBQSxFQUNmO0FBRUEsU0FBTztBQUNYOzs7QUR0QkEsT0FBT0EsU0FBUTs7O0FFQ2YsT0FBT0MsV0FBVTtBQUNqQixPQUFPQyxhQUFZO0FBRW5CLE9BQU9DLFNBQVE7QUFDZixPQUFPLFNBQVM7QUFDaEIsT0FBTyxRQUFROzs7QUNSZjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFB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LFVBQ2pELGVBQWUsR0FBRztBQUFBLFVBQ2xCLEtBQUssVUFBVSxLQUFLO0FBQUEsUUFDeEIsQ0FBQztBQUFBLE1BQ0w7QUFBQSxJQUNKO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxVQUFVO0FBQUE7QUFBQSxNQUNWLGFBQWE7QUFBQSxNQUNiLFFBQVEsUUFBUSxlQUFlLE1BQU07QUFBQSxNQUNyQyxXQUFXO0FBQUEsTUFDWCxRQUFRLENBQUMsWUFBWSxhQUFhLFlBQVk7QUFBQTtBQUFBLE1BQzlDLGVBQWU7QUFBQSxRQUNYLE9BQU87QUFBQSxVQUNILENBQUMsZUFBZSxPQUFPLEdBQUcsZUFBZTtBQUFBLFFBQzdDO0FBQUEsUUFDQSxRQUFRO0FBQUEsVUFDSixnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxVQUN6QyxnQkFBZ0IsR0FBRyxlQUFlLE9BQU87QUFBQSxRQUM3QztBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsSUFDQSxTQUFTO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDUixPQUFPO0FBQUEsVUFDSCxZQUFZO0FBQUEsWUFDUixTQUFTLENBQUMsbUJBQW1CO0FBQUE7QUFBQSxVQUNqQztBQUFBLFFBQ0o7QUFBQSxNQUNKLENBQUM7QUFBQSxNQUNELGNBQWM7QUFBQSxRQUNWLFlBQVk7QUFBQSxVQUNSLFdBQVc7QUFBQSxVQUNYLGNBQWM7QUFBQSxRQUNsQjtBQUFBLE1BQ0osQ0FBQztBQUFBLElBQ0w7QUFBQSxJQUNBLFFBQVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg== From ec245e5c6f815e4ed295fd205fb3a15595529451 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 09:36:04 +0100 Subject: [PATCH 47/91] =?UTF-8?q?=F0=9F=90=9B=20merge=20arrays=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1732291572062-MigrateSlopeCharts.ts | 11 ++----- .../utils/src/grapherConfigUtils.ts | 32 +++++++++++++------ packages/@ourworldindata/utils/src/index.ts | 1 + 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/db/migration/1732291572062-MigrateSlopeCharts.ts b/db/migration/1732291572062-MigrateSlopeCharts.ts index 92674e2e219..98cb50953ac 100644 --- a/db/migration/1732291572062-MigrateSlopeCharts.ts +++ b/db/migration/1732291572062-MigrateSlopeCharts.ts @@ -3,6 +3,7 @@ import { GrapherInterface, ScaleType, } from "@ourworldindata/types" +import { simpleMerge } from "@ourworldindata/utils" import { MigrationInterface, QueryRunner } from "typeorm" export class MigrateSlopeCharts1732291572062 implements MigrationInterface { @@ -28,14 +29,8 @@ export class MigrateSlopeCharts1732291572062 implements MigrationInterface { const patchConfig = JSON.parse(chart.patch) const fullConfig = JSON.parse(chart.full) - const newPatchConfig = { - ...patchConfig, - ...migrationConfig, - } - const newFullConfig = { - ...fullConfig, - ...migrationConfig, - } + const newPatchConfig = simpleMerge(patchConfig, migrationConfig) + const newFullConfig = simpleMerge(fullConfig, migrationConfig) await queryRunner.query( ` diff --git a/packages/@ourworldindata/utils/src/grapherConfigUtils.ts b/packages/@ourworldindata/utils/src/grapherConfigUtils.ts index 2a8db8e0120..7649db3d09f 100644 --- a/packages/@ourworldindata/utils/src/grapherConfigUtils.ts +++ b/packages/@ourworldindata/utils/src/grapherConfigUtils.ts @@ -22,6 +22,27 @@ const KEYS_EXCLUDED_FROM_INHERITANCE = [ "isPublished", ] +/** + * Simple merge function that doesn't do any Grapher-specific checks. + * + * You usually want to use `mergeGrapherConfigs` instead that implements the + * inheritance model correctly. Only use this if you know what you're doing. + */ +export function simpleMerge( + ...grapherConfigs: GrapherInterface[] +): GrapherInterface { + return mergeWith( + {}, // mergeWith mutates the first argument + ...grapherConfigs, + (_: unknown, childValue: unknown): any => { + // don't concat arrays, just use the last one + if (Array.isArray(childValue)) { + return childValue + } + } + ) +} + export function mergeGrapherConfigs( ...grapherConfigs: GrapherInterface[] ): GrapherInterface { @@ -60,16 +81,7 @@ export function mergeGrapherConfigs( return omit(config, KEYS_EXCLUDED_FROM_INHERITANCE) }) - return mergeWith( - {}, // mergeWith mutates the first argument - ...cleanedConfigs, - (_: unknown, childValue: unknown): any => { - // don't concat arrays, just use the last one - if (Array.isArray(childValue)) { - return childValue - } - } - ) + return simpleMerge(...cleanedConfigs) } export function diffGrapherConfigs( diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 75f99afdd16..1e74033b5d5 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -335,6 +335,7 @@ export { export { isAndroid, isIOS } from "./BrowserUtils.js" export { + simpleMerge, diffGrapherConfigs, mergeGrapherConfigs, } from "./grapherConfigUtils.js" From 804016ff9da7b8e7f198296e2180e55e01a21abd Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 09:37:38 +0100 Subject: [PATCH 48/91] =?UTF-8?q?Revert=20"=F0=9F=90=9B=20hide=20slope=20c?= =?UTF-8?q?hart=20tab=20if=20line=20chart=20really=20is=20a=20bar=20chart"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/core/Grapher.tsx | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 6f1ad7a6dbe..be71dddfc0f 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1562,26 +1562,10 @@ export class Grapher // if the given combination is not valid, then ignore all but the first chart type if (!validChartTypes) return chartTypes.slice(0, 1) - // make sure showing a slope chart tab next to a line chart tab is sensible + // projected data is only supported for line charts const isLineChart = validChartTypes[0] === GRAPHER_CHART_TYPES.LineChart - if (isLineChart) { - // projected data is only supported for line charts - if (this.hasProjectedData) return [GRAPHER_CHART_TYPES.LineChart] - - // if the line chart really is a bar chart, don't show the slope chart tab - const minTime = minTimeBoundFromJSONOrNegativeInfinity( - this.legacyConfigAsAuthored.minTime - ) - const maxTime = maxTimeBoundFromJSONOrPositiveInfinity( - this.legacyConfigAsAuthored.maxTime - ) - const times = - this.tableAfterAuthorTimelineFilter.timeColumn.uniqValues - const [startTime, endTime] = [minTime, maxTime].map((time) => - findClosestTime(times, time) - ) - const isDiscreteBar = this.hideTimeline && startTime === endTime - if (isDiscreteBar) return [GRAPHER_CHART_TYPES.LineChart] + if (isLineChart && this.hasProjectedData) { + return [GRAPHER_CHART_TYPES.LineChart] } return validChartTypes From 0fa51bd66d52a287319efa80dd0b1458ba4a3f3d Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 14:49:35 +0100 Subject: [PATCH 49/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20incorporate=20pr=20?= =?UTF-8?q?feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controls/settings/AbsRelToggle.tsx | 4 +- .../grapher/src/lineCharts/LineChart.tsx | 2 +- .../src/scatterCharts/ScatterPlotChart.tsx | 4 +- .../src/slopeCharts/SlopeChart.test.ts | 4 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 53 ++++++------------- .../src/slopeCharts/SlopeChartConstants.ts | 2 + 6 files changed, 25 insertions(+), 44 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx b/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx index b90e825960b..d24ce59e359 100644 --- a/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx +++ b/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx @@ -8,7 +8,7 @@ import { } from "@ourworldindata/types" import { LabeledSwitch } from "@ourworldindata/components" -const { LineChart, ScatterPlot } = GRAPHER_CHART_TYPES +const { LineChart, ScatterPlot, SlopeChart } = GRAPHER_CHART_TYPES export interface AbsRelToggleManager { stackMode?: StackMode @@ -38,7 +38,7 @@ export class AbsRelToggle extends React.Component<{ const { activeChartType } = this.manager return activeChartType === ScatterPlot ? "Show the percentage change per year over the the selected time range." - : activeChartType === LineChart + : activeChartType === LineChart || activeChartType === SlopeChart ? "Show proportional changes over time or actual values in their original units." : "Show values as their share of the total or as actual values in their original units." } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 87c2cb800c6..c1159cbc695 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -107,7 +107,7 @@ import { getAnnotationsMap, getColorKey, getSeriesName, -} from "./lineChartHelpers" +} from "./LineChartHelpers" const LINE_CHART_CLASS_NAME = "LineChart" diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index fc505bb6459..5906adeb482 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -935,7 +935,9 @@ export class ScatterPlotChart timeLabel = timeRange + (isRelativeMode ? " (avg. annual change)" : "") - const columns = [xColumn, yColumn, sizeColumn] + const columns = [xColumn, yColumn, sizeColumn].filter( + (column) => !column.isMissing + ) const allRoundedToSigFigs = columns.every( (column) => column.roundsToSignificantFigures ) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts index fdc342b3b43..bdfb7d1885c 100755 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts @@ -71,7 +71,7 @@ it("can filter points with negative values when using a log scale", () => { } const chart = new SlopeChart({ manager }) // expect(chart.series.length).toEqual(2) - expect(chart.allValues.length).toEqual(4) + expect(chart.allYValues.length).toEqual(4) const logScaleManager = { ...manager, @@ -82,7 +82,7 @@ it("can filter points with negative values when using a log scale", () => { const logChart = new SlopeChart({ manager: logScaleManager }) expect(logChart.yAxis.domain[0]).toBeGreaterThan(0) // expect(logChart.series.length).toEqual(2) - expect(logChart.allValues.length).toEqual(2) + expect(logChart.allYValues.length).toEqual(2) }) describe("series naming in multi-column mode", () => { diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index b81b4e60de2..8e928278f04 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -71,7 +71,7 @@ import { getAnnotationsMap, getColorKey, getSeriesName, -} from "../lineCharts/lineChartHelpers" +} from "../lineCharts/LineChartHelpers" type SVGMouseOrTouchEvent = | React.MouseEvent @@ -295,6 +295,7 @@ export class SlopeChart ) return { + column, seriesName, entityName, color, @@ -403,7 +404,7 @@ export class SlopeChart return new AxisConfig(this.manager.yAxisConfig, this) } - @computed get allValues(): number[] { + @computed get allYValues(): number[] { return this.series.flatMap((series) => [ series.startValue, series.endValue, @@ -415,7 +416,7 @@ export class SlopeChart } @computed private get yDomainDefault(): [number, number] { - return domainExtent(this.allValues, this.yScaleType) + return domainExtent(this.allYValues, this.yScaleType) } @computed private get yDomain(): [number, number] { @@ -496,10 +497,8 @@ export class SlopeChart this.bounds.x + Math.max(0.25 * chartAreaWidth, this.yAxisWidth + 4) let endX = this.bounds.x + - Math.min( - chartAreaWidth - 0.25 * chartAreaWidth, - chartAreaWidth - lineLegendWidth - ) + chartAreaWidth - + Math.max(0.25 * chartAreaWidth, lineLegendWidth) const currentSlopeWidth = endX - startX if (currentSlopeWidth > maxSlopeWidth) { @@ -537,7 +536,7 @@ export class SlopeChart private playIntroAnimation() { // Nice little intro animation select(this.slopeAreaRef.current) - .select(".slopes") + .selectAll(".slope") .attr("stroke-dasharray", "100%") .attr("stroke-dashoffset", "100%") .transition() @@ -672,29 +671,13 @@ export class SlopeChart ? `% change since ${formatColumn.formatTime(startTime)}` : timeRange - const columns = this.yColumns - const allRoundedToSigFigs = columns.every( - (column) => column.roundsToSignificantFigures - ) - const anyRoundedToSigFigs = columns.some( - (column) => column.roundsToSignificantFigures - ) - const sigFigs = excludeUndefined( - columns.map((column) => - column.roundsToSignificantFigures - ? column.numSignificantFigures - : undefined - ) - ) - - const roundingNotice = anyRoundedToSigFigs + const roundingNotice = series.column.roundsToSignificantFigures ? { - icon: allRoundedToSigFigs - ? TooltipFooterIcon.none - : TooltipFooterIcon.significance, - text: makeTooltipRoundingNotice(sigFigs, { - plural: sigFigs.length > 1, - }), + icon: TooltipFooterIcon.none, + text: makeTooltipRoundingNotice( + [series.column.numSignificantFigures], + { plural: !isRelativeMode } + ), } : undefined const footer = excludeUndefined([roundingNotice]) @@ -803,13 +786,6 @@ export class SlopeChart return ( - {this.renderSlopes()} @@ -914,6 +890,7 @@ function Slope({ return ( onMouseOver?.(series)} onMouseLeave={() => onMouseLeave?.()} > diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 544410718b7..85cd862866e 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -1,7 +1,9 @@ import { EntityName, PartialBy, PointVector } from "@ourworldindata/utils" import { ChartSeries } from "../chart/ChartInterface" +import { CoreColumn } from "@ourworldindata/core-table" export interface SlopeChartSeries extends ChartSeries { + column: CoreColumn entityName: EntityName startValue: number endValue: number From 195acc18d0b1196ae211ff5bc8a7149b372d98a4 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 16:49:48 +0100 Subject: [PATCH 50/91] =?UTF-8?q?=F0=9F=94=A8=20rename=20line=20chart=20he?= =?UTF-8?q?lpers=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lineCharts/{lineChartHelpers.ts => LineChartHelpers.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/@ourworldindata/grapher/src/lineCharts/{lineChartHelpers.ts => LineChartHelpers.ts} (100%) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts b/packages/@ourworldindata/grapher/src/lineCharts/LineChartHelpers.ts similarity index 100% rename from packages/@ourworldindata/grapher/src/lineCharts/lineChartHelpers.ts rename to packages/@ourworldindata/grapher/src/lineCharts/LineChartHelpers.ts From 37ce3ddaba0a2d6d50a01863124de829431a00d9 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 15:52:13 +0100 Subject: [PATCH 51/91] =?UTF-8?q?=F0=9F=94=A8=20(owid=20table)=20add=20tim?= =?UTF-8?q?e=20to=20owidRows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/core-table/src/CoreTableColumns.ts | 8 +++++--- packages/@ourworldindata/core-table/src/OwidTable.test.ts | 8 ++++---- .../grapher/src/entitySelector/EntitySelector.tsx | 2 +- .../@ourworldindata/grapher/src/mapCharts/MapChart.tsx | 4 ++-- .../grapher/src/mapCharts/MapSparkline.tsx | 2 +- .../@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx | 6 +++--- .../grapher/src/stackedCharts/AbstractStackedChart.tsx | 4 ++-- .../grapher/src/stackedCharts/MarimekkoChart.tsx | 4 ++-- .../grapher/src/stackedCharts/StackedDiscreteBarChart.tsx | 2 +- .../types/src/domainTypes/CoreTableTypes.ts | 1 + 10 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts index 390dfb88bcd..89014482505 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts @@ -536,14 +536,16 @@ export abstract class AbstractCoreColumn { // assumes table is sorted by time @imemo get owidRows(): OwidVariableRow[] { const entities = this.allEntityNames - const times = this.originalTimes + const times = this.allTimes const values = this.values + const originalTimes = this.originalTimes const originalValues = this.originalValues - return range(0, times.length).map((index) => { + return range(0, originalTimes.length).map((index) => { return omitUndefinedValues({ entityName: entities[index], time: times[index], value: values[index], + originalTime: originalTimes[index], originalValue: originalValues[index], }) }) @@ -577,7 +579,7 @@ export abstract class AbstractCoreColumn { valueByEntityNameAndTime.set(row.entityName, new Map()) valueByEntityNameAndTime .get(row.entityName)! - .set(row.time, row.value) + .set(row.originalTime, row.value) }) return valueByEntityNameAndTime } diff --git a/packages/@ourworldindata/core-table/src/OwidTable.test.ts b/packages/@ourworldindata/core-table/src/OwidTable.test.ts index a58250a1ec7..32208310965 100755 --- a/packages/@ourworldindata/core-table/src/OwidTable.test.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.test.ts @@ -72,7 +72,7 @@ it("can parse data to Javascript data structures", () => { table.get("Population").owidRows.forEach((row) => { expect(typeof row.entityName).toBe("string") expect(row.value).toBeGreaterThan(100) - expect(row.time).toBeGreaterThan(1999) + expect(row.originalTime).toBeGreaterThan(1999) }) }) @@ -632,7 +632,7 @@ describe("tolerance", () => { }) }) -it("assigns originalTime as 'time' in owidRows", () => { +it("assigns originalTime as 'originalTime' in owidRows", () => { const csv = `gdp,year,entityName,entityId,entityCode 1000,2019,USA,, 1001,2020,UK,,` @@ -642,7 +642,7 @@ it("assigns originalTime as 'time' in owidRows", () => { expect.not.arrayContaining([ expect.objectContaining({ entityName: "USA", - time: 2020, + originalTime: 2020, value: 1000, }), ]) @@ -651,7 +651,7 @@ it("assigns originalTime as 'time' in owidRows", () => { expect.not.arrayContaining([ expect.objectContaining({ entityName: "UK", - time: 2019, + originalTime: 2019, value: 1001, }), ]) diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx index ebce9d92057..a6590dcf0be 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx @@ -453,7 +453,7 @@ export class EntitySelector extends React.Component<{ const rows = column.owidRowsByEntityName.get(entityName) ?? [] searchableEntity[column.slug] = maxBy( rows, - (row) => row.time + (row) => row.originalTime )?.value } diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx index f641a1b939b..a07da5438fa 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx @@ -346,12 +346,12 @@ export class MapChart return mapColumn.owidRows .map((row) => { - const { entityName, value, time } = row + const { entityName, value, originalTime } = row const color = this.colorScale.getColor(value) || "red" // todo: color fix if (!color) return undefined return { seriesName: entityName, - time, + time: originalTime, value, isSelected: selectionArray.selectedSet.has(entityName), color, diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx index 7fa2a61dbfb..72977920cdb 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx @@ -118,7 +118,7 @@ export class MapSparkline extends React.Component<{ lineStrokeWidth: 2, entityYearHighlight: { entityName: this.manager.entityName, - year: this.manager.datum?.time, + year: this.manager.datum?.originalTime, }, yAxisConfig: { hideAxis: true, diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx index e58b90c5484..65775d51db1 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx @@ -119,8 +119,8 @@ export class MapTooltip : targetTime?.toString() const displayDatumTime = timeColumn && datum - ? timeColumn.formatValue(datum?.time) - : (datum?.time.toString() ?? "") + ? timeColumn.formatValue(datum?.originalTime) + : (datum?.originalTime.toString() ?? "") const valueColor: string | undefined = darkenColorForHighContrastText( lineColorScale?.getColor(datum?.value) ?? "#333" ) @@ -143,7 +143,7 @@ export class MapTooltip const yColumn = this.mapTable.get(this.mapColumnSlug) const targetNotice = - datum && datum.time !== targetTime ? displayTime : undefined + datum && datum.originalTime !== targetTime ? displayTime : undefined const toleranceNotice = targetNotice ? { icon: TooltipFooterIcon.notice, diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx index 9004f813505..172d7e0bf9c 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx @@ -405,8 +405,8 @@ export class AbstractStackedChart const pointColor = row.value > 0 ? POSITIVE_COLOR : NEGATIVE_COLOR return { - position: row.time, - time: row.time, + position: row.originalTime, + time: row.originalTime, value: row.value, valueOffset: 0, interpolated: diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx index 9fa7f41afd6..e89b176b4da 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx @@ -383,7 +383,7 @@ export class MarimekkoChart col.def.color ?? colorScheme.getColors(yColumns.length)[i], points: col.owidRows.map((row) => ({ - time: row.time, + time: row.originalTime, position: row.entityName, value: row.value, valueOffset: 0, @@ -417,7 +417,7 @@ export class MarimekkoChart const points: SimplePoint[] = [] for (const row of rows) { points.push({ - time: row.time, + time: row.originalTime, value: row.value, entity: row.entityName, }) diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx index c575e65c476..b298d2ae160 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx @@ -1006,7 +1006,7 @@ export class StackedDiscreteBarChart col.displayName ), points: col.owidRows.map((row) => ({ - time: row.time, + time: row.originalTime, position: row.entityName, value: row.value, valueOffset: 0, diff --git a/packages/@ourworldindata/types/src/domainTypes/CoreTableTypes.ts b/packages/@ourworldindata/types/src/domainTypes/CoreTableTypes.ts index 5fa5ba92b00..a44d6d42459 100644 --- a/packages/@ourworldindata/types/src/domainTypes/CoreTableTypes.ts +++ b/packages/@ourworldindata/types/src/domainTypes/CoreTableTypes.ts @@ -300,5 +300,6 @@ export interface OwidVariableRow { entityName: EntityName time: Time value: ValueType + originalTime: Time originalValue?: ValueType } From fbef0e8fe422c85910ea66c739b7b773881725b2 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 27 Nov 2024 15:52:33 +0100 Subject: [PATCH 52/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20add=20tolerance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core-table/src/CoreTableColumns.ts | 17 +++ .../grapher/src/core/Grapher.tsx | 5 +- .../src/slopeCharts/SlopeChart.test.ts | 3 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 124 ++++++++++++++---- .../src/slopeCharts/SlopeChartConstants.ts | 12 +- .../grapher/src/tooltip/TooltipContents.tsx | 8 +- 6 files changed, 135 insertions(+), 34 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts index 89014482505..64b355d867e 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts @@ -564,6 +564,23 @@ export abstract class AbstractCoreColumn { return map } + // todo: remove? Should not be on CoreTable + @imemo get owidRowByEntityNameAndTime(): Map< + EntityName, + Map> + > { + const valueByEntityNameAndTime = new Map< + EntityName, + Map> + >() + this.owidRows.forEach((row) => { + if (!valueByEntityNameAndTime.has(row.entityName)) + valueByEntityNameAndTime.set(row.entityName, new Map()) + valueByEntityNameAndTime.get(row.entityName)!.set(row.time, row) + }) + return valueByEntityNameAndTime + } + // todo: remove? Should not be on CoreTable // NOTE: this uses the original times, so any tolerance is effectively unapplied. @imemo get valueByEntityNameAndOriginalTime(): Map< diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index be71dddfc0f..c9edeaa86dc 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -886,7 +886,10 @@ export class Grapher ) if (this.isOnSlopeChartTab) - return table.filterByTargetTimes([startTime, endTime]) + return table.filterByTargetTimes( + [startTime, endTime], + table.get(this.yColumnSlugs[0]).tolerance + ) return table.filterByTimeRange(startTime, endTime) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts index bdfb7d1885c..8fdabb7dd4e 100755 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.test.ts @@ -49,7 +49,8 @@ it("filters non-numeric values", () => { expect(chart.series.length).toEqual(1) expect( chart.series.every( - (series) => isNumber(series.startValue) && isNumber(series.endValue) + (series) => + isNumber(series.start.value) && isNumber(series.end.value) ) ).toBeTruthy() }) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 8e928278f04..7e10f64669c 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -60,6 +60,7 @@ import { ColorSchemes } from "../color/ColorSchemes" import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend" import { makeTooltipRoundingNotice, + makeTooltipToleranceNotice, Tooltip, TooltipState, TooltipValueRange, @@ -114,12 +115,20 @@ export class SlopeChart if (this.isLogScale) table = table.replaceNonPositiveCellsForLogScale(this.yColumnSlugs) + this.yColumnSlugs.forEach((slug) => { + table = table.interpolateColumnWithTolerance(slug) + }) + return table } transformTableForSelection(table: OwidTable): OwidTable { table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs) + this.yColumnSlugs.forEach((slug) => { + table = table.interpolateColumnWithTolerance(slug) + }) + // if time selection is disabled, then filter all entities that // don't have data for the current time period if (!this.manager.hasTimeline && this.startTime !== this.endTime) { @@ -276,10 +285,9 @@ export class SlopeChart canSelectMultipleEntities, }) - const valueByTime = - column.valueByEntityNameAndOriginalTime.get(entityName) - const startValue = valueByTime?.get(startTime) - const endValue = valueByTime?.get(endTime) + const owidRowByTime = column.owidRowByEntityNameAndTime.get(entityName) + const start = owidRowByTime?.get(startTime) + const end = owidRowByTime?.get(endTime) const colorKey = getColorKey({ entityName, @@ -299,8 +307,8 @@ export class SlopeChart seriesName, entityName, color, - startValue, - endValue, + start, + end, annotation, } } @@ -308,7 +316,29 @@ export class SlopeChart private isSeriesValid( series: RawSlopeChartSeries ): series is SlopeChartSeries { - return series.startValue !== undefined && series.endValue !== undefined + const { + start, + end, + column: { tolerance }, + } = series + + // if the start or end value is missing, we can't draw the slope + if (start?.value === undefined || end?.value === undefined) return false + + // sanity check (might happen if tolerance is enabled) + if (start.originalTime >= end.originalTime) return false + + const isToleranceAppliedToStartValue = + start.originalTime !== this.startTime + const isToleranceAppliedToEndValue = end.originalTime !== this.endTime + + // if tolerance has been applied to one of the values, then we require + // a minimal distance between the original times + if (isToleranceAppliedToStartValue || isToleranceAppliedToEndValue) { + return end.originalTime - start.originalTime >= 2 * tolerance + } + + return true } // Usually we drop rows with missing data in the transformTable function. @@ -370,8 +400,8 @@ export class SlopeChart const { yAxis, startX, endX } = this return this.series.map((series) => { - const startY = yAxis.place(series.startValue) - const endY = yAxis.place(series.endValue) + const startY = yAxis.place(series.start.value) + const endY = yAxis.place(series.end.value) const startPoint = new PointVector(startX, startY) const endPoint = new PointVector(endX, endY) @@ -406,8 +436,8 @@ export class SlopeChart @computed get allYValues(): number[] { return this.series.flatMap((series) => [ - series.startValue, - series.endValue, + series.start.value, + series.end.value, ]) } @@ -522,13 +552,13 @@ export class SlopeChart // used in LineLegend @computed get labelSeries(): LineLabelSeries[] { return this.series.map((series) => { - const { seriesName, color, endValue, annotation } = series + const { seriesName, color, end, annotation } = series return { color, seriesName, label: seriesName, annotation, - yValue: endValue, + yValue: end.value, } }) } @@ -660,17 +690,45 @@ export class SlopeChart const { series } = target || {} if (!series) return + const formatTime = (time: Time) => formatColumn.formatTime(time) + const title = isRelativeMode ? `${series.seriesName}, ${formatColumn.formatTime(endTime)}` : series.seriesName - const timeRange = [startTime, endTime] - .map((t) => formatColumn.formatTime(t)) - .join(" to ") + const isStartValueOriginal = series.start.originalTime === startTime + const isEndValueOriginal = series.end.originalTime === endTime + const actualStartTime = isStartValueOriginal + ? startTime + : series.start.originalTime + const actualEndTime = isEndValueOriginal + ? endTime + : series.end.originalTime + + const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}` const timeLabel = isRelativeMode ? `% change since ${formatColumn.formatTime(startTime)}` : timeRange + const constructTargetYearForToleranceNotice = () => { + if (!isStartValueOriginal && !isEndValueOriginal) { + return `${formatTime(startTime)} and ${formatTime(endTime)}` + } else if (!isStartValueOriginal) { + return formatTime(startTime) + } else if (!isEndValueOriginal) { + return formatTime(endTime) + } else { + return undefined + } + } + + const targetYear = constructTargetYearForToleranceNotice() + const toleranceNotice = targetYear + ? { + icon: TooltipFooterIcon.notice, + text: makeTooltipToleranceNotice(targetYear), + } + : undefined const roundingNotice = series.column.roundsToSignificantFigures ? { icon: TooltipFooterIcon.none, @@ -680,11 +738,11 @@ export class SlopeChart ), } : undefined - const footer = excludeUndefined([roundingNotice]) + const footer = excludeUndefined([toleranceNotice, roundingNotice]) const values = isRelativeMode - ? [series.endValue] - : [series.startValue, series.endValue] + ? [series.end.value] + : [series.start.value, series.end.value] return ( (this.tooltipState.target = null)} @@ -707,16 +766,35 @@ export class SlopeChart } private makeMissingDataLabel(series: RawSlopeChartSeries): string { - const { seriesName } = series + const { seriesName, start, end } = series + const startTime = this.formatColumn.formatTime(this.startTime) const endTime = this.formatColumn.formatTime(this.endTime) - if (series.startValue === undefined && series.endValue === undefined) { + + // mention the start or end value if they're missing + if (start?.value === undefined && end?.value === undefined) { return `${seriesName} (${startTime} & ${endTime})` - } else if (series.startValue === undefined) { + } else if (start?.value === undefined) { return `${seriesName} (${startTime})` - } else if (series.endValue === undefined) { + } else if (end?.value === undefined) { return `${seriesName} (${endTime})` } + + // if both values are given but the series shows up in the No Data + // section, then tolerance has been applied to one of the values + // in such a way that we decided not to render the slope after all + // (e.g. when the original times are too close to each other) + const isToleranceAppliedToStartValue = + start.originalTime !== this.startTime + const isToleranceAppliedToEndValue = end.originalTime !== this.endTime + if (isToleranceAppliedToStartValue && isToleranceAppliedToEndValue) { + return `${seriesName} (${startTime} & ${endTime})` + } else if (isToleranceAppliedToStartValue) { + return `${seriesName} (${startTime})` + } else if (isToleranceAppliedToEndValue) { + return `${seriesName} (${endTime})` + } + return seriesName } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts index 85cd862866e..a976bed8e2c 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts @@ -1,19 +1,17 @@ -import { EntityName, PartialBy, PointVector } from "@ourworldindata/utils" +import { PartialBy, PointVector } from "@ourworldindata/utils" +import { EntityName, OwidVariableRow } from "@ourworldindata/types" import { ChartSeries } from "../chart/ChartInterface" import { CoreColumn } from "@ourworldindata/core-table" export interface SlopeChartSeries extends ChartSeries { column: CoreColumn entityName: EntityName - startValue: number - endValue: number + start: Pick, "value" | "originalTime"> + end: Pick, "value" | "originalTime"> annotation?: string } -export type RawSlopeChartSeries = PartialBy< - SlopeChartSeries, - "startValue" | "endValue" -> +export type RawSlopeChartSeries = PartialBy export interface PlacedSlopeChartSeries extends SlopeChartSeries { startPoint: PointVector diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx index 3078fd31d53..a3a8b7f0f1d 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx @@ -337,8 +337,12 @@ export function IconCircledS({ ) } -export function makeTooltipToleranceNotice(targetYear: string): string { - return `Data not available for ${targetYear}. Showing closest available data point instead` +export function makeTooltipToleranceNotice( + targetYear: string, + { plural }: { plural: boolean } = { plural: false } +): string { + const dataPoint = plural ? "data points" : "data point" + return `Data not available for ${targetYear}. Showing closest available ${dataPoint} instead` } export function makeTooltipRoundingNotice( From da28f26d3d8a3807ea00e3db1ba1c84286b204a1 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 15:29:41 +0100 Subject: [PATCH 53/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20incorporate=20pr=20?= =?UTF-8?q?feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 7e10f64669c..4f2130bf8db 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -335,7 +335,7 @@ export class SlopeChart // if tolerance has been applied to one of the values, then we require // a minimal distance between the original times if (isToleranceAppliedToStartValue || isToleranceAppliedToEndValue) { - return end.originalTime - start.originalTime >= 2 * tolerance + return end.originalTime - start.originalTime >= tolerance } return true @@ -692,25 +692,17 @@ export class SlopeChart const formatTime = (time: Time) => formatColumn.formatTime(time) - const title = isRelativeMode - ? `${series.seriesName}, ${formatColumn.formatTime(endTime)}` - : series.seriesName - - const isStartValueOriginal = series.start.originalTime === startTime - const isEndValueOriginal = series.end.originalTime === endTime - const actualStartTime = isStartValueOriginal - ? startTime - : series.start.originalTime - const actualEndTime = isEndValueOriginal - ? endTime - : series.end.originalTime - + const actualStartTime = series.start.originalTime + const actualEndTime = series.end.originalTime const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}` const timeLabel = isRelativeMode - ? `% change since ${formatColumn.formatTime(startTime)}` + ? `% change between ${formatColumn.formatTime(actualStartTime)} and ${formatColumn.formatTime(actualEndTime)}` : timeRange const constructTargetYearForToleranceNotice = () => { + const isStartValueOriginal = series.start.originalTime === startTime + const isEndValueOriginal = series.end.originalTime === endTime + if (!isStartValueOriginal && !isEndValueOriginal) { return `${formatTime(startTime)} and ${formatTime(endTime)}` } else if (!isStartValueOriginal) { @@ -753,7 +745,7 @@ export class SlopeChart offsetX={20} offsetY={-16} style={{ maxWidth: "250px" }} - title={title} + title={series.seriesName} subtitle={timeLabel} subtitleFormat={targetYear ? "notice" : undefined} dissolve={fading} From c3aec00fffb46fcb2d1fb90675b21a1b74201674 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 26 Nov 2024 14:55:15 +0100 Subject: [PATCH 54/91] =?UTF-8?q?=F0=9F=94=A8=20(line=20legend)=20add=20ab?= =?UTF-8?q?ility=20to=20render=20right-aligned=20legend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 01613c1a48c..12880022676 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -91,27 +91,41 @@ class LineLabels extends React.Component<{ series: PlacedSeries[] uniqueKey: string needsLines: boolean + anchor?: "start" | "end" isFocus?: boolean isStatic?: boolean onClick?: (series: PlacedSeries) => void onMouseOver?: (series: PlacedSeries) => void onMouseLeave?: (series: PlacedSeries) => void }> { - @computed get markers(): { + @computed private get textOpacity(): number { + return this.props.isFocus ? 1 : 0.6 + } + + @computed private get anchor(): "start" | "end" { + return this.props.anchor ?? "start" + } + + @computed private get markers(): { series: PlacedSeries labelText: { x: number; y: number } connectorLine: { x1: number; x2: number } }[] { return this.props.series.map((series) => { + const markerMargin = + this.anchor === "start" ? MARKER_MARGIN : -MARKER_MARGIN + const leftPadding = + this.anchor === "start" ? LEFT_PADDING : -LEFT_PADDING + const { x } = series.origBounds const connectorLine = { - x1: x + MARKER_MARGIN, - x2: x + LEFT_PADDING - MARKER_MARGIN, + x1: x + markerMargin, + x2: x + leftPadding - markerMargin, } const textX = this.props.needsLines - ? connectorLine.x2 + MARKER_MARGIN - : x + MARKER_MARGIN + ? connectorLine.x2 + markerMargin + : x + markerMargin const textY = series.bounds.y return { @@ -122,11 +136,7 @@ class LineLabels extends React.Component<{ }) } - @computed get textOpacity(): number { - return this.props.isFocus ? 1 : 0.6 - } - - @computed get textLabels(): React.ReactElement { + @computed private get textLabels(): React.ReactElement { return ( {this.markers.map(({ series, labelText }, index) => { @@ -143,6 +153,7 @@ class LineLabels extends React.Component<{ textProps: { fill: textColor, opacity: this.textOpacity, + textAnchor: this.anchor, }, })} @@ -152,7 +163,7 @@ class LineLabels extends React.Component<{ ) } - @computed get textAnnotations(): React.ReactElement | void { + @computed private get textAnnotations(): React.ReactElement | void { const markersWithAnnotations = this.markers.filter( ({ series }) => series.annotationTextWrap !== undefined ) @@ -175,6 +186,7 @@ class LineLabels extends React.Component<{ textProps: { fill: "#333", opacity: this.textOpacity, + textAnchor: this.anchor, style: { fontWeight: 300 }, }, } @@ -186,7 +198,7 @@ class LineLabels extends React.Component<{ ) } - @computed get connectorLines(): React.ReactElement | void { + @computed private get connectorLines(): React.ReactElement | void { if (!this.props.needsLines) return return ( @@ -224,7 +236,7 @@ class LineLabels extends React.Component<{ ) } - @computed get interactions(): React.ReactElement | void { + @computed private get interactions(): React.ReactElement | void { return ( {this.props.series.map((series, index) => { @@ -243,7 +255,7 @@ class LineLabels extends React.Component<{ style={{ cursor: "default" }} > void @@ -290,6 +303,10 @@ export interface LineLegendManager { export class LineLegend extends React.Component<{ manager: LineLegendManager }> { + @computed private get xAnchor(): "start" | "end" { + return this.props.manager.lineLegendAnchorX ?? "start" + } + @computed private get fontSize(): number { return GRAPHER_FONT_SCALE_12 * (this.manager.fontSize ?? BASE_FONT_SIZE) } @@ -669,6 +686,7 @@ export class LineLegend extends React.Component<{ series={this.backgroundSeries} needsLines={this.needsLines} isFocus={false} + anchor={this.xAnchor} isStatic={this.manager.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) @@ -686,6 +704,7 @@ export class LineLegend extends React.Component<{ series={this.focusedSeries} needsLines={this.needsLines} isFocus={true} + anchor={this.xAnchor} isStatic={this.manager.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) From ade96fe6270b3ad59076de275f9fa40985ee1f64 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 26 Nov 2024 15:37:31 +0100 Subject: [PATCH 55/91] =?UTF-8?q?=F0=9F=94=A8=20(line=20legend)=20remove?= =?UTF-8?q?=20manager=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.tsx | 46 ++++++---- .../src/lineLegend/LineLegend.test.tsx | 8 +- .../grapher/src/lineLegend/LineLegend.tsx | 89 +++++++++++-------- .../grapher/src/slopeCharts/SlopeChart.tsx | 16 +++- .../src/stackedCharts/StackedAreaChart.tsx | 47 ++++++---- 5 files changed, 129 insertions(+), 77 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index c1159cbc695..65e620029eb 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -31,11 +31,7 @@ import { select } from "d3-selection" import { easeLinear } from "d3-ease" import { DualAxisComponent } from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" -import { - LineLegend, - LineLabelSeries, - LineLegendManager, -} from "../lineLegend/LineLegend" +import { LineLegend, LineLabelSeries } from "../lineLegend/LineLegend" import { ComparisonLine } from "../scatterCharts/ComparisonLine" import { TooltipFooterIcon } from "../tooltip/TooltipProps.js" import { @@ -365,7 +361,6 @@ export class LineChart }> implements ChartInterface, - LineLegendManager, AxisManager, ColorScaleManager, HorizontalColorLegendManager @@ -847,7 +842,7 @@ export class LineChart } @computed get lineLegendX(): number { - return this.bounds.right - (this.lineLegendDimensions?.width || 0) + return this.bounds.right - this.lineLegendWidth } @computed get lineLegendY(): [number, number] { @@ -880,10 +875,17 @@ export class LineChart .on("end", () => this.forceUpdate()) // Important in case bounds changes during transition } - @computed private get lineLegendDimensions(): LineLegend | undefined { - return !this.manager.showLegend - ? undefined - : new LineLegend({ manager: this }) + @computed private get lineLegendWidth(): number { + if (!this.manager.showLegend) return 0 + + // only pass props that are required to calculate + // the width to avoid circular dependencies + return LineLegend.width({ + labelSeries: this.lineLegendSeries, + maxWidth: this.maxLineLegendWidth, + fontSize: this.fontSize, + fontWeight: this.fontWeight, + }) } @computed get availableFacetStrategies(): FacetStrategy[] { @@ -944,7 +946,21 @@ export class LineChart backgroundColor={this.manager.backgroundColor} /> ))} - {manager.showLegend && } + {manager.showLegend && ( + + )} { - const legend = new LineLegend({ manager }) + const legend = new LineLegend(props) expect(legend.sizedLabels.length).toEqual(2) }) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 12880022676..d0caa6d2c9c 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -22,6 +22,7 @@ import { EntityName } from "@ourworldindata/types" import { BASE_FONT_SIZE, GRAPHER_FONT_SCALE_12 } from "../core/GrapherConstants" import { ChartSeries } from "../chart/ChartInterface" import { darkenColorForText } from "../color/ColorUtils" +import { AxisConfig } from "../axis/AxisConfig.js" // Minimum vertical space between two legend items const LEGEND_ITEM_MIN_SPACING = 2 @@ -281,42 +282,53 @@ class LineLabels extends React.Component<{ } } -export interface LineLegendManager { +export interface LineLegendProps { labelSeries: LineLabelSeries[] - maxLineLegendWidth?: number + yAxis?: VerticalAxis + + // positioning + x?: number + yRange?: [number, number] + maxWidth?: number lineLegendAnchorX?: "start" | "end" + + // presentation fontSize?: number fontWeight?: number - onLineLegendMouseOver?: (key: EntityName) => void - onLineLegendClick?: (key: EntityName) => void - onLineLegendMouseLeave?: () => void - focusedSeriesNames: EntityName[] - yAxis: VerticalAxis - lineLegendY?: [number, number] - lineLegendX?: number + // used to determine which series should be labelled when there is limited space seriesSortedByImportance?: EntityName[] - isStatic?: boolean + + // interactions + isStatic?: boolean // don't add interactions if true + focusedSeriesNames?: EntityName[] // currently in focus + onClick?: (key: EntityName) => void + onMouseOver?: (key: EntityName) => void + onMouseLeave?: () => void } @observer -export class LineLegend extends React.Component<{ - manager: LineLegendManager -}> { - @computed private get xAnchor(): "start" | "end" { - return this.props.manager.lineLegendAnchorX ?? "start" +export class LineLegend extends React.Component { + static width(props: LineLegendProps): number { + const test = new LineLegend(props) + if (test.sizedLabels.length === 0) return 0 + return max(test.sizedLabels.map((d) => d.width)) ?? 0 } @computed private get fontSize(): number { - return GRAPHER_FONT_SCALE_12 * (this.manager.fontSize ?? BASE_FONT_SIZE) + return GRAPHER_FONT_SCALE_12 * (this.props.fontSize ?? BASE_FONT_SIZE) } @computed private get fontWeight(): number { - return this.manager.fontWeight ?? DEFAULT_FONT_WEIGHT + return this.props.fontWeight ?? DEFAULT_FONT_WEIGHT } @computed private get maxWidth(): number { - return this.manager.maxLineLegendWidth ?? 300 + return this.props.maxWidth ?? 300 + } + + @computed private get yAxis(): VerticalAxis { + return this.props.yAxis ?? new VerticalAxis(new AxisConfig()) } @computed.struct get sizedLabels(): SizedSeries[] { @@ -324,7 +336,7 @@ export class LineLegend extends React.Component<{ const maxTextWidth = maxWidth - LEFT_PADDING const maxAnnotationWidth = Math.min(maxTextWidth, 150) - return this.manager.labelSeries.map((label) => { + return this.props.labelSeries.map((label) => { const annotationTextWrap = label.annotation ? new TextWrap({ text: label.annotation, @@ -365,34 +377,37 @@ export class LineLegend extends React.Component<{ } @computed get onMouseOver(): any { - return this.manager.onLineLegendMouseOver ?? noop + return this.props.onMouseOver ?? noop } @computed get onMouseLeave(): any { - return this.manager.onLineLegendMouseLeave ?? noop + return this.props.onMouseLeave ?? noop } @computed get onClick(): any { - return this.manager.onLineLegendClick ?? noop + return this.props.onClick ?? noop + } + + @computed get focusedSeriesNames(): EntityName[] { + return this.props.focusedSeriesNames ?? [] } @computed get isFocusMode(): boolean { return this.sizedLabels.some((label) => - this.manager.focusedSeriesNames.includes(label.seriesName) + this.focusedSeriesNames.includes(label.seriesName) ) } @computed get legendX(): number { - return this.manager.lineLegendX ?? 0 + return this.props.x ?? 0 } @computed get legendY(): [number, number] { - const range = this.manager.lineLegendY ?? this.manager.yAxis.range + const range = this.props.yRange ?? this.yAxis.range return [Math.min(range[1], range[0]), Math.max(range[1], range[0])] } // Naive initial placement of each mark at the target height, before collision detection @computed private get initialSeries(): PlacedSeries[] { - const { yAxis } = this.manager - const { legendX, legendY } = this + const { yAxis, legendX, legendY } = this const [legendYMin, legendYMax] = legendY @@ -507,9 +522,9 @@ export class LineLegend extends React.Component<{ } @computed get sortedSeriesByImportance(): PlacedSeries[] | undefined { - if (!this.manager.seriesSortedByImportance) return undefined + if (!this.props.seriesSortedByImportance) return undefined return excludeUndefined( - this.manager.seriesSortedByImportance.map((seriesName) => + this.props.seriesSortedByImportance.map((seriesName) => this.initialSeriesByName.get(seriesName) ) ) @@ -657,7 +672,7 @@ export class LineLegend extends React.Component<{ } @computed private get backgroundSeries(): PlacedSeries[] { - const { focusedSeriesNames } = this.manager + const { focusedSeriesNames } = this const { isFocusMode } = this return this.placedSeries.filter( (mark) => @@ -666,7 +681,7 @@ export class LineLegend extends React.Component<{ } @computed private get focusedSeries(): PlacedSeries[] { - const { focusedSeriesNames } = this.manager + const { focusedSeriesNames } = this const { isFocusMode } = this return this.placedSeries.filter( (mark) => @@ -686,8 +701,8 @@ export class LineLegend extends React.Component<{ series={this.backgroundSeries} needsLines={this.needsLines} isFocus={false} - anchor={this.xAnchor} - isStatic={this.manager.isStatic} + anchor={this.props.lineLegendAnchorX} + isStatic={this.props.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) } @@ -704,8 +719,8 @@ export class LineLegend extends React.Component<{ series={this.focusedSeries} needsLines={this.needsLines} isFocus={true} - anchor={this.xAnchor} - isStatic={this.manager.isStatic} + anchor={this.props.lineLegendAnchorX} + isStatic={this.props.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) } @@ -717,10 +732,6 @@ export class LineLegend extends React.Component<{ ) } - @computed get manager(): LineLegendManager { - return this.props.manager - } - render(): React.ReactElement { return ( { const { seriesName, color, end, annotation } = series return { @@ -917,7 +917,19 @@ export class SlopeChart return ( {this.renderChartArea()} - {this.manager.showLegend && } + {this.manager.showLegend && ( + + )} {this.showNoDataSection && this.renderNoDataSection()} {this.tooltip} diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 58eb7020266..f0aa6998b81 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -29,11 +29,7 @@ import { import { observer } from "mobx-react" import { DualAxisComponent } from "../axis/AxisViews" import { DualAxis } from "../axis/Axis" -import { - LineLabelSeries, - LineLegend, - LineLegendManager, -} from "../lineLegend/LineLegend" +import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend" import { NoDataModal } from "../noDataModal/NoDataModal" import { TooltipFooterIcon } from "../tooltip/TooltipProps.js" import { @@ -263,10 +259,7 @@ class Areas extends React.Component { } @observer -export class StackedAreaChart - extends AbstractStackedChart - implements LineLegendManager -{ +export class StackedAreaChart extends AbstractStackedChart { constructor(props: AbstractStackedChartProps) { super(props) } @@ -319,9 +312,16 @@ export class StackedAreaChart return Math.min(150, this.bounds.width / 3) } - @computed get legendDimensions(): LineLegend | undefined { - if (!this.manager.showLegend) return undefined - return new LineLegend({ manager: this }) + @computed get lineLegendWidth(): number { + if (!this.manager.showLegend) return 0 + + // only pass props that are required to calculate + // the width to avoid circular dependencies + return LineLegend.width({ + labelSeries: this.labelSeries, + maxWidth: this.maxLineLegendWidth, + fontSize: this.fontSize, + }) } @observable tooltipState = new TooltipState<{ @@ -348,8 +348,7 @@ export class StackedAreaChart @observable private hoverTimer?: NodeJS.Timeout @computed protected get paddingForLegendRight(): number { - const { legendDimensions } = this - return legendDimensions ? legendDimensions.width : 0 + return this.lineLegendWidth } @computed get seriesSortedByImportance(): string[] { @@ -656,7 +655,21 @@ export class StackedAreaChart renderLegend(): React.ReactElement | void { if (!this.manager.showLegend) return - return + return ( + + ) } renderStatic(): React.ReactElement { @@ -735,8 +748,8 @@ export class StackedAreaChart } @computed get lineLegendX(): number { - return this.legendDimensions - ? this.bounds.right - this.legendDimensions.width + return this.manager.showLegend + ? this.bounds.right - this.lineLegendWidth : 0 } From fcc9698ee9902e1239d79c64d62fea661faf650c Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 26 Nov 2024 17:23:57 +0100 Subject: [PATCH 56/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20add=20second=20lege?= =?UTF-8?q?nd=20on=20the=20left?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.tsx | 2 +- .../grapher/src/lineLegend/LineLegend.tsx | 77 +++++---- .../grapher/src/slopeCharts/SlopeChart.tsx | 151 +++++++++++++----- .../src/stackedCharts/StackedAreaChart.tsx | 2 +- 4 files changed, 162 insertions(+), 70 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 65e620029eb..9a8a343b240 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -880,7 +880,7 @@ export class LineChart // only pass props that are required to calculate // the width to avoid circular dependencies - return LineLegend.width({ + return LineLegend.incorrectWidth({ labelSeries: this.lineLegendSeries, maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index d0caa6d2c9c..88652c9a9df 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -31,8 +31,7 @@ const MARKER_MARGIN = 4 // Space between the label and the annotation const ANNOTATION_PADDING = 2 -const LEFT_PADDING = 35 - +const DEFAULT_CONNECTOR_LINE_WIDTH = 35 const DEFAULT_FONT_WEIGHT = 400 export interface LineLabelSeries extends ChartSeries { @@ -91,7 +90,8 @@ function stackGroupVertically( class LineLabels extends React.Component<{ series: PlacedSeries[] uniqueKey: string - needsLines: boolean + needsConnectorLines: boolean + connectorLineWidth?: number anchor?: "start" | "end" isFocus?: boolean isStatic?: boolean @@ -107,24 +107,27 @@ class LineLabels extends React.Component<{ return this.props.anchor ?? "start" } + @computed private get connectorLineWidth(): number { + return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH + } + @computed private get markers(): { series: PlacedSeries labelText: { x: number; y: number } connectorLine: { x1: number; x2: number } }[] { return this.props.series.map((series) => { - const markerMargin = - this.anchor === "start" ? MARKER_MARGIN : -MARKER_MARGIN - const leftPadding = - this.anchor === "start" ? LEFT_PADDING : -LEFT_PADDING + const direction = this.anchor === "start" ? 1 : -1 + const markerMargin = direction * MARKER_MARGIN + const connectorLineWidth = direction * this.connectorLineWidth const { x } = series.origBounds const connectorLine = { x1: x + markerMargin, - x2: x + leftPadding - markerMargin, + x2: x + connectorLineWidth - markerMargin, } - const textX = this.props.needsLines + const textX = this.props.needsConnectorLines ? connectorLine.x2 + markerMargin : x + markerMargin const textY = series.bounds.y @@ -200,7 +203,7 @@ class LineLabels extends React.Component<{ } @computed private get connectorLines(): React.ReactElement | void { - if (!this.props.needsLines) return + if (!this.props.needsConnectorLines) return return ( {this.markers.map(({ series, connectorLine }, index) => { @@ -293,6 +296,7 @@ export interface LineLegendProps { lineLegendAnchorX?: "start" | "end" // presentation + connectorLineWidth?: number fontSize?: number fontWeight?: number @@ -311,8 +315,18 @@ export interface LineLegendProps { export class LineLegend extends React.Component { static width(props: LineLegendProps): number { const test = new LineLegend(props) - if (test.sizedLabels.length === 0) return 0 - return max(test.sizedLabels.map((d) => d.width)) ?? 0 + const connectorLineWidth = test.needsLines ? test.connectorLineWidth : 0 + return test.maxLabelWidth + connectorLineWidth + MARKER_MARGIN + } + + /** + * Always adds the width of connector lines, which leads to an incorrect + * result if no connector lines are rendered. We sometimes can't use the + * correct width above due to circular dependencies. + */ + static incorrectWidth(props: LineLegendProps): number { + const test = new LineLegend(props) + return test.maxLabelWidth + test.connectorLineWidth + MARKER_MARGIN } @computed private get fontSize(): number { @@ -331,9 +345,13 @@ export class LineLegend extends React.Component { return this.props.yAxis ?? new VerticalAxis(new AxisConfig()) } + @computed private get connectorLineWidth(): number { + return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH + } + @computed.struct get sizedLabels(): SizedSeries[] { const { fontSize, fontWeight, maxWidth } = this - const maxTextWidth = maxWidth - LEFT_PADDING + const maxTextWidth = maxWidth - this.connectorLineWidth const maxAnnotationWidth = Math.min(maxTextWidth, 150) return this.props.labelSeries.map((label) => { @@ -356,12 +374,10 @@ export class LineLegend extends React.Component { ...label, textWrap, annotationTextWrap, - width: - LEFT_PADDING + - Math.max( - textWrap.width, - annotationTextWrap ? annotationTextWrap.width : 0 - ), + width: Math.max( + textWrap.width, + annotationTextWrap ? annotationTextWrap.width : 0 + ), height: textWrap.height + (annotationTextWrap @@ -371,9 +387,9 @@ export class LineLegend extends React.Component { }) } - @computed get width(): number { - if (this.sizedLabels.length === 0) return 0 - return max(this.sizedLabels.map((d) => d.width)) ?? 0 + @computed private get maxLabelWidth(): number { + const { sizedLabels = [] } = this + return max(sizedLabels.map((d) => d.width)) ?? 0 } @computed get onMouseOver(): any { @@ -412,22 +428,25 @@ export class LineLegend extends React.Component { const [legendYMin, legendYMax] = legendY return this.sizedLabels.map((label) => { + const labelHeight = label.height + const labelWidth = label.width + this.connectorLineWidth + // place vertically centered at Y value const midY = yAxis.place(label.yValue) const initialY = midY - label.height / 2 const origBounds = new Bounds( legendX, initialY, - label.width, - label.height + labelWidth, + labelHeight ) // ensure label doesn't go beyond the top or bottom of the chart const y = Math.min( Math.max(initialY, legendYMin), - legendYMax - label.height + legendYMax - labelHeight ) - const bounds = new Bounds(legendX, y, label.width, label.height) + const bounds = new Bounds(legendX, y, labelWidth, labelHeight) return { ...label, @@ -699,7 +718,8 @@ export class LineLegend extends React.Component { { - Bounds.forText(series.seriesName, { fontSize }).width - ) - ) ?? 0 - ) + @computed get maxLineLegendWidth(): number { + return this.bounds.width / 6 + } + + @computed get lineLegendWidthLeft(): number { + if (!this.manager.showLegend) return 0 + return LineLegend.width({ + labelSeries: this.lineLegendSeriesLeft, + yAxis: this.yAxis, + maxWidth: this.maxLineLegendWidth, + connectorLineWidth: this.lineLegendConnectorLinesWidth, + fontSize: this.fontSize, + isStatic: this.manager.isStatic, + }) } - @computed get maxLineLegendWidth(): number { - // todo: copied from line legend (left padding, marker margin) - return Math.min(this.maxLabelWidth + 35 + 4, this.bounds.width / 3) + @computed get lineLegendWidthRight(): number { + if (!this.manager.showLegend) return 0 + return LineLegend.width({ + labelSeries: this.lineLegendSeriesRight, + yAxis: this.yAxis, + maxWidth: this.maxLineLegendWidth, + connectorLineWidth: this.lineLegendConnectorLinesWidth, + fontSize: this.fontSize, + isStatic: this.manager.isStatic, + }) } @computed get xRange(): [number, number] { - const lineLegendWidth = this.maxLineLegendWidth + LINE_LEGEND_PADDING + const lineLegendWidthLeft = + this.lineLegendWidthLeft + LINE_LEGEND_PADDING + const lineLegendWidthRight = + this.lineLegendWidthRight + LINE_LEGEND_PADDING // pick a reasonable max width based on an ideal aspect ratio const idealAspectRatio = 0.6 const chartAreaWidth = this.bounds.width - this.sidebarWidth const availableWidth = - chartAreaWidth - this.yAxisWidth - lineLegendWidth + chartAreaWidth - + this.yAxisWidth - + lineLegendWidthLeft - + lineLegendWidthRight const idealWidth = idealAspectRatio * this.bounds.height const maxSlopeWidth = Math.min(idealWidth, availableWidth) - let startX = - this.bounds.x + Math.max(0.25 * chartAreaWidth, this.yAxisWidth + 4) - let endX = - this.bounds.x + - chartAreaWidth - - Math.max(0.25 * chartAreaWidth, lineLegendWidth) + let startX: number, endX: number + if (this.manager.isSemiNarrow) { + startX = this.bounds.x + this.yAxisWidth + 4 + lineLegendWidthLeft + endX = this.bounds.x + chartAreaWidth - lineLegendWidthRight + } else { + startX = + this.bounds.x + + Math.max( + 0.25 * chartAreaWidth, + this.yAxisWidth + 4 + lineLegendWidthLeft + ) + endX = + this.bounds.x + + chartAreaWidth - + Math.max(0.25 * chartAreaWidth, lineLegendWidthRight) + } const currentSlopeWidth = endX - startX if (currentSlopeWidth > maxSlopeWidth) { @@ -544,13 +569,31 @@ export class SlopeChart return this.xRange[1] + LINE_LEGEND_PADDING } + @computed get useCompactLineLegend(): boolean { + return !!this.manager.isSemiNarrow + } + // used by LineLegend @computed get focusedSeriesNames(): SeriesName[] { return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] } - // used in LineLegend - @computed get lineLegendSeries(): LineLabelSeries[] { + @computed get lineLegendSeriesLeft(): LineLabelSeries[] { + return this.series.map((series) => { + const { seriesName, color, start, annotation } = series + return { + color, + seriesName, + label: this.useCompactLineLegend + ? this.formatColumn.formatValueShort(start.value) + : this.formatColumn.formatValueLong(start.value), + annotation, + yValue: start.value, + } + }) + } + + @computed get lineLegendSeriesRight(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, end, annotation } = series return { @@ -563,6 +606,10 @@ export class SlopeChart }) } + @computed private get lineLegendConnectorLinesWidth(): number { + return this.useCompactLineLegend ? 15 : 35 + } + private playIntroAnimation() { // Nice little intro animation select(this.slopeAreaRef.current) @@ -790,7 +837,9 @@ export class SlopeChart return seriesName } - private renderNoDataSection(): React.ReactElement { + private renderNoDataSection(): React.ReactElement | void { + if (!this.showNoDataSection) return + const bounds = new Bounds( this.bounds.right - this.sidebarWidth, this.bounds.top, @@ -903,6 +952,40 @@ export class SlopeChart ) } + private renderLineLegends(): React.ReactElement | void { + if (!this.manager.showLegend) return + return ( + <> + + + + ) + } + render() { if (this.failMessage) return ( @@ -917,20 +1000,8 @@ export class SlopeChart return ( {this.renderChartArea()} - {this.manager.showLegend && ( - - )} - {this.showNoDataSection && this.renderNoDataSection()} + {this.renderLineLegends()} + {this.renderNoDataSection()} {this.tooltip} ) diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index f0aa6998b81..80ebe0977a7 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -317,7 +317,7 @@ export class StackedAreaChart extends AbstractStackedChart { // only pass props that are required to calculate // the width to avoid circular dependencies - return LineLegend.width({ + return LineLegend.incorrectWidth({ labelSeries: this.labelSeries, maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, From c37ca8929a2be34ef877f04f51836deb025af0b9 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 26 Nov 2024 18:01:00 +0100 Subject: [PATCH 57/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20show=20value=20in?= =?UTF-8?q?=20the=20line=20legend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 85 +++++++++++++++++-- .../grapher/src/slopeCharts/SlopeChart.tsx | 7 ++ .../src/stackedCharts/StackedAreaChart.tsx | 6 +- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 88652c9a9df..d974b4adcdb 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -38,12 +38,14 @@ export interface LineLabelSeries extends ChartSeries { label: string yValue: number annotation?: string + formattedValue?: string yRange?: [number, number] } interface SizedSeries extends LineLabelSeries { textWrap: TextWrap annotationTextWrap?: TextWrap + valueTextWrap?: TextWrap width: number height: number } @@ -202,6 +204,41 @@ class LineLabels extends React.Component<{ ) } + @computed private get textValues(): React.ReactElement | void { + const markersWithTextValues = this.markers.filter( + ({ series }) => series.valueTextWrap !== undefined + ) + if (!markersWithTextValues) return + return ( + + {markersWithTextValues.map(({ series, labelText }, index) => { + const textColor = darkenColorForText(series.color) + return ( + + {series.valueTextWrap?.render( + labelText.x, + labelText.y + series.textWrap.height, + { + textProps: { + fill: textColor, + opacity: this.textOpacity, + textAnchor: this.anchor, + }, + } + )} + + ) + })} + + ) + } + @computed private get connectorLines(): React.ReactElement | void { if (!this.props.needsConnectorLines) return return ( @@ -278,6 +315,7 @@ class LineLabels extends React.Component<{ <> {this.connectorLines} {this.textAnnotations} + {this.textValues} {this.textLabels} {!this.props.isStatic && this.interactions} @@ -299,6 +337,7 @@ export interface LineLegendProps { connectorLineWidth?: number fontSize?: number fontWeight?: number + showValueLabelsInline?: boolean // used to determine which series should be labelled when there is limited space seriesSortedByImportance?: EntityName[] @@ -349,20 +388,26 @@ export class LineLegend extends React.Component { return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH } + @computed private get showValueLabelsInline(): boolean { + return this.props.showValueLabelsInline ?? true + } + + @computed private get hasValueLabels(): boolean { + return this.props.labelSeries.some( + (series) => series.formattedValue !== undefined + ) + } + + @computed private get hideAnnotations(): boolean { + return this.hasValueLabels && !this.showValueLabelsInline + } + @computed.struct get sizedLabels(): SizedSeries[] { const { fontSize, fontWeight, maxWidth } = this const maxTextWidth = maxWidth - this.connectorLineWidth const maxAnnotationWidth = Math.min(maxTextWidth, 150) return this.props.labelSeries.map((label) => { - const annotationTextWrap = label.annotation - ? new TextWrap({ - text: label.annotation, - maxWidth: maxAnnotationWidth, - fontSize: fontSize * 0.9, - lineHeight: 1, - }) - : undefined const textWrap = new TextWrap({ text: label.label, maxWidth: maxTextWidth, @@ -370,18 +415,40 @@ export class LineLegend extends React.Component { fontWeight, lineHeight: 1, }) + const annotationTextWrap = + !this.hideAnnotations && label.annotation + ? new TextWrap({ + text: label.annotation, + maxWidth: maxAnnotationWidth, + fontSize: fontSize * 0.9, + lineHeight: 1, + }) + : undefined + const valueTextWrap = label.formattedValue + ? new TextWrap({ + text: label.formattedValue, + maxWidth: maxAnnotationWidth, + fontSize: fontSize * 0.9, + lineHeight: 1, + }) + : undefined return { ...label, textWrap, annotationTextWrap, + valueTextWrap, width: Math.max( textWrap.width, - annotationTextWrap ? annotationTextWrap.width : 0 + annotationTextWrap ? annotationTextWrap.width : 0, + valueTextWrap ? valueTextWrap.width : 0 ), height: textWrap.height + (annotationTextWrap ? ANNOTATION_PADDING + annotationTextWrap.height + : 0) + + (valueTextWrap + ? ANNOTATION_PADDING + valueTextWrap.height : 0), } }) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 65cf11a410d..638a9333f8b 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -504,6 +504,7 @@ export class SlopeChart yAxis: this.yAxis, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, + showValueLabelsInline: false, fontSize: this.fontSize, isStatic: this.manager.isStatic, }) @@ -516,6 +517,7 @@ export class SlopeChart yAxis: this.yAxis, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, + showValueLabelsInline: false, fontSize: this.fontSize, isStatic: this.manager.isStatic, }) @@ -596,11 +598,13 @@ export class SlopeChart @computed get lineLegendSeriesRight(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, end, annotation } = series + const formattedValue = this.formatColumn.formatValueLong(end.value) return { color, seriesName, label: seriesName, annotation, + formattedValue, yValue: end.value, } }) @@ -962,8 +966,10 @@ export class SlopeChart x={this.xRange[1] + LINE_LEGEND_PADDING} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="start" + showValueLabelsInline={false} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} + fontWeight={700} isStatic={this.manager.isStatic} focusedSeriesNames={this.focusedSeriesNames} onMouseLeave={this.onLineLegendMouseLeave} @@ -975,6 +981,7 @@ export class SlopeChart x={this.xRange[0] - LINE_LEGEND_PADDING} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="end" + showValueLabelsInline={false} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} isStatic={this.manager.isStatic} diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 80ebe0977a7..4b8daa16c31 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -294,7 +294,7 @@ export class StackedAreaChart extends AbstractStackedChart { }) } - @computed get labelSeries(): LineLabelSeries[] { + @computed get lineLegendSeries(): LineLabelSeries[] { const { midpoints } = this return this.series .map((series, index) => ({ @@ -318,7 +318,7 @@ export class StackedAreaChart extends AbstractStackedChart { // only pass props that are required to calculate // the width to avoid circular dependencies return LineLegend.incorrectWidth({ - labelSeries: this.labelSeries, + labelSeries: this.lineLegendSeries, maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, }) @@ -657,7 +657,7 @@ export class StackedAreaChart extends AbstractStackedChart { if (!this.manager.showLegend) return return ( Date: Tue, 26 Nov 2024 18:22:18 +0100 Subject: [PATCH 58/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20show=20value=20labe?= =?UTF-8?q?l=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 71 ++++++++++++------- .../grapher/src/slopeCharts/SlopeChart.tsx | 4 +- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index d974b4adcdb..de80249c44b 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -95,6 +95,7 @@ class LineLabels extends React.Component<{ needsConnectorLines: boolean connectorLineWidth?: number anchor?: "start" | "end" + showValueLabelsInline?: boolean isFocus?: boolean isStatic?: boolean onClick?: (series: PlacedSeries) => void @@ -113,6 +114,10 @@ class LineLabels extends React.Component<{ return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH } + @computed private get showValueLabelsInline(): boolean { + return this.props.showValueLabelsInline ?? true + } + @computed private get markers(): { series: PlacedSeries labelText: { x: number; y: number } @@ -213,6 +218,12 @@ class LineLabels extends React.Component<{ {markersWithTextValues.map(({ series, labelText }, index) => { const textColor = darkenColorForText(series.color) + const x = this.showValueLabelsInline + ? labelText.x + series.textWrap.width + 4 + : labelText.x + const y = this.showValueLabelsInline + ? labelText.y + : labelText.y + series.textWrap.height return ( - {series.valueTextWrap?.render( - labelText.x, - labelText.y + series.textWrap.height, - { - textProps: { - fill: textColor, - opacity: this.textOpacity, - textAnchor: this.anchor, - }, - } - )} + {series.valueTextWrap?.render(x, y, { + textProps: { + fill: textColor, + opacity: this.textOpacity, + textAnchor: this.anchor, + }, + })} ) })} @@ -281,6 +288,10 @@ class LineLabels extends React.Component<{ return ( {this.props.series.map((series, index) => { + const x = + this.anchor === "start" + ? series.origBounds.x + : series.origBounds.x - series.bounds.width return ( { const valueTextWrap = label.formattedValue ? new TextWrap({ text: label.formattedValue, - maxWidth: maxAnnotationWidth, + maxWidth: Infinity, fontSize: fontSize * 0.9, lineHeight: 1, }) : undefined + + const annotationWidth = annotationTextWrap + ? annotationTextWrap.width + : 0 + const annotationHeight = annotationTextWrap + ? ANNOTATION_PADDING + annotationTextWrap.height + : 0 + + const valueWidth = valueTextWrap ? valueTextWrap.width : 0 + const valueHeight = valueTextWrap + ? ANNOTATION_PADDING + valueTextWrap.height + : 0 + return { ...label, textWrap, annotationTextWrap, valueTextWrap, - width: Math.max( - textWrap.width, - annotationTextWrap ? annotationTextWrap.width : 0, - valueTextWrap ? valueTextWrap.width : 0 - ), - height: - textWrap.height + - (annotationTextWrap - ? ANNOTATION_PADDING + annotationTextWrap.height - : 0) + - (valueTextWrap - ? ANNOTATION_PADDING + valueTextWrap.height - : 0), + width: this.showValueLabelsInline + ? Math.max(textWrap.width + 4 + valueWidth, annotationWidth) + : Math.max(textWrap.width, annotationWidth, valueWidth), + height: this.showValueLabelsInline + ? textWrap.height + annotationHeight + : textWrap.height + annotationHeight + valueHeight, } }) } @@ -787,6 +804,7 @@ export class LineLegend extends React.Component { series={this.backgroundSeries} needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} + showValueLabelsInline={this.showValueLabelsInline} isFocus={false} anchor={this.props.lineLegendAnchorX} isStatic={this.props.isStatic} @@ -806,6 +824,7 @@ export class LineLegend extends React.Component { series={this.focusedSeries} needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} + showValueLabelsInline={this.showValueLabelsInline} isFocus={true} anchor={this.props.lineLegendAnchorX} isStatic={this.props.isStatic} diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 638a9333f8b..3624eb38f01 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -966,7 +966,7 @@ export class SlopeChart x={this.xRange[1] + LINE_LEGEND_PADDING} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="start" - showValueLabelsInline={false} + showValueLabelsInline={!this.useCompactLineLegend} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} fontWeight={700} @@ -981,7 +981,7 @@ export class SlopeChart x={this.xRange[0] - LINE_LEGEND_PADDING} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="end" - showValueLabelsInline={false} + showValueLabelsInline={!this.useCompactLineLegend} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} isStatic={this.manager.isStatic} From 27a2bfc307b7eb6d15d5dd0ad9dde864d885186b Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 10:55:50 +0100 Subject: [PATCH 59/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20only=20show=200%=20?= =?UTF-8?q?label=20once=20in=20relative=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 3 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 99 ++++++++++++------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index de80249c44b..241ed444586 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -218,8 +218,9 @@ class LineLabels extends React.Component<{ {markersWithTextValues.map(({ series, labelText }, index) => { const textColor = darkenColorForText(series.color) + const direction = this.anchor === "start" ? 1 : -1 const x = this.showValueLabelsInline - ? labelText.x + series.textWrap.width + 4 + ? labelText.x + direction * (series.textWrap.width + 4) : labelText.x const y = this.showValueLabelsInline ? labelText.y diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 3624eb38f01..2d2083673f6 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -13,6 +13,7 @@ import { max, getRelativeMouse, minBy, + dyFromAlign, } from "@ourworldindata/utils" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" @@ -33,6 +34,7 @@ import { SeriesStrategy, EntityName, RenderMode, + VerticalAlign, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" @@ -583,12 +585,13 @@ export class SlopeChart @computed get lineLegendSeriesLeft(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, start, annotation } = series + const formattedValue = this.formatColumn.formatValueShort( + start.value + ) return { color, seriesName, - label: this.useCompactLineLegend - ? this.formatColumn.formatValueShort(start.value) - : this.formatColumn.formatValueLong(start.value), + label: formattedValue, annotation, yValue: start.value, } @@ -598,7 +601,7 @@ export class SlopeChart @computed get lineLegendSeriesRight(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, end, annotation } = series - const formattedValue = this.formatColumn.formatValueLong(end.value) + const formattedValue = this.formatColumn.formatValueShort(end.value) return { color, seriesName, @@ -611,7 +614,7 @@ export class SlopeChart } @computed private get lineLegendConnectorLinesWidth(): number { - return this.useCompactLineLegend ? 15 : 35 + return this.useCompactLineLegend ? 15 : 25 } private playIntroAnimation() { @@ -956,39 +959,67 @@ export class SlopeChart ) } + private renderLineLegendRight(): React.ReactElement { + return ( + + ) + } + + private renderLineLegendLeft(): React.ReactElement { + // in relative mode, all slopes start from 0% + if (this.manager.isRelativeMode) + return ( + + {this.formatColumn.formatValueShort(0)} + + ) + + return ( + + ) + } + private renderLineLegends(): React.ReactElement | void { if (!this.manager.showLegend) return + return ( <> - - + {this.renderLineLegendLeft()} + {this.renderLineLegendRight()} ) } From 0e51f4bcf76077b2e6e479b4fde5f12eee9414c2 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 11:38:55 +0100 Subject: [PATCH 60/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20increase=20font=20s?= =?UTF-8?q?ize,=20use=20consistent=20grid=20line=20color?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 5 ++- .../grapher/src/slopeCharts/SlopeChart.tsx | 34 ++++++++----------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 241ed444586..09dc45fb37b 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -381,7 +381,10 @@ export class LineLegend extends React.Component { } @computed private get fontSize(): number { - return GRAPHER_FONT_SCALE_12 * (this.props.fontSize ?? BASE_FONT_SIZE) + return Math.max( + GRAPHER_FONT_SCALE_12 * (this.props.fontSize ?? BASE_FONT_SIZE), + 11.5 + ) } @computed private get fontWeight(): number { diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 2d2083673f6..261ff5b56ee 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -185,6 +185,11 @@ export class SlopeChart return this.props.bounds ?? DEFAULT_BOUNDS } + private sidebarMargin = 10 + @computed private get innerBounds(): Bounds { + return this.bounds.padRight(this.sidebarWidth + 8) + } + @computed get fontSize() { return this.manager.fontSize ?? BASE_FONT_SIZE } @@ -206,8 +211,7 @@ export class SlopeChart } @computed private get lineStrokeWidth(): number { - const factor = this.manager.isStaticAndSmall ? 2 : 1 - return factor * 2 + return this.manager.isStaticAndSmall ? 3 : 1.5 } @computed private get backgroundColor(): string { @@ -533,7 +537,7 @@ export class SlopeChart // pick a reasonable max width based on an ideal aspect ratio const idealAspectRatio = 0.6 - const chartAreaWidth = this.bounds.width - this.sidebarWidth + const chartAreaWidth = this.innerBounds.width const availableWidth = chartAreaWidth - this.yAxisWidth - @@ -848,7 +852,7 @@ export class SlopeChart if (!this.showNoDataSection) return const bounds = new Bounds( - this.bounds.right - this.sidebarWidth, + this.innerBounds.right + this.sidebarMargin, this.bounds.top, this.sidebarWidth, this.bounds.height @@ -877,7 +881,7 @@ export class SlopeChart color={series.color} mode={mode} strokeWidth={this.lineStrokeWidth} - outlineWidth={0.25} + outlineWidth={0.5} outlineStroke={this.backgroundColor} /> ) @@ -915,8 +919,7 @@ export class SlopeChart @@ -1000,6 +1004,7 @@ export class SlopeChart labelSeries={this.lineLegendSeriesLeft} yAxis={this.yAxis} x={this.xRange[0] - LINE_LEGEND_PADDING} + yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="end" showValueLabelsInline={!this.useCompactLineLegend} @@ -1157,11 +1162,10 @@ function HaloLine(props: HaloLineProps): React.ReactElement { interface GridLinesProps { bounds: Bounds yAxis: VerticalAxis - startX: number endX: number } -function GridLines({ bounds, yAxis, startX, endX }: GridLinesProps) { +function GridLines({ bounds, yAxis, endX }: GridLinesProps) { return ( {yAxis.tickLabels.map((tick) => { @@ -1174,19 +1178,9 @@ function GridLines({ bounds, yAxis, startX, endX }: GridLinesProps) { )} key={tick.formattedValue} > - {/* grid lines connecting the chart area to the axis */} - {/* grid lines within the chart area */} - Date: Fri, 29 Nov 2024 17:27:04 +0100 Subject: [PATCH 61/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20add=20entity=20anno?= =?UTF-8?q?tation=20to=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 18 +++++++++++++----- .../grapher/src/tooltip/Tooltip.scss | 7 +++++++ .../grapher/src/tooltip/Tooltip.tsx | 10 +++++++++- .../grapher/src/tooltip/TooltipProps.ts | 1 + 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 261ff5b56ee..657dd04a1a7 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -581,6 +581,10 @@ export class SlopeChart return !!this.manager.isSemiNarrow } + @computed private get showValueLabelsInline(): boolean { + return this.useCompactLineLegend + } + // used by LineLegend @computed get focusedSeriesNames(): SeriesName[] { return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] @@ -588,7 +592,7 @@ export class SlopeChart @computed get lineLegendSeriesLeft(): LineLabelSeries[] { return this.series.map((series) => { - const { seriesName, color, start, annotation } = series + const { seriesName, color, start } = series const formattedValue = this.formatColumn.formatValueShort( start.value ) @@ -596,7 +600,6 @@ export class SlopeChart color, seriesName, label: formattedValue, - annotation, yValue: start.value, } }) @@ -750,6 +753,11 @@ export class SlopeChart const formatTime = (time: Time) => formatColumn.formatTime(time) + const title = series.seriesName + const titleAnnotation = !this.showValueLabelsInline + ? series.annotation + : undefined + const actualStartTime = series.start.originalTime const actualEndTime = series.end.originalTime const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}` @@ -803,7 +811,8 @@ export class SlopeChart offsetX={20} offsetY={-16} style={{ maxWidth: "250px" }} - title={series.seriesName} + title={title} + titleAnnotation={titleAnnotation} subtitle={timeLabel} subtitleFormat={targetYear ? "notice" : undefined} dissolve={fading} @@ -971,7 +980,7 @@ export class SlopeChart yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="start" - showValueLabelsInline={!this.useCompactLineLegend} + showValueLabelsInline={this.showValueLabelsInline} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} fontWeight={700} @@ -1007,7 +1016,6 @@ export class SlopeChart yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="end" - showValueLabelsInline={!this.useCompactLineLegend} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} isStatic={this.manager.isStatic} diff --git a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss index c8e74e151f4..7e75dbac713 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss +++ b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss @@ -67,6 +67,13 @@ font-size: 14px; font-weight: $bold; letter-spacing: 0; + + .annotation { + margin-left: 4px; + font-weight: normal; + color: $grey; + font-size: 0.9em; + } } .subtitle { margin: 4px 0 2px 0; diff --git a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx index 5e4523480a9..01d11012823 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx @@ -103,6 +103,7 @@ class TooltipCard extends React.Component< let { id, title, + titleAnnotation, subtitle, subtitleFormat, footer, @@ -189,7 +190,14 @@ class TooltipCard extends React.Component< > {hasHeader && (
- {title &&
{title}
} + {title && ( +
+ {title}{" "} + + {titleAnnotation} + +
+ )} {subtitle && (
{timeNotice && TOOLTIP_ICON.notice} diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts b/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts index e3222a8949b..9fae113da6b 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts @@ -28,6 +28,7 @@ export interface TooltipProps { offsetXDirection?: "left" | "right" offsetYDirection?: "upward" | "downward" title?: string | number // header text + titleAnnotation?: string // rendered next to the title, but muted subtitle?: string | number // header deck subtitleFormat?: "notice" | "unit" // optional postprocessing for subtitle footer?: { icon: TooltipFooterIcon; text: string }[] From 59e6fe4bc30b1721bf85d4e3d3d016863dc11a47 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 29 Nov 2024 18:01:41 +0100 Subject: [PATCH 62/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20render=20halo=20for?= =?UTF-8?q?=20entity=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/src/TextWrap/TextWrap.tsx | 4 +- .../grapher/src/lineLegend/LineLegend.tsx | 62 ++++++++++--------- .../grapher/src/slopeCharts/SlopeChart.tsx | 2 +- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx index 4f0ece86fa4..6e971e82e42 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx @@ -266,10 +266,10 @@ export class TextWrap { textProps, id, }: { textProps?: React.SVGProps; id?: string } = {} - ): React.ReactElement | null { + ): React.ReactElement { const { props, lines, fontSize, fontWeight, lineHeight } = this - if (lines.length === 0) return null + if (lines.length === 0) return <> const [correctedX, correctedY] = this.getPositionForSvgRendering(x, y) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 09dc45fb37b..b43f81cdab7 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -23,9 +23,10 @@ import { BASE_FONT_SIZE, GRAPHER_FONT_SCALE_12 } from "../core/GrapherConstants" import { ChartSeries } from "../chart/ChartInterface" import { darkenColorForText } from "../color/ColorUtils" import { AxisConfig } from "../axis/AxisConfig.js" +import { Halo } from "../halo/Halo" // Minimum vertical space between two legend items -const LEGEND_ITEM_MIN_SPACING = 2 +const LEGEND_ITEM_MIN_SPACING = 4 // Horizontal distance from the end of the chart to the start of the marker const MARKER_MARGIN = 4 // Space between the label and the annotation @@ -151,15 +152,14 @@ class LineLabels extends React.Component<{ return ( {this.markers.map(({ series, labelText }, index) => { + const key = getSeriesKey( + series, + index, + this.props.uniqueKey + ) const textColor = darkenColorForText(series.color) return ( - + {series.textWrap.render(labelText.x, labelText.y, { textProps: { fill: textColor, @@ -167,7 +167,7 @@ class LineLabels extends React.Component<{ textAnchor: this.anchor, }, })} - + ) })} @@ -182,17 +182,19 @@ class LineLabels extends React.Component<{ return ( {markersWithAnnotations.map(({ series, labelText }, index) => { + const key = getSeriesKey( + series, + index, + this.props.uniqueKey + ) + if (!series.annotationTextWrap) return return ( - - {series.annotationTextWrap?.render( + + {series.annotationTextWrap.render( labelText.x, - labelText.y + series.textWrap.height, + labelText.y + + series.textWrap.height + + ANNOTATION_PADDING, { textProps: { fill: "#333", @@ -202,7 +204,7 @@ class LineLabels extends React.Component<{ }, } )} - + ) })} @@ -217,6 +219,12 @@ class LineLabels extends React.Component<{ return ( {markersWithTextValues.map(({ series, labelText }, index) => { + if (!series.valueTextWrap) return + const key = getSeriesKey( + series, + index, + this.props.uniqueKey + ) const textColor = darkenColorForText(series.color) const direction = this.anchor === "start" ? 1 : -1 const x = this.showValueLabelsInline @@ -224,23 +232,19 @@ class LineLabels extends React.Component<{ : labelText.x const y = this.showValueLabelsInline ? labelText.y - : labelText.y + series.textWrap.height + : labelText.y + + series.textWrap.height + + ANNOTATION_PADDING return ( - - {series.valueTextWrap?.render(x, y, { + + {series.valueTextWrap.render(x, y, { textProps: { fill: textColor, opacity: this.textOpacity, textAnchor: this.anchor, }, })} - + ) })} diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 657dd04a1a7..a7cf8c967e3 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -928,7 +928,7 @@ export class SlopeChart Date: Mon, 2 Dec 2024 11:07:45 +0100 Subject: [PATCH 63/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20render=20values=20n?= =?UTF-8?q?ext=20to=20entity=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../components/src/TextWrap/TextWrap.tsx | 67 ++++++-- .../src/TextWrap/TextWrapGroup.test.ts | 102 ++++++++++++ .../components/src/TextWrap/TextWrapGroup.tsx | 156 ++++++++++++++++++ .../@ourworldindata/components/src/index.ts | 1 + .../grapher/src/lineLegend/LineLegend.tsx | 79 ++------- 6 files changed, 323 insertions(+), 83 deletions(-) create mode 100644 packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts create mode 100644 packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx diff --git a/.gitignore b/.gitignore index e33cb39db80..0c56cb718ec 100755 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ dist/ .dev.vars **/tsup.config.bundled*.mjs cfstorage/ +vite.*.mjs diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx index 6e971e82e42..10a1f9daa8a 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx @@ -1,4 +1,4 @@ -import { max, stripHTML, Bounds, FontFamily } from "@ourworldindata/utils" +import { max, stripHTML, Bounds, FontFamily, last } from "@ourworldindata/utils" import { computed } from "mobx" import React from "react" import { Fragment, joinFragments, splitIntoFragments } from "./TextWrapUtils" @@ -11,6 +11,7 @@ interface TextWrapProps { lineHeight?: number fontSize: FontSize fontWeight?: number + firstLineOffset?: number separators?: string[] rawHtml?: boolean } @@ -80,6 +81,9 @@ export class TextWrap { @computed get separators(): string[] { return this.props.separators ?? [" "] } + @computed get firstLineOffset(): number { + return this.props.firstLineOffset ?? 0 + } // We need to take care that HTML tags are not split across lines. // Instead, we want every line to have opening and closing tags for all tags that appear. @@ -148,11 +152,18 @@ export class TextWrap { ? stripHTML(joinFragments(nextLine)) : joinFragments(nextLine) - const nextBounds = Bounds.forText(text, { + let nextBounds = Bounds.forText(text, { fontSize, fontWeight, }) + // add offset to the first line if given + if (lines.length === 0 && this.firstLineOffset) { + nextBounds = nextBounds.set({ + width: nextBounds.width + this.firstLineOffset, + }) + } + if ( startsWithNewline(fragment.text) || (nextBounds.width + 10 > maxWidth && line.length >= 1) @@ -194,16 +205,37 @@ export class TextWrap { else return lines } + @computed get lineCount(): number { + return this.lines.length + } + + @computed get singleLineHeight(): number { + return this.fontSize * this.lineHeight + } + @computed get height(): number { - const { lines, lineHeight, fontSize } = this - if (lines.length === 0) return 0 - return lines.length * lineHeight * fontSize + if (this.lineCount === 0) return 0 + return this.lineCount * this.singleLineHeight + } + + @computed get heightWithoutOffsetedLine(): number { + if (this.firstLineOffset === 0) { + return this.height + } else if (this.lineCount > 1) { + return (this.lineCount - 1) * this.singleLineHeight + } else { + return 0 + } } @computed get width(): number { return max(this.lines.map((l) => l.width)) ?? 0 } + @computed get lastLineWidth(): number { + return last(this.lines)?.width ?? 0 + } + @computed get htmlStyle(): any { const { fontSize, fontWeight, lineHeight } = this return { @@ -267,7 +299,14 @@ export class TextWrap { id, }: { textProps?: React.SVGProps; id?: string } = {} ): React.ReactElement { - const { props, lines, fontSize, fontWeight, lineHeight } = this + const { + props, + lines, + fontSize, + fontWeight, + lineHeight, + firstLineOffset, + } = this if (lines.length === 0) return <> @@ -283,25 +322,21 @@ export class TextWrap { {...textProps} > {lines.map((line, i) => { + const x = correctedX + (i === 0 ? firstLineOffset : 0) + const y = correctedY + lineHeight * fontSize * i + if (props.rawHtml) return ( ) else return ( - + {line.text} ) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts new file mode 100644 index 00000000000..5162484c35f --- /dev/null +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts @@ -0,0 +1,102 @@ +#! /usr/bin/env jest + +import { TextWrap } from "./TextWrap" +import { TextWrapGroup } from "./TextWrapGroup" + +const FONT_SIZE = 14 +const TEXT = "Lower middle-income countries" +const MAX_WIDTH = 150 + +const textWrap = new TextWrap({ + text: TEXT, + maxWidth: MAX_WIDTH, + fontSize: FONT_SIZE, +}) + +it("should work like TextWrap for a single fragment", () => { + const textWrapGroup = new TextWrapGroup({ + fragments: [{ text: TEXT }], + maxWidth: MAX_WIDTH, + fontSize: FONT_SIZE, + }) + + const firstTextWrap = textWrapGroup.textWraps[0] + expect(firstTextWrap.text).toEqual(textWrap.text) + expect(firstTextWrap.width).toEqual(textWrap.width) + expect(firstTextWrap.height).toEqual(textWrap.height) + expect(firstTextWrap.lines).toEqual(textWrap.lines) +}) + +it("should place fragments in-line if there is space", () => { + const textWrapGroup = new TextWrapGroup({ + fragments: [{ text: TEXT }, { text: "30 million" }], + maxWidth: MAX_WIDTH, + fontSize: FONT_SIZE, + }) + + expect(textWrapGroup.text).toEqual([TEXT, "30 million"].join(" ")) + expect(textWrapGroup.height).toEqual(textWrap.height) +}) + +it("should place the second segment in a new line if preferred", () => { + const maxWidth = 250 + const textWrapGroup = new TextWrapGroup({ + fragments: [ + { text: TEXT }, + { text: "30 million", preferLineBreakOverWrapping: true }, + ], + maxWidth, + fontSize: FONT_SIZE, + }) + + // 30 million should be placed in a new line, thus the group's height + // should be greater than the textWrap's height + expect(textWrapGroup.height).toBeGreaterThan( + new TextWrap({ + text: TEXT, + maxWidth, + fontSize: FONT_SIZE, + }).height + ) +}) + +it("should place the second segment in the same line if possible", () => { + const maxWidth = 1000 + const textWrapGroup = new TextWrapGroup({ + fragments: [ + { text: TEXT }, + { text: "30 million", preferLineBreakOverWrapping: true }, + ], + maxWidth, + fontSize: FONT_SIZE, + }) + + // since the max width is large, "30 million" fits into the same line + // as the text of the first fragmemt + expect(textWrapGroup.height).toEqual( + new TextWrap({ + text: TEXT, + maxWidth, + fontSize: FONT_SIZE, + }).height + ) +}) + +it("should use all available space when one fragment exceeds the given max width", () => { + const maxWidth = 150 + const textWrap = new TextWrap({ + text: "Long-word-that-can't-be-broken-up more words", + maxWidth, + fontSize: FONT_SIZE, + }) + const textWrapGroup = new TextWrapGroup({ + fragments: [ + { text: "Long-word-that-can't-be-broken-up more words" }, + { text: "30 million" }, + ], + maxWidth, + fontSize: FONT_SIZE, + }) + expect(textWrap.width).toBeGreaterThan(maxWidth) + expect(textWrapGroup.maxWidth).toEqual(textWrap.width) +}) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx new file mode 100644 index 00000000000..f6b9b0fd997 --- /dev/null +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx @@ -0,0 +1,156 @@ +import React from "react" +import { computed } from "mobx" +import { TextWrap } from "./TextWrap" +import { splitIntoFragments } from "./TextWrapUtils" +import { Bounds, last, max } from "@ourworldindata/utils" + +interface TextWrapFragment { + text: string + fontWeight?: number + preferLineBreakOverWrapping?: boolean +} + +interface PlacedTextWrap { + textWrap: TextWrap + yOffset: number +} + +interface TextWrapGroupProps { + fragments: TextWrapFragment[] + maxWidth: number + lineHeight?: number + fontSize: number + fontWeight?: number +} + +export class TextWrapGroup { + props: TextWrapGroupProps + constructor(props: TextWrapGroupProps) { + this.props = props + } + + @computed get lineHeight(): number { + return this.props.lineHeight ?? 1.1 + } + + @computed get fontSize(): number { + return this.props.fontSize + } + + @computed get fontWeight(): number | undefined { + return this.props.fontWeight + } + + @computed get text(): string { + return this.props.fragments.map((fragment) => fragment.text).join(" ") + } + + @computed get maxWidth(): number { + const wordWidths = this.props.fragments.flatMap((fragment) => + splitIntoFragments(fragment.text).map( + ({ text }) => + Bounds.forText(text, { + fontSize: this.fontSize, + fontWeight: fragment.fontWeight ?? this.fontWeight, + }).width + ) + ) + return max([...wordWidths, this.props.maxWidth]) ?? Infinity + } + + private makeTextWrapForFragment( + fragment: TextWrapFragment, + offset = 0 + ): TextWrap { + return new TextWrap({ + text: fragment.text, + maxWidth: this.maxWidth, + lineHeight: this.lineHeight, + fontSize: this.fontSize, + fontWeight: fragment.fontWeight ?? this.fontWeight, + firstLineOffset: offset, + }) + } + + @computed get placedTextWraps(): PlacedTextWrap[] { + const { fragments } = this.props + if (fragments.length === 0) return [] + + const whitespaceWidth = Bounds.forText(" ", { + fontSize: this.fontSize, + }).width + + const textWraps: PlacedTextWrap[] = [ + { + textWrap: this.makeTextWrapForFragment(fragments[0]), + yOffset: 0, + }, + ] + + for (let i = 1; i < fragments.length; i++) { + const fragment = fragments[i] + const { textWrap: lastTextWrap, yOffset: lastYOffset } = + textWraps[i - 1] + + let textWrap = this.makeTextWrapForFragment( + fragment, + lastTextWrap.lastLineWidth + whitespaceWidth + ) + + let yOffset = lastYOffset + if (textWrap.firstLineOffset === 0) { + yOffset += lastTextWrap.height + } else { + yOffset += + (lastTextWrap.lineCount - 1) * lastTextWrap.singleLineHeight + } + + // some fragments are preferred to break into a new line + // instead of being wrapped + if ( + fragment.preferLineBreakOverWrapping && + textWrap.lineCount > 1 + ) { + textWrap = this.makeTextWrapForFragment(fragment) + yOffset += lastTextWrap.singleLineHeight + } + + textWraps.push({ textWrap, yOffset }) + } + + return textWraps + } + + @computed get textWraps(): TextWrap[] { + return this.placedTextWraps.map(({ textWrap }) => textWrap) + } + + @computed get height(): number { + if (this.placedTextWraps.length === 0) return 0 + const { textWrap, yOffset } = last(this.placedTextWraps)! + return yOffset + textWrap.height + } + + @computed get width(): number { + return max(this.textWraps.map((textWrap) => textWrap.width)) ?? 0 + } + + render( + x: number, + y: number, + { + textProps, + id, + }: { textProps?: React.SVGProps; id?: string } = {} + ): React.ReactElement { + return ( + + {this.placedTextWraps.map(({ textWrap, yOffset }, index) => ( + + {textWrap.render(x, y + yOffset, { textProps, id })} + + ))} + + ) + } +} diff --git a/packages/@ourworldindata/components/src/index.ts b/packages/@ourworldindata/components/src/index.ts index c7118e4ab21..57b17d1e8ce 100644 --- a/packages/@ourworldindata/components/src/index.ts +++ b/packages/@ourworldindata/components/src/index.ts @@ -1,4 +1,5 @@ export { TextWrap, shortenForTargetWidth } from "./TextWrap/TextWrap.js" +export { TextWrapGroup } from "./TextWrap/TextWrapGroup.js" export { MarkdownTextWrap, diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index b43f81cdab7..e40aa954c24 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -14,7 +14,7 @@ import { last, maxBy, } from "@ourworldindata/utils" -import { TextWrap } from "@ourworldindata/components" +import { TextWrap, TextWrapGroup } from "@ourworldindata/components" import { computed } from "mobx" import { observer } from "mobx-react" import { VerticalAxis } from "../axis/Axis" @@ -44,9 +44,8 @@ export interface LineLabelSeries extends ChartSeries { } interface SizedSeries extends LineLabelSeries { - textWrap: TextWrap + textWrap: TextWrapGroup annotationTextWrap?: TextWrap - valueTextWrap?: TextWrap width: number height: number } @@ -211,46 +210,6 @@ class LineLabels extends React.Component<{ ) } - @computed private get textValues(): React.ReactElement | void { - const markersWithTextValues = this.markers.filter( - ({ series }) => series.valueTextWrap !== undefined - ) - if (!markersWithTextValues) return - return ( - - {markersWithTextValues.map(({ series, labelText }, index) => { - if (!series.valueTextWrap) return - const key = getSeriesKey( - series, - index, - this.props.uniqueKey - ) - const textColor = darkenColorForText(series.color) - const direction = this.anchor === "start" ? 1 : -1 - const x = this.showValueLabelsInline - ? labelText.x + direction * (series.textWrap.width + 4) - : labelText.x - const y = this.showValueLabelsInline - ? labelText.y - : labelText.y + - series.textWrap.height + - ANNOTATION_PADDING - return ( - - {series.valueTextWrap.render(x, y, { - textProps: { - fill: textColor, - opacity: this.textOpacity, - textAnchor: this.anchor, - }, - })} - - ) - })} - - ) - } - @computed private get connectorLines(): React.ReactElement | void { if (!this.props.needsConnectorLines) return return ( @@ -331,7 +290,6 @@ class LineLabels extends React.Component<{ <> {this.connectorLines} {this.textAnnotations} - {this.textValues} {this.textLabels} {!this.props.isStatic && this.interactions} @@ -427,11 +385,16 @@ export class LineLegend extends React.Component { const maxAnnotationWidth = Math.min(maxTextWidth, 150) return this.props.labelSeries.map((label) => { - const textWrap = new TextWrap({ - text: label.label, + const labelFragments = excludeUndefined([ + { text: label.label, fontWeight }, + label.formattedValue + ? { text: label.formattedValue } + : undefined, + ]) + const textWrap = new TextWrapGroup({ + fragments: labelFragments, maxWidth: maxTextWidth, fontSize, - fontWeight, lineHeight: 1, }) const annotationTextWrap = @@ -443,14 +406,6 @@ export class LineLegend extends React.Component { lineHeight: 1, }) : undefined - const valueTextWrap = label.formattedValue - ? new TextWrap({ - text: label.formattedValue, - maxWidth: Infinity, - fontSize: fontSize * 0.9, - lineHeight: 1, - }) - : undefined const annotationWidth = annotationTextWrap ? annotationTextWrap.width @@ -459,22 +414,12 @@ export class LineLegend extends React.Component { ? ANNOTATION_PADDING + annotationTextWrap.height : 0 - const valueWidth = valueTextWrap ? valueTextWrap.width : 0 - const valueHeight = valueTextWrap - ? ANNOTATION_PADDING + valueTextWrap.height - : 0 - return { ...label, textWrap, annotationTextWrap, - valueTextWrap, - width: this.showValueLabelsInline - ? Math.max(textWrap.width + 4 + valueWidth, annotationWidth) - : Math.max(textWrap.width, annotationWidth, valueWidth), - height: this.showValueLabelsInline - ? textWrap.height + annotationHeight - : textWrap.height + annotationHeight + valueHeight, + width: Math.max(textWrap.width, annotationWidth), + height: textWrap.height + annotationHeight, } }) } From 085f8c0e8e4f2a20d299490060de3555700c9ace Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 11:32:15 +0100 Subject: [PATCH 64/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20give=20labels=20a?= =?UTF-8?q?=20bit=20more=20space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 39 ++++--------------- .../grapher/src/slopeCharts/SlopeChart.tsx | 13 ++----- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index e40aa954c24..bf94e552eb3 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -95,7 +95,6 @@ class LineLabels extends React.Component<{ needsConnectorLines: boolean connectorLineWidth?: number anchor?: "start" | "end" - showValueLabelsInline?: boolean isFocus?: boolean isStatic?: boolean onClick?: (series: PlacedSeries) => void @@ -114,10 +113,6 @@ class LineLabels extends React.Component<{ return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH } - @computed private get showValueLabelsInline(): boolean { - return this.props.showValueLabelsInline ?? true - } - @computed private get markers(): { series: PlacedSeries labelText: { x: number; y: number } @@ -311,7 +306,6 @@ export interface LineLegendProps { connectorLineWidth?: number fontSize?: number fontWeight?: number - showValueLabelsInline?: boolean // used to determine which series should be labelled when there is limited space seriesSortedByImportance?: EntityName[] @@ -365,20 +359,6 @@ export class LineLegend extends React.Component { return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH } - @computed private get showValueLabelsInline(): boolean { - return this.props.showValueLabelsInline ?? true - } - - @computed private get hasValueLabels(): boolean { - return this.props.labelSeries.some( - (series) => series.formattedValue !== undefined - ) - } - - @computed private get hideAnnotations(): boolean { - return this.hasValueLabels && !this.showValueLabelsInline - } - @computed.struct get sizedLabels(): SizedSeries[] { const { fontSize, fontWeight, maxWidth } = this const maxTextWidth = maxWidth - this.connectorLineWidth @@ -397,15 +377,14 @@ export class LineLegend extends React.Component { fontSize, lineHeight: 1, }) - const annotationTextWrap = - !this.hideAnnotations && label.annotation - ? new TextWrap({ - text: label.annotation, - maxWidth: maxAnnotationWidth, - fontSize: fontSize * 0.9, - lineHeight: 1, - }) - : undefined + const annotationTextWrap = label.annotation + ? new TextWrap({ + text: label.annotation, + maxWidth: maxAnnotationWidth, + fontSize: fontSize * 0.9, + lineHeight: 1, + }) + : undefined const annotationWidth = annotationTextWrap ? annotationTextWrap.width @@ -757,7 +736,6 @@ export class LineLegend extends React.Component { series={this.backgroundSeries} needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} - showValueLabelsInline={this.showValueLabelsInline} isFocus={false} anchor={this.props.lineLegendAnchorX} isStatic={this.props.isStatic} @@ -777,7 +755,6 @@ export class LineLegend extends React.Component { series={this.focusedSeries} needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} - showValueLabelsInline={this.showValueLabelsInline} isFocus={true} anchor={this.props.lineLegendAnchorX} isStatic={this.props.isStatic} diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index a7cf8c967e3..23d379a4c64 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -500,7 +500,7 @@ export class SlopeChart } @computed get maxLineLegendWidth(): number { - return this.bounds.width / 6 + return this.innerBounds.width / 4 } @computed get lineLegendWidthLeft(): number { @@ -510,7 +510,6 @@ export class SlopeChart yAxis: this.yAxis, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, - showValueLabelsInline: false, fontSize: this.fontSize, isStatic: this.manager.isStatic, }) @@ -523,7 +522,6 @@ export class SlopeChart yAxis: this.yAxis, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, - showValueLabelsInline: false, fontSize: this.fontSize, isStatic: this.manager.isStatic, }) @@ -581,10 +579,6 @@ export class SlopeChart return !!this.manager.isSemiNarrow } - @computed private get showValueLabelsInline(): boolean { - return this.useCompactLineLegend - } - // used by LineLegend @computed get focusedSeriesNames(): SeriesName[] { return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] @@ -613,7 +607,7 @@ export class SlopeChart color, seriesName, label: seriesName, - annotation, + annotation: this.useCompactLineLegend ? undefined : annotation, formattedValue, yValue: end.value, } @@ -754,7 +748,7 @@ export class SlopeChart const formatTime = (time: Time) => formatColumn.formatTime(time) const title = series.seriesName - const titleAnnotation = !this.showValueLabelsInline + const titleAnnotation = this.useCompactLineLegend ? series.annotation : undefined @@ -980,7 +974,6 @@ export class SlopeChart yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} lineLegendAnchorX="start" - showValueLabelsInline={this.showValueLabelsInline} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} fontWeight={700} From 70c09b42c98d5b80a11f523ebd051854de3e2a4a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 13:01:03 +0100 Subject: [PATCH 65/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20vertically=20align?= =?UTF-8?q?=20label=20and=20dot=20on=20top?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/src/TextWrap/TextWrapGroup.tsx | 5 +++ .../grapher/src/lineCharts/LineChart.tsx | 3 ++ .../grapher/src/lineLegend/LineLegend.tsx | 31 ++++++++++++++----- .../grapher/src/slopeCharts/SlopeChart.tsx | 7 +++-- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx index f6b9b0fd997..6a34e48406d 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx @@ -131,6 +131,11 @@ export class TextWrapGroup { return yOffset + textWrap.height } + @computed get singleLineHeight(): number { + if (this.textWraps.length === 0) return 0 + return this.textWraps[0].singleLineHeight + } + @computed get width(): number { return max(this.textWraps.map((textWrap) => textWrap.width)) ?? 0 } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 9a8a343b240..b51fbbc047f 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -52,6 +52,7 @@ import { MissingDataStrategy, ColorScaleConfigInterface, ColorSchemeName, + VerticalAlign, } from "@ourworldindata/types" import { GRAPHER_AXIS_LINE_WIDTH_THICK, @@ -885,6 +886,7 @@ export class LineChart maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, fontWeight: this.fontWeight, + verticalAlign: VerticalAlign.top, }) } @@ -953,6 +955,7 @@ export class LineChart x={this.lineLegendX} yRange={this.lineLegendY} maxWidth={this.maxLineLegendWidth} + verticalAlign={VerticalAlign.top} fontSize={this.fontSize} fontWeight={this.fontWeight} isStatic={this.isStatic} diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index bf94e552eb3..a6874e75bbc 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -18,7 +18,7 @@ import { TextWrap, TextWrapGroup } from "@ourworldindata/components" import { computed } from "mobx" import { observer } from "mobx-react" import { VerticalAxis } from "../axis/Axis" -import { EntityName } from "@ourworldindata/types" +import { EntityName, VerticalAlign } from "@ourworldindata/types" import { BASE_FONT_SIZE, GRAPHER_FONT_SCALE_12 } from "../core/GrapherConstants" import { ChartSeries } from "../chart/ChartInterface" import { darkenColorForText } from "../color/ColorUtils" @@ -300,7 +300,8 @@ export interface LineLegendProps { x?: number yRange?: [number, number] maxWidth?: number - lineLegendAnchorX?: "start" | "end" + xAnchor?: "start" | "end" + verticalAlign?: VerticalAlign // presentation connectorLineWidth?: number @@ -359,6 +360,10 @@ export class LineLegend extends React.Component { return this.props.connectorLineWidth ?? DEFAULT_CONNECTOR_LINE_WIDTH } + @computed private get verticalAlign(): VerticalAlign { + return this.props.verticalAlign ?? VerticalAlign.middle + } + @computed.struct get sizedLabels(): SizedSeries[] { const { fontSize, fontWeight, maxWidth } = this const maxTextWidth = maxWidth - this.connectorLineWidth @@ -437,6 +442,19 @@ export class LineLegend extends React.Component { return [Math.min(range[1], range[0]), Math.max(range[1], range[0])] } + private getYPositionForSeriesLabel(series: SizedSeries): number { + const y = this.yAxis.place(series.yValue) + const lineHeight = series.textWrap.singleLineHeight + switch (this.verticalAlign) { + case VerticalAlign.middle: + return y - series.height / 2 + case VerticalAlign.top: + return y - lineHeight / 2 + case VerticalAlign.bottom: + return y - series.height + lineHeight / 2 + } + } + // Naive initial placement of each mark at the target height, before collision detection @computed private get initialSeries(): PlacedSeries[] { const { yAxis, legendX, legendY } = this @@ -447,17 +465,16 @@ export class LineLegend extends React.Component { const labelHeight = label.height const labelWidth = label.width + this.connectorLineWidth - // place vertically centered at Y value const midY = yAxis.place(label.yValue) - const initialY = midY - label.height / 2 const origBounds = new Bounds( legendX, - initialY, + midY - label.height / 2, labelWidth, labelHeight ) // ensure label doesn't go beyond the top or bottom of the chart + const initialY = this.getYPositionForSeriesLabel(label) const y = Math.min( Math.max(initialY, legendYMin), legendYMax - labelHeight @@ -737,7 +754,7 @@ export class LineLegend extends React.Component { needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} isFocus={false} - anchor={this.props.lineLegendAnchorX} + anchor={this.props.xAnchor} isStatic={this.props.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) @@ -756,7 +773,7 @@ export class LineLegend extends React.Component { needsConnectorLines={this.needsLines} connectorLineWidth={this.connectorLineWidth} isFocus={true} - anchor={this.props.lineLegendAnchorX} + anchor={this.props.xAnchor} isStatic={this.props.isStatic} onMouseOver={(series): void => this.onMouseOver(series.seriesName) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 23d379a4c64..654d24cf65f 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -10,7 +10,6 @@ import { guid, excludeUndefined, partition, - max, getRelativeMouse, minBy, dyFromAlign, @@ -973,7 +972,8 @@ export class SlopeChart x={this.xRange[1] + LINE_LEGEND_PADDING} yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} - lineLegendAnchorX="start" + xAnchor="start" + verticalAlign={VerticalAlign.top} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} fontWeight={700} @@ -1008,7 +1008,8 @@ export class SlopeChart x={this.xRange[0] - LINE_LEGEND_PADDING} yRange={[this.bounds.top, this.bounds.bottom]} maxWidth={this.maxLineLegendWidth} - lineLegendAnchorX="end" + xAnchor="end" + verticalAlign={VerticalAlign.top} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} isStatic={this.manager.isStatic} From 6bb9671f2aecdc55609096cc88baf2fe3a26a2fd Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 13:12:26 +0100 Subject: [PATCH 66/91] =?UTF-8?q?=E2=9C=A8=20incrase=20line=20height=20of?= =?UTF-8?q?=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index a6874e75bbc..81e3471dbd7 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -380,14 +380,12 @@ export class LineLegend extends React.Component { fragments: labelFragments, maxWidth: maxTextWidth, fontSize, - lineHeight: 1, }) const annotationTextWrap = label.annotation ? new TextWrap({ text: label.annotation, maxWidth: maxAnnotationWidth, fontSize: fontSize * 0.9, - lineHeight: 1, }) : undefined From e97e206b9fc1203de5bd12624045209744b0d55f Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 13:57:29 +0100 Subject: [PATCH 67/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20consistent=20spacin?= =?UTF-8?q?g=20between=20label=20and=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/src/TextWrap/TextWrapGroup.tsx | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx index 6a34e48406d..891a607b572 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx @@ -140,6 +140,45 @@ export class TextWrapGroup { return max(this.textWraps.map((textWrap) => textWrap.width)) ?? 0 } + @computed get lines(): { + fragments: Omit[] + textWrap: TextWrap + yOffset: number + }[] { + const lines = [] + for (const { textWrap, yOffset } of this.placedTextWraps) { + for (let i = 0; i < textWrap.lineCount; i++) { + const textWrapLine = textWrap.lines[i] + const isFirstTextWrapLine = i === 0 + + const fragment = { + textWrap, + text: textWrapLine.text, + fontWeight: textWrap.fontWeight, + } + + const lastLine = last(lines) + if ( + textWrap.firstLineOffset > 0 && + isFirstTextWrapLine && + lastLine + ) { + // if the current line is offsetted, add it to the previous line + lastLine.fragments.push(fragment) + } else { + // else, push a new line + lines.push({ + textWrap, + fragments: [fragment], + yOffset: yOffset + i * textWrap.singleLineHeight, + }) + } + } + } + + return lines + } + render( x: number, y: number, @@ -148,13 +187,36 @@ export class TextWrapGroup { id, }: { textProps?: React.SVGProps; id?: string } = {} ): React.ReactElement { + // Alternatively, we could render each TextWrap one by one. That would + // give us a good but not pixel-perfect result since the text + // measurements are not 100% accurate. To avoid inconsistent spacing + // between text wraps, we split the text into lines and render + // the different styles as tspans within the same text element. return ( - - {this.placedTextWraps.map(({ textWrap, yOffset }, index) => ( - - {textWrap.render(x, y + yOffset, { textProps, id })} - - ))} + + {this.lines.map((line) => { + const [textX, textY] = + line.textWrap.getPositionForSvgRendering(x, y) + return ( + + {line.fragments.map((fragment, index) => ( + + {index === 0 ? "" : " "} + {fragment.text} + + ))} + + ) + })} ) } From df05912de76ed7f8de3c64bcbeeae5de5d1414b4 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 14:08:25 +0100 Subject: [PATCH 68/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20prevent=20x-label?= =?UTF-8?q?=20and=20entity=20labels=20from=20overlapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/lineLegend/LineLegend.tsx | 5 ++++- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 81e3471dbd7..4f51bc210ae 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -373,7 +373,10 @@ export class LineLegend extends React.Component { const labelFragments = excludeUndefined([ { text: label.label, fontWeight }, label.formattedValue - ? { text: label.formattedValue } + ? { + text: label.formattedValue, + preferLineBreakOverWrapping: true, + } : undefined, ]) const textWrap = new TextWrapGroup({ diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 654d24cf65f..91ef03a7e03 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -970,7 +970,7 @@ export class SlopeChart labelSeries={this.lineLegendSeriesRight} yAxis={this.yAxis} x={this.xRange[1] + LINE_LEGEND_PADDING} - yRange={[this.bounds.top, this.bounds.bottom]} + yRange={[this.bounds.top, this.bounds.bottom - BOTTOM_PADDING]} maxWidth={this.maxLineLegendWidth} xAnchor="start" verticalAlign={VerticalAlign.top} @@ -1006,7 +1006,7 @@ export class SlopeChart labelSeries={this.lineLegendSeriesLeft} yAxis={this.yAxis} x={this.xRange[0] - LINE_LEGEND_PADDING} - yRange={[this.bounds.top, this.bounds.bottom]} + yRange={[this.bounds.top, this.bounds.bottom - BOTTOM_PADDING]} maxWidth={this.maxLineLegendWidth} xAnchor="end" verticalAlign={VerticalAlign.top} From 98510d34560eb2cf08994b64e48f415220feeebf Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 15:40:57 +0100 Subject: [PATCH 69/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20only=20show=20zero?= =?UTF-8?q?=20label=20once=20if=20all=20slopes=20start=20from=20zero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 2 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 4f51bc210ae..a200fcafbfe 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -30,7 +30,7 @@ const LEGEND_ITEM_MIN_SPACING = 4 // Horizontal distance from the end of the chart to the start of the marker const MARKER_MARGIN = 4 // Space between the label and the annotation -const ANNOTATION_PADDING = 2 +const ANNOTATION_PADDING = 1 const DEFAULT_CONNECTOR_LINE_WIDTH = 35 const DEFAULT_FONT_WEIGHT = 400 diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 91ef03a7e03..5d4abb40061 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -13,6 +13,7 @@ import { getRelativeMouse, minBy, dyFromAlign, + uniq, } from "@ourworldindata/utils" import { observable, computed, action } from "mobx" import { observer } from "mobx-react" @@ -986,8 +987,18 @@ export class SlopeChart } private renderLineLegendLeft(): React.ReactElement { - // in relative mode, all slopes start from 0% - if (this.manager.isRelativeMode) + const uniqYValues = uniq( + this.lineLegendSeriesLeft.map((series) => series.yValue) + ) + const allSlopesStartFromZero = + uniqYValues.length === 1 && uniqYValues[0] === 0 + + // if all values have a start value of 0, show the 0-label only once + if ( + // in relative mode, all slopes start from 0% + this.manager.isRelativeMode || + allSlopesStartFromZero + ) return ( Date: Mon, 2 Dec 2024 16:00:25 +0100 Subject: [PATCH 70/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20show=20entity=20lab?= =?UTF-8?q?el=20on=20the=20left=20if=20connector=20lines=20are=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 5 ++ .../grapher/src/slopeCharts/SlopeChart.tsx | 49 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index a200fcafbfe..a91c89cb792 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -327,6 +327,11 @@ export class LineLegend extends React.Component { return test.maxLabelWidth + connectorLineWidth + MARKER_MARGIN } + static needsConnectorLines(props: LineLegendProps): boolean { + const test = new LineLegend(props) + return test.needsLines + } + /** * Always adds the width of connector lines, which leads to an incorrect * result if no connector lines are rendered. We sometimes can't use the diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 5d4abb40061..c245fce8665 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -503,6 +503,33 @@ export class SlopeChart return this.innerBounds.width / 4 } + @computed private get lineLegendLeftHasConnectorLines(): boolean { + // can't use this.lineLegendSeriesLeft due to a circular dependency + const lineLegendSeries = this.series.map((series) => { + const { seriesName, color, start } = series + const showSeriesName = false + const value = this.formatColumn.formatValueShort(start.value) + const label = showSeriesName ? seriesName : value + const formattedValue = showSeriesName ? value : undefined + return { + color, + seriesName, + label, + formattedValue, + yValue: start.value, + } + }) + + return LineLegend.needsConnectorLines({ + labelSeries: lineLegendSeries, + yAxis: this.yAxis, + maxWidth: this.maxLineLegendWidth, + connectorLineWidth: this.lineLegendConnectorLinesWidth, + fontSize: this.fontSize, + isStatic: this.manager.isStatic, + }) + } + @computed get lineLegendWidthLeft(): number { if (!this.manager.showLegend) return 0 return LineLegend.width({ @@ -511,6 +538,7 @@ export class SlopeChart maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, fontSize: this.fontSize, + fontWeight: this.showSeriesNamesInLineLegendLeft ? 700 : undefined, isStatic: this.manager.isStatic, }) } @@ -584,16 +612,26 @@ export class SlopeChart return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] } + /** + * If the line legend uses connector lines, then we do show the series + * name to make it clear which slope the value belongs to + */ + @computed private get showSeriesNamesInLineLegendLeft(): boolean { + return this.lineLegendLeftHasConnectorLines + } + @computed get lineLegendSeriesLeft(): LineLabelSeries[] { + const { showSeriesNamesInLineLegendLeft: showSeriesName } = this return this.series.map((series) => { const { seriesName, color, start } = series - const formattedValue = this.formatColumn.formatValueShort( - start.value - ) + const value = this.formatColumn.formatValueShort(start.value) + const label = showSeriesName ? seriesName : value + const formattedValue = showSeriesName ? value : undefined return { color, seriesName, - label: formattedValue, + label, + formattedValue, yValue: start.value, } }) @@ -1023,6 +1061,9 @@ export class SlopeChart verticalAlign={VerticalAlign.top} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} + fontWeight={ + this.showSeriesNamesInLineLegendLeft ? 700 : undefined + } isStatic={this.manager.isStatic} focusedSeriesNames={this.focusedSeriesNames} onMouseLeave={this.onLineLegendMouseLeave} From 49e0bdc18a3632517f72a8e23886d83bd200a2f5 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 16:18:32 +0100 Subject: [PATCH 71/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20prevent=20x-label?= =?UTF-8?q?=20and=20line=20labels=20to=20overlap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 5 ++++ .../grapher/src/slopeCharts/SlopeChart.tsx | 25 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index a91c89cb792..48bc9cfc369 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -332,6 +332,11 @@ export class LineLegend extends React.Component { return test.needsLines } + static fontSize(props: Partial): number { + const test = new LineLegend(props as LineLegendProps) + return test.fontSize + } + /** * Always adds the width of connector lines, which leads to an incorrect * result if no connector lines are rendered. We sometimes can't use the diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index c245fce8665..e624e4d32e8 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -22,7 +22,6 @@ import { BASE_FONT_SIZE, GRAPHER_BACKGROUND_DEFAULT, GRAPHER_DARK_TEXT, - GRAPHER_FONT_SCALE_12, } from "../core/GrapherConstants" import { ScaleType, @@ -503,6 +502,23 @@ export class SlopeChart return this.innerBounds.width / 4 } + @computed get lineLegendFontSize(): number { + return LineLegend.fontSize({ fontSize: this.fontSize }) + } + + @computed get lineLegendYRange(): [number, number] { + const top = this.bounds.top + + const bottom = + this.bounds.bottom - + // leave space for the x-axis labels + BOTTOM_PADDING + + // but allow for a little extra space + this.lineLegendFontSize / 2 + + return [top, bottom] + } + @computed private get lineLegendLeftHasConnectorLines(): boolean { // can't use this.lineLegendSeriesLeft due to a circular dependency const lineLegendSeries = this.series.map((series) => { @@ -1009,7 +1025,7 @@ export class SlopeChart labelSeries={this.lineLegendSeriesRight} yAxis={this.yAxis} x={this.xRange[1] + LINE_LEGEND_PADDING} - yRange={[this.bounds.top, this.bounds.bottom - BOTTOM_PADDING]} + yRange={this.lineLegendYRange} maxWidth={this.maxLineLegendWidth} xAnchor="start" verticalAlign={VerticalAlign.top} @@ -1044,7 +1060,7 @@ export class SlopeChart textAnchor="end" dx={-LINE_LEGEND_PADDING - 4} dy={dyFromAlign(VerticalAlign.middle)} - fontSize={GRAPHER_FONT_SCALE_12 * this.fontSize} + fontSize={this.lineLegendFontSize} > {this.formatColumn.formatValueShort(0)} @@ -1055,7 +1071,7 @@ export class SlopeChart labelSeries={this.lineLegendSeriesLeft} yAxis={this.yAxis} x={this.xRange[0] - LINE_LEGEND_PADDING} - yRange={[this.bounds.top, this.bounds.bottom - BOTTOM_PADDING]} + yRange={this.lineLegendYRange} maxWidth={this.maxLineLegendWidth} xAnchor="end" verticalAlign={VerticalAlign.top} @@ -1269,7 +1285,6 @@ function MarkX({ textAnchor="middle" fill={GRAPHER_DARK_TEXT} fontSize={fontSize} - fontWeight={600} > {label} From cfea88b25def95242b1e97ad2507554c9cc2239a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 16:27:59 +0100 Subject: [PATCH 72/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20always=20show=20ann?= =?UTF-8?q?otation=20in=20the=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index e624e4d32e8..c56ad02b191 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -802,9 +802,7 @@ export class SlopeChart const formatTime = (time: Time) => formatColumn.formatTime(time) const title = series.seriesName - const titleAnnotation = this.useCompactLineLegend - ? series.annotation - : undefined + const titleAnnotation = series.annotation const actualStartTime = series.start.originalTime const actualEndTime = series.end.originalTime From 6707a109cdf69e549e8341b66f8ab4a9360759e9 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 19:24:40 +0100 Subject: [PATCH 73/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20hide=20tick=20marks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index c56ad02b191..53de7e7fe27 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -186,7 +186,7 @@ export class SlopeChart private sidebarMargin = 10 @computed private get innerBounds(): Bounds { - return this.bounds.padRight(this.sidebarWidth + 8) + return this.bounds.padRight(this.sidebarWidth + this.sidebarMargin) } @computed get fontSize() { @@ -480,7 +480,7 @@ export class SlopeChart } @computed private get yAxisWidth(): number { - return this.yAxis.width + 5 // 5px account for the tick marks + return this.yAxis.width } @computed private get xScale(): ScaleLinear { @@ -972,14 +972,13 @@ export class SlopeChart return ( Date: Mon, 2 Dec 2024 20:30:21 +0100 Subject: [PATCH 74/91] =?UTF-8?q?=F0=9F=94=A8=20replace=20slope=20chart=20?= =?UTF-8?q?icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/controls/ChartIcons.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/controls/ChartIcons.tsx b/packages/@ourworldindata/grapher/src/controls/ChartIcons.tsx index 00b89c46017..0d89bfc3abd 100644 --- a/packages/@ourworldindata/grapher/src/controls/ChartIcons.tsx +++ b/packages/@ourworldindata/grapher/src/controls/ChartIcons.tsx @@ -104,7 +104,10 @@ export const chartIcons: Record = { strokeLinejoin="round" strokeWidth="1.6" > - + + + + ), From 175f15cb19890990dc79303317757a2ed1a2ad64 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 20:40:08 +0100 Subject: [PATCH 75/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20use=20short=20name?= =?UTF-8?q?=20of=20entity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 53de7e7fe27..e2e82678044 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -49,6 +49,7 @@ import { autoDetectSeriesStrategy, autoDetectYColumnSlugs, getDefaultFailMessage, + getShortNameForEntity, makeSelectionArray, } from "../chart/ChartUtils" import { AxisConfig } from "../axis/AxisConfig" @@ -281,9 +282,11 @@ export class SlopeChart const { canSelectMultipleEntities = false } = this.manager const { availableEntityNames } = this.transformedTable + const displayEntityName = + getShortNameForEntity(entityName) ?? entityName const columnName = column.nonEmptyDisplayName const seriesName = getSeriesName({ - entityName, + entityName: displayEntityName, columnName, seriesStrategy, availableEntityNames, From ce3e37a80b22508160c0736954b31213ba2c00d4 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 10:33:59 +0100 Subject: [PATCH 76/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20refactor=20&=20s?= =?UTF-8?q?mall=20adjustments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/TextWrap/TextWrapGroup.test.ts | 23 +++++++- .../components/src/TextWrap/TextWrapGroup.tsx | 27 ++++++---- .../grapher/src/lineLegend/LineLegend.tsx | 5 +- .../grapher/src/slopeCharts/SlopeChart.tsx | 53 ++++++++++--------- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts index 5162484c35f..3b10adaf3ed 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.test.ts @@ -43,7 +43,7 @@ it("should place the second segment in a new line if preferred", () => { const textWrapGroup = new TextWrapGroup({ fragments: [ { text: TEXT }, - { text: "30 million", preferLineBreakOverWrapping: true }, + { text: "30 million", newLine: "avoid-wrap" }, ], maxWidth, fontSize: FONT_SIZE, @@ -65,7 +65,7 @@ it("should place the second segment in the same line if possible", () => { const textWrapGroup = new TextWrapGroup({ fragments: [ { text: TEXT }, - { text: "30 million", preferLineBreakOverWrapping: true }, + { text: "30 million", newLine: "avoid-wrap" }, ], maxWidth, fontSize: FONT_SIZE, @@ -82,6 +82,25 @@ it("should place the second segment in the same line if possible", () => { ) }) +it("should place the second segment in the same line if specified", () => { + const maxWidth = 1000 + const textWrapGroup = new TextWrapGroup({ + fragments: [{ text: TEXT }, { text: "30 million", newLine: "always" }], + maxWidth, + fontSize: FONT_SIZE, + }) + + // since the max width is large, "30 million" fits into the same line + // as the text of the first fragmemt + expect(textWrapGroup.height).toBeGreaterThan( + new TextWrap({ + text: TEXT, + maxWidth, + fontSize: FONT_SIZE, + }).height + ) +}) + it("should use all available space when one fragment exceeds the given max width", () => { const maxWidth = 150 const textWrap = new TextWrap({ diff --git a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx index 891a607b572..5515573dd7e 100644 --- a/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx +++ b/packages/@ourworldindata/components/src/TextWrap/TextWrapGroup.tsx @@ -7,7 +7,9 @@ import { Bounds, last, max } from "@ourworldindata/utils" interface TextWrapFragment { text: string fontWeight?: number - preferLineBreakOverWrapping?: boolean + // "always" places the fragment in a new line in all cases + // "avoid-wrap" places the fragment in a new line only if the fragment would wrap otherwise + newLine?: "always" | "avoid-wrap" } interface PlacedTextWrap { @@ -92,10 +94,18 @@ export class TextWrapGroup { const { textWrap: lastTextWrap, yOffset: lastYOffset } = textWraps[i - 1] - let textWrap = this.makeTextWrapForFragment( - fragment, - lastTextWrap.lastLineWidth + whitespaceWidth - ) + // x-offset for the new text wrap + const offset = lastTextWrap.lastLineWidth + whitespaceWidth + + // place the text wrap in a new line + if (fragment.newLine === "always" || offset > this.maxWidth) { + const textWrap = this.makeTextWrapForFragment(fragment) + const yOffset = lastYOffset + lastTextWrap.height + textWraps.push({ textWrap, yOffset }) + continue + } + + let textWrap = this.makeTextWrapForFragment(fragment, offset) let yOffset = lastYOffset if (textWrap.firstLineOffset === 0) { @@ -107,10 +117,7 @@ export class TextWrapGroup { // some fragments are preferred to break into a new line // instead of being wrapped - if ( - fragment.preferLineBreakOverWrapping && - textWrap.lineCount > 1 - ) { + if (fragment.newLine === "avoid-wrap" && textWrap.lineCount > 1) { textWrap = this.makeTextWrapForFragment(fragment) yOffset += lastTextWrap.singleLineHeight } @@ -141,7 +148,7 @@ export class TextWrapGroup { } @computed get lines(): { - fragments: Omit[] + fragments: Omit[] textWrap: TextWrap yOffset: number }[] { diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 48bc9cfc369..4c602b4ee64 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -40,6 +40,7 @@ export interface LineLabelSeries extends ChartSeries { yValue: number annotation?: string formattedValue?: string + placeFormattedValueInNewLine?: boolean yRange?: [number, number] } @@ -385,7 +386,9 @@ export class LineLegend extends React.Component { label.formattedValue ? { text: label.formattedValue, - preferLineBreakOverWrapping: true, + newLine: label.placeFormattedValueInNewLine + ? "always" + : "avoid-wrap", } : undefined, ]) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index e2e82678044..63f60e2ed30 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -502,7 +502,7 @@ export class SlopeChart } @computed get maxLineLegendWidth(): number { - return this.innerBounds.width / 4 + return 0.25 * this.innerBounds.width } @computed get lineLegendFontSize(): number { @@ -526,15 +526,13 @@ export class SlopeChart // can't use this.lineLegendSeriesLeft due to a circular dependency const lineLegendSeries = this.series.map((series) => { const { seriesName, color, start } = series - const showSeriesName = false - const value = this.formatColumn.formatValueShort(start.value) - const label = showSeriesName ? seriesName : value - const formattedValue = showSeriesName ? value : undefined + const formattedValue = this.formatColumn.formatValueShort( + start.value + ) return { color, seriesName, - label, - formattedValue, + label: formattedValue, yValue: start.value, } }) @@ -579,10 +577,28 @@ export class SlopeChart this.lineLegendWidthLeft + LINE_LEGEND_PADDING const lineLegendWidthRight = this.lineLegendWidthRight + LINE_LEGEND_PADDING + const chartAreaWidth = this.innerBounds.width + + // start and end value when the slopes are as wide as possible + const minStartX = + this.innerBounds.x + this.yAxisWidth + lineLegendWidthLeft + const maxEndX = this.innerBounds.right - lineLegendWidthRight + + // use all available space if the chart is narrow + if (this.manager.isSemiNarrow) { + return [minStartX, maxEndX] + } + + const offset = 0.25 + let startX = this.innerBounds.x + offset * chartAreaWidth + let endX = this.innerBounds.right - offset * chartAreaWidth + + // make sure the start and end values are within the bounds + startX = Math.max(startX, minStartX) + endX = Math.min(endX, maxEndX) // pick a reasonable max width based on an ideal aspect ratio - const idealAspectRatio = 0.6 - const chartAreaWidth = this.innerBounds.width + const idealAspectRatio = 0.9 const availableWidth = chartAreaWidth - this.yAxisWidth - @@ -591,23 +607,6 @@ export class SlopeChart const idealWidth = idealAspectRatio * this.bounds.height const maxSlopeWidth = Math.min(idealWidth, availableWidth) - let startX: number, endX: number - if (this.manager.isSemiNarrow) { - startX = this.bounds.x + this.yAxisWidth + 4 + lineLegendWidthLeft - endX = this.bounds.x + chartAreaWidth - lineLegendWidthRight - } else { - startX = - this.bounds.x + - Math.max( - 0.25 * chartAreaWidth, - this.yAxisWidth + 4 + lineLegendWidthLeft - ) - endX = - this.bounds.x + - chartAreaWidth - - Math.max(0.25 * chartAreaWidth, lineLegendWidthRight) - } - const currentSlopeWidth = endX - startX if (currentSlopeWidth > maxSlopeWidth) { const padding = currentSlopeWidth - maxSlopeWidth @@ -651,6 +650,7 @@ export class SlopeChart seriesName, label, formattedValue, + valueInNewLine: this.useCompactLineLegend, yValue: start.value, } }) @@ -666,6 +666,7 @@ export class SlopeChart label: seriesName, annotation: this.useCompactLineLegend ? undefined : annotation, formattedValue, + valueInNewLine: this.useCompactLineLegend, yValue: end.value, } }) From 8bac63c5e6d923ec3ed58941f5d121b24daac893 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 11:05:50 +0100 Subject: [PATCH 77/91] =?UTF-8?q?=F0=9F=90=9B=20fix=20type=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineLegend/LineLegend.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 4c602b4ee64..5e70c929e1d 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -381,17 +381,16 @@ export class LineLegend extends React.Component { const maxAnnotationWidth = Math.min(maxTextWidth, 150) return this.props.labelSeries.map((label) => { - const labelFragments = excludeUndefined([ - { text: label.label, fontWeight }, - label.formattedValue - ? { - text: label.formattedValue, - newLine: label.placeFormattedValueInNewLine - ? "always" - : "avoid-wrap", - } - : undefined, - ]) + const mainLabel = { text: label.label, fontWeight } + const valueLabel = label.formattedValue + ? { + text: label.formattedValue, + newLine: (label.placeFormattedValueInNewLine + ? "always" + : "avoid-wrap") as "always" | "avoid-wrap", + } + : undefined + const labelFragments = excludeUndefined([mainLabel, valueLabel]) const textWrap = new TextWrapGroup({ fragments: labelFragments, maxWidth: maxTextWidth, From d064416cb6893b2ba3265f70e0852627b5ec7901 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 15:09:32 +0100 Subject: [PATCH 78/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20make=20layout=20?= =?UTF-8?q?less=20jumpy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.tsx | 2 +- .../grapher/src/lineLegend/LineLegend.tsx | 20 ++++++++----------- .../grapher/src/slopeCharts/SlopeChart.tsx | 3 +++ .../src/stackedCharts/StackedAreaChart.tsx | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index b51fbbc047f..710b04f0397 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -881,7 +881,7 @@ export class LineChart // only pass props that are required to calculate // the width to avoid circular dependencies - return LineLegend.incorrectWidth({ + return LineLegend.width({ labelSeries: this.lineLegendSeries, maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 5e70c929e1d..33aa7a9ba0a 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -322,10 +322,16 @@ export interface LineLegendProps { @observer export class LineLegend extends React.Component { + /** + * Larger than the actual width since the width of the connector lines + * is always added, even if they're not rendered. + * + * This is partly due to a circular dependency (in line and stacked area + * charts), partly to avoid jumpy layout changes (slope charts). + */ static width(props: LineLegendProps): number { const test = new LineLegend(props) - const connectorLineWidth = test.needsLines ? test.connectorLineWidth : 0 - return test.maxLabelWidth + connectorLineWidth + MARKER_MARGIN + return test.maxLabelWidth + test.connectorLineWidth + MARKER_MARGIN } static needsConnectorLines(props: LineLegendProps): boolean { @@ -338,16 +344,6 @@ export class LineLegend extends React.Component { return test.fontSize } - /** - * Always adds the width of connector lines, which leads to an incorrect - * result if no connector lines are rendered. We sometimes can't use the - * correct width above due to circular dependencies. - */ - static incorrectWidth(props: LineLegendProps): number { - const test = new LineLegend(props) - return test.maxLabelWidth + test.connectorLineWidth + MARKER_MARGIN - } - @computed private get fontSize(): number { return Math.max( GRAPHER_FONT_SCALE_12 * (this.props.fontSize ?? BASE_FONT_SIZE), diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 63f60e2ed30..737c57365d1 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -565,9 +565,12 @@ export class SlopeChart return LineLegend.width({ labelSeries: this.lineLegendSeriesRight, yAxis: this.yAxis, + yRange: this.lineLegendYRange, + verticalAlign: VerticalAlign.top, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, fontSize: this.fontSize, + fontWeight: 700, isStatic: this.manager.isStatic, }) } diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 4b8daa16c31..eaf919a43d1 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -317,7 +317,7 @@ export class StackedAreaChart extends AbstractStackedChart { // only pass props that are required to calculate // the width to avoid circular dependencies - return LineLegend.incorrectWidth({ + return LineLegend.width({ labelSeries: this.lineLegendSeries, maxWidth: this.maxLineLegendWidth, fontSize: this.fontSize, From 84bddf98076c1e9317752582cc03aabb53375c59 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 15:54:53 +0100 Subject: [PATCH 79/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20avoid=20visible?= =?UTF-8?q?=20overlap=20when=20the=20slope=20is=20transparent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/slopeCharts/SlopeChart.tsx | 113 +++++++++--------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 737c57365d1..ca77cea4801 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -1141,7 +1141,7 @@ function Slope({ series, color, mode = RenderMode.default, - dotRadius = 3.5, + dotRadius = 2.5, strokeWidth = 2, outlineWidth = 0.5, outlineStroke = "#fff", @@ -1150,6 +1150,8 @@ function Slope({ }: SlopeProps) { const { seriesName, startPoint, endPoint } = series + const showOutline = mode === RenderMode.default || mode === RenderMode.focus + const opacity = { [RenderMode.default]: 1, [RenderMode.focus]: 1, @@ -1164,72 +1166,63 @@ function Slope({ onMouseOver={() => onMouseOver?.(series)} onMouseLeave={() => onMouseLeave?.()} > - + )} + - - ) } -interface HaloLineProps extends SVGProps { +/** + * Line with two dots at the ends, drawn as a single path element. + */ +function LineWithDots({ + startPoint, + endPoint, + radius, + color, + lineWidth = 2, + opacity = 1, +}: { startPoint: PointVector endPoint: PointVector - strokeWidth?: number - outlineWidth?: number - outlineStroke?: string -} + radius?: number + color: string + lineWidth?: number + opacity?: number +}): React.ReactElement { + const startDotPath = makeCirclePath(startPoint.x, startPoint.y, radius) + const endDotPath = makeCirclePath(endPoint.x, endPoint.y, radius) + + const linePath = makeLinePath( + startPoint.x, + startPoint.y, + endPoint.x, + endPoint.y + ) -function HaloLine(props: HaloLineProps): React.ReactElement { - const { - startPoint, - endPoint, - outlineWidth = 0.5, - outlineStroke = "#fff", - ...styleProps - } = props return ( - <> - - - + ) } @@ -1295,3 +1288,13 @@ function MarkX({ ) } + +const makeCirclePath = (centerX: number, centerY: number, radius: number) => { + const topX = centerX + const topY = centerY - radius + return `M ${topX},${topY} A ${radius},${radius} 0 1,1 ${topX - 0.0001},${topY}` +} + +const makeLinePath = (x1: number, y1: number, x2: number, y2: number) => { + return `M ${x1},${y1} L ${x2},${y2}` +} From daf24044400429899d35d976c49c1c4d34a665a5 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 15:57:37 +0100 Subject: [PATCH 80/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20rephrase=20fail?= =?UTF-8?q?=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/chart/ChartUtils.tsx | 6 +++--- .../grapher/src/slopeCharts/SlopeChart.tsx | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 501d646456d..249920b44f0 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -26,11 +26,11 @@ export const autoDetectYColumnSlugs = (manager: ChartManager): string[] => { } export const getDefaultFailMessage = (manager: ChartManager): string => { - if (manager.table.rootTable.isBlank) return `No table loaded yet.` + if (manager.table.rootTable.isBlank) return `No table loaded yet` if (manager.table.rootTable.entityNameColumn.isMissing) - return `Table is missing an EntityName column.` + return `Table is missing an EntityName column` if (manager.table.rootTable.timeColumn.isMissing) - return `Table is missing a Time column.` + return `Table is missing a Time column` const yColumnSlugs = autoDetectYColumnSlugs(manager) if (!yColumnSlugs.length) return "Missing Y axis column" const selection = makeSelectionArray(manager.selection) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index ca77cea4801..4b9fb1dd47b 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -776,17 +776,13 @@ export class SlopeChart @computed get failMessage(): string { const message = getDefaultFailMessage(this.manager) if (message) return message - else if (this.startTime === this.endTime) - return "No data to display for the selected time period" + else if (this.startTime === this.endTime) return "Single date selected" return "" } @computed get helpMessage(): string | undefined { - if ( - this.failMessage === - "No data to display for the selected time period" - ) - return "Try dragging the time slider to display data." + if (this.failMessage === "Single date selected") + return "Please select two dates to display data." return undefined } @@ -1200,7 +1196,7 @@ function LineWithDots({ }: { startPoint: PointVector endPoint: PointVector - radius?: number + radius: number color: string lineWidth?: number opacity?: number From a19a5141efeca0227cffb079c412ed7d692fc829 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 16:06:22 +0100 Subject: [PATCH 81/91] =?UTF-8?q?=E2=9C=A8=20(slope)=20slow=20down=20anima?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 4b9fb1dd47b..b0e019ac372 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -686,6 +686,7 @@ export class SlopeChart .attr("stroke-dasharray", "100%") .attr("stroke-dashoffset", "100%") .transition() + .duration(600) .attr("stroke-dashoffset", "0%") } From 068a503124d6d92571acfeae41afdb736e919488 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Tue, 3 Dec 2024 16:26:38 +0100 Subject: [PATCH 82/91] =?UTF-8?q?=F0=9F=94=A8=20(slope)=20update=20fail=20?= =?UTF-8?q?message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index b0e019ac372..1fac048d8d9 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -777,13 +777,14 @@ export class SlopeChart @computed get failMessage(): string { const message = getDefaultFailMessage(this.manager) if (message) return message - else if (this.startTime === this.endTime) return "Single date selected" + else if (this.startTime === this.endTime) + return "Two time points needed for comparison" return "" } @computed get helpMessage(): string | undefined { - if (this.failMessage === "Single date selected") - return "Please select two dates to display data." + if (this.failMessage === "Two time points needed for compariso") + return "Click or drag the timeline to select two different points in time" return undefined } From c88906113abb516e1ef1db4aa1ae17d4e059180b Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 4 Dec 2024 09:11:41 +0100 Subject: [PATCH 83/91] =?UTF-8?q?=F0=9F=90=9B=20(slope)=20hide=20the=20No?= =?UTF-8?q?=20Data=20section=20for=20svg=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 1fac048d8d9..e5d6ce8b54d 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -427,6 +427,9 @@ export class SlopeChart // nothing to show if there are no series with missing data if (this.noDataSeries.length === 0) return false + // the No Data section is HTML and won't show up in the SVG export + if (this.manager.isStatic) return false + // we usually don't show the no data section if columns are plotted // (since columns don't appear in the entity selector there is no need // to explain that a column is missing – it just adds noise). but if From 93fa1cd20dcbddefb3797a16a723e7b749944801 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 20:24:40 +0100 Subject: [PATCH 84/91] =?UTF-8?q?=F0=9F=8E=89=20(slope)=20support=20facett?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartManager.ts | 3 +- .../grapher/src/controls/SettingsMenu.tsx | 1 + .../grapher/src/facetChart/FacetChart.tsx | 22 ++- .../grapher/src/slopeCharts/SlopeChart.tsx | 147 +++++++++++++----- 4 files changed, 127 insertions(+), 46 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index d00e2912578..8b24bdd70a3 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -68,6 +68,7 @@ export interface ChartManager { hidePoints?: boolean // for line options startHandleTimeBound?: TimeBound // for relative-to-first-year line chart + hideNoDataSection?: boolean // for slope charts // we need endTime so DiscreteBarCharts and StackedDiscreteBarCharts can // know what date the timeline is set to. and let's pass startTime in, too. @@ -78,7 +79,7 @@ export interface ChartManager { seriesStrategy?: SeriesStrategy sortConfig?: SortConfig - showNoDataArea?: boolean + showNoDataArea?: boolean // No data area in Marimekko charts externalLegendHoverBin?: ColorScaleBin | undefined disableIntroAnimation?: boolean diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index fb7f4351ce7..961863e4d64 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -195,6 +195,7 @@ export class SettingsMenu extends React.Component<{ StackedBar, StackedDiscreteBar, LineChart, + SlopeChart, ].includes(this.chartType as any) const hasProjection = filledDimensions.some( diff --git a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx index ee2870aac03..6ae1555fb84 100644 --- a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx +++ b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx @@ -295,7 +295,9 @@ export class FacetChart return series.map((series, index) => { const { bounds } = gridBoundsArr[index] const showLegend = !this.hideFacetLegends + const hidePoints = true + const hideNoDataSection = true // NOTE: The order of overrides is important! // We need to preserve most config coming in. @@ -319,6 +321,7 @@ export class FacetChart endTime, missingDataStrategy, backgroundColor, + hideNoDataSection, ...series.manager, xAxisConfig: { ...globalXAxisConfig, @@ -373,6 +376,13 @@ export class FacetChart ) } + @computed private get isYAxisHidden(): boolean { + return ( + this.chartTypeName === GRAPHER_CHART_TYPES.SlopeChart && + this.facetCount >= SHARED_X_AXIS_MIN_FACET_COUNT + ) + } + // Only made public for testing @computed get placedSeries(): PlacedFacetSeries[] { const { intermediateChartInstances } = this @@ -495,11 +505,10 @@ export class FacetChart ...axes.x.config, }, yAxisConfig: { - hideAxis: shouldHideFacetAxis( - yAxis, - cellEdges, - sharedAxesSizes - ), + hideAxis: + this.isYAxisHidden || + shouldHideFacetAxis(yAxis, cellEdges, sharedAxesSizes), + hideGridlines: this.isYAxisHidden, ...series.manager.yAxisConfig, ...axes.y.config, }, @@ -756,7 +765,8 @@ export class FacetChart ) if (this.facetStrategy === FacetStrategy.metric && newBins.length <= 1) return [] - return newBins + const sortedBins = sortBy(newBins, (bin) => bin.label) + return sortedBins } @observable.ref private legendHoverBin: ColorScaleBin | undefined = diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index e5d6ce8b54d..5e8858841df 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -34,6 +34,7 @@ import { EntityName, RenderMode, VerticalAlign, + FacetStrategy, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" @@ -75,6 +76,8 @@ import { getColorKey, getSeriesName, } from "../lineCharts/LineChartHelpers" +import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends" +import { CategoricalBin } from "../color/ColorScaleBin" type SVGMouseOrTouchEvent = | React.MouseEvent @@ -83,6 +86,7 @@ type SVGMouseOrTouchEvent = export interface SlopeChartManager extends ChartManager { canSelectMultipleEntities?: boolean // used to pick an appropriate series name hasTimeline?: boolean // used to filter the table for the entity selector + hideNoDataSection?: boolean } const TOP_PADDING = 6 // leave room for overflowing dots @@ -219,7 +223,7 @@ export class SlopeChart } @computed private get isFocusModeActive(): boolean { - return this.hoveredSeriesName !== undefined + return this.focusedSeriesNames.length > 0 } @computed private get yColumns(): CoreColumn[] { @@ -258,6 +262,17 @@ export class SlopeChart return autoDetectSeriesStrategy(this.manager, true) } + @computed get availableFacetStrategies(): FacetStrategy[] { + const strategies: FacetStrategy[] = [FacetStrategy.none] + + if (this.selectionArray.numSelectedEntities > 1) + strategies.push(FacetStrategy.entity) + + if (this.yColumns.length > 1) strategies.push(FacetStrategy.metric) + + return strategies + } + @computed private get categoricalColorAssigner(): CategoricalColorAssigner { return new CategoricalColorAssigner({ colorScheme: this.colorScheme, @@ -424,6 +439,8 @@ export class SlopeChart } @computed private get showNoDataSection(): boolean { + if (this.manager.hideNoDataSection) return false + // nothing to show if there are no series with missing data if (this.noDataSeries.length === 0) return false @@ -504,6 +521,22 @@ export class SlopeChart : 0 } + @computed get externalLegend(): HorizontalColorLegendManager | undefined { + if (!this.manager.showLegend) { + const categoricalLegendData = this.series.map( + (series, index) => + new CategoricalBin({ + index, + value: series.seriesName, + label: series.seriesName, + color: series.color, + }) + ) + return { categoricalLegendData } + } + return undefined + } + @computed get maxLineLegendWidth(): number { return 0.25 * this.innerBounds.width } @@ -551,7 +584,6 @@ export class SlopeChart } @computed get lineLegendWidthLeft(): number { - if (!this.manager.showLegend) return 0 return LineLegend.width({ labelSeries: this.lineLegendSeriesLeft, yAxis: this.yAxis, @@ -564,7 +596,6 @@ export class SlopeChart } @computed get lineLegendWidthRight(): number { - if (!this.manager.showLegend) return 0 return LineLegend.width({ labelSeries: this.lineLegendSeriesRight, yAxis: this.yAxis, @@ -573,7 +604,7 @@ export class SlopeChart maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, fontSize: this.fontSize, - fontWeight: 700, + fontWeight: this.manager.showLegend ? 700 : undefined, isStatic: this.manager.isStatic, }) } @@ -628,12 +659,28 @@ export class SlopeChart } @computed get useCompactLineLegend(): boolean { - return !!this.manager.isSemiNarrow + return !!this.manager.isSemiNarrow || this.bounds.width < 400 } - // used by LineLegend @computed get focusedSeriesNames(): SeriesName[] { - return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] + const focusedSeriesNames: SeriesName[] = [] + + // hovered series name + if (this.hoveredSeriesName) + focusedSeriesNames.push(this.hoveredSeriesName) + + // hovered legend item in the external facet legend + if (this.manager.externalLegendHoverBin) { + focusedSeriesNames.push( + ...this.series + .map((s) => s.seriesName) + .filter((name) => + this.manager.externalLegendHoverBin?.contains(name) + ) + ) + } + + return focusedSeriesNames } /** @@ -641,7 +688,7 @@ export class SlopeChart * name to make it clear which slope the value belongs to */ @computed private get showSeriesNamesInLineLegendLeft(): boolean { - return this.lineLegendLeftHasConnectorLines + return this.lineLegendLeftHasConnectorLines && !!this.manager.showLegend } @computed get lineLegendSeriesLeft(): LineLabelSeries[] { @@ -665,14 +712,19 @@ export class SlopeChart @computed get lineLegendSeriesRight(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, end, annotation } = series - const formattedValue = this.formatColumn.formatValueShort(end.value) + const value = this.formatColumn.formatValueShort(end.value) + const label = this.manager.showLegend ? seriesName : value + const formattedValue = this.manager.showLegend ? value : undefined return { color, seriesName, - label: seriesName, - annotation: this.useCompactLineLegend ? undefined : annotation, + label, formattedValue, valueInNewLine: this.useCompactLineLegend, + annotation: + this.manager.showLegend && this.useCompactLineLegend + ? undefined + : annotation, yValue: end.value, } }) @@ -782,6 +834,7 @@ export class SlopeChart if (message) return message else if (this.startTime === this.endTime) return "Two time points needed for comparison" + else if (this.series.length === 0) return "No matching data" return "" } @@ -957,7 +1010,7 @@ export class SlopeChart const [focusedSeries, backgroundSeries] = partition( this.placedSeries, - (series) => series.seriesName === this.hoveredSeriesName + (series) => this.focusedSeriesNames.includes(series.seriesName) ) return ( @@ -972,23 +1025,30 @@ export class SlopeChart ) } - private renderChartArea() { - const { bounds, xDomain, yRange, startX, endX } = this + private renderYAxis() { + return ( + <> + {!this.yAxis.hideGridlines && ( + + )} + {!this.yAxis.hideAxis && ( + + )} + + ) + } + + private renderXAxis() { + const { xDomain, yRange, startX, endX } = this const [bottom, top] = yRange return ( - - - + <> + + ) + } + + private renderChartArea() { + return ( + + {this.renderYAxis()} + {this.renderXAxis()} {this.renderSlopes()} @@ -1036,7 +1105,7 @@ export class SlopeChart verticalAlign={VerticalAlign.top} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} - fontWeight={700} + fontWeight={this.manager.showLegend ? 700 : undefined} isStatic={this.manager.isStatic} focusedSeriesNames={this.focusedSeriesNames} onMouseLeave={this.onLineLegendMouseLeave} @@ -1094,8 +1163,6 @@ export class SlopeChart } private renderLineLegends(): React.ReactElement | void { - if (!this.manager.showLegend) return - return ( <> {this.renderLineLegendLeft()} @@ -1107,12 +1174,15 @@ export class SlopeChart render() { if (this.failMessage) return ( - + <> + {this.renderYAxis()} + + ) return ( @@ -1230,10 +1300,9 @@ function LineWithDots({ interface GridLinesProps { bounds: Bounds yAxis: VerticalAxis - endX: number } -function GridLines({ bounds, yAxis, endX }: GridLinesProps) { +function GridLines({ bounds, yAxis }: GridLinesProps) { return ( {yAxis.tickLabels.map((tick) => { @@ -1249,7 +1318,7 @@ function GridLines({ bounds, yAxis, endX }: GridLinesProps) { Date: Fri, 22 Nov 2024 16:57:55 +0100 Subject: [PATCH 85/91] =?UTF-8?q?=F0=9F=8E=89=20(admin)=20add=20slope=20ch?= =?UTF-8?q?art=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/EditorBasicTab.tsx | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/adminSiteClient/EditorBasicTab.tsx b/adminSiteClient/EditorBasicTab.tsx index fa60a83d82d..1f48d420f64 100644 --- a/adminSiteClient/EditorBasicTab.tsx +++ b/adminSiteClient/EditorBasicTab.tsx @@ -13,6 +13,7 @@ import { StackMode, ALL_GRAPHER_CHART_TYPES, GrapherChartType, + GRAPHER_CHART_TYPES, } from "@ourworldindata/types" import { DimensionSlot, @@ -367,6 +368,10 @@ export class EditorBasicTab< ? [] : [value as GrapherChartType] + if (grapher.isLineChart) { + this.addSlopeChart() + } + if (grapher.isMarimekko) { grapher.hideRelativeToggle = false grapher.stackMode = StackMode.relative @@ -414,6 +419,32 @@ export class EditorBasicTab< ] } + private addSlopeChart(): void { + const { grapher } = this.props.editor + if (grapher.hasSlopeChart) return + grapher.chartTypes = [ + ...grapher.chartTypes, + GRAPHER_CHART_TYPES.SlopeChart, + ] + } + + private removeSlopeChart(): void { + const { grapher } = this.props.editor + grapher.chartTypes = grapher.chartTypes.filter( + (type) => type !== GRAPHER_CHART_TYPES.SlopeChart + ) + } + + @action.bound toggleSecondarySlopeChart( + shouldHaveSlopeChart: boolean + ): void { + if (shouldHaveSlopeChart) { + this.addSlopeChart() + } else { + this.removeSlopeChart() + } + } + render() { const { editor } = this.props const { grapher } = editor @@ -438,6 +469,13 @@ export class EditorBasicTab< (grapher.hasMapTab = shouldHaveMapTab) } /> + {grapher.isLineChart && ( + + )} {!isIndicatorChart && ( From c59b9a7663cfc970b4c7ea2adadc3b145b29255b Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 22 Nov 2024 16:58:22 +0100 Subject: [PATCH 86/91] =?UTF-8?q?=F0=9F=8E=89=20enable=20line/slope=20char?= =?UTF-8?q?t=20switching=20in=20explorers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/explorer/src/GrapherGrammar.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/explorer/src/GrapherGrammar.ts b/packages/@ourworldindata/explorer/src/GrapherGrammar.ts index 78d6a82344a..9c12109ffd0 100644 --- a/packages/@ourworldindata/explorer/src/GrapherGrammar.ts +++ b/packages/@ourworldindata/explorer/src/GrapherGrammar.ts @@ -3,6 +3,7 @@ import { ColorSchemeName, FacetAxisDomain, FacetStrategy, + GRAPHER_CHART_TYPES, GRAPHER_TAB_OPTIONS, MissingDataStrategy, StackMode, @@ -66,10 +67,11 @@ export const GrapherGrammar: Grammar = { description: `The type of chart to show such as LineChart or ScatterPlot. If set to None, then the chart tab is hidden.`, terminalOptions: toTerminalOptions([ ...ALL_GRAPHER_CHART_TYPES, + `${GRAPHER_CHART_TYPES.LineChart} ${GRAPHER_CHART_TYPES.SlopeChart}`, "None", ]), toGrapherObject: (value) => ({ - chartTypes: value === "None" ? [] : [value], + chartTypes: value === "None" ? [] : value.split(" "), }), }, grapherId: { From 79865dd142af2737430db33f6b641c86ac284dc5 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 2 Dec 2024 18:06:11 +0100 Subject: [PATCH 87/91] =?UTF-8?q?=F0=9F=9A=A7=20(for=20testing)=20fix=20Mu?= =?UTF-8?q?ltiembedder=20on=20staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...63041212-EnableSlopeTabForAllLineCharts.ts | 18 ++++++ site/multiembedder/MultiEmbedder.tsx | 64 ++++++++++--------- 2 files changed, 52 insertions(+), 30 deletions(-) create mode 100644 db/migration/1733163041212-EnableSlopeTabForAllLineCharts.ts diff --git a/db/migration/1733163041212-EnableSlopeTabForAllLineCharts.ts b/db/migration/1733163041212-EnableSlopeTabForAllLineCharts.ts new file mode 100644 index 00000000000..77599659203 --- /dev/null +++ b/db/migration/1733163041212-EnableSlopeTabForAllLineCharts.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class EnableSlopeTabForAllLineCharts1733163041212 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update chart_configs cc + join charts c on cc.id = c.configId + set + patch = JSON_SET(cc.patch, '$.chartTypes', JSON_ARRAY('LineChart', 'SlopeChart')), + full = JSON_SET(cc.full, '$.chartTypes', JSON_ARRAY('LineChart', 'SlopeChart')) + where cc.chartType = 'LineChart' + `) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/site/multiembedder/MultiEmbedder.tsx b/site/multiembedder/MultiEmbedder.tsx index b7b84b59516..3089954f249 100644 --- a/site/multiembedder/MultiEmbedder.tsx +++ b/site/multiembedder/MultiEmbedder.tsx @@ -19,6 +19,7 @@ import { merge, MultiDimDataPageConfig, extractMultiDimChoicesFromQueryStr, + deserializeJSONFromHTML, } from "@ourworldindata/utils" import { action } from "mobx" import React from "react" @@ -161,8 +162,10 @@ class MultiEmbedder { dataApiUrl: DATA_API_URL, } + const html = await fetchText(fullUrl) + if (isExplorer) { - const html = await fetchText(fullUrl) + // const html = await fetchText(fullUrl) const props: ExplorerProps = await buildExplorerProps( html, queryStr, @@ -173,35 +176,36 @@ class MultiEmbedder { ReactDOM.render(, figure) } else { figure.classList.remove(GRAPHER_PREVIEW_CLASS) - const url = new URL(fullUrl) - const slug = url.pathname.split("/").pop() - let configUrl - if (isMultiDim) { - const mdimConfigUrl = `${MULTI_DIM_DYNAMIC_CONFIG_URL}/${slug}.json` - const mdimJsonConfig = await fetch(mdimConfigUrl).then((res) => - res.json() - ) - const mdimConfig = - MultiDimDataPageConfig.fromObject(mdimJsonConfig) - const dimensions = extractMultiDimChoicesFromQueryStr( - url.search, - mdimConfig - ) - const view = mdimConfig.findViewByDimensions(dimensions) - if (!view) { - throw new Error( - `No view found for dimensions ${JSON.stringify( - dimensions - )}` - ) - } - configUrl = `${GRAPHER_DYNAMIC_CONFIG_URL}/by-uuid/${view.fullConfigId}.config.json` - } else { - configUrl = `${GRAPHER_DYNAMIC_CONFIG_URL}/${slug}.config.json` - } - const grapherPageConfig = await fetch(configUrl).then((res) => - res.json() - ) + const grapherPageConfig = deserializeJSONFromHTML(html) + // const url = new URL(fullUrl) + // const slug = url.pathname.split("/").pop() + // let configUrl + // if (isMultiDim) { + // const mdimConfigUrl = `${MULTI_DIM_DYNAMIC_CONFIG_URL}/${slug}.json` + // const mdimJsonConfig = await fetch(mdimConfigUrl).then((res) => + // res.json() + // ) + // const mdimConfig = + // MultiDimDataPageConfig.fromObject(mdimJsonConfig) + // const dimensions = extractMultiDimChoicesFromQueryStr( + // url.search, + // mdimConfig + // ) + // const view = mdimConfig.findViewByDimensions(dimensions) + // if (!view) { + // throw new Error( + // `No view found for dimensions ${JSON.stringify( + // dimensions + // )}` + // ) + // } + // configUrl = `${GRAPHER_DYNAMIC_CONFIG_URL}/by-uuid/${view.fullConfigId}.config.json` + // } else { + // configUrl = `${GRAPHER_DYNAMIC_CONFIG_URL}/${slug}.config.json` + // } + // const grapherPageConfig = await fetch(configUrl).then((res) => + // res.json() + // ) const figureConfigAttr = figure.getAttribute( GRAPHER_EMBEDDED_FIGURE_CONFIG_ATTR From 420d4e39a60eeefdcfece61064b05c4b6d4e3c38 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 4 Dec 2024 16:20:10 +0100 Subject: [PATCH 88/91] =?UTF-8?q?=F0=9F=8E=89=20(line+slope)=20support=20f?= =?UTF-8?q?ocus=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartInterface.ts | 6 + .../grapher/src/chart/ChartManager.ts | 1 + .../grapher/src/core/Grapher.tsx | 3 + .../grapher/src/lineCharts/LineChart.tsx | 569 +++++++++--------- .../src/lineCharts/LineChartConstants.ts | 16 +- .../grapher/src/lineLegend/LineLegend.tsx | 159 ++--- .../grapher/src/slopeCharts/SlopeChart.tsx | 193 ++++-- .../src/slopeCharts/SlopeChartConstants.ts | 6 + .../src/stackedCharts/StackedAreaChart.tsx | 25 +- .../types/src/grapherTypes/GrapherTypes.ts | 7 - packages/@ourworldindata/types/src/index.ts | 1 - 11 files changed, 523 insertions(+), 463 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts b/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts index cfd81654f90..5f5907b3063 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts @@ -17,6 +17,12 @@ export interface ChartSeries { color: Color } +export type RenderChartSeries = TChartSeries & { + background?: boolean + hovered?: boolean + muted?: boolean +} + export type ChartTableTransformer = (inputTable: OwidTable) => OwidTable export interface ChartInterface { diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 8b24bdd70a3..2b076f7c9d8 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -64,6 +64,7 @@ export interface ChartManager { colorColumnSlug?: ColumnSlug selection?: SelectionArray | EntityName[] + focusedSeriesNames?: string[] entityType?: string hidePoints?: boolean // for line options diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index c9edeaa86dc..ec65c1ce73f 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -462,6 +462,9 @@ export class Grapher @observable.ref windowInnerWidth?: number @observable.ref windowInnerHeight?: number + // TODO: internal only for now + @observable private focusedSeriesNames: string[] = [] + owidDataset?: MultipleOwidVariableDataDimensionsMap = undefined // This is used for passing data for testing manuallyProvideData? = false // This will be removed. diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 710b04f0397..8e0da5dcc22 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -24,6 +24,7 @@ import { Color, HorizontalAlign, makeIdForHumanConsumption, + partition, } from "@ourworldindata/utils" import { computed, action, observable } from "mobx" import { observer } from "mobx-react" @@ -70,6 +71,7 @@ import { LinePoint, PlacedLineChartSeries, PlacedPoint, + RenderLineChartSeries, } from "./LineChartConstants" import { OwidTable, @@ -123,237 +125,6 @@ const VARIABLE_COLOR_LINE_OUTLINE_WIDTH = 1.0 // legend const LEGEND_PADDING = 25 -@observer -class Lines extends React.Component { - @computed get bounds(): Bounds { - const { horizontalAxis, verticalAxis } = this.props.dualAxis - return Bounds.fromCorners( - new PointVector(horizontalAxis.range[0], verticalAxis.range[0]), - new PointVector(horizontalAxis.range[1], verticalAxis.range[1]) - ) - } - - @computed private get focusedLines(): PlacedLineChartSeries[] { - const { focusedSeriesNames } = this.props - // If nothing is focused, everything is - if (!focusedSeriesNames.length) return this.props.placedSeries - return this.props.placedSeries.filter((series) => - focusedSeriesNames.includes(series.seriesName) - ) - } - - @computed private get backgroundLines(): PlacedLineChartSeries[] { - const { focusedSeriesNames } = this.props - // if nothing is focused, everything is focused, so nothing is in the background - if (!focusedSeriesNames.length) return [] - return this.props.placedSeries.filter( - (series) => !focusedSeriesNames.includes(series.seriesName) - ) - } - - // Don't display point markers if there are very many of them for performance reasons - // Note that we're using circle elements instead of marker-mid because marker performance in Safari 10 is very poor for some reason - @computed private get hasMarkers(): boolean { - if (this.props.hidePoints) return false - return ( - sum(this.focusedLines.map((series) => series.placedPoints.length)) < - 500 - ) - } - - @computed private get markerRadius(): number { - return this.props.markerRadius ?? DEFAULT_MARKER_RADIUS - } - - @computed private get strokeWidth(): number { - return this.props.lineStrokeWidth ?? DEFAULT_STROKE_WIDTH - } - - @computed private get lineOutlineWidth(): number { - return this.props.lineOutlineWidth ?? DEFAULT_LINE_OUTLINE_WIDTH - } - - private renderPathForSeries( - series: PlacedLineChartSeries, - props: Partial> - ): React.ReactElement { - const strokeDasharray = series.isProjection ? "2,3" : undefined - return ( - [value.x, value.y]) as [ - number, - number, - ][] - )} - /> - ) - } - - private renderFocusLines(): React.ReactElement | void { - if (this.focusedLines.length === 0) return - return ( - - {this.focusedLines.map((series) => { - const strokeDasharray = series.isProjection - ? "2,3" - : undefined - return ( - - {this.renderPathForSeries(series, { - id: makeIdForHumanConsumption( - "outline", - series.seriesName - ), - stroke: - this.props.backgroundColor ?? - GRAPHER_BACKGROUND_DEFAULT, - strokeWidth: - this.strokeWidth + - this.lineOutlineWidth * 2, - })} - {this.props.multiColor ? ( - - ) : ( - this.renderPathForSeries(series, { - id: makeIdForHumanConsumption( - "line", - series.seriesName - ), - stroke: - series.placedPoints[0]?.color ?? - DEFAULT_LINE_COLOR, - }) - )} - - ) - })} - - ) - } - - private renderLineMarkers(): React.ReactElement | void { - const { horizontalAxis } = this.props.dualAxis - if (this.focusedLines.length === 0) return - return ( - - {this.focusedLines.map((series) => { - // If the series only contains one point, then we will always want to show a marker/circle - // because we can't draw a line. - const showMarkers = - (this.hasMarkers || series.placedPoints.length === 1) && - !series.isProjection - return ( - showMarkers && ( - - {series.placedPoints.map((value, index) => ( - - ))} - - ) - ) - })} - - ) - } - - private renderFocusGroups(): React.ReactElement | void { - return ( - <> - {this.renderFocusLines()} - {this.renderLineMarkers()} - - ) - } - - private renderBackgroundGroups(): React.ReactElement | void { - if (this.backgroundLines.length === 0) return - return ( - - {this.backgroundLines.map((series) => ( - - {this.renderPathForSeries(series, { - id: makeIdForHumanConsumption( - "background-line", - series.seriesName - ), - stroke: series.color, - strokeWidth: 1, - strokeOpacity: 0.3, - })} - - ))} - - ) - } - - renderStatic(): React.ReactElement { - return ( - <> - {this.renderBackgroundGroups()} - {this.renderFocusGroups()} - - ) - } - - renderInteractive(): React.ReactElement { - const { bounds } = this - return ( - - - {this.renderBackgroundGroups()} - {this.renderFocusGroups()} - - ) - } - - render(): React.ReactElement { - return this.props.isStatic - ? this.renderStatic() - : this.renderInteractive() - } -} - @observer export class LineChart extends React.Component<{ @@ -412,6 +183,14 @@ export class LineChart return table } + @computed get selectionArray(): SelectionArray { + return makeSelectionArray(this.manager.selection) + } + + @computed private get focusedSeriesNames(): SeriesName[] { + return this.manager.focusedSeriesNames ?? [] + } + @computed private get missingDataStrategy(): MissingDataStrategy { return this.manager.missingDataStrategy || MissingDataStrategy.auto } @@ -542,17 +321,6 @@ export class LineChart : DEFAULT_MARKER_RADIUS } - @computed get selectionArray(): SelectionArray { - return makeSelectionArray(this.manager.selection) - } - - seriesIsBlurred(series: LineChartSeries): boolean { - return ( - this.isFocusModeActive && - !this.focusedSeriesNames.includes(series.seriesName) - ) - } - @computed get activeX(): number | undefined { return ( this.tooltipState.target?.x ?? @@ -575,11 +343,21 @@ export class LineChart y2={verticalAxis.range[1]} stroke="rgba(180,180,180,.4)" /> - {this.series.map((series) => { + {this.renderSeries.map((series) => { const value = series.points.find( (point) => point.x === activeX ) - if (!value || this.seriesIsBlurred(series)) return null + if (!value) return null + + const seriesColor = this.hasColorScale + ? darkenColorForLine( + this.getColorScaleColor(value.colorValue) + ) + : series.color + + const color = this.seriesIsInBackground(series) + ? "#E7E7E7" + : seriesColor return ( name !== seriesName + ) + } else if (this.manager.focusedSeriesNames) { + this.manager.focusedSeriesNames.push(seriesName) + } + } + + // TODO + @computed get highlightedSeriesName(): string | undefined { + return this.props.manager.entityYearHighlight?.entityName + } + + @computed get facetLegendHoveredSeriesNames(): string[] { const { externalLegendHoverBin } = this.manager - const focusedSeriesNames = excludeUndefined([ - this.props.manager.entityYearHighlight?.entityName, + if (!externalLegendHoverBin) return [] + return this.series + .map((s) => s.seriesName) + .filter((name) => externalLegendHoverBin.contains(name)) + } + + @computed get hoveredSeriesNames(): string[] { + return excludeUndefined([ this.hoveredSeriesName, + ...this.facetLegendHoveredSeriesNames, ]) - if (externalLegendHoverBin) { - focusedSeriesNames.push( - ...this.series - .map((s) => s.seriesName) - .filter((name) => externalLegendHoverBin.contains(name)) - ) - } - return focusedSeriesNames + } + + @computed get isHoverModeActive(): boolean { + return this.hoveredSeriesNames.length > 0 } @computed get isFocusModeActive(): boolean { @@ -959,17 +748,17 @@ export class LineChart fontSize={this.fontSize} fontWeight={this.fontWeight} isStatic={this.isStatic} - focusedSeriesNames={this.focusedSeriesNames} + onClick={this.onLineLegendClick} onMouseOver={this.onLineLegendMouseOver} onMouseLeave={this.onLineLegendMouseLeave} /> )} { + return { + ...series, + background: this.seriesIsInBackground(series), + hovered: this.seriesIsHovered(series), + muted: this.seriesIsMuted(series), + } + }) + + const sortedSeries = sortBy(series, (series) => { + if (series.background && !series.hovered) return 0 + if (!series.background && !series.hovered) return 1 + if (series.background && series.hovered) return 2 + if (!series.background && series.hovered) return 3 + return 4 + }) + + return sortedSeries + } + + private seriesIsFocused(series: LineChartSeries): boolean { + return this.focusedSeriesNames.includes(series.seriesName) + } + + private seriesIsHovered(series: LineChartSeries): boolean { + return this.hoveredSeriesNames.includes(series.seriesName) + } + + private seriesIsInBackground(series: LineChartSeries): boolean { + return this.isFocusModeActive && !this.seriesIsFocused(series) + } + + private seriesIsMuted(series: LineChartSeries): boolean { + return this.isHoverModeActive && !this.seriesIsHovered(series) + } + // Order of the legend items on a line chart should visually correspond // to the order of the lines as the approach the legend @computed get lineLegendSeries(): LineLabelSeries[] { @@ -1344,6 +1170,9 @@ export class LineChart seriesName ), yValue: lastValue, + background: this.seriesIsInBackground(series), + hovered: this.seriesIsHovered(series), + muted: this.seriesIsMuted(series), } }) } @@ -1458,3 +1287,207 @@ export class LineChart return undefined } } + +@observer +class Lines extends React.Component { + @computed get bounds(): Bounds { + const { horizontalAxis, verticalAxis } = this.props.dualAxis + return Bounds.fromCorners( + new PointVector(horizontalAxis.range[0], verticalAxis.range[0]), + new PointVector(horizontalAxis.range[1], verticalAxis.range[1]) + ) + } + + @computed private get markerRadius(): number { + return this.props.markerRadius ?? DEFAULT_MARKER_RADIUS + } + + @computed private get strokeWidth(): number { + return this.props.lineStrokeWidth ?? DEFAULT_STROKE_WIDTH + } + + @computed private get lineOutlineWidth(): number { + return this.props.lineOutlineWidth ?? DEFAULT_LINE_OUTLINE_WIDTH + } + + // Don't display point markers if there are very many of them for performance reasons + // Note that we're using circle elements instead of marker-mid because marker performance in Safari 10 is very poor for some reason + @computed private get hasMarkers(): boolean { + if (this.props.hidePoints) return false + const totalPoints = sum( + this.props.series + .filter((s) => !s.background || s.hovered) + .map((series) => series.placedPoints.length) + ) + return totalPoints < 500 + } + + private renderPathForSeries( + series: PlacedLineChartSeries, + props: Partial> + ): React.ReactElement { + const strokeDasharray = series.isProjection ? "2,3" : undefined + return ( + [value.x, value.y]) as [ + number, + number, + ][] + )} + /> + ) + } + + private seriesHasMarkers(series: RenderLineChartSeries): boolean { + // Don't show markers for lines in the background + if (series.background && !series.hovered) return false + + // If the series only contains one point, then we will always want to + // show a marker/circle because we can't draw a line. + return ( + (this.hasMarkers || series.placedPoints.length === 1) && + !series.isProjection + ) + } + + renderLine(series: RenderLineChartSeries): React.ReactElement { + const color = + !series.background || series.hovered + ? (series.placedPoints[0]?.color ?? DEFAULT_LINE_COLOR) + : "#E7E7E7" + const strokeWidth = series.muted ? 1 : this.strokeWidth + const strokeOpacity = series.muted && !series.background ? 0.3 : 1 + const strokeDasharray = series.isProjection ? "2,3" : undefined + + const outline = this.renderPathForSeries(series, { + id: makeIdForHumanConsumption("outline", series.seriesName), + stroke: this.props.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT, + strokeWidth: strokeWidth + this.lineOutlineWidth * 2, + }) + + if (this.props.multiColor) { + return ( + <> + {outline} + + + ) + } + + return ( + <> + {outline} + {this.renderPathForSeries(series, { + id: makeIdForHumanConsumption("line", series.seriesName), + stroke: color, + strokeWidth, + strokeOpacity, + })} + + ) + } + + renderLineMarkers( + series: RenderLineChartSeries + ): React.ReactElement | void { + if (!this.seriesHasMarkers(series)) return + + const { horizontalAxis } = this.props.dualAxis + const opacity = series.muted ? 0.3 : 1 + + return ( + + {series.placedPoints.map((value, index) => ( + + ))} + + ) + } + + renderLineWithMarkers(series: RenderLineChartSeries): React.ReactElement { + return ( + <> + {this.renderLine(series)} + {this.renderLineMarkers(series)} + + ) + } + + renderStatic(): React.ReactElement { + return ( + <> + {this.props.series.map((series) => { + const showMarkers = this.seriesHasMarkers(series) + return ( + <> + {this.renderLine(series)} + {showMarkers && this.renderLineMarkers(series)} + + ) + })} + + ) + } + + renderInteractive(): React.ReactElement { + const { bounds } = this + return ( + + + {this.props.series.map((series) => { + const showMarkers = this.seriesHasMarkers(series) + return ( + <> + {this.renderLine(series)} + {showMarkers && this.renderLineMarkers(series)} + + ) + })} + + ) + } + + render(): React.ReactElement { + return this.props.isStatic + ? this.renderStatic() + : this.renderInteractive() + } +} diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts b/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts index fe5d5a258bb..03bebc245af 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChartConstants.ts @@ -1,10 +1,6 @@ import { DualAxis } from "../axis/Axis" import { ChartManager } from "../chart/ChartManager" -import { - SeriesName, - CoreValueType, - EntityYearHighlight, -} from "@ourworldindata/types" +import { CoreValueType, EntityYearHighlight } from "@ourworldindata/types" import { ChartSeries } from "../chart/ChartInterface" import { Color } from "@ourworldindata/utils" @@ -30,10 +26,16 @@ export interface PlacedLineChartSeries extends LineChartSeries { placedPoints: PlacedPoint[] } +export interface RenderLineChartSeries extends PlacedLineChartSeries { + background: boolean + hovered: boolean + muted: boolean +} + export interface LinesProps { dualAxis: DualAxis - placedSeries: PlacedLineChartSeries[] - focusedSeriesNames: SeriesName[] + series: RenderLineChartSeries[] + isHoverModeActive: boolean hidePoints?: boolean lineStrokeWidth?: number lineOutlineWidth?: number diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index 33aa7a9ba0a..a4087dc088b 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -42,6 +42,10 @@ export interface LineLabelSeries extends ChartSeries { formattedValue?: string placeFormattedValueInNewLine?: boolean yRange?: [number, number] + + background?: boolean + hovered?: boolean + muted?: boolean } interface SizedSeries extends LineLabelSeries { @@ -60,14 +64,6 @@ interface PlacedSeries extends SizedSeries { midY: number } -function getSeriesKey( - series: PlacedSeries, - index: number, - key: string -): string { - return `${key}-${index}-` + series.seriesName -} - function groupBounds(group: PlacedSeries[]): Bounds { const first = group[0] const last = group[group.length - 1] @@ -92,18 +88,22 @@ function stackGroupVertically( @observer class LineLabels extends React.Component<{ series: PlacedSeries[] - uniqueKey: string needsConnectorLines: boolean connectorLineWidth?: number anchor?: "start" | "end" - isFocus?: boolean - isStatic?: boolean + interactive?: boolean onClick?: (series: PlacedSeries) => void onMouseOver?: (series: PlacedSeries) => void onMouseLeave?: (series: PlacedSeries) => void }> { - @computed private get textOpacity(): number { - return this.props.isFocus ? 1 : 0.6 + private opacityForSeries(series: PlacedSeries): number { + return !series.muted ? 1 : 0.6 + } + + private textColorForSeries(series: PlacedSeries): string { + return !series.background || series.hovered + ? darkenColorForText(series.color) + : "#DADADA" } @computed private get anchor(): "start" | "end" { @@ -146,19 +146,14 @@ class LineLabels extends React.Component<{ @computed private get textLabels(): React.ReactElement { return ( - {this.markers.map(({ series, labelText }, index) => { - const key = getSeriesKey( - series, - index, - this.props.uniqueKey - ) - const textColor = darkenColorForText(series.color) + {this.markers.map(({ series, labelText }) => { + const textColor = this.textColorForSeries(series) return ( - + {series.textWrap.render(labelText.x, labelText.y, { textProps: { fill: textColor, - opacity: this.textOpacity, + opacity: this.opacityForSeries(series), textAnchor: this.anchor, }, })} @@ -176,15 +171,10 @@ class LineLabels extends React.Component<{ if (!markersWithAnnotations) return return ( - {markersWithAnnotations.map(({ series, labelText }, index) => { - const key = getSeriesKey( - series, - index, - this.props.uniqueKey - ) + {markersWithAnnotations.map(({ series, labelText }) => { if (!series.annotationTextWrap) return return ( - + {series.annotationTextWrap.render( labelText.x, labelText.y + @@ -193,7 +183,7 @@ class LineLabels extends React.Component<{ { textProps: { fill: "#333", - opacity: this.textOpacity, + opacity: this.opacityForSeries(series), textAnchor: this.anchor, style: { fontWeight: 300 }, }, @@ -210,8 +200,7 @@ class LineLabels extends React.Component<{ if (!this.props.needsConnectorLines) return return ( - {this.markers.map(({ series, connectorLine }, index) => { - const { isFocus } = this.props + {this.markers.map(({ series, connectorLine }) => { const { x1, x2 } = connectorLine const { level, @@ -223,16 +212,13 @@ class LineLabels extends React.Component<{ const step = (x2 - x1) / (totalLevels + 1) const markerXMid = x1 + step + level * step const d = `M${x1},${leftCenterY} H${markerXMid} V${rightCenterY} H${x2}` - const lineColor = isFocus ? "#999" : "#eee" + const lineColor = + !series.background || series.hovered ? "#999" : "#eee" return ( - {this.props.series.map((series, index) => { + {this.props.series.map((series) => { const x = this.anchor === "start" ? series.origBounds.x : series.origBounds.x - series.bounds.width return ( this.props.onClick?.(series)} onMouseOver={() => this.props.onMouseOver?.(series)} onMouseLeave={() => this.props.onMouseLeave?.(series) } - onClick={() => this.props.onClick?.(series)} style={{ cursor: "default" }} > ) } @@ -314,7 +296,6 @@ export interface LineLegendProps { // interactions isStatic?: boolean // don't add interactions if true - focusedSeriesNames?: EntityName[] // currently in focus onClick?: (key: EntityName) => void onMouseOver?: (key: EntityName) => void onMouseLeave?: () => void @@ -432,16 +413,6 @@ export class LineLegend extends React.Component { return this.props.onClick ?? noop } - @computed get focusedSeriesNames(): EntityName[] { - return this.props.focusedSeriesNames ?? [] - } - - @computed get isFocusMode(): boolean { - return this.sizedLabels.some((label) => - this.focusedSeriesNames.includes(label.seriesName) - ) - } - @computed get legendX(): number { return this.props.x ?? 0 } @@ -732,67 +703,13 @@ export class LineLegend extends React.Component { } } - @computed private get backgroundSeries(): PlacedSeries[] { - const { focusedSeriesNames } = this - const { isFocusMode } = this - return this.placedSeries.filter( - (mark) => - isFocusMode && !focusedSeriesNames.includes(mark.seriesName) - ) - } - - @computed private get focusedSeries(): PlacedSeries[] { - const { focusedSeriesNames } = this - const { isFocusMode } = this - return this.placedSeries.filter( - (mark) => - !isFocusMode || focusedSeriesNames.includes(mark.seriesName) - ) - } - // Does this placement need line markers or is the position of the labels already clear? @computed private get needsLines(): boolean { return this.placedSeries.some((series) => series.totalLevels > 1) } - private renderBackground(): React.ReactElement { - return ( - - this.onMouseOver(series.seriesName) - } - onClick={(series): void => this.onClick(series.seriesName)} - /> - ) - } - - // All labels are focused by default, moved to background when mouseover of other label - private renderFocus(): React.ReactElement { - return ( - - this.onMouseOver(series.seriesName) - } - onClick={(series): void => this.onClick(series.seriesName)} - onMouseLeave={(series): void => - this.onMouseLeave(series.seriesName) - } - /> - ) + @computed private get isStatic(): boolean { + return this.props.isStatic ?? false } render(): React.ReactElement { @@ -801,8 +718,20 @@ export class LineLegend extends React.Component { id={makeIdForHumanConsumption("line-labels")} className="LineLabels" > - {this.renderBackground()} - {this.renderFocus()} + this.onClick(series.seriesName)} + onMouseOver={(series): void => + this.onMouseOver(series.seriesName) + } + onMouseLeave={(series): void => + this.onMouseLeave(series.seriesName) + } + /> ) } diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 5e8858841df..51f997294b8 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -1,4 +1,4 @@ -import React, { SVGProps } from "react" +import React from "react" import { Bounds, DEFAULT_BOUNDS, @@ -32,7 +32,6 @@ import { Time, SeriesStrategy, EntityName, - RenderMode, VerticalAlign, FacetStrategy, } from "@ourworldindata/types" @@ -43,6 +42,7 @@ import { select } from "d3-selection" import { PlacedSlopeChartSeries, RawSlopeChartSeries, + RenderSlopeChartSeries, SlopeChartSeries, } from "./SlopeChartConstants" import { CoreColumn, OwidTable } from "@ourworldindata/core-table" @@ -222,6 +222,10 @@ export class SlopeChart return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT } + @computed private get isHoverModeActive(): boolean { + return this.hoveredSeriesNames.length > 0 + } + @computed private get isFocusModeActive(): boolean { return this.focusedSeriesNames.length > 0 } @@ -433,6 +437,33 @@ export class SlopeChart }) } + private seriesIsFocused(series: SlopeChartSeries): boolean { + return this.focusedSeriesNames.includes(series.seriesName) + } + + private seriesIsHovered(series: SlopeChartSeries): boolean { + return this.hoveredSeriesNames.includes(series.seriesName) + } + + private seriesIsInBackground(series: SlopeChartSeries): boolean { + return this.isFocusModeActive && !this.seriesIsFocused(series) + } + + private seriesIsMuted(series: SlopeChartSeries): boolean { + return this.isHoverModeActive && !this.seriesIsHovered(series) + } + + @computed get renderSeries(): RenderSlopeChartSeries[] { + return this.placedSeries.map((series) => { + return { + ...series, + background: this.seriesIsInBackground(series), + hovered: this.seriesIsHovered(series), + muted: this.seriesIsMuted(series), + } + }) + } + @computed private get noDataSeries(): RawSlopeChartSeries[] { return this.rawSeries.filter((series) => !this.isSeriesValid(series)) @@ -662,16 +693,16 @@ export class SlopeChart return !!this.manager.isSemiNarrow || this.bounds.width < 400 } - @computed get focusedSeriesNames(): SeriesName[] { - const focusedSeriesNames: SeriesName[] = [] + @computed get hoveredSeriesNames(): SeriesName[] { + const hoveredSeriesNames: SeriesName[] = [] // hovered series name if (this.hoveredSeriesName) - focusedSeriesNames.push(this.hoveredSeriesName) + hoveredSeriesNames.push(this.hoveredSeriesName) // hovered legend item in the external facet legend if (this.manager.externalLegendHoverBin) { - focusedSeriesNames.push( + hoveredSeriesNames.push( ...this.series .map((s) => s.seriesName) .filter((name) => @@ -680,7 +711,7 @@ export class SlopeChart ) } - return focusedSeriesNames + return hoveredSeriesNames } /** @@ -705,6 +736,9 @@ export class SlopeChart formattedValue, valueInNewLine: this.useCompactLineLegend, yValue: start.value, + background: this.seriesIsInBackground(series), + hovered: this.seriesIsHovered(series), + muted: this.seriesIsMuted(series), } }) } @@ -726,6 +760,9 @@ export class SlopeChart ? undefined : annotation, yValue: end.value, + background: this.seriesIsInBackground(series), + hovered: this.seriesIsHovered(series), + muted: this.seriesIsMuted(series), } }) } @@ -793,6 +830,20 @@ export class SlopeChart }) } + @computed private get focusedSeriesNames(): SeriesName[] { + return this.manager.focusedSeriesNames ?? [] + } + + @action.bound onLineLegendClick(seriesName: SeriesName): void { + if (this.focusedSeriesNames.includes(seriesName)) { + this.manager.focusedSeriesNames = this.focusedSeriesNames.filter( + (name) => name !== seriesName + ) + } else if (this.manager.focusedSeriesNames) { + this.manager.focusedSeriesNames.push(seriesName) + } + } + private hoverTimer?: NodeJS.Timeout @action.bound onLineLegendMouseOver(seriesName: SeriesName): void { clearTimeout(this.hoverTimer) @@ -986,42 +1037,26 @@ export class SlopeChart ) } - private renderSlope( - series: PlacedSlopeChartSeries, - mode?: RenderMode - ): React.ReactElement { - return ( - - ) - } + // private renderSlope(series: RenderSlopeChartSeries): React.ReactElement { + // return ( + // + // ) + // } private renderSlopes() { - if (!this.isFocusModeActive) { - return this.placedSeries.map((series) => this.renderSlope(series)) - } - - const [focusedSeries, backgroundSeries] = partition( - this.placedSeries, - (series) => this.focusedSeriesNames.includes(series.seriesName) - ) - return ( - <> - {backgroundSeries.map((series) => - this.renderSlope(series, RenderMode.mute) - )} - {focusedSeries.map((series) => - this.renderSlope(series, RenderMode.focus) - )} - + ) } @@ -1107,7 +1142,7 @@ export class SlopeChart fontSize={this.fontSize} fontWeight={this.manager.showLegend ? 700 : undefined} isStatic={this.manager.isStatic} - focusedSeriesNames={this.focusedSeriesNames} + onClick={this.onLineLegendClick} onMouseLeave={this.onLineLegendMouseLeave} onMouseOver={this.onLineLegendMouseOver} /> @@ -1155,7 +1190,7 @@ export class SlopeChart this.showSeriesNamesInLineLegendLeft ? 700 : undefined } isStatic={this.manager.isStatic} - focusedSeriesNames={this.focusedSeriesNames} + onClick={this.onLineLegendClick} onMouseLeave={this.onLineLegendMouseLeave} onMouseOver={this.onLineLegendMouseOver} /> @@ -1196,10 +1231,64 @@ export class SlopeChart } } +@observer +class Slopes extends React.Component<{ + series: RenderSlopeChartSeries[] + lineStrokeWidth?: number + backgroundColor?: string +}> { + @computed get foregroundSeries(): { + hover: RenderSlopeChartSeries[] + nonHover: RenderSlopeChartSeries[] + } { + const foregroundSeries = this.props.series.filter((s) => !s.background) + const [hover, nonHover] = partition(foregroundSeries, (s) => s.hovered) + return { hover, nonHover } + } + + @computed get backgroundSeries(): { + hover: RenderSlopeChartSeries[] + nonHover: RenderSlopeChartSeries[] + } { + const backgroundSeries = this.props.series.filter((s) => s.background) + const [hover, nonHover] = partition(backgroundSeries, (s) => s.hovered) + return { hover, nonHover } + } + + private renderSlope(series: RenderSlopeChartSeries) { + return ( + + ) + } + + render() { + return ( + + {this.backgroundSeries.nonHover.map((series) => + this.renderSlope(series) + )} + {this.foregroundSeries.nonHover.map((series) => + this.renderSlope(series) + )} + {this.backgroundSeries.hover.map((series) => + this.renderSlope(series) + )} + {this.foregroundSeries.hover.map((series) => + this.renderSlope(series) + )} + + ) + } +} + interface SlopeProps { - series: PlacedSlopeChartSeries - color: string - mode?: RenderMode + series: RenderSlopeChartSeries dotRadius?: number strokeWidth?: number outlineWidth?: number @@ -1210,8 +1299,6 @@ interface SlopeProps { function Slope({ series, - color, - mode = RenderMode.default, dotRadius = 2.5, strokeWidth = 2, outlineWidth = 0.5, @@ -1221,14 +1308,10 @@ function Slope({ }: SlopeProps) { const { seriesName, startPoint, endPoint } = series - const showOutline = mode === RenderMode.default || mode === RenderMode.focus - - const opacity = { - [RenderMode.default]: 1, - [RenderMode.focus]: 1, - [RenderMode.mute]: 0.3, - [RenderMode.background]: 0.3, - }[mode] + const color = + !series.background || series.hovered ? series.color : "#E7E7E7" + const showOutline = !series.muted + const opacity = series.muted ? 0.3 : 1 return ( { onAreaMouseLeave?: () => void } +enum RenderMode { + default = "default", + focus = "focus", // hovered or focused + mute = "mute", // not hovered + background = "background", // not focused +} + const STACKED_AREA_CHART_CLASS_NAME = "StackedArea" const AREA_OPACITY: Partial> = { @@ -303,6 +310,10 @@ export class StackedAreaChart extends AbstractStackedChart { label: series.seriesName, yValue: midpoints[index], isAllZeros: series.isAllZeros, + hovered: series.seriesName === this.hoveredSeriesName, + muted: + !!this.hoveredSeriesName && + series.seriesName !== this.hoveredSeriesName, })) .filter((series) => !series.isAllZeros) .reverse() @@ -416,7 +427,7 @@ export class StackedAreaChart extends AbstractStackedChart { return hoveredSeries?.seriesName } - @computed get focusedSeriesName(): SeriesName | undefined { + @computed get hoveredSeriesName(): SeriesName | undefined { return ( // if the chart area is hovered this.tooltipState.target?.series ?? @@ -427,11 +438,6 @@ export class StackedAreaChart extends AbstractStackedChart { ) } - // used by the line legend component - @computed get focusedSeriesNames(): string[] { - return this.focusedSeriesName ? [this.focusedSeriesName] : [] - } - @action.bound private onCursorMove( ev: React.MouseEvent | React.TouchEvent ): void { @@ -665,7 +671,6 @@ export class StackedAreaChart extends AbstractStackedChart { fontSize={this.fontSize} seriesSortedByImportance={this.seriesSortedByImportance} isStatic={this.isStatic} - focusedSeriesNames={this.focusedSeriesNames} onMouseOver={this.onLineLegendMouseOver} onMouseLeave={this.onLineLegendMouseLeave} /> @@ -680,7 +685,7 @@ export class StackedAreaChart extends AbstractStackedChart { ) @@ -718,7 +723,7 @@ export class StackedAreaChart extends AbstractStackedChart { diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 8306197da2f..1d73e6a6470 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -208,13 +208,6 @@ export interface AnnotationFieldsInTitle { changeInPrefix?: boolean } -export enum RenderMode { - default = "default", - focus = "focus", // hovered or focused - mute = "mute", // not hovered - background = "background", // not focused -} - export interface Tickmark { value: number priority: number diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index 0f42e50f1ad..562dd93191d 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -113,7 +113,6 @@ export { GrapherWindowType, AxisMinMaxValueStr, GrapherTooltipAnchor, - RenderMode, } from "./grapherTypes/GrapherTypes.js" export { From aefcd40a3f75676a75145cb1ee831cc26a0190e8 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 4 Dec 2024 17:45:33 +0100 Subject: [PATCH 89/91] =?UTF-8?q?=F0=9F=9A=A7=20hover=20&=20focus=20state?= =?UTF-8?q?=20enums?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartInterface.ts | 21 +- .../grapher/src/chart/ChartManager.ts | 2 +- .../grapher/src/chart/ChartUtils.tsx | 42 +++- .../grapher/src/core/Grapher.tsx | 2 +- .../grapher/src/lineCharts/LineChart.tsx | 119 +++++++----- .../src/lineCharts/LineChartConstants.ts | 8 +- .../grapher/src/lineLegend/LineLegend.tsx | 28 ++- .../grapher/src/slopeCharts/SlopeChart.tsx | 179 ++++++------------ .../src/slopeCharts/SlopeChartConstants.ts | 8 +- 9 files changed, 213 insertions(+), 196 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts b/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts index 5f5907b3063..5a3e0f17003 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartInterface.ts @@ -17,12 +17,25 @@ export interface ChartSeries { color: Color } -export type RenderChartSeries = TChartSeries & { - background?: boolean - hovered?: boolean - muted?: boolean +export enum FocusState { + off = "off", // nothing is currently focused + active = "active", // actively focused + background = "background", // another series is actively focused } +export enum HoverState { + off = "off", // nothing is currently hovered + active = "active", // actively hovered + background = "background", // another series is actively hovered +} + +export interface ChartSeriesStates { + focus: FocusState + hover: HoverState +} + +export type RenderChartSeries = TChartSeries & ChartSeriesStates + export type ChartTableTransformer = (inputTable: OwidTable) => OwidTable export interface ChartInterface { diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 2b076f7c9d8..006e61001ba 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -64,7 +64,7 @@ export interface ChartManager { colorColumnSlug?: ColumnSlug selection?: SelectionArray | EntityName[] - focusedSeriesNames?: string[] + focusedSeriesNameSet?: Set entityType?: string hidePoints?: boolean // for line options diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 249920b44f0..33147bbcc83 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -1,5 +1,10 @@ import React from "react" -import { areSetsEqual, Box, getCountryByName } from "@ourworldindata/utils" +import { + areSetsEqual, + Box, + getCountryByName, + sortBy, +} from "@ourworldindata/utils" import { SeriesStrategy, EntityName, @@ -17,6 +22,7 @@ import { GRAPHER_SETTINGS_CLASS, validChartTypeCombinations, } from "../core/GrapherConstants" +import { FocusState, HoverState, RenderChartSeries } from "./ChartInterface.js" export const autoDetectYColumnSlugs = (manager: ChartManager): string[] => { if (manager.yColumnSlugs && manager.yColumnSlugs.length) @@ -188,3 +194,37 @@ export function findValidChartTypeCombination( } return undefined } + +function byFocusState( + series: RenderChartSeries +): number { + switch (series.focus) { + case FocusState.background: + return 1 + case FocusState.off: + return 2 + case FocusState.active: + return 3 + } +} + +function byHoverState( + series: RenderChartSeries +): number { + switch (series.hover) { + case HoverState.background: + return 1 + case HoverState.off: + return 2 + case HoverState.active: + return 3 + } +} + +export function byHoverThenFocusState( + series: RenderChartSeries +): number { + const hoverScore = byHoverState(series) + const focusScore = byFocusState(series) + return 10 * hoverScore + focusScore +} diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index ec65c1ce73f..9f059dcf7e8 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -463,7 +463,7 @@ export class Grapher @observable.ref windowInnerHeight?: number // TODO: internal only for now - @observable private focusedSeriesNames: string[] = [] + @observable focusedSeriesNameSet = new Set() owidDataset?: MultipleOwidVariableDataDimensionsMap = undefined // This is used for passing data for testing diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 8e0da5dcc22..1d1854968fe 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -63,7 +63,7 @@ import { } from "../core/GrapherConstants" import { ColorSchemes } from "../color/ColorSchemes" import { AxisConfig, AxisManager } from "../axis/AxisConfig" -import { ChartInterface } from "../chart/ChartInterface" +import { ChartInterface, FocusState, HoverState } from "../chart/ChartInterface" import { LinesProps, LineChartSeries, @@ -81,6 +81,7 @@ import { import { autoDetectSeriesStrategy, autoDetectYColumnSlugs, + byHoverThenFocusState, getDefaultFailMessage, getSeriesKey, isTargetOutsideElement, @@ -187,8 +188,8 @@ export class LineChart return makeSelectionArray(this.manager.selection) } - @computed private get focusedSeriesNames(): SeriesName[] { - return this.manager.focusedSeriesNames ?? [] + @computed private get focusedSeriesNameSet(): Set { + return this.manager.focusedSeriesNameSet ?? new Set() } @computed private get missingDataStrategy(): MissingDataStrategy { @@ -355,9 +356,10 @@ export class LineChart ) : series.color - const color = this.seriesIsInBackground(series) - ? "#E7E7E7" - : seriesColor + const color = + series.focus === FocusState.background + ? "#E7E7E7" + : seriesColor return ( name !== seriesName - ) - } else if (this.manager.focusedSeriesNames) { - this.manager.focusedSeriesNames.push(seriesName) + if (this.focusedSeriesNameSet.has(seriesName)) { + this.manager.focusedSeriesNameSet?.delete(seriesName) + } else { + this.manager.focusedSeriesNameSet?.add(seriesName) } } @@ -561,19 +561,21 @@ export class LineChart .filter((name) => externalLegendHoverBin.contains(name)) } - @computed get hoveredSeriesNames(): string[] { - return excludeUndefined([ - this.hoveredSeriesName, - ...this.facetLegendHoveredSeriesNames, - ]) + @computed get hoveredSeriesNameSet(): Set { + return new Set( + excludeUndefined([ + this.hoveredSeriesName, + ...this.facetLegendHoveredSeriesNames, + ]) + ) } @computed get isHoverModeActive(): boolean { - return this.hoveredSeriesNames.length > 0 + return this.hoveredSeriesNameSet.size > 0 } @computed get isFocusModeActive(): boolean { - return this.focusedSeriesNames.length > 0 + return this.focusedSeriesNameSet.size > 0 } @action.bound onDocumentClick(e: MouseEvent): void { @@ -1112,40 +1114,43 @@ export class LineChart } @computed get renderSeries(): RenderLineChartSeries[] { - const series = this.placedSeries.map((series) => { - return { - ...series, - background: this.seriesIsInBackground(series), - hovered: this.seriesIsHovered(series), - muted: this.seriesIsMuted(series), + const series: RenderLineChartSeries[] = this.placedSeries.map( + (series) => { + return { + ...series, + focus: this.seriesFocusState(series), + hover: this.seriesHoverState(series), + } } - }) + ) - const sortedSeries = sortBy(series, (series) => { - if (series.background && !series.hovered) return 0 - if (!series.background && !series.hovered) return 1 - if (series.background && series.hovered) return 2 - if (!series.background && series.hovered) return 3 - return 4 - }) + if (this.isFocusModeActive || this.isHoverModeActive) { + return sortBy(series, byHoverThenFocusState) + } - return sortedSeries + return series } private seriesIsFocused(series: LineChartSeries): boolean { - return this.focusedSeriesNames.includes(series.seriesName) + return this.focusedSeriesNameSet.has(series.seriesName) } private seriesIsHovered(series: LineChartSeries): boolean { - return this.hoveredSeriesNames.includes(series.seriesName) + return this.hoveredSeriesNameSet.has(series.seriesName) } - private seriesIsInBackground(series: LineChartSeries): boolean { - return this.isFocusModeActive && !this.seriesIsFocused(series) + private seriesFocusState(series: LineChartSeries): FocusState { + if (!this.isFocusModeActive) return FocusState.off + return this.seriesIsFocused(series) + ? FocusState.active + : FocusState.background } - private seriesIsMuted(series: LineChartSeries): boolean { - return this.isHoverModeActive && !this.seriesIsHovered(series) + private seriesHoverState(series: LineChartSeries): HoverState { + if (!this.isHoverModeActive) return HoverState.off + return this.seriesIsHovered(series) + ? HoverState.active + : HoverState.background } // Order of the legend items on a line chart should visually correspond @@ -1170,9 +1175,8 @@ export class LineChart seriesName ), yValue: lastValue, - background: this.seriesIsInBackground(series), - hovered: this.seriesIsHovered(series), - muted: this.seriesIsMuted(series), + hover: this.seriesHoverState(series), + focus: this.seriesFocusState(series), } }) } @@ -1316,7 +1320,11 @@ class Lines extends React.Component { if (this.props.hidePoints) return false const totalPoints = sum( this.props.series - .filter((s) => !s.background || s.hovered) + .filter((s) => { + const nonFocused = s.focus === FocusState.background + const hovered = s.hover === HoverState.active + return !nonFocused || hovered + }) .map((series) => series.placedPoints.length) ) return totalPoints < 500 @@ -1347,8 +1355,11 @@ class Lines extends React.Component { } private seriesHasMarkers(series: RenderLineChartSeries): boolean { + const nonFocused = series.focus === FocusState.background + const hovered = series.hover === HoverState.active + // Don't show markers for lines in the background - if (series.background && !series.hovered) return false + if (nonFocused && !hovered) return false // If the series only contains one point, then we will always want to // show a marker/circle because we can't draw a line. @@ -1359,12 +1370,17 @@ class Lines extends React.Component { } renderLine(series: RenderLineChartSeries): React.ReactElement { + const nonFocused = series.focus === FocusState.background + const hovered = series.hover === HoverState.active + const nonHovered = series.hover === HoverState.background const color = - !series.background || series.hovered + !nonFocused || hovered ? (series.placedPoints[0]?.color ?? DEFAULT_LINE_COLOR) : "#E7E7E7" - const strokeWidth = series.muted ? 1 : this.strokeWidth - const strokeOpacity = series.muted && !series.background ? 0.3 : 1 + const strokeWidth = nonHovered ? 1 : this.strokeWidth + const strokeOpacity = nonHovered && !nonFocused ? 0.3 : 1 + // console.log(series.seriesName, series.nonHovered) + // const strokeOpacity = series.nonHovered ? 0.3 : 1 const strokeDasharray = series.isProjection ? "2,3" : undefined const outline = this.renderPathForSeries(series, { @@ -1411,7 +1427,8 @@ class Lines extends React.Component { if (!this.seriesHasMarkers(series)) return const { horizontalAxis } = this.props.dualAxis - const opacity = series.muted ? 0.3 : 1 + const nonHovered = series.hover === HoverState.background + const opacity = nonHovered ? 0.3 : 1 return ( export interface LinesProps { dualAxis: DualAxis diff --git a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx index a4087dc088b..78744001983 100644 --- a/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx +++ b/packages/@ourworldindata/grapher/src/lineLegend/LineLegend.tsx @@ -20,7 +20,12 @@ import { observer } from "mobx-react" import { VerticalAxis } from "../axis/Axis" import { EntityName, VerticalAlign } from "@ourworldindata/types" import { BASE_FONT_SIZE, GRAPHER_FONT_SCALE_12 } from "../core/GrapherConstants" -import { ChartSeries } from "../chart/ChartInterface" +import { + ChartSeries, + ChartSeriesStates, + FocusState, + HoverState, +} from "../chart/ChartInterface" import { darkenColorForText } from "../color/ColorUtils" import { AxisConfig } from "../axis/AxisConfig.js" import { Halo } from "../halo/Halo" @@ -35,17 +40,15 @@ const ANNOTATION_PADDING = 1 const DEFAULT_CONNECTOR_LINE_WIDTH = 35 const DEFAULT_FONT_WEIGHT = 400 -export interface LineLabelSeries extends ChartSeries { +export interface LineLabelSeries + extends ChartSeries, + Partial { label: string yValue: number annotation?: string formattedValue?: string placeFormattedValueInNewLine?: boolean yRange?: [number, number] - - background?: boolean - hovered?: boolean - muted?: boolean } interface SizedSeries extends LineLabelSeries { @@ -97,11 +100,14 @@ class LineLabels extends React.Component<{ onMouseLeave?: (series: PlacedSeries) => void }> { private opacityForSeries(series: PlacedSeries): number { - return !series.muted ? 1 : 0.6 + const nonHovered = series.hover === HoverState.background + return !nonHovered ? 1 : 0.6 } private textColorForSeries(series: PlacedSeries): string { - return !series.background || series.hovered + const nonFocused = series.focus === FocusState.background + const hovered = series.hover === HoverState.active + return !nonFocused || hovered ? darkenColorForText(series.color) : "#DADADA" } @@ -209,11 +215,13 @@ class LineLabels extends React.Component<{ bounds: { centerY: rightCenterY }, } = series + const nonFocused = series.focus === FocusState.background + const hovered = series.hover === HoverState.active + const step = (x2 - x1) / (totalLevels + 1) const markerXMid = x1 + step + level * step const d = `M${x1},${leftCenterY} H${markerXMid} V${rightCenterY} H${x2}` - const lineColor = - !series.background || series.hovered ? "#999" : "#eee" + const lineColor = !nonFocused || hovered ? "#999" : "#eee" return ( 0 + return this.hoveredSeriesNameSet.size > 0 } @computed private get isFocusModeActive(): boolean { - return this.focusedSeriesNames.length > 0 + return this.focusedSeriesNameSet.size > 0 } @computed private get yColumns(): CoreColumn[] { @@ -438,30 +440,41 @@ export class SlopeChart } private seriesIsFocused(series: SlopeChartSeries): boolean { - return this.focusedSeriesNames.includes(series.seriesName) + return this.focusedSeriesNameSet.has(series.seriesName) } private seriesIsHovered(series: SlopeChartSeries): boolean { - return this.hoveredSeriesNames.includes(series.seriesName) + return this.hoveredSeriesNameSet.has(series.seriesName) } - private seriesIsInBackground(series: SlopeChartSeries): boolean { - return this.isFocusModeActive && !this.seriesIsFocused(series) + private seriesFocusState(series: SlopeChartSeries): FocusState { + if (!this.isFocusModeActive) return FocusState.off + return this.seriesIsFocused(series) + ? FocusState.active + : FocusState.background } - private seriesIsMuted(series: SlopeChartSeries): boolean { - return this.isHoverModeActive && !this.seriesIsHovered(series) + private seriesHoverState(series: SlopeChartSeries): HoverState { + if (!this.isHoverModeActive) return HoverState.off + return this.seriesIsHovered(series) + ? HoverState.active + : HoverState.background } @computed get renderSeries(): RenderSlopeChartSeries[] { - return this.placedSeries.map((series) => { + const series = this.placedSeries.map((series) => { return { ...series, - background: this.seriesIsInBackground(series), - hovered: this.seriesIsHovered(series), - muted: this.seriesIsMuted(series), + focus: this.seriesFocusState(series), + hover: this.seriesHoverState(series), } }) + + if (this.isFocusModeActive || this.isHoverModeActive) { + return sortBy(series, byHoverThenFocusState) + } + + return series } @computed @@ -693,22 +706,20 @@ export class SlopeChart return !!this.manager.isSemiNarrow || this.bounds.width < 400 } - @computed get hoveredSeriesNames(): SeriesName[] { - const hoveredSeriesNames: SeriesName[] = [] + @computed get hoveredSeriesNameSet(): Set { + const hoveredSeriesNames = new Set() // hovered series name if (this.hoveredSeriesName) - hoveredSeriesNames.push(this.hoveredSeriesName) + hoveredSeriesNames.add(this.hoveredSeriesName) // hovered legend item in the external facet legend - if (this.manager.externalLegendHoverBin) { - hoveredSeriesNames.push( - ...this.series - .map((s) => s.seriesName) - .filter((name) => - this.manager.externalLegendHoverBin?.contains(name) - ) - ) + const { externalLegendHoverBin } = this.manager + if (externalLegendHoverBin) { + this.series + .map((s) => s.seriesName) + .filter((name) => externalLegendHoverBin?.contains(name)) + .forEach((name) => hoveredSeriesNames.add(name)) } return hoveredSeriesNames @@ -736,9 +747,8 @@ export class SlopeChart formattedValue, valueInNewLine: this.useCompactLineLegend, yValue: start.value, - background: this.seriesIsInBackground(series), - hovered: this.seriesIsHovered(series), - muted: this.seriesIsMuted(series), + focus: this.seriesFocusState(series), + hover: this.seriesHoverState(series), } }) } @@ -760,9 +770,8 @@ export class SlopeChart ? undefined : annotation, yValue: end.value, - background: this.seriesIsInBackground(series), - hovered: this.seriesIsHovered(series), - muted: this.seriesIsMuted(series), + focus: this.seriesFocusState(series), + hover: this.seriesHoverState(series), } }) } @@ -830,17 +839,15 @@ export class SlopeChart }) } - @computed private get focusedSeriesNames(): SeriesName[] { - return this.manager.focusedSeriesNames ?? [] + @computed private get focusedSeriesNameSet(): Set { + return this.manager.focusedSeriesNameSet ?? new Set() } @action.bound onLineLegendClick(seriesName: SeriesName): void { - if (this.focusedSeriesNames.includes(seriesName)) { - this.manager.focusedSeriesNames = this.focusedSeriesNames.filter( - (name) => name !== seriesName - ) - } else if (this.manager.focusedSeriesNames) { - this.manager.focusedSeriesNames.push(seriesName) + if (this.focusedSeriesNameSet.has(seriesName)) { + this.manager.focusedSeriesNameSet?.delete(seriesName) + } else { + this.manager.focusedSeriesNameSet?.add(seriesName) } } @@ -1037,26 +1044,19 @@ export class SlopeChart ) } - // private renderSlope(series: RenderSlopeChartSeries): React.ReactElement { - // return ( - // - // ) - // } - private renderSlopes() { return ( - + + {this.renderSeries.map((series) => ( + + ))} + ) } @@ -1231,62 +1231,6 @@ export class SlopeChart } } -@observer -class Slopes extends React.Component<{ - series: RenderSlopeChartSeries[] - lineStrokeWidth?: number - backgroundColor?: string -}> { - @computed get foregroundSeries(): { - hover: RenderSlopeChartSeries[] - nonHover: RenderSlopeChartSeries[] - } { - const foregroundSeries = this.props.series.filter((s) => !s.background) - const [hover, nonHover] = partition(foregroundSeries, (s) => s.hovered) - return { hover, nonHover } - } - - @computed get backgroundSeries(): { - hover: RenderSlopeChartSeries[] - nonHover: RenderSlopeChartSeries[] - } { - const backgroundSeries = this.props.series.filter((s) => s.background) - const [hover, nonHover] = partition(backgroundSeries, (s) => s.hovered) - return { hover, nonHover } - } - - private renderSlope(series: RenderSlopeChartSeries) { - return ( - - ) - } - - render() { - return ( - - {this.backgroundSeries.nonHover.map((series) => - this.renderSlope(series) - )} - {this.foregroundSeries.nonHover.map((series) => - this.renderSlope(series) - )} - {this.backgroundSeries.hover.map((series) => - this.renderSlope(series) - )} - {this.foregroundSeries.hover.map((series) => - this.renderSlope(series) - )} - - ) - } -} - interface SlopeProps { series: RenderSlopeChartSeries dotRadius?: number @@ -1308,10 +1252,13 @@ function Slope({ }: SlopeProps) { const { seriesName, startPoint, endPoint } = series - const color = - !series.background || series.hovered ? series.color : "#E7E7E7" - const showOutline = !series.muted - const opacity = series.muted ? 0.3 : 1 + const background = series.focus === FocusState.background + const hovered = series.hover === HoverState.active + const muted = series.hover === HoverState.background + + const color = !background || hovered ? series.color : "#E7E7E7" + const showOutline = !muted + const opacity = muted ? 0.3 : 1 return ( export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e" From d5c3083dcdd2a203e60159155334c5e3c01867a6 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 4 Dec 2024 18:12:07 +0100 Subject: [PATCH 90/91] =?UTF-8?q?=F0=9F=9A=A7=20add=20focused=20entities?= =?UTF-8?q?=20to=20selectionArray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartManager.ts | 1 - .../grapher/src/core/Grapher.tsx | 3 -- .../grapher/src/lineCharts/LineChart.tsx | 9 ++---- .../grapher/src/selection/SelectionArray.ts | 31 +++++++++++++++++++ .../grapher/src/slopeCharts/SlopeChart.tsx | 14 +++------ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 006e61001ba..8b24bdd70a3 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -64,7 +64,6 @@ export interface ChartManager { colorColumnSlug?: ColumnSlug selection?: SelectionArray | EntityName[] - focusedSeriesNameSet?: Set entityType?: string hidePoints?: boolean // for line options diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 9f059dcf7e8..c9edeaa86dc 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -462,9 +462,6 @@ export class Grapher @observable.ref windowInnerWidth?: number @observable.ref windowInnerHeight?: number - // TODO: internal only for now - @observable focusedSeriesNameSet = new Set() - owidDataset?: MultipleOwidVariableDataDimensionsMap = undefined // This is used for passing data for testing manuallyProvideData? = false // This will be removed. diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 1d1854968fe..a36ecb21805 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -24,7 +24,6 @@ import { Color, HorizontalAlign, makeIdForHumanConsumption, - partition, } from "@ourworldindata/utils" import { computed, action, observable } from "mobx" import { observer } from "mobx-react" @@ -189,7 +188,7 @@ export class LineChart } @computed private get focusedSeriesNameSet(): Set { - return this.manager.focusedSeriesNameSet ?? new Set() + return this.selectionArray.focusedEntityNameSet } @computed private get missingDataStrategy(): MissingDataStrategy { @@ -541,11 +540,7 @@ export class LineChart } @action.bound onLineLegendClick(seriesName: SeriesName): void { - if (this.focusedSeriesNameSet.has(seriesName)) { - this.manager.focusedSeriesNameSet?.delete(seriesName) - } else { - this.manager.focusedSeriesNameSet?.add(seriesName) - } + this.selectionArray.toggleFocus(seriesName) } // TODO diff --git a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts index 8f1c06a0133..10405feb450 100644 --- a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts +++ b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts @@ -15,11 +15,15 @@ export class SelectionArray { ) { this.selectedEntityNames = selectedEntityNames.slice() this.availableEntities = availableEntities.slice() + + this.focusedEntityNames = [] } @observable selectedEntityNames: EntityName[] @observable private availableEntities: Entity[] + @observable focusedEntityNames: EntityName[] + @computed get availableEntityNames(): string[] { return this.availableEntities.map((entity) => entity.entityName) } @@ -103,6 +107,33 @@ export class SelectionArray { return this } + @computed get focusedEntityNameSet(): Set { + return new Set(this.focusedEntityNames) + } + + @computed get hoveredEntityNameSet(): Set { + return new Set(this.focusedEntityNames) + } + + @action.bound focusEntity(entityName: EntityName): this { + if (!this.focusedEntityNameSet.has(entityName)) + this.focusedEntityNames.push(entityName) + return this + } + + @action.bound unfocusEntity(entityName: EntityName): this { + this.focusedEntityNames = this.focusedEntityNames.filter( + (name) => name !== entityName + ) + return this + } + + @action.bound toggleFocus(entityName: EntityName): this { + return this.focusedEntityNameSet.has(entityName) + ? this.unfocusEntity(entityName) + : this.focusEntity(entityName) + } + // Mainly for testing @action.bound selectSample(howMany = 1): this { return this.setSelectedEntities( diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 8d888691383..7b23a461d38 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -212,6 +212,10 @@ export class SlopeChart return makeSelectionArray(this.manager.selection) } + @computed private get focusedSeriesNameSet(): Set { + return this.selectionArray.focusedEntityNameSet + } + @computed private get formatColumn() { return this.yColumns[0] } @@ -839,16 +843,8 @@ export class SlopeChart }) } - @computed private get focusedSeriesNameSet(): Set { - return this.manager.focusedSeriesNameSet ?? new Set() - } - @action.bound onLineLegendClick(seriesName: SeriesName): void { - if (this.focusedSeriesNameSet.has(seriesName)) { - this.manager.focusedSeriesNameSet?.delete(seriesName) - } else { - this.manager.focusedSeriesNameSet?.add(seriesName) - } + this.selectionArray.toggleFocus(seriesName) } private hoverTimer?: NodeJS.Timeout From b26a81d5853c8f89f055475895bbc8df9e203d0a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 4 Dec 2024 19:49:31 +0100 Subject: [PATCH 91/91] =?UTF-8?q?=F0=9F=9A=A7=20introduce=20InteractionArr?= =?UTF-8?q?ay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartManager.ts | 2 + .../grapher/src/core/Grapher.tsx | 9 ++++ .../grapher/src/lineCharts/LineChart.tsx | 9 +++- .../src/schema/grapher-schema.006.yaml | 9 ++++ .../grapher/src/selection/InteractionArray.ts | 48 +++++++++++++++++++ .../grapher/src/selection/SelectionArray.ts | 31 ------------ .../grapher/src/slopeCharts/SlopeChart.tsx | 9 +++- .../types/src/grapherTypes/GrapherTypes.ts | 2 + 8 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 packages/@ourworldindata/grapher/src/selection/InteractionArray.ts diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 8b24bdd70a3..83009ae205a 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -17,6 +17,7 @@ import { TooltipManager } from "../tooltip/TooltipProps" import { OwidTable, CoreColumn } from "@ourworldindata/core-table" import { SelectionArray } from "../selection/SelectionArray" +import { InteractionArray } from "../selection/InteractionArray" import { ColumnSlug, SortConfig, TimeBound } from "@ourworldindata/utils" import { ColorScaleBin } from "../color/ColorScaleBin" import { ColorScale } from "../color/ColorScale" @@ -63,6 +64,7 @@ export interface ChartManager { sizeColumnSlug?: ColumnSlug colorColumnSlug?: ColumnSlug + interactionArray?: InteractionArray selection?: SelectionArray | EntityName[] entityType?: string diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index c9edeaa86dc..d970b43aef0 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -111,6 +111,7 @@ import { GRAPHER_TAB_NAMES, GRAPHER_TAB_QUERY_PARAMS, GrapherTabOption, + SeriesName, } from "@ourworldindata/types" import { BlankOwidTable, @@ -222,6 +223,7 @@ import { } from "../entitySelector/EntitySelector" import { SlideInDrawer } from "../slideInDrawer/SlideInDrawer" import { BodyDiv } from "../bodyDiv/BodyDiv" +import { InteractionArray } from "../selection/InteractionArray" declare global { interface Window { @@ -433,6 +435,7 @@ export class Grapher // Initializing arrays with `undefined` ensures that empty arrays get serialised @observable selectedEntityNames?: EntityName[] = undefined + @observable focusedSeriesNames?: SeriesName[] = undefined @observable excludedEntities?: number[] = undefined /** IncludedEntities are usually empty which means use all available entities. When includedEntities is set it means "only use these entities". excludedEntities @@ -565,6 +568,7 @@ export class Grapher ) obj.selectedEntityNames = this.selection.selectedEntityNames + obj.focusedSeriesNames = this.interactionArray.focusedEntityNames deleteRuntimeAndUnchangedProps(obj, defaultObject) @@ -606,6 +610,9 @@ export class Grapher if (obj.selectedEntityNames) this.selection.setSelectedEntities(obj.selectedEntityNames) + if (obj.focusedSeriesNames) + this.interactionArray.setFocusedEntities(obj.focusedSeriesNames) + // JSON doesn't support Infinity, so we use strings instead. this.minTime = minTimeBoundFromJSONOrNegativeInfinity(obj.minTime) this.maxTime = maxTimeBoundFromJSONOrPositiveInfinity(obj.maxTime) @@ -2537,6 +2544,8 @@ export class Grapher this.props.table?.availableEntities ?? [] ) + interactionArray = new InteractionArray() + @computed get availableEntities(): Entity[] { return this.tableForSelection.availableEntities } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index a36ecb21805..8dc6cfe25a7 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -107,6 +107,7 @@ import { getColorKey, getSeriesName, } from "./LineChartHelpers" +import { InteractionArray } from "../selection/InteractionArray" const LINE_CHART_CLASS_NAME = "LineChart" @@ -187,8 +188,12 @@ export class LineChart return makeSelectionArray(this.manager.selection) } + @computed get interactionArray(): InteractionArray { + return this.manager.interactionArray ?? new InteractionArray() + } + @computed private get focusedSeriesNameSet(): Set { - return this.selectionArray.focusedEntityNameSet + return this.interactionArray.focusedEntityNameSet } @computed private get missingDataStrategy(): MissingDataStrategy { @@ -540,7 +545,7 @@ export class LineChart } @action.bound onLineLegendClick(seriesName: SeriesName): void { - this.selectionArray.toggleFocus(seriesName) + this.interactionArray.toggleFocus(seriesName) } // TODO diff --git a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml index 67ae88895c5..72aeb7cbac9 100644 --- a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml +++ b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml @@ -104,6 +104,15 @@ properties: items: type: - string + focusedSeriesNames: + type: array + description: | + The initially focused chart elements (e.g. line or bar). + Is either a list of entity or variable names. + Only works to line and slope charts for now. + items: + type: + - string baseColorScheme: type: string description: | diff --git a/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts b/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts new file mode 100644 index 00000000000..9ae3c8f3746 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts @@ -0,0 +1,48 @@ +import { EntityName } from "@ourworldindata/types" +import { action, computed, observable } from "mobx" + +export class InteractionArray { + constructor(focusedEntityNames: EntityName[] = []) { + this.focusedEntityNames = focusedEntityNames.slice() + } + + @observable focusedEntityNames: EntityName[] + + @computed get focusedEntityNameSet(): Set { + return new Set(this.focusedEntityNames) + } + + @action.bound focusEntity(entityName: EntityName): this { + if (!this.focusedEntityNameSet.has(entityName)) + this.focusedEntityNames.push(entityName) + return this + } + + @action.bound unfocusEntity(entityName: EntityName): this { + this.focusedEntityNames = this.focusedEntityNames.filter( + (name) => name !== entityName + ) + return this + } + + @action.bound toggleFocus(entityName: EntityName): this { + return this.focusedEntityNameSet.has(entityName) + ? this.unfocusEntity(entityName) + : this.focusEntity(entityName) + } + + @action.bound clear(): void { + this.focusedEntityNames = [] + } + + @action.bound addToFocusedEntities(entityNames: EntityName[]): this { + this.focusedEntityNames = this.focusedEntityNames.concat(entityNames) + return this + } + + // Clears and sets focused entities + @action.bound setFocusedEntities(entityNames: EntityName[]): this { + this.clear() + return this.addToFocusedEntities(entityNames) + } +} diff --git a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts index 10405feb450..8f1c06a0133 100644 --- a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts +++ b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts @@ -15,15 +15,11 @@ export class SelectionArray { ) { this.selectedEntityNames = selectedEntityNames.slice() this.availableEntities = availableEntities.slice() - - this.focusedEntityNames = [] } @observable selectedEntityNames: EntityName[] @observable private availableEntities: Entity[] - @observable focusedEntityNames: EntityName[] - @computed get availableEntityNames(): string[] { return this.availableEntities.map((entity) => entity.entityName) } @@ -107,33 +103,6 @@ export class SelectionArray { return this } - @computed get focusedEntityNameSet(): Set { - return new Set(this.focusedEntityNames) - } - - @computed get hoveredEntityNameSet(): Set { - return new Set(this.focusedEntityNames) - } - - @action.bound focusEntity(entityName: EntityName): this { - if (!this.focusedEntityNameSet.has(entityName)) - this.focusedEntityNames.push(entityName) - return this - } - - @action.bound unfocusEntity(entityName: EntityName): this { - this.focusedEntityNames = this.focusedEntityNames.filter( - (name) => name !== entityName - ) - return this - } - - @action.bound toggleFocus(entityName: EntityName): this { - return this.focusedEntityNameSet.has(entityName) - ? this.unfocusEntity(entityName) - : this.focusEntity(entityName) - } - // Mainly for testing @action.bound selectSample(howMany = 1): this { return this.setSelectedEntities( diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 7b23a461d38..5c29720ccda 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -80,6 +80,7 @@ import { } from "../lineCharts/LineChartHelpers" import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends" import { CategoricalBin } from "../color/ColorScaleBin" +import { InteractionArray } from "../selection/InteractionArray" type SVGMouseOrTouchEvent = | React.MouseEvent @@ -212,8 +213,12 @@ export class SlopeChart return makeSelectionArray(this.manager.selection) } + @computed get interactionArray(): InteractionArray { + return this.manager.interactionArray ?? new InteractionArray() + } + @computed private get focusedSeriesNameSet(): Set { - return this.selectionArray.focusedEntityNameSet + return this.interactionArray.focusedEntityNameSet } @computed private get formatColumn() { @@ -844,7 +849,7 @@ export class SlopeChart } @action.bound onLineLegendClick(seriesName: SeriesName): void { - this.selectionArray.toggleFocus(seriesName) + this.interactionArray.toggleFocus(seriesName) } private hoverTimer?: NodeJS.Timeout diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 1d73e6a6470..cb146f77cc3 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -580,6 +580,7 @@ export interface GrapherInterface extends SortConfig { includedEntities?: number[] selectedEntityNames?: EntityName[] selectedEntityColors?: { [entityName: string]: string | undefined } + focusedSeriesNames?: SeriesName[] missingDataStrategy?: MissingDataStrategy hideFacetControl?: boolean facettingLabelByYVariables?: string @@ -698,6 +699,7 @@ export const grapherKeysToSerialize = [ "dimensions", "selectedEntityNames", "selectedEntityColors", + "focusedSeriesNames", "sortBy", "sortOrder", "sortColumnSlug",