This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new b6bf6a83b51 UI: Fix misleading Calendar Total Runs coloring behavior
(#67595)
b6bf6a83b51 is described below
commit b6bf6a83b519881d7805e269cac0895409132232
Author: YeoooonShin <[email protected]>
AuthorDate: Wed Jun 17 23:46:26 2026 +0900
UI: Fix misleading Calendar Total Runs coloring behavior (#67595)
* fix: add missing queued state color to CalendarToolTip (May 15)
* fix: calendar cell coloring to prioritize severity in Total Runs view
* refactor: clarify calendar cell color keys and extract priority
resolution logic
* test: add missing test cases for calendar scale total runs
* fix: expand calendar legend to clarify running and failed states
---
.../ui/src/pages/Dag/Calendar/CalendarCell.tsx | 10 +-
.../ui/src/pages/Dag/Calendar/CalendarLegend.tsx | 109 ++++++++++----
.../ui/src/pages/Dag/Calendar/CalendarTooltip.tsx | 1 +
.../src/pages/Dag/Calendar/calendarUtils.test.ts | 134 ++++++++++++++++-
.../ui/src/pages/Dag/Calendar/calendarUtils.ts | 164 ++++++++++++++-------
.../src/airflow/ui/src/pages/Dag/Calendar/types.ts | 4 +-
6 files changed, 326 insertions(+), 96 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
index ea91e34309b..6660194329e 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
@@ -28,8 +28,8 @@ type Props = {
| Record<string, string>
| string
| {
- actual: string | { _dark: string; _light: string };
- planned: string | { _dark: string; _light: string };
+ primary: string | { _dark: string; _light: string };
+ secondary: string | { _dark: string; _light: string };
};
readonly cellData: CalendarCellData | undefined;
readonly index?: number;
@@ -64,7 +64,7 @@ export const CalendarCell = ({
: [];
const isMixedState =
- typeof backgroundColor === "object" && "planned" in backgroundColor &&
"actual" in backgroundColor;
+ typeof backgroundColor === "object" && "secondary" in backgroundColor &&
"primary" in backgroundColor;
const cellBox = isMixedState ? (
<Box
@@ -82,14 +82,14 @@ export const CalendarCell = ({
width="14px"
>
<Box
- bg={backgroundColor.planned}
+ bg={backgroundColor.secondary}
clipPath="polygon(0 100%, 100% 100%, 0 0)"
height="100%"
position="absolute"
width="100%"
/>
<Box
- bg={backgroundColor.actual}
+ bg={backgroundColor.primary}
clipPath="polygon(100% 0, 100% 100%, 0 0)"
height="100%"
position="absolute"
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
index 480c1a8e077..7f12162bb5b 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
@@ -30,8 +30,48 @@ type Props = {
readonly viewMode: CalendarColorMode;
};
+type LegendColorType =
+ | Record<string, string>
+ | string
+ | { primary: Record<string, string> | string; secondary: Record<string,
string> | string };
+
+const LegendIcon = ({ color, cursor }: { readonly color: LegendColorType;
readonly cursor?: string }) => {
+ const isMixedState = typeof color === "object" && "primary" in color &&
"secondary" in color;
+
+ if (isMixedState) {
+ return (
+ <Box
+ borderRadius="2px"
+ boxShadow="sm"
+ cursor={cursor}
+ height="14px"
+ overflow="hidden"
+ position="relative"
+ width="14px"
+ >
+ <Box
+ bg={color.secondary}
+ clipPath="polygon(0 100%, 100% 100%, 0 0)"
+ height="100%"
+ position="absolute"
+ width="100%"
+ />
+ <Box
+ bg={color.primary}
+ clipPath="polygon(100% 0, 100% 100%, 0 0)"
+ height="100%"
+ position="absolute"
+ width="100%"
+ />
+ </Box>
+ );
+ }
+
+ return <Box bg={color} borderRadius="2px" boxShadow="sm" cursor={cursor}
height="14px" width="14px" />;
+};
+
export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props)
=> {
- const { t: translate } = useTranslation("dag");
+ const { t: translate } = useTranslation(["dag", "common"]);
const legendTitle =
viewMode === "failed" ? translate("overview.buttons.failedRun_other") :
translate("calendar.totalRuns");
@@ -54,7 +94,9 @@ export const CalendarLegend = ({ scale, vertical = false,
viewMode }: Props) =>
<VStack gap={0.5}>
{[...scale.legendItems].reverse().map(({ color, label }) => (
<Tooltip content={`${label} ${viewMode === "failed" ? "failed"
: "runs"}`} key={label}>
- <Box bg={color} borderRadius="2px" cursor="pointer"
height="14px" width="14px" />
+ <Box>
+ <LegendIcon color={color} cursor="pointer" />
+ </Box>
</Tooltip>
))}
</VStack>
@@ -70,7 +112,9 @@ export const CalendarLegend = ({ scale, vertical = false,
viewMode }: Props) =>
<HStack gap={0.5}>
{scale.legendItems.map(({ color, label }) => (
<Tooltip content={`${label} ${viewMode === "failed" ? "failed"
: "runs"}`} key={label}>
- <Box bg={color} borderRadius="2px" cursor="pointer"
height="14px" width="14px" />
+ <Box>
+ <LegendIcon color={color} cursor="pointer" />
+ </Box>
</Tooltip>
))}
</HStack>
@@ -83,42 +127,49 @@ export const CalendarLegend = ({ scale, vertical = false,
viewMode }: Props) =>
<Box>
<HStack gap={4} justify="center" wrap="wrap">
+ {viewMode === "total" && (
+ <>
+ <HStack gap={2}>
+ <LegendIcon color={{ _dark: "green.700", _light: "green.400"
}} />
+ <Text color="fg.muted" fontSize="xs">
+ {translate("common:states.success")}
+ </Text>
+ </HStack>
+ <HStack gap={2}>
+ <LegendIcon color={{ _dark: "cyan.700", _light: "cyan.400" }}
/>
+ <Text color="fg.muted" fontSize="xs">
+ {translate("common:states.running")}
+ </Text>
+ </HStack>
+ </>
+ )}
+
+ <HStack gap={2}>
+ <LegendIcon color={{ _dark: "red.700", _light: "red.400" }} />
+ <Text color="fg.muted" fontSize="xs">
+ {translate("common:states.failed")}
+ </Text>
+ </HStack>
+
<HStack gap={2}>
<Box bg={PLANNED_COLOR} borderRadius="2px" boxShadow="sm"
height="14px" width="14px" />
<Text color="fg.muted" fontSize="xs">
{translate("common:states.planned")}
</Text>
</HStack>
+
<HStack gap={2}>
- <Box
- borderRadius="2px"
- boxShadow="sm"
- height="14px"
- overflow="hidden"
- position="relative"
- width="14px"
- >
- <Box
- bg={PLANNED_COLOR}
- clipPath="polygon(0 100%, 100% 100%, 0 0)"
- height="100%"
- position="absolute"
- width="100%"
- />
- <Box
- bg={
+ <LegendIcon
+ color={{
+ primary:
viewMode === "failed"
? { _dark: "red.700", _light: "red.400" }
- : { _dark: "green.700", _light: "green.400" }
- }
- clipPath="polygon(100% 0, 100% 100%, 0 0)"
- height="100%"
- position="absolute"
- width="100%"
- />
- </Box>
+ : { _dark: "green.700", _light: "green.400" },
+ secondary: PLANNED_COLOR,
+ }}
+ />
<Text color="fg.muted" fontSize="xs">
- {translate("calendar.legend.mixed")}
+ {translate("dag:calendar.legend.mixed")}
</Text>
</HStack>
</HStack>
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
index f5a3a50ecf9..90ae1d8ab36 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
@@ -32,6 +32,7 @@ type Props = {
const stateColorMap = {
failed: "failed.solid",
planned: "stone.solid",
+ queued: "queued.solid",
running: "running.solid",
success: "success.solid",
};
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
index 4cb86deec29..c35225cebdc 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
@@ -27,6 +27,7 @@ const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" };
const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" };
const DEFAULT_TOTAL_COLOR = { _dark: "green.700", _light: "green.400" };
const DEFAULT_FAILED_COLOR = { _dark: "red.700", _light: "red.400" };
+const DEFAULT_RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" };
const EMPTY_COUNTS: RunCounts = {
failed: 0,
@@ -158,8 +159,8 @@ describe("createCalendarScale", () => {
});
expect(scale.getColor({ ...EMPTY_COUNTS, planned: 1, success: 1, total: 2
})).toEqual({
- actual: DEFAULT_TOTAL_COLOR,
- planned: PLANNED_COLOR,
+ primary: DEFAULT_TOTAL_COLOR,
+ secondary: PLANNED_COLOR,
});
});
@@ -171,8 +172,57 @@ describe("createCalendarScale", () => {
});
expect(scale.getColor({ ...EMPTY_COUNTS, queued: 1, success: 1, total: 2
})).toEqual({
- actual: DEFAULT_TOTAL_COLOR,
- planned: PLANNED_COLOR,
+ primary: DEFAULT_TOTAL_COLOR,
+ secondary: PLANNED_COLOR,
+ });
+ });
+
+ it("returns the failed color for a failed-only cell in total mode", () => {
+ const scale = createCalendarScale([run("failed", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1
})).toEqual(DEFAULT_FAILED_COLOR);
+ });
+
+ it("returns a mixed red and green color for failed and success runs in total
mode", () => {
+ const scale = createCalendarScale([run("failed", 1), run("success", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, success: 1, total: 2
})).toEqual({
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: DEFAULT_TOTAL_COLOR,
+ });
+ });
+
+ it("returns a mixed cyan and green color for running and success runs in
total mode", () => {
+ const scale = createCalendarScale([run("running", 1), run("success", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, running: 1, success: 1, total: 2
})).toEqual({
+ primary: DEFAULT_RUNNING_COLOR,
+ secondary: DEFAULT_TOTAL_COLOR,
+ });
+ });
+
+ it("returns a mixed cyan and red color for running and failed runs in total
mode", () => {
+ const scale = createCalendarScale([run("running", 1), run("failed", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, running: 1, total: 2
})).toEqual({
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: DEFAULT_RUNNING_COLOR,
});
});
@@ -195,8 +245,8 @@ describe("createCalendarScale", () => {
});
expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, planned: 1, total: 2
})).toEqual({
- actual: DEFAULT_FAILED_COLOR,
- planned: PLANNED_COLOR,
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: PLANNED_COLOR,
});
});
@@ -218,8 +268,76 @@ describe("createCalendarScale", () => {
});
expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, queued: 1, total: 2
})).toEqual({
- actual: DEFAULT_FAILED_COLOR,
- planned: PLANNED_COLOR,
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: PLANNED_COLOR,
+ });
+ });
+
+ it("returns the correct gradient color when runs span across different
dates", () => {
+ const scale = createCalendarScale(
+ [run("failed", 1, "2026-04-08T10:00:00Z"), run("failed", 5,
"2026-04-09T10:00:00Z")],
+ {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ },
+ );
+
+ const lowIntensityColor = { _dark: "red.900", _light: "red.200" };
+ const highIntensityColor = { _dark: "red.300", _light: "red.800" };
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1
})).toEqual(lowIntensityColor);
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 5, total: 5
})).toEqual(highIntensityColor);
+ });
+
+ it("prioritizes failed over running over success when multiple actual states
coexist with pending", () => {
+ const scale = createCalendarScale([run("planned", 1), run("failed", 1),
run("success", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, planned: 1, success:
1, total: 3 })).toEqual({
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: PLANNED_COLOR,
});
});
+
+ it("returns an empty scale when no data is provided", () => {
+ const scale = createCalendarScale([], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.type).toBe("empty");
+ expect(scale.getColor(EMPTY_COUNTS)).toEqual(EMPTY_COLOR);
+ expect(scale.legendItems).toEqual([{ color: EMPTY_COLOR, label: "0" }]);
+ });
+
+ it("prioritizes running and failed colors when failed, running, and success
coexist without pending states", () => {
+ const scale = createCalendarScale([run("failed", 1), run("running", 1),
run("success", 1)], {
+ granularity: "hourly",
+ timezone: "UTC",
+ viewMode: "total",
+ });
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, running: 1, success:
1, total: 3 })).toEqual({
+ primary: DEFAULT_FAILED_COLOR,
+ secondary: DEFAULT_RUNNING_COLOR,
+ });
+ });
+
+ it("returns the correct gradient color for failed mode when failed runs span
across different dates", () => {
+ const scale = createCalendarScale(
+ [run("failed", 1, "2026-04-08T10:00:00Z"), run("failed", 10,
"2026-04-09T10:00:00Z")],
+ { granularity: "hourly", timezone: "UTC", viewMode: "failed" },
+ );
+
+ const lowIntensityFailedColor = { _dark: "red.900", _light: "red.200" };
+ const highIntensityFailedColor = { _dark: "red.300", _light: "red.800" };
+
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1
})).toEqual(lowIntensityFailedColor);
+ expect(scale.getColor({ ...EMPTY_COUNTS, failed: 10, total: 10
})).toEqual(highIntensityFailedColor);
+ });
});
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
index 3f57eb35030..527ebc6389c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
@@ -40,6 +40,7 @@ dayjs.extend(tz);
// Calendar color constants
export const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" };
const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" };
+const RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" };
const TOTAL_COLOR_INTENSITIES = [
EMPTY_COLOR, // 0
@@ -244,6 +245,75 @@ type ScaleOptions = {
viewMode: CalendarColorMode;
};
+type ColorValue = string | { _dark: string; _light: string };
+
+type ResolveColorParams = {
+ failedColor: ColorValue;
+ failedCount: number;
+ hasPending: boolean;
+ runningCount: number;
+ successColor: ColorValue;
+ successCount: number;
+};
+
+const resolveCellColor = ({
+ failedColor,
+ failedCount,
+ hasPending,
+ runningCount,
+ successColor,
+ successCount,
+}: ResolveColorParams): ColorValue | { primary: ColorValue; secondary:
ColorValue } => {
+ const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0;
+
+ if (hasPending && hasActual) {
+ let primaryColor: ColorValue = EMPTY_COLOR;
+
+ if (failedCount > 0) {
+ primaryColor = failedColor;
+ } else if (runningCount > 0) {
+ primaryColor = RUNNING_COLOR;
+ } else if (successCount > 0) {
+ primaryColor = successColor;
+ }
+
+ return {
+ primary: primaryColor,
+ secondary: PLANNED_COLOR,
+ };
+ }
+
+ if (hasPending && !hasActual) {
+ return PLANNED_COLOR;
+ }
+
+ if (hasActual) {
+ if (failedCount > 0 && runningCount > 0) {
+ return { primary: failedColor, secondary: RUNNING_COLOR };
+ }
+
+ if (failedCount > 0 && successCount > 0) {
+ return { primary: failedColor, secondary: successColor };
+ }
+
+ if (runningCount > 0 && successCount > 0) {
+ return { primary: RUNNING_COLOR, secondary: successColor };
+ }
+
+ if (failedCount > 0) {
+ return failedColor;
+ }
+ if (runningCount > 0) {
+ return RUNNING_COLOR;
+ }
+ if (successCount > 0) {
+ return successColor;
+ }
+ }
+
+ return EMPTY_COLOR;
+};
+
export const createCalendarScale = (
data: Array<CalendarTimeRangeResponse>,
options: ScaleOptions,
@@ -267,22 +337,23 @@ export const createCalendarScale = (
return {
getColor: (counts: RunCounts) => {
- const actualCount = getActualRunCount(counts, viewMode);
- const hasPending = getPendingRunCount(counts) > 0;
- const hasActual = actualCount > 0;
-
- if (hasPending && hasActual) {
- return {
- actual: singleColor,
- planned: PLANNED_COLOR,
- };
- }
+ const failedCount = counts.failed;
+ const runningCount = viewMode === "total" ? counts.running : 0;
+ const successCount = viewMode === "total" ? counts.success : 0;
- if (hasPending && !hasActual) {
- return PLANNED_COLOR;
- }
+ const hasPending = getPendingRunCount(counts) > 0;
- return actualCount === 0 ? EMPTY_COLOR : singleColor;
+ const failedColor = FAILURE_COLOR_INTENSITIES[2] ?? EMPTY_COLOR;
+ const successColor = TOTAL_COLOR_INTENSITIES[2] ?? EMPTY_COLOR;
+
+ return resolveCellColor({
+ failedColor,
+ failedCount,
+ hasPending,
+ runningCount,
+ successColor,
+ successCount,
+ });
},
legendItems: [
{ color: EMPTY_COLOR, label: "0" },
@@ -312,54 +383,43 @@ export const createCalendarScale = (
| string
| { _dark: string; _light: string }
| {
- actual: string | { _dark: string; _light: string };
- planned: string | { _dark: string; _light: string };
+ primary: string | { _dark: string; _light: string };
+ secondary: string | { _dark: string; _light: string };
} => {
- const actualCount = getActualRunCount(counts, viewMode);
- const hasPending = getPendingRunCount(counts) > 0;
- const hasActual = actualCount > 0;
+ const failedCount = counts.failed;
+ const runningCount = viewMode === "total" ? counts.running : 0;
+ const successCount = viewMode === "total" ? counts.success : 0;
- if (hasPending && hasActual) {
- let actualColor = colorScheme[0] ?? EMPTY_COLOR;
+ const hasPending = getPendingRunCount(counts) > 0;
+ const getIntensityColor = (count: number, scheme: Array<ColorValue>) => {
+ if (count === 0) {
+ return scheme[0] ?? EMPTY_COLOR;
+ }
for (let index = uniqueThresholds.length - 1; index >= 1; index -= 1) {
const threshold = uniqueThresholds[index];
- if (threshold !== undefined && actualCount >= threshold) {
- actualColor = colorScheme[Math.min(index, colorScheme.length - 1)]
?? EMPTY_COLOR;
- break;
+ if (threshold !== undefined && count >= threshold) {
+ return scheme[Math.min(index, scheme.length - 1)] ?? EMPTY_COLOR;
}
}
- if (actualCount > 0 && actualColor === colorScheme[0]) {
- actualColor = colorScheme[1] ?? EMPTY_COLOR;
- }
-
- return {
- actual: actualColor,
- planned: PLANNED_COLOR,
- };
- }
-
- if (hasPending && !hasActual) {
- return PLANNED_COLOR;
- }
-
- const targetCount = actualCount;
-
- if (targetCount === 0) {
- return colorScheme[0] ?? EMPTY_COLOR;
- }
-
- for (let index = uniqueThresholds.length - 1; index >= 1; index -= 1) {
- const threshold = uniqueThresholds[index];
-
- if (threshold !== undefined && targetCount >= threshold) {
- return colorScheme[Math.min(index, colorScheme.length - 1)] ??
EMPTY_COLOR;
- }
- }
+ return scheme[1] ?? EMPTY_COLOR;
+ };
- return colorScheme[1] ?? EMPTY_COLOR;
+ const failedColor =
+ failedCount > 0 ? getIntensityColor(failedCount,
FAILURE_COLOR_INTENSITIES) : EMPTY_COLOR;
+ const successColor =
+ successCount > 0 ? getIntensityColor(successCount,
TOTAL_COLOR_INTENSITIES) : EMPTY_COLOR;
+
+ return resolveCellColor({
+ failedColor,
+ failedCount,
+ hasPending,
+ runningCount,
+ successColor,
+ successCount,
+ });
};
const legendItems: Array<LegendItem> = [];
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
index 8ef78a66af4..7e93c637734 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
@@ -69,8 +69,8 @@ export type CalendarScale = {
| string
| { _dark: string; _light: string }
| {
- actual: string | { _dark: string; _light: string };
- planned: string | { _dark: string; _light: string };
+ primary: string | { _dark: string; _light: string };
+ secondary: string | { _dark: string; _light: string };
};
readonly legendItems: Array<LegendItem>;
readonly type: CalendarScaleType;