Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
useData,
useDatabaseChangeNotifier,
} from "@/assets/src/calendar-storage";
import { getFlowTypeString } from "@/constants/Flow";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add - import { getFlowTypeString } from "@/constants/Flow";

import { useTheme, Text, Button } from "react-native-paper";
import FadeInView from "@/components/animations/FadeInView";
import { useState, useCallback } from "react";
Expand Down Expand Up @@ -93,12 +94,15 @@
day: "numeric",
});

const flowType = getFlowTypeString(day.flow_intensity ?? 0);
const backgroundColor = flowType ? FlowColors[flowType] : FlowColors.white;

Check failure on line 98 in app/(tabs)/index.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Replace `·?·FlowColors[flowType]` with `⏎····················?·FlowColors[flowType]⏎···················`

return (
<View
key={index}
style={[

Check failure on line 103 in app/(tabs)/index.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Replace `⏎························styles.flowLogItem,⏎························{·backgroundColor·},⏎······················` with `styles.flowLogItem,·{·backgroundColor·}`
styles.flowLogItem,
{ backgroundColor: FlowColors[day.flow_intensity] },
{ backgroundColor },
]}
>
<Text style={styles.flowLogText}>{weekday}</Text>
Expand Down
210 changes: 161 additions & 49 deletions components/FlowChart.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { View } from "react-native";
import { Dimensions } from "react-native";
import Svg, { Circle, Text, TSpan, Path } from "react-native-svg";
import { useEffect, useRef } from "react";
import { FlowColors } from "@/constants/Colors";
import Svg, {
Circle,
Text,
TSpan,
Path,
Defs,
LinearGradient,
Stop,
} from "react-native-svg";
import { useEffect, useRef, useMemo } from "react";
import { FlowColors, FlowType } from "@/constants/Colors";
import { useTheme } from "react-native-paper";
import { useData, useFlowData } from "@/assets/src/calendar-storage";
import { useFocusEffect } from "@react-navigation/native";
import React from "react";
import { useFetchFlowData } from "@/hooks/useFetchFlowData";
import {
getFlowTypeString,
MAX_FLOW_LENGTH,
FLOW_TAIL_PERCENT,
FLOW_TAIL_COLOR,
} from "@/constants/Flow";
import Animated, {
Easing,
useAnimatedProps,
Expand All @@ -17,6 +31,7 @@
} from "react-native-reanimated";

const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const AnimatedPath = Animated.createAnimatedComponent(Path);
const { height } = Dimensions.get("window");

export default function FlowChart() {
Expand All @@ -41,33 +56,6 @@
const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const numberOfDaysInMonth = lastDayOfMonth.getDate();

const renderMarks = () => {
return flowDataForCurrentMonth.map((data, index) => {
// Convert date to day of the month
const dayNumber = new Date(data.date + "T00:00:00Z").getUTCDate();

let angle = 8 + ((dayNumber - 1) * (352 - 8)) / (numberOfDaysInMonth - 1);
angle = (angle + startingPoint) % 360;

const x = centerX + circleRadius * Math.cos((angle * Math.PI) / 180);
const y = centerY + circleRadius * Math.sin((angle * Math.PI) / 180);

const markColor = FlowColors[data.flow_intensity ?? 0];

return (
<Circle
key={index}
cx={x}
cy={y}
r="5"
fill={markColor}
stroke={theme.colors.onSecondary}
strokeWidth="0.5"
/>
); //
});
};

// Calculate "today" circle angle and position
const todayNumber = today.getDate();
let todayAngle =
Expand All @@ -90,25 +78,20 @@
};

// Animated props for today circle
const animatedProps = useAnimatedProps(() => {
const animatedCircleProps = useAnimatedProps(() => {
const todayX =
centerX + circleRadius * Math.cos((position.value * Math.PI) / 180);
const todayY =
centerY + circleRadius * Math.sin((position.value * Math.PI) / 180);
return {
cx: todayX,
cy: todayY,
};
return { cx: todayX, cy: todayY };
});

// Create a circular path for chart
const arcstartX = 55.7;
const arcstartY = 5.5;
const arcendX = 44.3;
const arcendY = 5.5;
const arcPath = `M ${arcstartX},${arcstartY} A 45,45 0 0,1 95,50 A 45,45 0 0,1 50,95 A 45,45 0 0,1 5,50 A 45,45 0 0,1 ${arcendX},${arcendY}`;
const arcPath = `M ${arcstartX},${arcstartY} A 45,45 0 0,1 95,50 A 45,45 0 0,1 50,95 A 45,45 0 0,1 5,50 A 45,45 0 0,1 ${arcendX},${arcendY}`;

// Format the current month and today's date
const todayFormatted = {
year: today.toLocaleString("default", { year: "numeric" }),
month: today.toLocaleString("default", { month: "long" }),
Expand All @@ -119,29 +102,26 @@
const todayMonthDayFormatted = `${todayFormatted.month} ${todayFormatted.day},`;
const todayWeekdayFormattedWithComma = `${todayFormatted.weekday},`;

// Use refs to satisfy lint
const fetchFlowDataRef = useRef(fetchFlowData);
const positionRef = useRef(position);
const triggerAnimationRef = useRef(triggerAnimation);
fetchFlowDataRef.current = fetchFlowData;
positionRef.current = position;
triggerAnimationRef.current = triggerAnimation;

// useFocusEffect to fetch flow data and run animation when the screen is focused
useFocusEffect(
React.useCallback(() => {
fetchFlowDataRef.current();
runOnJS(() => {
positionRef.current.value = initialPosition.current; // Reset position to initial value
positionRef.current.value = initialPosition.current;
})();

setTimeout(() => {
triggerAnimationRef.current(); // Run animation after the position is set
triggerAnimationRef.current();
}, 10);
}, []),
);

// Filter flow data for dates within the current month
const filterFlowDataForCurrentMonth = (flowData: any[]) => {
if (flowData.length === 0) {
setFlowDataForCurrentMonth([]);
Expand All @@ -154,28 +134,159 @@
return (
dayDateString >= firstDayString &&
dayDateString <= lastDayString &&
day.flow_intensity
day.flow_intensity &&
day.flow_intensity > 0 // Explicitly exclude "None" (0)
);
});
setFlowDataForCurrentMonth(filteredData);
}
};

// Updates the current month's data when there are changes in flowData
useEffect(() => {
filterFlowDataForCurrentMonth(flowData);
}, [flowData]); // eslint-disable-line react-hooks/exhaustive-deps

// ===== Gradient + Progress Logic =====
const flowDays = flowDataForCurrentMonth.length || 0;
const progress = Math.min(flowDays / MAX_FLOW_LENGTH, 1);
const C = 2 * Math.PI * circleRadius;

const visible = C * progress;
const dashOffset = 0; // makes arc grow right -> left

const tailLen = Math.min(C * FLOW_TAIL_PERCENT, visible);
const tailOffset = C - visible;

const animatedDashProps = useAnimatedProps(() => {
// strokeDasharray accepts string or number array
const dashArray: string | number[] = [visible, C];
return {
strokeDasharray: dashArray,
strokeDashoffset: dashOffset,
};
});

// ===== Dynamic Gradient Based on Actual Flow States =====
// Get unique flow states in chronological order
const flowStatesInOrder = useMemo(() => {
if (flowDataForCurrentMonth.length === 0) return [];

Check failure on line 173 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
// Sort by date to get chronological order
const sortedData = [...flowDataForCurrentMonth].sort((a, b) => {
const dateA = new Date(a.date + "T00:00:00Z").getTime();
const dateB = new Date(b.date + "T00:00:00Z").getTime();
return dateA - dateB;
});

// Extract unique flow types in order of first appearance
// Explicitly exclude "None" (flow_intensity 0) from gradient
const seen = new Set<FlowType>();
const uniqueFlowTypes: FlowType[] = [];

Check failure on line 185 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
for (const data of sortedData) {
// Skip "None" (flow_intensity 0) - it should not affect the gradient
if (!data.flow_intensity || data.flow_intensity === 0) {
continue;
}
const flowType = getFlowTypeString(data.flow_intensity);
if (flowType && !seen.has(flowType)) {
seen.add(flowType);
uniqueFlowTypes.push(flowType);
}
}

return uniqueFlowTypes;
}, [flowDataForCurrentMonth]);

// Create gradient stops based on actual flow states
// Scale to 0-90% to leave room for tail fade at 95%
const gradientStops = useMemo((): React.ReactElement[] => {
const maxOffset = 90; // Leave room for tail fade

Check failure on line 205 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
let stops: React.ReactElement[] = [];

Check failure on line 207 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
if (flowStatesInOrder.length === 0) {
// Default gradient if no flow data
stops = [
<Stop key="0" offset="0%" stopColor={FlowColors.spotting} />,
<Stop key="1" offset={`${maxOffset}%`} stopColor={FlowColors.heavy} />,
];
} else if (flowStatesInOrder.length === 1) {
// Single flow state - solid color
const color = FlowColors[flowStatesInOrder[0]];
stops = [
<Stop key="0" offset="0%" stopColor={color} />,
<Stop key="1" offset={`${maxOffset}%`} stopColor={color} />,
];
} else {
// Multiple flow states - create gradient with proportional stops
stops = flowStatesInOrder.map((flowType, index) => {
const offset = (index / (flowStatesInOrder.length - 1)) * maxOffset;
const color = FlowColors[flowType];
return <Stop key={index} offset={`${offset}%`} stopColor={color} />;
});
}

Check failure on line 229 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
// Add tail fade stop at 95% to create smooth transition to purple
stops.push(<Stop key="tail" offset="95%" stopColor={FLOW_TAIL_COLOR} />);

Check failure on line 232 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Delete `····`
return stops;
}, [flowStatesInOrder]);

// =====================================

return (
<View style={{ padding: 2 }}>
<Svg height={height * 0.5} width="100%" viewBox="-5 -5 110 110">
<Defs>
{/* Main flow gradient - dynamically generated based on actual flow states */}
<LinearGradient id="flowGradient" x1="100%" y1="0%" x2="0%" y2="100%">
{gradientStops}
</LinearGradient>

{/* Fade-out tail mask */}
<LinearGradient id="fadeTail" x1="100%" y1="0%" x2="0%" y2="100%">
<Stop offset="0%" stopColor={FLOW_TAIL_COLOR} stopOpacity="0.7" />
<Stop offset="100%" stopColor={FLOW_TAIL_COLOR} stopOpacity="0" />
</LinearGradient>
</Defs>

{/* Base purple ring (always visible) */}
<Path
d={arcPath}
fill="transparent"
stroke={theme.colors.secondary}
stroke={FLOW_TAIL_COLOR}
strokeWidth="9"
strokeLinecap="round"
/>

{/* Gradient progress ring overlay */}
{flowDays > 0 && (
<>
{/* Main gradient arc */}
<AnimatedPath
d={arcPath}
fill="transparent"
stroke="url(#flowGradient)"
strokeWidth="9"
strokeLinecap="round"
animatedProps={animatedDashProps}
accessibilityLabel={`Flow progress: ${flowDays} day${flowDays !== 1 ? "s" : ""} logged`}
/>
{/* Soft fade overlay */}
<Path
d={arcPath}
fill="transparent"
stroke="url(#fadeTail)"
strokeWidth="9"
strokeLinecap="round"
strokeDasharray={`${tailLen} ${C}`}
strokeDashoffset={tailOffset}
/>
</>
)}

{/* Inner circle and date text */}
<Circle
cx="50"
cy="50"
Expand All @@ -199,15 +310,16 @@
{todayFormatted.year}
</TSpan>
</Text>
{renderMarks()}

{/* Animated circle marker */}
<AnimatedCircle
r="5"
fill="transparent"
stroke={theme.colors.onSecondaryContainer}
strokeWidth="1.5"
animatedProps={animatedProps}
animatedProps={animatedCircleProps}
/>
</Svg>
</View>
);
}
}

Check failure on line 325 in components/FlowChart.tsx

View workflow job for this annotation

GitHub Actions / Check Formatting

Insert `⏎`
Loading
Loading