From 1313a12a2046771c4c0ee99c6dc42ca8cdbb295f Mon Sep 17 00:00:00 2001
From: grigo
Date: Fri, 26 Jun 2026 07:52:07 +0300
Subject: [PATCH] added mesuurments
---
backend/app/chat/notices.py | 1 +
backend/app/chat/service.py | 5 +-
backend/app/fitness/charts.py | 32 +++-
backend/app/fitness/context.py | 40 ++---
backend/app/fitness/service.py | 2 +-
backend/app/homelab/comfyui.py | 22 +--
backend/app/homelab/watcher.py | 15 +-
backend/app/homelab_scoped/watcher.py | 15 +-
backend/app/tools/fitness.py | 26 ++++
frontend/src/components/FitnessCharts.css | 23 +++
frontend/src/components/FitnessCharts.tsx | 173 ++++++++++++----------
11 files changed, 238 insertions(+), 116 deletions(-)
diff --git a/backend/app/chat/notices.py b/backend/app/chat/notices.py
index 86f8b56..0119f4b 100644
--- a/backend/app/chat/notices.py
+++ b/backend/app/chat/notices.py
@@ -66,6 +66,7 @@ TOOLS_SKIP_CHAT_NOTICE = frozenset({
"lookup_exercise",
"calc_fitness_targets",
"calc_body_composition",
+ "get_body_metrics",
"get_weather",
"get_morning_briefing",
"list_shopping_lists",
diff --git a/backend/app/chat/service.py b/backend/app/chat/service.py
index f330dde..5c9abe3 100644
--- a/backend/app/chat/service.py
+++ b/backend/app/chat/service.py
@@ -42,7 +42,10 @@ _DOMAIN_CACHE: dict[str, tuple[float, str]] = {}
_DOMAIN_TTL_SEC = 60.0
_DOMAIN_KEYWORDS: dict[str, tuple[str, ...]] = {
- "fitness": ("фитнес", "тренир", "калори", "еда", "вода", "вес", "workout", "meal", "белок", "жир"),
+ "fitness": (
+ "фитнес", "тренир", "калори", "еда", "вода", "вес", "workout", "meal", "белок", "жир",
+ "талия", "обхват", "антроп", "ffmi", "whr", "navy", "состав тела", "замер",
+ ),
"shopping": ("покуп", "магазин", "список", "shopping", "корзин"),
"reminders": ("напомин", "календар", "событи", "дедлайн", "встреч", "план"),
"projects": ("taiga", "gitea", "задач", "проект", "git", "issue", "коммит", "ветк"),
diff --git a/backend/app/fitness/charts.py b/backend/app/fitness/charts.py
index 9bde0c6..eed53b6 100644
--- a/backend/app/fitness/charts.py
+++ b/backend/app/fitness/charts.py
@@ -64,6 +64,21 @@ def _last(values: list[tuple[date, float]]) -> float | None:
return values[-1][1]
+def _day_has_activity(
+ day: date,
+ daily_flags: dict[date, set[str]],
+ workout_kcal_by_day: dict[date, float],
+) -> bool:
+ flags = daily_flags.get(day, set())
+ if "nutrition" in flags or "steps" in flags:
+ return True
+ return workout_kcal_by_day.get(day, 0) > 0
+
+
+def _day_has_nutrition(day: date, daily_flags: dict[date, set[str]]) -> bool:
+ return "nutrition" in daily_flags.get(day, set())
+
+
def _profile_for_charts(row: FitnessProfile | None) -> dict[str, float | int | str | None] | None:
if row is None:
return None
@@ -312,11 +327,15 @@ def build_fitness_charts(
week_daily_values.append(totals["water_ml"] / 1000.0)
elif metric == "steps" and totals and "steps" in flags:
week_daily_values.append(totals["steps"])
- elif metric == "tdee" and profile is not None and day_cursor <= end:
+ elif metric == "tdee" and profile is not None and _day_has_activity(
+ day_cursor, daily_flags, workout_kcal_by_day
+ ):
week_daily_values.append(
_tdee_actual_for_day(profile, steps_by_day, workout_kcal_by_day, day_cursor)
)
- elif metric == "tdee_expected" and profile is not None and day_cursor <= end:
+ elif metric == "tdee_expected" and profile is not None and _day_has_nutrition(
+ day_cursor, daily_flags
+ ):
week_daily_values.append(
_tdee_expected_for_day(profile, steps_by_day, workout_kcal_by_day, day_cursor)
)
@@ -463,7 +482,13 @@ def _build_daily_series(
elif key == "steps" and totals and "steps" in flags:
value = totals["steps"]
has_data = True
- elif key == "tdee" and profile is not None and steps_by_day is not None and workout_kcal_by_day is not None:
+ elif (
+ key == "tdee"
+ and profile is not None
+ and steps_by_day is not None
+ and workout_kcal_by_day is not None
+ and _day_has_activity(d, daily_flags, workout_kcal_by_day)
+ ):
value = _tdee_actual_for_day(profile, steps_by_day, workout_kcal_by_day, d)
has_data = True
elif (
@@ -471,6 +496,7 @@ def _build_daily_series(
and profile is not None
and steps_by_day is not None
and workout_kcal_by_day is not None
+ and _day_has_nutrition(d, daily_flags)
):
value = _tdee_expected_for_day(profile, steps_by_day, workout_kcal_by_day, d)
has_data = True
diff --git a/backend/app/fitness/context.py b/backend/app/fitness/context.py
index 24396bd..a47ba4b 100644
--- a/backend/app/fitness/context.py
+++ b/backend/app/fitness/context.py
@@ -105,31 +105,33 @@ def format_fitness_context(snapshot: dict[str, Any]) -> str:
f"(серия {stats.get('streak')} дн., {stats.get('active_kcal')} ккал активных)"
)
- latest = (snapshot.get("body_metrics") or [None])[0]
- if latest:
+ metrics = snapshot.get("body_metrics") or []
+ if metrics:
lines.append("")
- lines.append("Антропометрия (последняя):")
- parts = [f"{latest.get('weight_kg')} кг"]
- if latest.get("body_fat_pct") is not None:
- method = latest.get("body_fat_method") or "?"
- parts.append(f"жир {latest.get('body_fat_pct')}% ({method})")
- if latest.get("neck_cm"):
- parts.append(f"шея {latest.get('neck_cm')}")
- if latest.get("waist_cm"):
- parts.append(f"талия {latest.get('waist_cm')}")
- if latest.get("hip_cm"):
- parts.append(f"бёдра {latest.get('hip_cm')}")
- if latest.get("whr"):
- parts.append(f"WHR {latest.get('whr')}")
- if latest.get("ffmi"):
- parts.append(f"FFMI {latest.get('ffmi')}")
- lines.append(" · ".join(parts))
+ lines.append(f"Антропометрия — последние {len(metrics)} замеров (новые → старые):")
+ for m in metrics:
+ recorded = (m.get("recorded_at") or "")[:10]
+ parts = [f"{recorded}: {m.get('weight_kg')} кг"]
+ if m.get("body_fat_pct") is not None:
+ method = m.get("body_fat_method") or "?"
+ parts.append(f"жир {m.get('body_fat_pct')}% ({method})")
+ if m.get("waist_cm"):
+ parts.append(f"талия {m.get('waist_cm')}")
+ if m.get("neck_cm"):
+ parts.append(f"шея {m.get('neck_cm')}")
+ if m.get("hip_cm"):
+ parts.append(f"бёдра {m.get('hip_cm')}")
+ if m.get("whr"):
+ parts.append(f"WHR {m.get('whr')}")
+ if m.get("ffmi"):
+ parts.append(f"FFMI {m.get('ffmi')}")
+ lines.append(" · ".join(parts))
lines.append("")
lines.append(
"Правила: log_meal, log_water, log_weight (обхваты → Navy), log_steps (replace=true — итог за день), log_workout (date/days_ago), "
"update_fitness_log, delete_fitness_log (last=true — удалить последнюю), merge_fitness_logs, compare_fitness_days, "
- "calc_body_composition (расчёт без записи), get_fitness_summary (date/days_ago), get_fitness_history, "
+ "calc_body_composition (расчёт без записи), get_fitness_summary (date/days_ago), get_fitness_history, get_body_metrics, "
"set_fitness_profile, calc_fitness_targets, lookup_food, lookup_exercise. "
"TDEE = BMR + NEAT (200 ккал) + шаги + тренировки. "
"TDEE факт — по залогированной активности; TDEE план — среднее за неделю (или baseline) для утреннего бюджета углеводов. "
diff --git a/backend/app/fitness/service.py b/backend/app/fitness/service.py
index 918478c..40cf3a6 100644
--- a/backend/app/fitness/service.py
+++ b/backend/app/fitness/service.py
@@ -979,7 +979,7 @@ class FitnessService:
"today": self.get_daily_summary(today),
"history": self.get_history(days=7, end_day=today),
"workout_stats": self.get_workout_stats(days=7, end_day=today),
- "body_metrics": self.list_body_metrics(limit=10),
+ "body_metrics": self.list_body_metrics(limit=15),
"reminders": self.list_reminders(),
}
diff --git a/backend/app/homelab/comfyui.py b/backend/app/homelab/comfyui.py
index 6247eb0..611886f 100644
--- a/backend/app/homelab/comfyui.py
+++ b/backend/app/homelab/comfyui.py
@@ -13,15 +13,22 @@ ANIMA_DEFAULT_NEGATIVE = (
"worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts, sepia"
)
-ROFL_PROMPTS = [
- f"{ANIMA_QUALITY_PREFIX}, confused cat in tiny business suit, server room, meme, chibi",
- f"{ANIMA_QUALITY_PREFIX}, potato with sunglasses on skateboard, absurd cartoon, silly",
- f"{ANIMA_QUALITY_PREFIX}, astronaut watering houseplant on the moon, wholesome, cute",
- f"{ANIMA_QUALITY_PREFIX}, rubber duck as judge at programming contest, comic style",
- f"{ANIMA_QUALITY_PREFIX}, llama DJ at house party, neon lights, party, silly",
+ROFL_POSES = [
+ "full_body, standing, peace_sign, looking_at_viewer, smile",
+ "sitting, on_chair, legs_up, playful, blush",
+ "lying, on_bed, stretching, sleepy, relaxed",
+ "standing, arms_up, victory_pose, happy, open_mouth",
+ "sitting, hugging_knees, shy, looking_away",
+ "full_body, walking, wind, hair_blowing, outdoors",
+ "portrait, close-up, wink, finger_gun",
+ "sitting, holding_cup, cozy, sweater, indoors",
]
+def random_rofl_pose() -> str:
+ return random.choice(ROFL_POSES)
+
+
def _use_anima(settings) -> bool:
return bool(settings.comfyui_unet.strip()) and not settings.comfyui_checkpoint.strip()
@@ -272,6 +279,3 @@ class ComfyUIClient:
}
return {"ok": False, "error": f"Таймаут генерации ({self.timeout}s)"}
-
- def random_rofl_prompt(self) -> str:
- return random.choice(ROFL_PROMPTS)
diff --git a/backend/app/homelab/watcher.py b/backend/app/homelab/watcher.py
index 78b66a7..873b887 100644
--- a/backend/app/homelab/watcher.py
+++ b/backend/app/homelab/watcher.py
@@ -8,7 +8,9 @@ import httpx
from app.config import get_settings
from app.db.base import SessionLocal
-from app.homelab.comfyui import ComfyUIClient
+from app.character.service import CharacterService
+from app.homelab.comfyui import ComfyUIClient, random_rofl_pose
+from app.homelab.image_gen import generate_image
from app.homelab.context import resolve_timezone
from app.homelab.digest import build_morning_digest
from app.homelab.monitoring import check_netdata_alerts
@@ -128,10 +130,15 @@ async def _tick_rofl() -> None:
if not await _comfyui_reachable(client.base_url):
return
- prompt = client.random_rofl_prompt()
+ card = CharacterService(db, 1).get_card().get("data", {})
+ appearance = (card.get("appearance_tags") or "").strip()
+ if not appearance:
+ return
+
+ pose = random_rofl_pose()
try:
result = await asyncio.wait_for(
- client.generate_image(prompt),
+ generate_image(db, user_id=1, draw_self=True, scene_description=pose),
timeout=settings.comfyui_timeout_sec + 15,
)
except (asyncio.TimeoutError, httpx.TimeoutException, httpx.ConnectError) as exc:
@@ -143,7 +150,7 @@ async def _tick_rofl() -> None:
url = result.get("url", "")
post_chat_notice(
- f"🎨 **Рофл дня**\n\n\n\n_{prompt}_"
+ f"🎨 **Рофл дня**\n\n\n\n_{pose}_"
)
set_state(db, f"comfy_rofl_count_{today}", str(count + 1))
set_state(db, "last_comfy_rofl_at", now.isoformat())
diff --git a/backend/app/homelab_scoped/watcher.py b/backend/app/homelab_scoped/watcher.py
index 4d7fbf9..503453d 100644
--- a/backend/app/homelab_scoped/watcher.py
+++ b/backend/app/homelab_scoped/watcher.py
@@ -10,9 +10,11 @@ from sqlalchemy import select
from app.config import get_settings
from app.db.base import SessionLocal
from app.db.models import User
-from app.homelab.comfyui import ComfyUIClient
+from app.character.service import CharacterService
+from app.homelab.comfyui import ComfyUIClient, random_rofl_pose
from app.homelab.context import resolve_timezone
from app.homelab.digest import build_morning_digest
+from app.homelab.image_gen import generate_image
from app.homelab.monitoring import check_netdata_alerts
from app.homelab_scoped.notices import post_chat_notice
from app.homelab.state import get_state, set_state
@@ -140,10 +142,15 @@ async def _tick_rofl() -> None:
if not await _comfyui_reachable(client.base_url):
continue
- prompt = client.random_rofl_prompt()
+ card = CharacterService(db, user.id).get_card().get("data", {})
+ appearance = (card.get("appearance_tags") or "").strip()
+ if not appearance:
+ continue
+
+ pose = random_rofl_pose()
try:
result = await asyncio.wait_for(
- client.generate_image(prompt),
+ generate_image(db, user_id=user.id, draw_self=True, scene_description=pose),
timeout=settings.comfyui_timeout_sec + 15,
)
except (asyncio.TimeoutError, httpx.TimeoutException, httpx.ConnectError) as exc:
@@ -155,7 +162,7 @@ async def _tick_rofl() -> None:
url = result.get("url", "")
post_chat_notice(
- f"🎨 **Рофл дня**\n\n\n\n_{prompt}_",
+ f"🎨 **Рофл дня**\n\n\n\n_{pose}_",
user.id,
)
set_state(db, count_key, str(count + 1))
diff --git a/backend/app/tools/fitness.py b/backend/app/tools/fitness.py
index dcfec68..ee7dd01 100644
--- a/backend/app/tools/fitness.py
+++ b/backend/app/tools/fitness.py
@@ -10,6 +10,7 @@ from app.tools._dispatch import NOT_HANDLED, ToolContext
TOOL_NAMES = frozenset({
"get_fitness_summary",
"get_fitness_history",
+ "get_body_metrics",
"compare_fitness_days",
"set_fitness_profile",
"calc_fitness_targets",
@@ -25,6 +26,7 @@ TOOL_NAMES = frozenset({
"lookup_food",
"lookup_exercise",
"set_fitness_reminder",
+ "get_body_metrics",
})
TOOL_DEFINITIONS: list[dict[str, Any]] = [
@@ -49,6 +51,26 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
},
},
},
+ {
+ "type": "function",
+ "function": {
+ "name": "get_body_metrics",
+ "description": (
+ "История антропометрии: вес, % жира, обхваты, WHR, FFMI по датам. "
+ "«Как менялся вес», «динамика жира», «прошлые замеры», «что было месяц назад»."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "description": "Сколько последних записей, по умолчанию 20, макс. 50",
+ },
+ },
+ "required": [],
+ },
+ },
+ },
{
"type": "function",
"function": {
@@ -407,6 +429,10 @@ async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any
elif arguments.get("days_ago") is not None:
day = datetime.now(timezone.utc).date() - timedelta(days=int(arguments["days_ago"]))
return fitness.get_daily_summary(day)
+ if name == "get_body_metrics":
+ limit = max(1, min(int(arguments.get("limit") or 20), 50))
+ metrics = fitness.list_body_metrics(limit=limit)
+ return {"metrics": metrics, "count": len(metrics)}
if name == "get_fitness_history":
end_day = None
if arguments.get("end_date"):
diff --git a/frontend/src/components/FitnessCharts.css b/frontend/src/components/FitnessCharts.css
index 3405376..cebf3f4 100644
--- a/frontend/src/components/FitnessCharts.css
+++ b/frontend/src/components/FitnessCharts.css
@@ -135,6 +135,29 @@
opacity: 0.9;
}
+.fitness-chart-line {
+ fill: none;
+ stroke: #5b9bd5;
+ stroke-width: 1.5;
+ opacity: 0.55;
+}
+
+.fitness-chart-empty {
+ margin: 0.5rem 0 0;
+ color: #8b95a8;
+ font-size: 0.85rem;
+}
+
+.fitness-chart-empty-reset {
+ background: none;
+ border: none;
+ color: #5b9bd5;
+ cursor: pointer;
+ padding: 0;
+ font: inherit;
+ text-decoration: underline;
+}
+
@media (max-width: 768px) {
.fitness-charts-grid {
grid-template-columns: 1fr;
diff --git a/frontend/src/components/FitnessCharts.tsx b/frontend/src/components/FitnessCharts.tsx
index 1aff4e5..be026a0 100644
--- a/frontend/src/components/FitnessCharts.tsx
+++ b/frontend/src/components/FitnessCharts.tsx
@@ -14,6 +14,7 @@ const CHART_KEYS = [
"body_fat_pct",
] as const;
+const CONNECTED_KEYS = new Set(["weight_kg", "body_fat_pct"]);
const MIN_VIEW_SPAN = 2;
const ZOOM_FACTOR = 0.85;
@@ -28,7 +29,9 @@ interface MetricChartProps {
granularity: "week" | "day";
view: ChartView;
pointCount: number;
+ connectPoints: boolean;
onViewChange: (view: ChartView) => void;
+ onResetView: () => void;
}
function formatTick(point: FitnessChartPoint, granularity: "week" | "day") {
@@ -69,12 +72,22 @@ function MetricChart({
granularity,
view,
pointCount,
+ connectPoints,
onViewChange,
+ onResetView,
}: MetricChartProps) {
const svgRef = useRef(null);
const dragRef = useRef<{ x: number; view: ChartView } | null>(null);
const [dragging, setDragging] = useState(false);
+ const visibleActive = useMemo(
+ () =>
+ series.points.filter(
+ (p) => p.has_data && p.value != null && p.index >= view.start && p.index <= view.end,
+ ),
+ [series.points, view.end, view.start],
+ );
+
const layout = useMemo(() => {
const width = 640;
const height = 168;
@@ -83,9 +96,6 @@ function MetricChart({
const plotH = height - pad.top - pad.bottom;
const viewSpan = Math.max(view.end - view.start, 0.001);
- const visibleActive = series.points.filter(
- (p) => p.has_data && p.value != null && p.index >= view.start && p.index <= view.end,
- );
if (visibleActive.length === 0) {
return null;
}
@@ -122,6 +132,15 @@ function MetricChart({
trendPath = trendPoints;
}
+ const linePath =
+ connectPoints && dots.length >= 2
+ ? dots
+ .slice()
+ .sort((a, b) => a.index - b.index)
+ .map((dot) => `${dot.x},${dot.y}`)
+ .join(" ")
+ : "";
+
const yTicks = [yMin, (yMin + yMax) / 2, yMax];
const visibleIndexes = series.points
@@ -143,12 +162,13 @@ function MetricChart({
yScale,
xScale,
dots,
+ linePath,
trendPath,
yTicks,
labelIndexes,
viewSpan,
};
- }, [granularity, series, showTrend, view]);
+ }, [connectPoints, granularity, series, showTrend, view, visibleActive]);
const applyView = useCallback(
(nextView: ChartView) => {
@@ -210,12 +230,6 @@ function MetricChart({
e.currentTarget.releasePointerCapture(e.pointerId);
}, []);
- if (!layout) {
- return null;
- }
-
- const clipId = `clip-${series.key}`;
-
const slopeLabel =
granularity === "day" && series.trend && "slope_per_day" in series.trend
? `${(series.trend as { slope_per_day: number }).slope_per_day > 0 ? "+" : ""}${(series.trend as { slope_per_day: number }).slope_per_day}/день`
@@ -223,6 +237,29 @@ function MetricChart({
? `${(series.trend as { slope_per_week: number }).slope_per_week > 0 ? "+" : ""}${(series.trend as { slope_per_week: number }).slope_per_week}/нед`
: null;
+ if (!layout) {
+ return (
+
+
+
+ {series.label} ({series.unit})
+
+
+ {series.data_points} {granularity === "day" ? "дн." : "нед."}
+
+
+
+ Нет точек в этом диапазоне.{" "}
+
+
+
+ );
+ }
+
+ const clipId = `clip-${series.key}`;
+
return (
@@ -277,6 +314,10 @@ function MetricChart({
) : null}
+ {layout.linePath ? (
+
+ ) : null}
+
{layout.dots.map((dot) => (
@@ -313,19 +354,8 @@ interface FitnessChartsProps {
loading?: boolean;
}
-function formatViewRange(
- points: FitnessChartPoint[],
- view: ChartView,
- granularity: "week" | "day",
-): string {
- const startPoint = points.find((p) => p.index >= Math.floor(view.start));
- const endPoint = [...points].reverse().find((p) => p.index <= Math.ceil(view.end));
- if (!startPoint || !endPoint) return "";
- return `${formatTick(startPoint, granularity)} — ${formatTick(endPoint, granularity)}`;
-}
-
export default function FitnessCharts({ data, showTrend, loading }: FitnessChartsProps) {
- const [view, setView] = useState({ start: 0, end: 0 });
+ const [views, setViews] = useState>({});
const granularity = data?.granularity ?? "week";
const source =
@@ -338,36 +368,28 @@ export default function FitnessCharts({ data, showTrend, loading }: FitnessChart
);
}, [source]);
- const pointCount = charts[0]?.points.length ?? 0;
- const referencePoints = charts[0]?.points ?? [];
-
useEffect(() => {
- setView(fullView(pointCount));
- }, [pointCount, data?.end_date, data?.granularity]);
+ setViews((prev) => {
+ const next: Record = {};
+ for (const series of charts) {
+ const pointCount = series.points.length;
+ next[series.key] = prev[series.key] ?? fullView(pointCount);
+ }
+ return next;
+ });
+ }, [charts, data?.end_date, data?.granularity]);
- const zoomBy = useCallback(
- (factor: number) => {
- if (pointCount <= 1) return;
- const span = view.end - view.start;
- const center = (view.start + view.end) / 2;
- const newSpan = Math.max(MIN_VIEW_SPAN, Math.min(pointCount, span * factor));
- setView(
- clampView({ start: center - newSpan / 2, end: center + newSpan / 2 }, pointCount),
- );
- },
- [pointCount, view.end, view.start],
- );
+ const resetAllViews = useCallback(() => {
+ const next: Record = {};
+ for (const series of charts) {
+ next[series.key] = fullView(series.points.length);
+ }
+ setViews(next);
+ }, [charts]);
- const resetView = useCallback(() => {
- setView(fullView(pointCount));
- }, [pointCount]);
-
- const handleViewChange = useCallback(
- (next: ChartView) => {
- setView(clampView(next, pointCount));
- },
- [pointCount],
- );
+ const handleViewChange = useCallback((key: string, view: ChartView, pointCount: number) => {
+ setViews((prev) => ({ ...prev, [key]: clampView(view, pointCount) }));
+ }, []);
if (loading) {
return Загрузка графиков…
;
@@ -376,8 +398,10 @@ export default function FitnessCharts({ data, showTrend, loading }: FitnessChart
return null;
}
- const rangeLabel = formatViewRange(referencePoints, view, granularity);
- const isZoomed = view.start > 0.01 || view.end < pointCount - 1.01;
+ const anyZoomed = charts.some((series) => {
+ const view = views[series.key] ?? fullView(series.points.length);
+ return view.start > 0.01 || view.end < series.points.length - 1.01;
+ });
return (
@@ -390,20 +414,11 @@ export default function FitnessCharts({ data, showTrend, loading }: FitnessChart
: `Недельные точки · заполнено ${data.weeks_with_data} из ${data.weeks} нед.`}
- {charts.length > 0 && pointCount > MIN_VIEW_SPAN ? (
+ {charts.length > 0 && anyZoomed ? (
-
) : null}
@@ -414,20 +429,28 @@ export default function FitnessCharts({ data, showTrend, loading }: FitnessChart
) : (
- {charts.map((series) => (
-
- ))}
+ {charts.map((series) => {
+ const pointCount = series.points.length;
+ const view = views[series.key] ?? fullView(pointCount);
+ return (
+ handleViewChange(series.key, next, pointCount)}
+ onResetView={() =>
+ handleViewChange(series.key, fullView(pointCount), pointCount)
+ }
+ />
+ );
+ })}
)}
- {charts.length > 0 && pointCount > MIN_VIEW_SPAN ? (
+ {charts.length > 0 ? (
Колесо мыши — масштаб · перетаскивание — прокрутка
) : null}