>
+
+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/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..54aa0321aaa 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/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..02a2bd75f00 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(
{
@@ -48,10 +38,9 @@ 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.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 8b865b803d6..08604f5c25b 100644
--- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx
+++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx
@@ -1,830 +1,457 @@
-import React from "react"
+import React, { SVGProps } from "react"
import {
Bounds,
DEFAULT_BOUNDS,
- intersection,
- without,
- uniq,
isEmpty,
- last,
- sortBy,
- max,
- getRelativeMouse,
domainExtent,
- minBy,
exposeInstanceOnWindow,
PointVector,
clamp,
- HorizontalAlign,
- difference,
makeIdForHumanConsumption,
+ guid,
+ excludeUndefined,
+ partition,
+ max,
+ getRelativeMouse,
+ minBy,
} 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 {
- VerticalColorLegend,
- VerticalColorLegendManager,
-} from "../verticalColorLegend/VerticalColorLegend"
-import { ColorScale, ColorScaleManager } from "../color/ColorScale"
import {
BASE_FONT_SIZE,
+ GRAPHER_BACKGROUND_DEFAULT,
GRAPHER_DARK_TEXT,
- GRAPHER_FONT_SCALE_9_6,
- GRAPHER_FONT_SCALE_10_5,
+ GRAPHER_FONT_SCALE_12,
} from "../core/GrapherConstants"
import {
ScaleType,
- EntitySelectionMode,
- Color,
SeriesName,
ColorSchemeName,
+ ColumnSlug,
+ MissingDataStrategy,
+ Time,
+ SeriesStrategy,
+ EntityName,
+ 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 {
- DEFAULT_SLOPE_CHART_COLOR,
- LabelledSlopesProps,
+ PlacedSlopeChartSeries,
+ RawSlopeChartSeries,
SlopeChartSeries,
- SlopeChartValue,
- SlopeEntryProps,
} from "./SlopeChartConstants"
-import { OwidTable } from "@ourworldindata/core-table"
+import { CoreColumn, OwidTable } from "@ourworldindata/core-table"
import {
+ autoDetectSeriesStrategy,
autoDetectYColumnSlugs,
+ getDefaultFailMessage,
makeSelectionArray,
- isElementInteractive,
} 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 {
- 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"
+import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend"
+import {
+ makeTooltipRoundingNotice,
+ makeTooltipToleranceNotice,
+ Tooltip,
+ TooltipState,
+ TooltipValueRange,
+} from "../tooltip/Tooltip"
+import { TooltipFooterIcon } from "../tooltip/TooltipProps"
+import {
+ AnnotationsMap,
+ getAnnotationsForSeries,
+ getAnnotationsMap,
+ getColorKey,
+ getSeriesName,
+} from "../lineCharts/lineChartHelpers"
+
+type SVGMouseOrTouchEvent =
+ | React.MouseEvent
+ | React.TouchEvent
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
+const LINE_LEGEND_PADDING = 4
+
@observer
export class SlopeChart
extends React.Component<{
bounds?: Bounds
manager: SlopeChartManager
}>
- implements
- ChartInterface,
- VerticalColorLegendManager,
- HorizontalColorLegendManager,
- ColorScaleManager
+ implements ChartInterface
{
- // currently hovered individual series key
- @observable hoverKey?: string
- // currently hovered legend color
- @observable hoverColor?: string
+ slopeAreaRef: React.RefObject = React.createRef()
+ defaultBaseColorScheme = ColorSchemeName.OwidDistinctLines
- private hasInteractedWithChart = false
+ @observable hoveredSeriesName?: string
+ @observable tooltipState = new TooltipState<{
+ series: SlopeChartSeries
+ }>({ fade: "immediate" })
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])
-
- return table
- .dropRowsWithErrorValuesForColumn(this.yColumnSlug)
- .interpolateColumnWithTolerance(this.yColumnSlug)
- }
-
- @computed get manager() {
- return this.props.manager
- }
-
- @computed.struct get bounds() {
- return this.props.bounds ?? DEFAULT_BOUNDS
- }
-
- @computed get isStatic(): boolean {
- return this.manager.isStatic ?? false
- }
-
- @computed get fontSize() {
- return this.manager.fontSize ?? BASE_FONT_SIZE
- }
-
- @computed private get isPortrait(): boolean {
- return !!(this.manager.isNarrow || this.manager.isStaticAndSmall)
- }
-
- @computed private get showHorizontalLegend(): boolean {
- return !!(this.manager.isSemiNarrow || this.manager.isStaticAndSmall)
- }
+ table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs)
- // 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,
- })
- )
- }
+ if (this.isLogScale)
+ table = table.replaceNonPositiveCellsForLogScale(this.yColumnSlugs)
- @action.bound onSlopeMouseOver(slopeProps: SlopeEntryProps) {
- this.hoverKey = slopeProps.seriesName
- }
+ this.yColumnSlugs.forEach((slug) => {
+ table = table.interpolateColumnWithTolerance(slug)
+ })
- @action.bound onSlopeMouseLeave() {
- this.hoverKey = undefined
+ return table
}
- @action.bound onSlopeClick() {
- const { hoverKey, isEntitySelectionEnabled } = this
- if (!isEntitySelectionEnabled || hoverKey === undefined) {
- return
+ 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)
+ .dropEntitiesThatHaveNoDataInSomeColumn(this.yColumnSlugs)
}
- 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.
- @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)
- }
- @computed private get selectedEntityNames() {
- return this.selectionArray.selectedEntityNames
+ return table
}
- @computed get isEntitySelectionEnabled(): boolean {
- const { manager } = this
- return !!(
- manager.addCountryMode !== EntitySelectionMode.Disabled &&
- manager.addCountryMode
+ @computed get transformedTableFromGrapher(): OwidTable {
+ return (
+ this.manager.transformedTable ??
+ this.transformTable(this.inputTable)
)
}
- // 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)
+ @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
}
- // 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 private get manager(): SlopeChartManager {
+ return this.props.manager
}
- @computed get focusKeys() {
- return this.selectedEntityNames
+ @computed get inputTable(): OwidTable {
+ return this.manager.table
}
- // 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
+ @computed private get bounds(): Bounds {
+ return this.props.bounds ?? DEFAULT_BOUNDS
}
- // 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)
- )
+ @computed get fontSize() {
+ return this.manager.fontSize ?? BASE_FONT_SIZE
}
- // Only show colors on legend that are actually in use
- @computed private get colorsInUse() {
- return uniq(this.series.map((series) => series.color))
+ @computed private get isLogScale(): boolean {
+ return this.yScaleType === ScaleType.log
}
- @computed get legendAlign(): HorizontalAlign {
- return HorizontalAlign.left
+ @computed private get missingDataStrategy(): MissingDataStrategy {
+ return this.manager.missingDataStrategy || MissingDataStrategy.auto
}
- @computed get verticalColorLegend(): VerticalColorLegend {
- return new VerticalColorLegend({ manager: this })
+ @computed private get selectionArray() {
+ return makeSelectionArray(this.manager.selection)
}
- @computed get horizontalColorLegend(): HorizontalCategoricalColorLegend {
- return new HorizontalCategoricalColorLegend({ manager: this })
+ @computed private get formatColumn() {
+ return this.yColumns[0]
}
- @computed get legendHeight(): number {
- return this.showHorizontalLegend
- ? this.horizontalColorLegend.height
- : this.verticalColorLegend.height
+ @computed private get sidebarWidth(): number {
+ return this.showNoDataSection
+ ? clamp(this.bounds.width * 0.125, 60, 140)
+ : 0
}
- @computed get legendWidth(): number {
- return this.showHorizontalLegend
- ? this.bounds.width
- : this.verticalColorLegend.width
+ // used by LineLegend
+ @computed get focusedSeriesNames(): SeriesName[] {
+ return this.hoveredSeriesName ? [this.hoveredSeriesName] : []
}
- @computed get maxLegendWidth(): number {
- return this.showHorizontalLegend
- ? this.bounds.width
- : this.bounds.width * 0.5
+ @computed private get isFocusModeActive(): boolean {
+ return this.hoveredSeriesName !== undefined
}
- @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)
- }
-
- // correction is to account for the space taken by the legend
- @computed private get innerBounds() {
- const { sidebarWidth, showLegend, legendHeight } = this
- let bounds = this.bounds
- if (showLegend) {
- bounds = this.showHorizontalLegend
- ? bounds.padTop(legendHeight + 8)
- : bounds.padRight(sidebarWidth + 16)
- }
- return bounds
+ @computed private get startX(): number {
+ return this.xScale(this.startTime)
}
- // 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 endX(): number {
+ return this.xScale(this.endTime)
}
- @computed
- private get selectedEntitiesWithoutData(): string[] {
- return difference(
- this.selectedEntityNames,
- this.series.map((s) => s.seriesName)
- )
+ private updateTooltipPosition(event: SVGMouseOrTouchEvent) {
+ 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.legendX,
- this.legendY + this.legendHeight + 12,
- this.sidebarWidth,
- this.bounds.height - this.legendHeight - 12
- )
- return (
-
- )
- }
+ private detectHoveredSlope(event: SVGMouseOrTouchEvent) {
+ const ref = this.slopeAreaRef.current
+ if (!ref) return
- render() {
- if (this.failMessage)
- return (
-
- )
-
- const { manager } = this.props
- const {
- series,
- focusKeys,
- hoverKeys,
- innerBounds,
- showLegend,
- showHorizontalLegend,
- selectedEntitiesWithoutData,
- } = this
-
- const legend = showHorizontalLegend ? (
-
- ) : (
-
- )
-
- return (
-
-
- {showLegend && legend}
- {/* only show the "No data" section if there is space */}
- {showLegend &&
- !showHorizontalLegend &&
- selectedEntitiesWithoutData.length > 0 &&
- this.noDataSection}
-
- )
- }
+ const mouse = getRelativeMouse(ref, event)
+ this.mouseFrame = requestAnimationFrame(() => {
+ if (this.placedSeries.length === 0) return
- @computed get categoryLegendY(): number {
- return this.bounds.top
- }
-
- @computed get legendY() {
- return this.bounds.top
- }
+ const distanceMap = new Map()
+ for (const series of this.placedSeries) {
+ distanceMap.set(
+ series,
+ PointVector.distanceFromPointToLineSegmentSq(
+ mouse,
+ series.startPoint,
+ series.endPoint
+ )
+ )
+ }
- @computed get legendX(): number {
- return this.showHorizontalLegend
- ? this.bounds.left
- : this.bounds.right - this.sidebarWidth
+ 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() {
- if (this.yColumn.isMissing) return "Missing Y column"
+ 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 ""
}
- 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
-
- @computed private get yColumn() {
- return this.transformedTable.get(this.yColumnSlug)
- }
-
- @computed protected get yColumnSlug() {
- return autoDetectYColumnSlugs(this.manager)[0]
+ @computed private get yColumns(): CoreColumn[] {
+ return this.yColumnSlugs.map((slug) => this.transformedTable.get(slug))
}
- @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 protected get yColumnSlugs(): ColumnSlug[] {
+ return autoDetectYColumnSlugs(this.manager)
}
- @computed get transformedTable() {
+ @computed private get colorScheme(): ColorScheme {
return (
- this.manager.transformedTable ??
- this.transformTable(this.inputTable)
+ (this.manager.baseColorScheme
+ ? ColorSchemes.get(this.manager.baseColorScheme)
+ : null) ?? ColorSchemes.get(this.defaultBaseColorScheme)
)
}
- @computed get inputTable() {
- 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()
-
- colorColumn.valueByEntityNameAndOriginalTime.forEach(
- (timeToColorMap, seriesName) => {
- const values = Array.from(timeToColorMap.values())
- const key = last(values)
- colorByEntity.set(seriesName, colorScale.getColor(key))
- }
- )
-
- return colorByEntity
+ @computed private get startTime(): Time {
+ return this.transformedTable.minTime ?? 0
}
- // 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 endTime(): Time {
+ return this.transformedTable.maxTime ?? 0
}
- @computed private get grapherElement() {
- return this.manager.base?.current
+ @computed get seriesStrategy(): SeriesStrategy {
+ return autoDetectSeriesStrategy(this.manager, true)
}
- 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)
+ @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,
+ })
}
- componentWillUnmount(): void {
- if (this.grapherElement) {
- this.grapherElement.removeEventListener(
- "mousedown",
- this.onGrapherClick
- )
- }
+ @computed private get annotationsMap(): AnnotationsMap | undefined {
+ return getAnnotationsMap(this.inputTable, this.yColumnSlugs[0])
}
- @computed get series() {
- const column = this.yColumn
- if (!column) return []
-
- const { colorBySeriesName } = this
- const { minTime, maxTime } = column
-
- const table = this.inputTable
-
- return column.uniqEntityNames
- .map((seriesName) => {
- const values: SlopeChartValue[] = []
-
- const yValues =
- column.valueByEntityNameAndOriginalTime.get(seriesName)! ||
- []
-
- yValues.forEach((value, time) => {
- if (time !== minTime && time !== maxTime) return
+ private constructSingleSeries(
+ entityName: EntityName,
+ column: CoreColumn
+ ): RawSlopeChartSeries {
+ const { startTime, endTime, seriesStrategy } = this
+ const { canSelectMultipleEntities = false } = this.manager
- values.push({
- x: time,
- y: value,
- })
- })
-
- // sort values by time
- const sortedValues = sortBy(values, (v) => v.x)
-
- const color =
- table.getColorForEntityName(seriesName) ??
- colorBySeriesName.get(seriesName) ??
- DEFAULT_SLOPE_CHART_COLOR
-
- return {
- seriesName,
- color,
- values: sortedValues,
- } as SlopeChartSeries
- })
- .filter((series) => series.values.length >= 2)
- }
-}
-
-@observer
-class SlopeEntry extends React.Component {
- line: SVGElement | null = null
+ const { availableEntityNames } = this.selectionArray
+ const columnName = column.nonEmptyDisplayName
+ const seriesName = getSeriesName({
+ entityName,
+ columnName,
+ seriesStrategy,
+ availableEntityNames,
+ canSelectMultipleEntities,
+ })
- @computed get isInBackground() {
- const { isLayerMode, isHovered, isFocused } = this.props
+ const owidRowByTime = column.owidRowByEntityNameAndTime.get(entityName)
+ const start = owidRowByTime?.get(startTime)
+ const end = owidRowByTime?.get(endTime)
- if (!isLayerMode) return false
+ const colorKey = getColorKey({
+ entityName,
+ columnName,
+ seriesStrategy,
+ availableEntityNames,
+ })
+ const color = this.categoricalColorAssigner.assign(colorKey)
- return !(isHovered || isFocused)
- }
+ const annotation = getAnnotationsForSeries(
+ this.annotationsMap,
+ seriesName
+ )
- render() {
- const {
- x1,
- y1,
- x2,
- y2,
- color,
- hasLeftLabel,
- hasRightLabel,
- leftValueLabel,
- leftEntityLabel,
- rightValueLabel,
- rightEntityLabel,
- leftEntityLabelBounds,
- rightEntityLabelBounds,
- isFocused,
- isHovered,
- isMultiHoverMode,
+ return {
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 showDots = isFocused || isHovered
- const showValueLabels = isFocused || isHovered
- const showLeftEntityLabel = isFocused || (isHovered && isMultiHoverMode)
-
- const sharedLabelProps = {
- fill: labelColor,
- style: { cursor: "default" },
+ entityName,
+ color,
+ start,
+ end,
+ annotation,
}
+ }
+ private isSeriesValid(
+ series: RawSlopeChartSeries
+ ): series is SlopeChartSeries {
+ const { start, end } = series
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:
- isFocused || isHovered ? "bold" : undefined,
- },
- }
- )}
-
+ start?.value !== undefined &&
+ end?.value !== undefined &&
+ start.originalTime < end.originalTime
)
}
-}
-@observer
-class LabelledSlopes
- extends React.Component
- implements AxisManager
-{
- base: React.RefObject = React.createRef()
-
- @computed private get data() {
- return this.props.seriesArr
- }
+ /**
+ * 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
- @computed private get yColumn() {
- return this.props.yColumn
- }
+ if (
+ this.seriesStrategy === SeriesStrategy.column &&
+ this.missingDataStrategy === MissingDataStrategy.hide
+ ) {
+ const entitySeries = this.rawSeriesByEntityName.get(
+ series.entityName
+ )
+ return !!entitySeries?.every((series) => this.isSeriesValid(series))
+ }
- @computed private get manager() {
- return this.props.manager
+ return true
}
- @computed private get bounds() {
- return this.props.bounds
+ @computed private get rawSeries(): RawSlopeChartSeries[] {
+ return this.yColumns.flatMap((column) =>
+ this.selectionArray.selectedEntityNames.map((entityName) =>
+ this.constructSingleSeries(entityName, column)
+ )
+ )
}
- @computed get fontSize() {
- return this.manager.fontSize ?? BASE_FONT_SIZE
+ @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 focusedSeriesNames() {
- return intersection(
- this.props.focusKeys || [],
- this.data.map((g) => g.seriesName)
+ @computed get series(): SlopeChartSeries[] {
+ return this.rawSeries.filter((series) =>
+ this.shouldSeriesBePlotted(series)
)
}
- @computed private get hoveredSeriesNames() {
- return intersection(
- this.props.hoverKeys || [],
- this.data.map((g) => g.seriesName)
- )
- }
+ @computed private get placedSeries(): PlacedSlopeChartSeries[] {
+ const { yAxis, startX, endX } = this
- // 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)
- )
- }
+ return this.series.map((series) => {
+ const startY = yAxis.place(series.start.value)
+ const endY = yAxis.place(series.end.value)
- @computed private get isMultiHoverMode() {
- return this.hoveredSeriesNames.length > 1
- }
+ const startPoint = new PointVector(startX, startY)
+ const endPoint = new PointVector(endX, endY)
- @computed get isPortrait(): boolean {
- return this.props.isPortrait
+ return { ...series, startPoint, endPoint }
+ })
}
- @computed private get allValues() {
- return this.props.seriesArr.flatMap((g) => g.values)
+ @computed
+ private get noDataSeries(): RawSlopeChartSeries[] {
+ return this.rawSeries.filter((series) => !this.isSeriesValid(series))
}
- @computed private get xDomainDefault(): [number, number] {
- return domainExtent(
- this.allValues.map((v) => v.x),
- ScaleType.linear
- )
+ @computed private get showNoDataSection(): boolean {
+ return this.noDataSeries.length > 0
}
@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.yColumn
- axis.label = ""
- return axis
+ @computed private get allValues(): number[] {
+ return this.series.flatMap((series) => [
+ series.start.value,
+ series.end.value,
+ ])
}
- @computed private get yScaleType() {
- return this.yAxisConfig.scaleType || ScaleType.linear
+ @computed private get yScaleType(): ScaleType {
+ return this.yAxisConfig.scaleType ?? ScaleType.linear
}
@computed private get yDomainDefault(): [number, number] {
- return domainExtent(
- this.allValues.map((v) => v.y),
- this.yScaleType || ScaleType.linear
- )
- }
-
- @computed private get xDomain(): [number, number] {
- return this.xDomainDefault
+ return domainExtent(this.allValues, this.yScaleType)
}
@computed private get yDomain(): [number, number] {
@@ -843,30 +470,17 @@ class LabelledSlopes
.yRange()
}
- @computed get yAxisWidth(): number {
- return this.yAxis.width + 5 // 5px account for the tick marks
+ @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 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()
+ @computed get yAxisWidth(): number {
+ return this.yAxis.width + 5 // 5px account for the tick marks
}
@computed private get xScale(): ScaleLinear {
@@ -874,446 +488,554 @@ 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
+ @computed private get xDomain(): [number, number] {
+ return [this.startTime, this.endTime]
+ }
+
+ @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
)
- })
- return max(maxLabelWidths) ?? 0
+ )!
}
- @computed get allowedLabelWidth() {
- return this.bounds.width * 0.2
+ @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 private get slopeLabels() {
- const { isPortrait, yColumn, allowedLabelWidth: maxWidth } = this
+ @computed get xRange(): [number, number] {
+ const lineLegendWidth = this.maxLineLegendWidth + 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
+ 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
+ )
- 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 = yColumn.formatValueShort(v1.y)
- const rightValueStr = yColumn.formatValueShort(v2.y)
+ const currentSlopeWidth = endX - startX
+ if (currentSlopeWidth > maxSlopeWidth) {
+ const padding = currentSlopeWidth - maxSlopeWidth
+ startX += padding / 2
+ endX -= padding / 2
+ }
- // 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 [startX, endX]
+ }
+ @computed get lineLegendX(): number {
+ return this.xRange[1] + LINE_LEGEND_PADDING
+ }
+
+ // used in LineLegend
+ @computed get labelSeries(): LineLabelSeries[] {
+ return this.series.map((series) => {
+ const { seriesName, color, end, annotation } = series
return {
- seriesName: series.seriesName,
- leftValueLabel,
- leftEntityLabel,
- rightValueLabel,
- rightEntityLabel,
+ color,
+ seriesName,
+ label: seriesName,
+ annotation,
+ yValue: end.value,
}
})
}
- @computed private get initialSlopeData() {
- const { data, slopeLabels, xScale, yAxis, yDomain } = this
+ private playIntroAnimation() {
+ // Nice little intro animation
+ select(this.slopeAreaRef.current)
+ .select(".slopes")
+ .attr("stroke-dasharray", "100%")
+ .attr("stroke-dashoffset", "100%")
+ .transition()
+ .attr("stroke-dashoffset", "0%")
+ }
- const slopeData: SlopeEntryProps[] = []
+ componentDidMount() {
+ exposeInstanceOnWindow(this)
- 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,
- isFocused: false,
- isHovered: false,
- hasLeftLabel: true,
- hasRightLabel: true,
- } as SlopeEntryProps)
- })
+ if (!this.manager.disableIntroAnimation) {
+ this.playIntroAnimation()
+ }
+ }
- return slopeData
+ private hoverTimer?: NodeJS.Timeout
+ @action.bound onLineLegendMouseOver(seriesName: SeriesName): void {
+ clearTimeout(this.hoverTimer)
+ this.hoveredSeriesName = seriesName
}
- @computed get backgroundGroups() {
- return this.slopeData.filter(
- (group) => !(group.isHovered || group.isFocused)
- )
+ @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)
}
- @computed get foregroundGroups() {
- return this.slopeData.filter(
- (group) => !!(group.isHovered || group.isFocused)
- )
+ @action.bound onSlopeMouseOver(series: SlopeChartSeries) {
+ this.hoveredSeriesName = series.seriesName
+ this.tooltipState.target = { series }
}
- // Get the final slope data with hover focusing and collision detection
- @computed get slopeData(): SlopeEntryProps[] {
- const { focusedSeriesNames, hoveredSeriesNames } = this
+ @action.bound onSlopeMouseLeave() {
+ this.hoveredSeriesName = undefined
+ this.tooltipState.target = null
+ }
- let slopeData = this.initialSlopeData
+ mouseFrame?: number
+ @action.bound onMouseMove(event: SVGMouseOrTouchEvent) {
+ this.updateTooltipPosition(event)
+ this.detectHoveredSlope(event)
+ }
- 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
- )
+ @action.bound onMouseLeave() {
+ if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame)
- // used to determine priority for labelling conflicts
- const isFocused = focusedSeriesNames.includes(slope.seriesName)
- const isHovered = hoveredSeriesNames.includes(slope.seriesName)
+ this.onSlopeMouseLeave()
+ }
- return {
- ...slope,
- leftEntityLabelBounds,
- rightEntityLabelBounds,
- isFocused,
- isHovered,
- }
- })
+ @computed private get lineStrokeWidth(): number {
+ const factor = this.manager.isStaticAndSmall ? 2 : 1
+ return factor * 2
+ }
- // 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.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
- else if (!s1.hasRightLabel && s2.hasRightLabel) return s2
- else return s1 // Equal priority, just do the first one
- }
+ @computed private get backgroundColor(): string {
+ return this.manager.backgroundColor ?? GRAPHER_BACKGROUND_DEFAULT
+ }
- // 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
- }
- })
- })
+ @computed get renderUid(): number {
+ return guid()
+ }
- 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
- }
- })
- })
+ @computed get tooltip(): React.ReactElement | undefined {
+ const {
+ tooltipState: { target, position, fading },
+ startTime,
+ endTime,
+ } = this
- // Order by focus/hover for draw order
- slopeData = sortBy(slopeData, (slope) =>
- slope.isFocused || slope.isHovered ? 1 : 0
- )
+ const { series } = target || {}
+ if (!series) return
- return slopeData
- }
+ const formatTime = (time: Time) => this.formatColumn.formatTime(time)
- mouseFrame?: number
- @action.bound onMouseLeave() {
- if (this.mouseFrame !== undefined) cancelAnimationFrame(this.mouseFrame)
+ 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
- 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()
- }
- }
- })
- }
- }
+ const { isRelativeMode } = this.manager,
+ timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}`,
+ timeLabel = timeRange + (isRelativeMode ? " (relative change)" : "")
- @action.bound onClick() {
- if (this.props.onClick) this.props.onClick()
- }
+ 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
+ )
+ )
- componentDidMount() {
- if (!this.manager.disableIntroAnimation) {
- this.playIntroAnimation()
+ 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
+ }
}
- }
- 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%")
+ const targetYear = constructTargetYearForToleranceNotice()
+ const toleranceNotice = targetYear
+ ? {
+ icon: TooltipFooterIcon.notice,
+ text: makeTooltipToleranceNotice(targetYear),
+ }
+ : undefined
+ const roundingNotice = anyRoundedToSigFigs
+ ? {
+ icon: allRoundedToSigFigs
+ ? TooltipFooterIcon.none
+ : TooltipFooterIcon.significance,
+ text: makeTooltipRoundingNotice(sigFigs, {
+ plural: sigFigs.length > 1,
+ }),
+ }
+ : undefined
+ const footer = excludeUndefined([toleranceNotice, roundingNotice])
+
+ return (
+ (this.tooltipState.target = null)}
+ >
+
+
+ )
}
- renderGroups(groups: SlopeEntryProps[]) {
- const { isLayerMode, isMultiHoverMode } = this
+ 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
+ )
- return groups.map((slope) => (
-
- ))
+ )
}
- render() {
- const { bounds, slopeData, xDomain, yAxis, yRange, onMouseMove } = this
+ private renderSlope(
+ series: PlacedSlopeChartSeries,
+ mode?: RenderMode
+ ): React.ReactElement {
+ return (
+
+ )
+ }
- if (isEmpty(slopeData))
- return
+ private renderSlopes() {
+ if (!this.isFocusModeActive) {
+ return this.placedSeries.map((series) => this.renderSlope(series))
+ }
- const { x1, x2 } = slopeData[0]
- const [y1, y2] = yRange
+ const [focusedSeries, backgroundSeries] = partition(
+ this.placedSeries,
+ (series) => series.seriesName === this.hoveredSeriesName
+ )
return (
-
+ <>
+ {backgroundSeries.map((series) =>
+ this.renderSlope(series, RenderMode.mute)
+ )}
+ {focusedSeries.map((series) =>
+ this.renderSlope(series, RenderMode.focus)
+ )}
+ >
+ )
+ }
+
+ private renderChartArea() {
+ const { bounds, xDomain, yRange, startX, endX } = this
+
+ const [bottom, top] = 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.yColumn.formatTime(xDomain[0])}
-
-
+
+
- {this.yColumn.formatTime(xDomain[1])}
-
-
- {this.renderGroups(this.backgroundGroups)}
- {this.renderGroups(this.foregroundGroups)}
+
+ {this.renderSlopes()}
)
}
+
+ render() {
+ if (this.failMessage)
+ return (
+
+ )
+
+ return (
+
+ {this.renderChartArea()}
+ {this.manager.showLegend && }
+ {this.showNoDataSection && this.renderNoDataSection()}
+ {this.tooltip}
+
+ )
+ }
+}
+
+interface SlopeProps {
+ series: PlacedSlopeChartSeries
+ color: string
+ mode?: RenderMode
+ dotRadius?: number
+ strokeWidth?: number
+ outlineWidth?: number
+ outlineStroke?: string
+ onMouseOver?: (series: SlopeChartSeries) => void
+ onMouseLeave?: () => void
+}
+
+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 opacity = {
+ [RenderMode.default]: 1,
+ [RenderMode.focus]: 1,
+ [RenderMode.mute]: 0.3,
+ [RenderMode.background]: 0.3,
+ }[mode]
+
+ return (
+ 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 (
+ <>
+
+
+ >
+ )
+}
+
+interface GridLinesProps {
+ bounds: Bounds
+ yAxis: VerticalAxis
+ startX: number
+ endX: number
+}
+
+function GridLines({ bounds, yAxis, startX, endX }: GridLinesProps) {
+ return (
+
+ {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 */}
+
+
+ )
+ })}
+
+ )
+}
+
+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 bb52f727212..321ac2e6932 100644
--- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts
+++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChartConstants.ts
@@ -1,62 +1,19 @@
-import { CoreColumn } from "@ourworldindata/core-table"
+import { PartialBy, PointVector } from "@ourworldindata/utils"
+import { EntityName, OwidVariableRow } from "@ourworldindata/types"
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
- y: number
-}
export interface SlopeChartSeries extends ChartSeries {
- size: number
- values: SlopeChartValue[]
+ entityName: EntityName
+ start: Pick, "value" | "originalTime">
+ end: Pick, "value" | "originalTime">
+ annotation?: string
}
-export const DEFAULT_SLOPE_CHART_COLOR = "#ff7f0e"
-
-export interface SlopeEntryProps extends ChartSeries {
- isLayerMode: boolean
- isMultiHoverMode: boolean
- x1: number
- y1: number
- x2: number
- y2: number
-
- hasLeftLabel: boolean
- leftEntityLabel: TextWrap
- leftValueLabel: TextWrap
- leftEntityLabelBounds: Bounds
+export type RawSlopeChartSeries = PartialBy
- hasRightLabel: boolean
- rightEntityLabel: TextWrap
- rightEntityLabelBounds: Bounds
- rightValueLabel: TextWrap
-
- isFocused: boolean
- isHovered: boolean
-}
-
-export interface LabelledSlopesProps {
- manager: ChartManager
- yColumn: CoreColumn
- bounds: Bounds
- seriesArr: SlopeChartSeries[]
- focusKeys: string[]
- hoverKeys: string[]
- onMouseOver: (slopeProps: SlopeEntryProps) => void
- onMouseLeave: () => void
- onClick: () => void
- isPortrait: boolean
+export interface PlacedSlopeChartSeries extends SlopeChartSeries {
+ startPoint: PointVector
+ endPoint: PointVector
}
-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/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/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx
index 857a08a1b4a..96bb76f8625 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 (
({
- time: row.time,
+ time: row.originalTime,
position: row.entityName,
value: row.value,
valueOffset: 0,
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(
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
}
diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
index 1d73e6a6470..8306197da2f 100644
--- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
+++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
@@ -208,6 +208,13 @@ 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 562dd93191d..0f42e50f1ad 100644
--- a/packages/@ourworldindata/types/src/index.ts
+++ b/packages/@ourworldindata/types/src/index.ts
@@ -113,6 +113,7 @@ export {
GrapherWindowType,
AxisMinMaxValueStr,
GrapherTooltipAnchor,
+ RenderMode,
} from "./grapherTypes/GrapherTypes.js"
export {
diff --git a/vite.config-site.mts.timestamp-1732528761102-d1ef0b3f0da3.mjs b/vite.config-site.mts.timestamp-1732528761102-d1ef0b3f0da3.mjs
new file mode 100644
index 00000000000..1ff6001c7dc
--- /dev/null
+++ b/vite.config-site.mts.timestamp-1732528761102-d1ef0b3f0da3.mjs
@@ -0,0 +1,358 @@
+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 = [
+ "es2019",
+ // Array.flat, Array.flatMap, Object.fromEntries, ...
+ "es2020",
+ // String.matchAll, Promise.allSettled, ...
+ "es2021",
+ // String.replaceAll, Promise.any, ...
+ "es2022",
+ // Array.at, String.at, ...
+ "es2023",
+ // Array.findLast, Array.toReversed, Array.toSorted, Array.with, ...
+ "IntersectionObserver",
+ "IntersectionObserverEntry",
+ "ResizeObserver",
+ "globalThis"
+ // some dependencies use this
+];
+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: ["chrome66", "firefox78", "safari12"],
+ // 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+XG4gICAgICAgIGZlYXR1cmVGbGFnc1Jhdy5pbmNsdWRlcyhrZXkpXG4gICAgKSBhcyBGZWF0dXJlRmxhZ0ZlYXR1cmVbXVxuKVxuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3NpdGUvU2l0ZUNvbnN0YW50cy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc29waGlhL2NvZGUvb3dpZC9vd2lkLWdyYXBoZXIvc2l0ZS9TaXRlQ29uc3RhbnRzLnRzXCI7aW1wb3J0IHsgZmFSc3MgfSBmcm9tIFwiQGZvcnRhd2Vzb21lL2ZyZWUtc29saWQtc3ZnLWljb25zXCJcbmltcG9ydCB7XG4gICAgZmFYVHdpdHRlcixcbiAgICBmYUZhY2Vib29rU3F1YXJlLFxuICAgIGZhSW5zdGFncmFtLFxuICAgIGZhVGhyZWFkcyxcbiAgICBmYUxpbmtlZGluLFxuICAgIGZhQmx1ZXNreSxcbn0gZnJvbSBcIkBmb3J0YXdlc29tZS9mcmVlLWJyYW5kcy1zdmctaWNvbnNcIlxuXG4vLyBTZWUgaHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9wb2x5ZmlsbC8gZm9yIGEgbGlzdCBvZiBhbGwgc3VwcG9ydGVkIGZlYXR1cmVzXG5jb25zdCBwb2x5ZmlsbEZlYXR1cmVzID0gW1xuICAgIFwiZXMyMDE5XCIsIC8vIEFycmF5LmZsYXQsIEFycmF5LmZsYXRNYXAsIE9iamVjdC5mcm9tRW50cmllcywgLi4uXG4gICAgXCJlczIwMjBcIiwgLy8gU3RyaW5nLm1hdGNoQWxsLCBQcm9taXNlLmFsbFNldHRsZWQsIC4uLlxuICAgIFwiZXMyMDIxXCIsIC8vIFN0cmluZy5yZXBsYWNlQWxsLCBQcm9taXNlLmFueSwgLi4uXG4gICAgXCJlczIwMjJcIiwgLy8gQXJyYXkuYXQsIFN0cmluZy5hdCwgLi4uXG4gICAgXCJlczIwMjNcIiwgLy8gQXJyYXkuZmluZExhc3QsIEFycmF5LnRvUmV2ZXJzZWQsIEFycmF5LnRvU29ydGVkLCBBcnJheS53aXRoLCAuLi5cbiAgICBcIkludGVyc2VjdGlvbk9ic2VydmVyXCIsXG4gICAgXCJJbnRlcnNlY3Rpb25PYnNlcnZlckVudHJ5XCIsXG4gICAgXCJSZXNpemVPYnNlcnZlclwiLFxuICAgIFwiZ2xvYmFsVGhpc1wiLCAvLyBzb21lIGRlcGVuZGVuY2llcyB1c2UgdGhpc1xuXVxuY29uc3QgUE9MWUZJTExfVkVSU0lPTiA9IFwiNC44LjBcIlxuZXhwb3J0IGNvbnN0IFBPTFlGSUxMX1VSTDogc3RyaW5nID0gYGh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vcG9seWZpbGwvdjMvcG9seWZpbGwubWluLmpzP3ZlcnNpb249JHtQT0xZRklMTF9WRVJTSU9OfSZmZWF0dXJlcz0ke3BvbHlmaWxsRmVhdHVyZXMuam9pbihcbiAgICBcIixcIlxuKX1gXG5cbmV4cG9ydCBjb25zdCBERUZBVUxUX0xPQ0FMX0JBS0VfRElSID0gXCJsb2NhbEJha2VcIlxuXG5leHBvcnQgY29uc3QgR1JBUEhFUl9QUkVWSUVXX0NMQVNTID0gXCJncmFwaGVyUHJldmlld1wiXG5cbmV4cG9ydCBjb25zdCBTTUFMTF9CUkVBS1BPSU5UX01FRElBX1FVRVJZID0gXCIobWF4LXdpZHRoOiA3NjhweClcIlxuXG5leHBvcnQgY29uc3QgVE9VQ0hfREVWSUNFX01FRElBX1FVRVJZID1cbiAgICBcIihob3Zlcjogbm9uZSksIChwb2ludGVyOiBjb2Fyc2UpLCAocG9pbnRlcjogbm9uZSlcIlxuXG5leHBvcnQgY29uc3QgREFUQV9JTlNJR0hUU19BVE9NX0ZFRURfTkFNRSA9IFwiYXRvbS1kYXRhLWluc2lnaHRzLnhtbFwiXG5cbmV4cG9ydCBjb25zdCBEQVRBX0lOU0lHSFRfQVRPTV9GRUVEX1BST1BTID0ge1xuICAgIHRpdGxlOiBcIkF0b20gZmVlZCBmb3IgRGFpbHkgRGF0YSBJbnNpZ2h0c1wiLFxuICAgIGhyZWY6IGBodHRwczovL291cndvcmxkaW5kYXRhLm9yZy8ke0RBVEFfSU5TSUdIVFNfQVRPTV9GRUVEX05BTUV9YCxcbn1cblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfVE9NQlNUT05FX1JFQVNPTiA9XG4gICAgXCJPdXIgV29ybGQgaW4gRGF0YSBpcyBkZXNpZ25lZCB0byBiZSBhbiBldmVyZ3JlZW4gcHVibGljYXRpb24uIFRoaXMgXCIgK1xuICAgIFwibWVhbnMgdGhhdCB3aGVuIGEgcGFnZSBjYW5ub3QgYmUgdXBkYXRlZCBkdWUgdG8gb3V0ZGF0ZWQgZGF0YSBvciBcIiArXG4gICAgXCJtaXNzaW5nIGluZm9ybWF0aW9uLCB3ZSBwcmVmZXIgdG8gcmVtb3ZlIGl0IHJhdGhlciB0aGFuIHByZXNlbnQgXCIgK1xuICAgIFwiaW5jb21wbGV0ZSBvciBpbmFjY3VyYXRlIHJlc2VhcmNoIGFuZCBkYXRhIHRvIG91ciByZWFkZXJzLlwiXG5cbmV4cG9ydCBjb25zdCBTT0NJQUxTID0gW1xuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiWFwiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly94LmNvbS9vdXJ3b3JsZGluZGF0YVwiLFxuICAgICAgICBpY29uOiBmYVhUd2l0dGVyLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJJbnN0YWdyYW1cIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3Lmluc3RhZ3JhbS5jb20vb3Vyd29ybGRpbmRhdGEvXCIsXG4gICAgICAgIGljb246IGZhSW5zdGFncmFtLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJUaHJlYWRzXCIsXG4gICAgICAgIHVybDogXCJodHRwczovL3d3dy50aHJlYWRzLm5ldC9Ab3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFUaHJlYWRzLFxuICAgIH0sXG4gICAge1xuICAgICAgICB0aXRsZTogXCJGYWNlYm9va1wiLFxuICAgICAgICB1cmw6IFwiaHR0cHM6Ly9mYWNlYm9vay5jb20vb3Vyd29ybGRpbmRhdGFcIixcbiAgICAgICAgaWNvbjogZmFGYWNlYm9va1NxdWFyZSxcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiTGlua2VkSW5cIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9jb21wYW55L291cndvcmxkaW5kYXRhXCIsXG4gICAgICAgIGljb246IGZhTGlua2VkaW4sXG4gICAgfSxcbiAgICB7XG4gICAgICAgIHRpdGxlOiBcIkJsdWVza3lcIixcbiAgICAgICAgdXJsOiBcImh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9vdXJ3b3JsZGluZGF0YS5vcmdcIixcbiAgICAgICAgaWNvbjogZmFCbHVlc2t5LFxuICAgIH0sXG5dXG5cbmV4cG9ydCBjb25zdCBSU1NfRkVFRFMgPSBbXG4gICAge1xuICAgICAgICB0aXRsZTogXCJSZXNlYXJjaCAmIFdyaXRpbmcgUlNTIEZlZWRcIixcbiAgICAgICAgdXJsOiBcIi9hdG9tLnhtbFwiLFxuICAgICAgICBpY29uOiBmYVJzcyxcbiAgICB9LFxuICAgIHtcbiAgICAgICAgdGl0bGU6IFwiRGFpbHkgRGF0YSBJbnNpZ2h0cyBSU1MgRmVlZFwiLFxuICAgICAgICB1cmw6IGAvJHtEQVRBX0lOU0lHSFRTX0FUT01fRkVFRF9OQU1FfWAsXG4gICAgICAgIGljb246IGZhUnNzLFxuICAgIH0sXG5dXG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlclwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3ZpdGUuY29uZmlnLWNvbW1vbi5tdHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgcGx1Z2luUmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCJcbmltcG9ydCBwbHVnaW5DaGVja2VyIGZyb20gXCJ2aXRlLXBsdWdpbi1jaGVja2VyXCJcbmltcG9ydCAqIGFzIGNsaWVudFNldHRpbmdzIGZyb20gXCIuL3NldHRpbmdzL2NsaWVudFNldHRpbmdzLmpzXCJcbmltcG9ydCB7XG4gICAgVklURV9BU1NFVF9TSVRFX0VOVFJZLFxuICAgIFZJVEVfRU5UUllQT0lOVF9JTkZPLFxuICAgIFZpdGVFbnRyeVBvaW50LFxufSBmcm9tIFwiLi9zaXRlL3ZpdGVVdGlscy5qc1wiXG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgY29uc3QgZGVmaW5lVml0ZUNvbmZpZ0ZvckVudHJ5cG9pbnQgPSAoZW50cnlwb2ludDogVml0ZUVudHJ5UG9pbnQpID0+IHtcbiAgICBjb25zdCBlbnRyeXBvaW50SW5mbyA9IFZJVEVfRU5UUllQT0lOVF9JTkZPW2VudHJ5cG9pbnRdXG5cbiAgICByZXR1cm4gZGVmaW5lQ29uZmlnKHtcbiAgICAgICAgcHVibGljRGlyOiBmYWxzZSwgLy8gZG9uJ3QgY29weSBwdWJsaWMgZm9sZGVyIHRvIGRpc3RcbiAgICAgICAgcmVzb2x2ZToge1xuICAgICAgICAgICAgLy8gcHJldHRpZXItaWdub3JlXG4gICAgICAgICAgICBhbGlhczoge1xuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXIvc3JjXCI6IFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXIvc3JjXCIsIC8vIG5lZWQgdGhpcyBmb3IgaW1wb3J0cyBvZiBAb3Vyd29ybGRpbmRhdGEvZ3JhcGhlci9zcmMvY29yZS9ncmFwaGVyLnNjc3MgdG8gd29ya1xuICAgICAgICAgICAgICAgIC8vIHdlIGFsaWFzIHRvIHRoZSBwYWNrYWdlcyBzb3VyY2UgZmlsZXMgaW4gZGV2IGFuZCBwcm9kOlxuICAgICAgICAgICAgICAgIC8vIHRoaXMgbWVhbnMgd2UgZ2V0IGluc3RhbnQgZGV2IHVwZGF0ZXMgd2hlbiB3ZSBjaGFuZ2Ugb25lIG9mIHRoZW0sXG4gICAgICAgICAgICAgICAgLy8gYW5kIHRoZSBwcm9kIGJ1aWxkIGJ1aWxkcyB0aGVtIGFsbCBhcyBlc20gbW9kdWxlcywgd2hpY2ggaGVscHMgd2l0aCB0cmVlIHNoYWtpbmdcbiAgICAgICAgICAgICAgICAvLyBJZGVhIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL0xpbnVzQm9yZy92dWUtbGliLXRlbXBsYXRlL2Jsb2IvMzc3NWU0OWIyMGE3YzMzNDlkZDQ5MzIxY2FkMmVkN2Y5ZDU3NTA1Ny9wYWNrYWdlcy9wbGF5Z3JvdW5kL3ZpdGUuY29uZmlnLnRzXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvY29tcG9uZW50c1wiOiBcIkBvdXJ3b3JsZGluZGF0YS9jb21wb25lbnRzL3NyYy9pbmRleC50c1wiLFxuICAgICAgICAgICAgICAgIFwiQG91cndvcmxkaW5kYXRhL2NvcmUtdGFibGVcIjogXCJAb3Vyd29ybGRpbmRhdGEvY29yZS10YWJsZS9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9leHBsb3JlclwiOiBcIkBvdXJ3b3JsZGluZGF0YS9leHBsb3Jlci9zcmMvaW5kZXgudHNcIixcbiAgICAgICAgICAgICAgICBcIkBvdXJ3b3JsZGluZGF0YS9ncmFwaGVyXCI6IFwiQG91cndvcmxkaW5kYXRhL2dyYXBoZXIvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvdHlwZXNcIjogXCJAb3Vyd29ybGRpbmRhdGEvdHlwZXMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICAgICAgXCJAb3Vyd29ybGRpbmRhdGEvdXRpbHNcIjogXCJAb3Vyd29ybGRpbmRhdGEvdXRpbHMvc3JjL2luZGV4LnRzXCIsXG4gICAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgICBjc3M6IHtcbiAgICAgICAgICAgIGRldlNvdXJjZW1hcDogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICAgICAgZGVmaW5lOiB7XG4gICAgICAgICAgICAvLyBSZXBsYWNlIGFsbCBjbGllbnRTZXR0aW5ncyB3aXRoIHRoZWlyIHJlc3BlY3RpdmUgdmFsdWVzLCBpLmUuIGFzc2lnbiBlLmcuIEJVR1NOQUdfQVBJX0tFWSB0byBwcm9jZXNzLmVudi5CVUdTTkFHX0FQSV9LRVlcbiAgICAgICAgICAgIC8vIGl0J3MgaW1wb3J0YW50IHRvIG5vdGUgdGhhdCB3ZSBvbmx5IGV4cG9zZSB2YWx1ZXMgdGhhdCBhcmUgcHJlc2VudCBpbiB0aGUgY2xpZW50U2V0dGluZ3MgZmlsZSAtIG5vdCBhbnkgb3RoZXIgdGhpbmdzIHRoYXQgYXJlIHN0b3JlZCBpbiAuZW52XG4gICAgICAgICAgICAuLi5PYmplY3QuZnJvbUVudHJpZXMoXG4gICAgICAgICAgICAgICAgT2JqZWN0LmVudHJpZXMoY2xpZW50U2V0dGluZ3MpLm1hcCgoW2tleSwgdmFsdWVdKSA9PiBbXG4gICAgICAgICAgICAgICAgICAgIGBwcm9jZXNzLmVudi4ke2tleX1gLFxuICAgICAgICAgICAgICAgICAgICBKU09OLnN0cmluZ2lmeSh2YWx1ZSksXG4gICAgICAgICAgICAgICAgXSlcbiAgICAgICAgICAgICksXG4gICAgICAgIH0sXG4gICAgICAgIGJ1aWxkOiB7XG4gICAgICAgICAgICBtYW5pZmVzdDogdHJ1ZSwgLy8gY3JlYXRlcyBhIG1hbmlmZXN0Lmpzb24gZmlsZSwgd2hpY2ggd2UgdXNlIHRvIGRldGVybWluZSB3aGljaCBmaWxlcyB0byBsb2FkIGluIHByb2RcbiAgICAgICAgICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICAgICAgICAgICAgb3V0RGlyOiBgZGlzdC8ke2VudHJ5cG9pbnRJbmZvLm91dERpcn1gLFxuICAgICAgICAgICAgc291cmNlbWFwOiB0cnVlLFxuICAgICAgICAgICAgdGFyZ2V0OiBbXCJjaHJvbWU2NlwiLCBcImZpcmVmb3g3OFwiLCBcInNhZmFyaTEyXCJdLCAvLyBzZWUgZG9jcy9icm93c2VyLXN1cHBvcnQubWRcbiAgICAgICAgICAgIHJvbGx1cE9wdGlvbnM6IHtcbiAgICAgICAgICAgICAgICBpbnB1dDoge1xuICAgICAgICAgICAgICAgICAgICBbZW50cnlwb2ludEluZm8ub3V0TmFtZV06IGVudHJ5cG9pbnRJbmZvLmVudHJ5UG9pbnRGaWxlLFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgb3V0cHV0OiB7XG4gICAgICAgICAgICAgICAgICAgIGFzc2V0RmlsZU5hbWVzOiBgJHtlbnRyeXBvaW50SW5mby5vdXROYW1lfS5jc3NgLFxuICAgICAgICAgICAgICAgICAgICBlbnRyeUZpbGVOYW1lczogYCR7ZW50cnlwb2ludEluZm8ub3V0TmFtZX0ubWpzYCxcbiAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICAgICAgcGx1Z2luczogW1xuICAgICAgICAgICAgcGx1Z2luUmVhY3Qoe1xuICAgICAgICAgICAgICAgIGJhYmVsOiB7XG4gICAgICAgICAgICAgICAgICAgIHBhcnNlck9wdHM6IHtcbiAgICAgICAgICAgICAgICAgICAgICAgIHBsdWdpbnM6IFtcImRlY29yYXRvcnMtbGVnYWN5XCJdLCAvLyBuZWVkZWQgc28gbW9ieCBkZWNvcmF0b3JzIHdvcmsgY29ycmVjdGx5XG4gICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH0pLFxuICAgICAgICAgICAgcGx1Z2luQ2hlY2tlcih7XG4gICAgICAgICAgICAgICAgdHlwZXNjcmlwdDoge1xuICAgICAgICAgICAgICAgICAgICBidWlsZE1vZGU6IHRydWUsXG4gICAgICAgICAgICAgICAgICAgIHRzY29uZmlnUGF0aDogXCJ0c2NvbmZpZy52aXRlLWNoZWNrZXIuanNvblwiLFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9KSxcbiAgICAgICAgXSxcbiAgICAgICAgc2VydmVyOiB7XG4gICAgICAgICAgICBwb3J0OiA4MDkwLFxuICAgICAgICAgICAgd2FybXVwOiB7IGNsaWVudEZpbGVzOiBbVklURV9BU1NFVF9TSVRFX0VOVFJZXSB9LFxuICAgICAgICB9LFxuICAgICAgICBwcmV2aWV3OiB7XG4gICAgICAgICAgICBwb3J0OiA4MDkwLFxuICAgICAgICB9LFxuICAgIH0pXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlclwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3NvcGhpYS9jb2RlL293aWQvb3dpZC1ncmFwaGVyL3ZpdGUuY29uZmlnLXNpdGUubXRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zb3BoaWEvY29kZS9vd2lkL293aWQtZ3JhcGhlci92aXRlLmNvbmZpZy1zaXRlLm10c1wiO2ltcG9ydCB7IFZpdGVFbnRyeVBvaW50IH0gZnJvbSBcIi4vc2l0ZS92aXRlVXRpbHMudHN4XCJcbmltcG9ydCB7IGRlZmluZVZpdGVDb25maWdGb3JFbnRyeXBvaW50IH0gZnJvbSBcIi4vdml0ZS5jb25maWctY29tbW9uLm10c1wiXG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZVZpdGVDb25maWdGb3JFbnRyeXBvaW50KFZpdGVFbnRyeVBvaW50LlNpdGUpXG4iXSwKICAibWFwcGluZ3MiOiAiOzs7Ozs7O0FBQUEsT0FBTyxXQUFXOzs7QUNBdVMsT0FBTyxVQUFVO0FBQzFVLE9BQU8sUUFBUTtBQVFBLFNBQVIsbUJBQW9DLE1BQWtDO0FBQ3pFLE1BQUksQ0FBQyxHQUFHLFdBQVksUUFBTztBQUUzQixNQUFJLE1BQU0sS0FBSyxRQUFRLElBQUk7QUFFM0IsU0FBTyxJQUFJLFFBQVE7QUFDZixRQUFJLEdBQUcsV0FBVyxLQUFLLFFBQVEsS0FBSyxjQUFjLENBQUMsRUFBRyxRQUFPO0FBRTdELFVBQU0sWUFBWSxLQUFLLFFBQVEsS0FBSyxJQUFJO0FBRXhDLFFBQUksY0FBYyxJQUFLO0FBQUEsUUFDbEIsT0FBTTtBQUFBLEVBQ2Y7QUFFQSxTQUFPO0FBQ1g7OztBRHRCQSxPQUFPQSxTQUFROzs7QUVDZixPQUFPQyxXQUFV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QVE7QUFBQSxNQUNKLE1BQU07QUFBQSxNQUNOLFFBQVEsRUFBRSxhQUFhLENBQUMscUJBQXFCLEVBQUU7QUFBQSxJQUNuRDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLElBQ1Y7QUFBQSxFQUNKLENBQUM7QUFDTDs7O0FDakZBLElBQU8sMkJBQVEsK0NBQWlEOyIsCiAgIm5hbWVzIjogWyJmcyIsICJwYXRoIiwgImRvdGVudiIsICJmcyIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJiYXNlRGlyIiwgIkZlYXR1cmVGbGFnRmVhdHVyZSIsICJwYXJzZUludE9yVW5kZWZpbmVkIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgIl9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lIiwgImRvdGVudiIsICJCQUtFRF9CQVNFX1VSTCIsICJBRE1JTl9CQVNFX1VSTCIsICJCQUtFRF9HUkFQSEVSX1VSTCIsICJCVUdTTkFHX0FQSV9LRVkiLCAicGFyc2VJbnRPclVuZGVmaW5lZCIsICJwYXRoIiwgIkdET0NTX0RFVEFJTFNfT05fREVNQU5EX0lEIiwgInBhdGgiLCAiZnMiLCAiSU1BR0VfSE9TVElOR19SMl9DRE5fVVJMIiwgIklNQUdFX0hPU1RJTkdfUjJfQlVDS0VUX1BBVEgiLCAiSU1BR0VfSE9TVElOR19SMl9CVUNLRVRfU1VCRk9MREVSX1BBVEgiLCAiQURNSU5fQkFTRV9VUkwiXQp9Cg==