This is an automated email from the ASF dual-hosted git repository.

bbovenzi 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 fa0e02c2f85 fix(ui): modify calendar cell colors (#56161)
fa0e02c2f85 is described below

commit fa0e02c2f85e9edea6fc6e29d62115552b635fdd
Author: LI,JHE-CHEN <[email protected]>
AuthorDate: Tue Sep 30 12:41:01 2025 -0400

    fix(ui): modify calendar cell colors (#56161)
    
    * fix(ui): modify calendar cell colors
    
    * feat(ui): Add legend for mixed state
---
 .../src/airflow/ui/public/i18n/locales/en/dag.json |  1 +
 .../ui/src/pages/Dag/Calendar/CalendarCell.tsx     | 39 ++++++++++-
 .../ui/src/pages/Dag/Calendar/CalendarLegend.tsx   | 33 +++++++++-
 .../ui/src/pages/Dag/Calendar/calendarUtils.ts     | 76 +++++++++++++++++-----
 .../src/airflow/ui/src/pages/Dag/Calendar/types.ts |  8 ++-
 5 files changed, 136 insertions(+), 21 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index fd1eef88f95..c5293ca7dba 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -10,6 +10,7 @@
     "hourly": "Hourly",
     "legend": {
       "less": "Less",
+      "mixed": "Mixed",
       "more": "More"
     },
     "navigation": {
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 da6973810ea..4428a2b4e42 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
@@ -25,7 +25,13 @@ import { CalendarTooltip } from "./CalendarTooltip";
 import type { CalendarCellData, CalendarColorMode } from "./types";
 
 type Props = {
-  readonly backgroundColor: Record<string, string> | string;
+  readonly backgroundColor:
+    | Record<string, string>
+    | string
+    | {
+        actual: string | { _dark: string; _light: string };
+        planned: string | { _dark: string; _light: string };
+      };
   readonly cellData: CalendarCellData | undefined;
   readonly index?: number;
   readonly marginRight?: string;
@@ -52,7 +58,36 @@ export const CalendarCell = ({
   const hasData = Boolean(cellData && relevantCount > 0);
   const hasTooltip = Boolean(cellData);
 
-  const cellBox = (
+  const isMixedState =
+    typeof backgroundColor === "object" && "planned" in backgroundColor && 
"actual" in backgroundColor;
+
+  const cellBox = isMixedState ? (
+    <Box
+      _hover={hasData ? { transform: "scale(1.1)" } : {}}
+      borderRadius="2px"
+      cursor={hasData ? "pointer" : "default"}
+      height="14px"
+      marginRight={computedMarginRight}
+      overflow="hidden"
+      position="relative"
+      width="14px"
+    >
+      <Box
+        bg={backgroundColor.planned}
+        clipPath="polygon(0 100%, 100% 100%, 0 0)"
+        height="100%"
+        position="absolute"
+        width="100%"
+      />
+      <Box
+        bg={backgroundColor.actual}
+        clipPath="polygon(100% 0, 100% 100%, 0 0)"
+        height="100%"
+        position="absolute"
+        width="100%"
+      />
+    </Box>
+  ) : (
     <Box
       _hover={hasData ? { transform: "scale(1.1)" } : {}}
       bg={backgroundColor}
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 86ed307f8d8..480c1a8e077 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
@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
 
 import { Tooltip } from "src/components/ui";
 
+import { PLANNED_COLOR } from "./calendarUtils";
 import type { CalendarScale, CalendarColorMode } from "./types";
 
 type Props = {
@@ -82,16 +83,42 @@ export const CalendarLegend = ({ scale, vertical = false, 
viewMode }: Props) =>
 
       <Box>
         <HStack gap={4} justify="center" wrap="wrap">
+          <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
-              bg={{ _dark: "stone.600", _light: "stone.200" }}
               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={
+                  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>
             <Text color="fg.muted" fontSize="xs">
-              {translate("common:states.planned")}
+              {translate("calendar.legend.mixed")}
             </Text>
           </HStack>
         </HStack>
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 4ea24f89f81..3d4fbaab846 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
@@ -1,3 +1,5 @@
+/* eslint-disable max-lines */
+
 /*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -34,23 +36,23 @@ import type {
 dayjs.extend(isSameOrBefore);
 
 // Calendar color constants
+export const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" };
 const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" };
-const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.200" };
 
 const TOTAL_COLOR_INTENSITIES = [
   EMPTY_COLOR, // 0
-  { _dark: "green.300", _light: "green.200" },
-  { _dark: "green.500", _light: "green.400" },
-  { _dark: "green.700", _light: "green.600" },
-  { _dark: "green.900", _light: "green.800" },
+  { _dark: "green.900", _light: "green.200" },
+  { _dark: "green.700", _light: "green.400" },
+  { _dark: "green.500", _light: "green.600" },
+  { _dark: "green.300", _light: "green.800" },
 ];
 
 const FAILURE_COLOR_INTENSITIES = [
   EMPTY_COLOR, // 0
-  { _dark: "red.300", _light: "red.200" },
-  { _dark: "red.500", _light: "red.400" },
-  { _dark: "red.700", _light: "red.600" },
-  { _dark: "red.900", _light: "red.800" },
+  { _dark: "red.900", _light: "red.200" },
+  { _dark: "red.700", _light: "red.400" },
+  { _dark: "red.500", _light: "red.600" },
+  { _dark: "red.300", _light: "red.800" },
 ];
 
 const createDailyDataMap = (data: Array<CalendarTimeRangeResponse>) => {
@@ -224,12 +226,22 @@ export const createCalendarScale = (
 
     return {
       getColor: (counts: RunCounts) => {
-        if (counts.planned > 0) {
+        const actualCount = viewMode === "total" ? counts.total - 
counts.planned : counts.failed;
+        const hasPlanned = counts.planned > 0;
+        const hasActual = actualCount > 0;
+
+        if (hasPlanned && hasActual) {
+          return {
+            actual: singleColor,
+            planned: PLANNED_COLOR,
+          };
+        }
+
+        if (hasPlanned && !hasActual) {
           return PLANNED_COLOR;
         }
-        const targetCount = viewMode === "total" ? counts.total : 
counts.failed;
 
-        return targetCount === 0 ? EMPTY_COLOR : singleColor;
+        return actualCount === 0 ? EMPTY_COLOR : singleColor;
       },
       legendItems: [
         { color: EMPTY_COLOR, label: "0" },
@@ -253,12 +265,46 @@ export const createCalendarScale = (
 
   const uniqueThresholds = [...new Set(thresholds)].sort((first, second) => 
first - second);
 
-  const getColor = (counts: RunCounts): string | { _dark: string; _light: 
string } => {
-    if (counts.planned > 0) {
+  const getColor = (
+    counts: RunCounts,
+  ):
+    | string
+    | { _dark: string; _light: string }
+    | {
+        actual: string | { _dark: string; _light: string };
+        planned: string | { _dark: string; _light: string };
+      } => {
+    const actualCount = viewMode === "total" ? counts.total - counts.planned : 
counts.failed;
+    const hasPlanned = counts.planned > 0;
+    const hasActual = actualCount > 0;
+
+    if (hasPlanned && hasActual) {
+      let actualColor = colorScheme[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 (actualCount > 0 && actualColor === colorScheme[0]) {
+        actualColor = colorScheme[1] ?? EMPTY_COLOR;
+      }
+
+      return {
+        actual: actualColor,
+        planned: PLANNED_COLOR,
+      };
+    }
+
+    if (hasPlanned && !hasActual) {
       return PLANNED_COLOR;
     }
 
-    const targetCount = viewMode === "total" ? counts.total : counts.failed;
+    const targetCount = actualCount;
 
     if (targetCount === 0) {
       return colorScheme[0] ?? EMPTY_COLOR;
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 147aadf7d8b..8ef78a66af4 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
@@ -65,7 +65,13 @@ export type LegendItem = {
 export type CalendarScaleType = "empty" | "gradient" | "single_value";
 
 export type CalendarScale = {
-  readonly getColor: (counts: RunCounts) => string | { _dark: string; _light: 
string };
+  readonly getColor: (counts: RunCounts) =>
+    | string
+    | { _dark: string; _light: string }
+    | {
+        actual: string | { _dark: string; _light: string };
+        planned: string | { _dark: string; _light: string };
+      };
   readonly legendItems: Array<LegendItem>;
   readonly type: CalendarScaleType;
 };

Reply via email to