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}