From f0b7a5e28f2a39605fe61fa9be1b005a2c0de69f Mon Sep 17 00:00:00 2001 From: AliReza Date: Fri, 16 Oct 2026 21:27:22 +0330 Subject: [PATCH] Diagrams: hide answer-giving counts in quizzes (HideCounts); bar/pie slices take the colour named by their label Co-Authored-By: Claude Sonnet 5.5 --- src/diagram/charts.tsx | 8 +++++--- src/diagram/index.tsx | 6 +++--- src/diagram/misc.tsx | 33 +++++++++++++++++++-------------- src/diagram/svg.tsx | 24 +++++++++++++++++++++++- src/screens/QuizScreen.tsx | 2 +- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/diagram/charts.tsx b/src/diagram/charts.tsx index a9bf954..06041eb 100644 --- a/src/diagram/charts.tsx +++ b/src/diagram/charts.tsx @@ -1,5 +1,5 @@ import type { ChartSpec } from './types'; -import { COLORS, GRID, INK, Label, SERIES, SOFT, bidi, fa, mathText, niceStep, ticksBetween } from './svg'; +import { COLORS, GRID, INK, Label, SOFT, bidi, fa, mathText, niceStep, seriesColors, ticksBetween } from './svg'; const W = 330; @@ -20,6 +20,7 @@ function Bars({ kind, labels, values, unit }: { kind: 'bar' | 'linechart'; label const Y = (v: number) => top + ((y1 - v) / (y1 - y0)) * plotH; const slot = plotW / labels.length; const X = (i: number) => left + slot * (i + 0.5); + const barColors = seriesColors(labels, values.length); const barW = Math.min(38, slot * 0.62); const long = labels.some((l) => l.length > 6); @@ -34,7 +35,7 @@ function Bars({ kind, labels, values, unit }: { kind: 'bar' | 'linechart'; label {unit && {mathText(unit)}} {kind === 'bar' ? values.map((v, i) => { - const c = COLORS[SERIES[i % SERIES.length]]; + const c = barColors[i]; return ( @@ -64,12 +65,13 @@ function Bars({ kind, labels, values, unit }: { kind: 'bar' | 'linechart'; label function Pie({ labels, values }: { labels: string[]; values: number[] }) { const total = values.reduce((a, v) => a + Math.max(0, v), 0); if (total <= 0) return null; + const sliceColors = seriesColors(labels, values.length); const cx = 105, cy = 105, r = 88; let a = -Math.PI / 2; const slices = values.map((v, i) => { const frac = Math.max(0, v) / total, a0 = a, a1 = a + frac * 2 * Math.PI; a = a1; - return { i, frac, a0, a1, c: COLORS[SERIES[i % SERIES.length]] }; + return { i, frac, a0, a1, c: sliceColors[i] }; }); const H = Math.max(215, 24 + labels.length * 22); return ( diff --git a/src/diagram/index.tsx b/src/diagram/index.tsx index f3d728a..52ff61e 100644 --- a/src/diagram/index.tsx +++ b/src/diagram/index.tsx @@ -7,7 +7,7 @@ import { Space } from './space'; import { Chart } from './charts'; import { Mapping, Tree, Venn } from './sets'; import { Clock, Fraction, Grid, Groups, Pattern, Table } from './misc'; -import { PAPER } from './svg'; +import { HideCounts, PAPER } from './svg'; import { MathText } from '@/lib/MathText'; export type { DiagramSpec } from './types'; @@ -45,13 +45,13 @@ class Guard extends Component<{ children: ReactNode }, { broken: boolean }> { render() { return this.state.broken ? null : this.props.children; } } -function DiagramBase({ spec }: { spec: DiagramSpec }) { +function DiagramBase({ spec, hideCounts = false }: { spec: DiagramSpec; hideCounts?: boolean }) { return ( {/* dir=ltr: محور و مختصات چپ‌به‌راست‌اند؛ در زمینه‌ی rtl «−۱» به «۱−» برمی‌گشت. برچسب‌های فارسی با unicode-bidi: plaintext جهتِ خودشان را می‌گیرند. */}
- + {spec.title &&
}
diff --git a/src/diagram/misc.tsx b/src/diagram/misc.tsx index 5c56422..f64c780 100644 --- a/src/diagram/misc.tsx +++ b/src/diagram/misc.tsx @@ -1,5 +1,6 @@ import type { ClockSpec, FractionSpec, GridSpec, GroupsSpec, PatternSpec, TableSpec } from './types'; -import { COLORS, INK, bidi, color, mathText } from './svg'; +import { useContext } from 'react'; +import { COLORS, HideCounts, INK, bidi, color, mathText } from './svg'; import { toFa } from '@/lib/format'; const W = 320; @@ -8,11 +9,12 @@ const SHADE = COLORS.yellow; /* ─── کسر: دایره یا نوارِ تکه‌تکه ─────────────────────────────────────────── */ export function Fraction({ spec }: { spec: FractionSpec }) { - if (spec.compare?.length) return ; - if (spec.wholes) return ; + const hide = useContext(HideCounts); + if (spec.compare?.length) return ; + if (spec.wholes) return ; const { parts, shaded } = spec; const fill = (i: number) => (i < shaded ? SHADE : '#fff'); - const caption = {`${toFa(shaded)} تکه از ${toFa(parts)} تکه`}; + const caption = hide ? null : {`${toFa(shaded)} تکه از ${toFa(parts)} تکه`}; if (spec.shape === 'rect') { const w = (W - 40) / parts; return ( @@ -36,7 +38,7 @@ export function Fraction({ spec }: { spec: FractionSpec }) { } /** چند کسر روی نوارهای هم‌اندازه، برای مقایسه (یک‌پنجم و نُه‌دهم). */ -function FractionCompare({ items }: { items: { parts: number; shaded: number; label?: string }[] }) { +function FractionCompare({ items, hide }: { items: { parts: number; shaded: number; label?: string }[]; hide: boolean }) { const barW = W - 110, h = 30, gap = 16; const H = items.length * (h + gap) + 8; const cols = [COLORS.yellow, COLORS.teal, COLORS.orange, COLORS.purple]; @@ -50,7 +52,7 @@ function FractionCompare({ items }: { items: { parts: number; shaded: number; la ))} - {f.label ?? `${toFa(f.shaded)} از ${toFa(f.parts)}`} + {f.label ?? (hide ? '' : `${toFa(f.shaded)} از ${toFa(f.parts)}`)}
); @@ -60,7 +62,7 @@ function FractionCompare({ items }: { items: { parts: number; shaded: number; la } /** عددِ مخلوط: چند واحدِ کامل و یک کسر (دو و هفت‌دهم). */ -function MixedNumber({ wholes, parts, shaded, shape }: { wholes: number; parts: number; shaded: number; shape: 'circle' | 'rect' }) { +function MixedNumber({ wholes, parts, shaded, shape, hide }: { wholes: number; parts: number; shaded: number; shape: 'circle' | 'rect'; hide: boolean }) { const n = wholes + 1, r = Math.min(40, (W - 20) / n / 2 - 6), cy = 52; const cx = (i: number) => W - 10 - r - 6 - i * (2 * r + 12); const slice = (x: number, i: number, full: boolean) => { @@ -73,9 +75,9 @@ function MixedNumber({ wholes, parts, shaded, shape }: { wholes: number; parts: ? : ))} {parts === 1 ? : Array.from({ length: parts }, (_, i) => slice(cx(wholes), i, false))} - + {!hide && {`${toFa(wholes)} واحد کامل و ${toFa(shaded)} تکه از ${toFa(parts)}`} - + } ); } @@ -83,6 +85,7 @@ function MixedNumber({ wholes, parts, shaded, shape }: { wholes: number; parts: /* ─── شبکه‌ی خانه‌ها: درصد، مساحت با شمردن، کسر روی جدول ─────────────────── */ export function Grid({ spec }: { spec: GridSpec }) { + const hide = useContext(HideCounts); const rows = spec.rows, cols = spec.cols; const cell = Math.min(26, (W - 30) / cols, 220 / rows); const gw = cols * cell, gh = rows * cell; @@ -100,9 +103,9 @@ export function Grid({ spec }: { spec: GridSpec }) { )))} - + {!hide && {`${toFa(count)} خانه از ${toFa(rows * cols)} خانه${100 % (rows * cols) === 0 || (rows * cols) % 10 === 0 ? `، یعنی ${toFa(Math.round((count / (rows * cols)) * 1000) / 10)}٪` : ''}`} - + } ); } @@ -110,6 +113,7 @@ export function Grid({ spec }: { spec: GridSpec }) { /* ─── گروه‌ها: مدلِ ضرب و تقسیم برای دبستان ───────────────────────────────── */ export function Groups({ spec }: { spec: GroupsSpec }) { + const hide = useContext(HideCounts); const g = spec.groups, each = spec.each; const perRow = Math.min(g, g <= 4 ? g : Math.ceil(g / 2)); const rowsG = Math.ceil(g / perRow); @@ -141,9 +145,9 @@ export function Groups({ spec }: { spec: GroupsSpec }) { ); })} - + {(spec.label != null || !hide) && {mathText(spec.label ?? `${g} × ${each} = ${g * each}`)} - + } ); } @@ -233,6 +237,7 @@ function patternCells(layout: PatternSpec['layout'], n: number): [number, number } export function Pattern({ spec }: { spec: PatternSpec }) { + const hide = useContext(HideCounts); const stages = spec.stages; const sticks = spec.unit === 'stick'; const sizes = stages.map((n) => { @@ -269,7 +274,7 @@ export function Pattern({ spec }: { spec: PatternSpec }) { : ; })} {`شکل ${toFa(i + 1)}`} - {spec.counts !== false && {`${toFa(count)} ${sticks ? 'چوب' : 'تا'}`}} + {spec.counts !== false && !hide && {`${toFa(count)} ${sticks ? 'چوب' : 'تا'}`}} ); }); diff --git a/src/diagram/svg.tsx b/src/diagram/svg.tsx index 872dddc..b912bf2 100644 --- a/src/diagram/svg.tsx +++ b/src/diagram/svg.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { createContext, type ReactNode } from 'react'; import type { ColorName } from './types'; import { toFa } from '@/lib/format'; @@ -25,6 +25,21 @@ export const COLORS: Record = { /** ترتیبِ رنگِ سری‌ها وقتی مدل رنگ نداده (نمودارِ دوم، ستونِ سوم، …). */ export const SERIES: ColorName[] = ['blue', 'red', 'green', 'orange', 'purple', 'teal', 'pink', 'yellow']; +/** رنگِ هر ستون یا برشِ نمودار: اگر برچسب نامِ یک رنگ باشد («قرمز»، «مهره‌ی آبی (۳)») همان رنگ، وگرنه رنگ‌های + * پیش‌فرض به ترتیب، بدون رنگ‌هایی که برچسبی به نامشان هست؛ تا «قرمز» در چرخنده یا جدولِ فراوانی آبی کشیده نشود. */ +const COLOR_WORDS: [string, ColorName][] = [ + ['قرمز', 'red'], ['سرخ', 'red'], ['آبی', 'blue'], ['سبز', 'green'], ['نارنجی', 'orange'], ['بنفش', 'purple'], + ['صورتی', 'pink'], ['زرد', 'yellow'], ['خاکستری', 'gray'], ['مشکی', 'black'], ['سیاه', 'black'], +]; +const namedColor = (label: string): ColorName | undefined => + COLOR_WORDS.find(([w]) => new RegExp(`(^|[\\s‌(«])${w}($|[\\s‌)»])`).test(label))?.[1]; +export function seriesColors(labels: string[], n: number): string[] { + const named = Array.from({ length: n }, (_, i) => namedColor(labels[i] ?? '')); + const free = SERIES.filter((c) => !named.includes(c)); + let k = 0; + return named.map((c) => COLORS[c ?? free[k++ % free.length]]); +} + export const INK = COLORS.black; export const SOFT = '#9EA3B8'; export const GRID = '#E4E6F0'; @@ -141,3 +156,10 @@ export function piLabel(t: number): string { const head = n === 1 ? 'π' : `${toFa(n)}π`; return den === 1 ? `${sign}${head}` : `${sign}${head}/${toFa(den)}`; } + +/** + * زیرِ شکلِ سوالِ آزمون، شمارشِ خودکار («۲ تکه از ۵ تکه»، «۶ خانه از ۱۲ خانه»، «۶ × ۴ = ۲۴»، «۹ تا») + * جوابِ سوال را لو می‌داد؛ آزمون این را روشن می‌کند و شکل فقط خودِ داده را نشان می‌دهد. + * در درس و جوابِ دستیار شمارش می‌ماند چون آنجا توضیح است، نه سوال. + */ +export const HideCounts = createContext(false); diff --git a/src/screens/QuizScreen.tsx b/src/screens/QuizScreen.tsx index 467cb89..dc5fbf5 100644 --- a/src/screens/QuizScreen.tsx +++ b/src/screens/QuizScreen.tsx @@ -346,7 +346,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s سوال {toFa(index + 1)}

- {question.figure ?
: null} + {question.figure ?
: null}