@mathscan/math-exercise-engine 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +21850 -7531
- package/dist/index.js +1734 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1734 -57
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +800 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -77,6 +77,25 @@ const AST_TYPES = {
|
|
|
77
77
|
/** Visual fraction inline widget */
|
|
78
78
|
VISUAL_FRACTION: "VisualFraction",
|
|
79
79
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
// Chart
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
|
+
/** Generic chart layout block (bar, pictograph, …) */
|
|
83
|
+
CHART_BLOCK: "ChartBlock",
|
|
84
|
+
/**
|
|
85
|
+
* X axis sub-block: when present the Bar label inlines are hidden.
|
|
86
|
+
* If both X_AXIS and Y_AXIS exist, X_AXIS acts as category axis.
|
|
87
|
+
*/
|
|
88
|
+
CHART_X_AXIS: "ChartXAxis",
|
|
89
|
+
/**
|
|
90
|
+
* Y axis sub-block: value axis for vertical bar charts.
|
|
91
|
+
* If only X_AXIS is present, the transformer uses a default Y value axis.
|
|
92
|
+
*/
|
|
93
|
+
CHART_Y_AXIS: "ChartYAxis",
|
|
94
|
+
/** A single bar inside a CHART[type="bar"] block */
|
|
95
|
+
CHART_BAR: "ChartBar",
|
|
96
|
+
/** A single pictograph item inside a CHART[type="pictograph"] block */
|
|
97
|
+
CHART_PICTO: "ChartPicto",
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
99
|
// Tools
|
|
81
100
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
101
|
/** Drag-and-drop options tool */
|
|
@@ -171,7 +190,14 @@ const JSON_TYPES = {
|
|
|
171
190
|
/** Pair-matcher widget (top-row ↔ bottom-row pairing) */
|
|
172
191
|
PAIR_MATCHER: "pair-matcher",
|
|
173
192
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
|
-
//
|
|
193
|
+
// Chart
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
+
/** Generic chart block (bar, pictograph, …) */
|
|
196
|
+
CHART: "chart",
|
|
197
|
+
/** A single bar rendered inside a chart */
|
|
198
|
+
CHART_BAR: "chart-bar",
|
|
199
|
+
/** A single pictograph item rendered inside a chart */
|
|
200
|
+
CHART_PICTO: "chart-picto",
|
|
175
201
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
176
202
|
/** Inline input widget (string or numeric) */
|
|
177
203
|
INLINE_INPUT: "inline-input",
|
|
@@ -424,6 +450,26 @@ const RULER = createToken({
|
|
|
424
450
|
name: "RULER",
|
|
425
451
|
pattern: /RULER/
|
|
426
452
|
});
|
|
453
|
+
const CHART = createToken({
|
|
454
|
+
name: "CHART",
|
|
455
|
+
pattern: /CHART/
|
|
456
|
+
});
|
|
457
|
+
const X_AXIS = createToken({
|
|
458
|
+
name: "X_AXIS",
|
|
459
|
+
pattern: /X_AXIS/
|
|
460
|
+
});
|
|
461
|
+
const Y_AXIS = createToken({
|
|
462
|
+
name: "Y_AXIS",
|
|
463
|
+
pattern: /Y_AXIS/
|
|
464
|
+
});
|
|
465
|
+
const BAR = createToken({
|
|
466
|
+
name: "BAR",
|
|
467
|
+
pattern: /BAR/
|
|
468
|
+
});
|
|
469
|
+
const PICTO = createToken({
|
|
470
|
+
name: "PICTO",
|
|
471
|
+
pattern: /PICTO/
|
|
472
|
+
});
|
|
427
473
|
const SEP = createToken({
|
|
428
474
|
name: "SEP",
|
|
429
475
|
pattern: /SEP(?![a-zA-Z0-9_])/
|
|
@@ -566,6 +612,11 @@ const allTokens = [
|
|
|
566
612
|
PROTRACTOR,
|
|
567
613
|
SET_SQUARE,
|
|
568
614
|
RULER,
|
|
615
|
+
CHART,
|
|
616
|
+
X_AXIS,
|
|
617
|
+
Y_AXIS,
|
|
618
|
+
BAR,
|
|
619
|
+
PICTO,
|
|
569
620
|
SEP,
|
|
570
621
|
CARRY,
|
|
571
622
|
INPUT,
|
|
@@ -735,6 +786,10 @@ class MathExerciseParser extends CstParser {
|
|
|
735
786
|
GATE: this.BACKTRACK(this.verticalOperation),
|
|
736
787
|
ALT: () => this.SUBRULE(this.verticalOperation)
|
|
737
788
|
},
|
|
789
|
+
{
|
|
790
|
+
GATE: this.BACKTRACK(this.chartBlock),
|
|
791
|
+
ALT: () => this.SUBRULE(this.chartBlock)
|
|
792
|
+
},
|
|
738
793
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
739
794
|
]);
|
|
740
795
|
});
|
|
@@ -1590,6 +1645,168 @@ class MathExerciseParser extends CstParser {
|
|
|
1590
1645
|
this.MANY4(() => this.CONSUME4(Space));
|
|
1591
1646
|
this.CONSUME(RBracket);
|
|
1592
1647
|
});
|
|
1648
|
+
this.chartBlock = this.RULE("chartBlock", () => {
|
|
1649
|
+
this.MANY(() => {
|
|
1650
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1651
|
+
});
|
|
1652
|
+
this.CONSUME(CHART);
|
|
1653
|
+
this.CONSUME(LBracket);
|
|
1654
|
+
this.SUBRULE(this.parameterList);
|
|
1655
|
+
this.CONSUME(RBracket);
|
|
1656
|
+
this.MANY2(() => {
|
|
1657
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1658
|
+
});
|
|
1659
|
+
this.CONSUME(LBrace);
|
|
1660
|
+
this.MANY3(() => {
|
|
1661
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1662
|
+
});
|
|
1663
|
+
this.MANY5({
|
|
1664
|
+
GATE: () => this.LA(1).tokenType === X_AXIS || this.LA(1).tokenType === Y_AXIS,
|
|
1665
|
+
DEF: () => {
|
|
1666
|
+
this.OR5([
|
|
1667
|
+
{ ALT: () => this.SUBRULE(this.chartXAxis) },
|
|
1668
|
+
{ ALT: () => this.SUBRULE(this.chartYAxis) }
|
|
1669
|
+
]);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
this.MANY6({
|
|
1673
|
+
GATE: () => {
|
|
1674
|
+
let k = 1;
|
|
1675
|
+
while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
|
|
1676
|
+
const t = this.LA(k).tokenType;
|
|
1677
|
+
return t === BAR || t === PICTO;
|
|
1678
|
+
},
|
|
1679
|
+
DEF: () => {
|
|
1680
|
+
this.OR6([
|
|
1681
|
+
{
|
|
1682
|
+
GATE: () => {
|
|
1683
|
+
let k = 1;
|
|
1684
|
+
while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
|
|
1685
|
+
return this.LA(k).tokenType === BAR;
|
|
1686
|
+
},
|
|
1687
|
+
ALT: () => this.SUBRULE(this.chartBarItem)
|
|
1688
|
+
},
|
|
1689
|
+
{ ALT: () => this.SUBRULE(this.chartPictoItem) }
|
|
1690
|
+
]);
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
this.CONSUME(RBrace);
|
|
1694
|
+
this.MANY4(() => {
|
|
1695
|
+
this.OR4([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
this.chartXAxis = this.RULE("chartXAxis", () => {
|
|
1699
|
+
this.CONSUME(X_AXIS);
|
|
1700
|
+
this.OPTION(() => {
|
|
1701
|
+
this.CONSUME(LBracket);
|
|
1702
|
+
this.OPTION2(() => {
|
|
1703
|
+
this.SUBRULE(this.parameterList);
|
|
1704
|
+
});
|
|
1705
|
+
this.CONSUME(RBracket);
|
|
1706
|
+
});
|
|
1707
|
+
this.MANY(() => {
|
|
1708
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1709
|
+
});
|
|
1710
|
+
this.OPTION3(() => {
|
|
1711
|
+
this.CONSUME(LBrace);
|
|
1712
|
+
this.MANY2(() => {
|
|
1713
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1714
|
+
});
|
|
1715
|
+
this.MANY3(() => {
|
|
1716
|
+
this.OR3([
|
|
1717
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1718
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1719
|
+
]);
|
|
1720
|
+
});
|
|
1721
|
+
this.CONSUME(RBrace);
|
|
1722
|
+
});
|
|
1723
|
+
this.MANY4(() => {
|
|
1724
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
this.chartYAxis = this.RULE("chartYAxis", () => {
|
|
1728
|
+
this.CONSUME(Y_AXIS);
|
|
1729
|
+
this.OPTION(() => {
|
|
1730
|
+
this.CONSUME(LBracket);
|
|
1731
|
+
this.OPTION2(() => {
|
|
1732
|
+
this.SUBRULE(this.parameterList);
|
|
1733
|
+
});
|
|
1734
|
+
this.CONSUME(RBracket);
|
|
1735
|
+
});
|
|
1736
|
+
this.MANY(() => {
|
|
1737
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1738
|
+
});
|
|
1739
|
+
this.OPTION3(() => {
|
|
1740
|
+
this.CONSUME(LBrace);
|
|
1741
|
+
this.MANY2(() => {
|
|
1742
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1743
|
+
});
|
|
1744
|
+
this.MANY3(() => {
|
|
1745
|
+
this.OR3([
|
|
1746
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1747
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1748
|
+
]);
|
|
1749
|
+
});
|
|
1750
|
+
this.CONSUME(RBrace);
|
|
1751
|
+
});
|
|
1752
|
+
this.MANY4(() => {
|
|
1753
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
this.chartBarItem = this.RULE("chartBarItem", () => {
|
|
1757
|
+
this.MANY(() => {
|
|
1758
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1759
|
+
});
|
|
1760
|
+
this.CONSUME(BAR);
|
|
1761
|
+
this.CONSUME(LBracket);
|
|
1762
|
+
this.SUBRULE(this.parameterList);
|
|
1763
|
+
this.CONSUME(RBracket);
|
|
1764
|
+
this.MANY2(() => {
|
|
1765
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1766
|
+
});
|
|
1767
|
+
this.OPTION(() => {
|
|
1768
|
+
this.CONSUME(LBrace);
|
|
1769
|
+
this.MANY3(() => {
|
|
1770
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1771
|
+
});
|
|
1772
|
+
this.MANY4(() => {
|
|
1773
|
+
this.OR4([
|
|
1774
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1775
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1776
|
+
]);
|
|
1777
|
+
});
|
|
1778
|
+
this.CONSUME(RBrace);
|
|
1779
|
+
});
|
|
1780
|
+
this.MANY5(() => {
|
|
1781
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
this.chartPictoItem = this.RULE("chartPictoItem", () => {
|
|
1785
|
+
this.MANY(() => {
|
|
1786
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1787
|
+
});
|
|
1788
|
+
this.CONSUME(PICTO);
|
|
1789
|
+
this.CONSUME(LBracket);
|
|
1790
|
+
this.SUBRULE(this.parameterList);
|
|
1791
|
+
this.CONSUME(RBracket);
|
|
1792
|
+
this.MANY2(() => {
|
|
1793
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1794
|
+
});
|
|
1795
|
+
this.CONSUME(LBrace);
|
|
1796
|
+
this.MANY3(() => {
|
|
1797
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1798
|
+
});
|
|
1799
|
+
this.MANY4(() => {
|
|
1800
|
+
this.OR4([
|
|
1801
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1802
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1803
|
+
]);
|
|
1804
|
+
});
|
|
1805
|
+
this.CONSUME(RBrace);
|
|
1806
|
+
this.MANY5(() => {
|
|
1807
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1593
1810
|
this.performSelfAnalysis();
|
|
1594
1811
|
}
|
|
1595
1812
|
/** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
|
|
@@ -1604,6 +1821,18 @@ class MathExerciseParser extends CstParser {
|
|
|
1604
1821
|
}
|
|
1605
1822
|
}
|
|
1606
1823
|
const parser = new MathExerciseParser();
|
|
1824
|
+
function normalize(value) {
|
|
1825
|
+
if (typeof value === "number") {
|
|
1826
|
+
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
1827
|
+
}
|
|
1828
|
+
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
1829
|
+
const asNum = Number(trimmed);
|
|
1830
|
+
if (trimmed !== "" && !isNaN(asNum)) {
|
|
1831
|
+
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
1832
|
+
}
|
|
1833
|
+
return trimmed;
|
|
1834
|
+
}
|
|
1835
|
+
const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
|
|
1607
1836
|
function parseOrThrow(schema, data, label) {
|
|
1608
1837
|
const result = schema.safeParse(data);
|
|
1609
1838
|
if (!result.success) {
|
|
@@ -1673,11 +1902,10 @@ const FractionInlineSchema = z.object({
|
|
|
1673
1902
|
simplified: z.boolean().default(true),
|
|
1674
1903
|
placeholder: z.string().max(32).optional()
|
|
1675
1904
|
});
|
|
1676
|
-
const IMAGE_SRC_SCHEME_RE$1 = /^(https?:\/\/|data:image\/)/i;
|
|
1677
1905
|
const ImageInlineSchema = z.object({
|
|
1678
1906
|
type: z.literal(AST_TYPES.IMAGE),
|
|
1679
1907
|
name: z.string().min(1),
|
|
1680
|
-
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE
|
|
1908
|
+
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE.test(s), {
|
|
1681
1909
|
message: "src must use http(s) or data:image scheme"
|
|
1682
1910
|
}),
|
|
1683
1911
|
alt: z.string(),
|
|
@@ -2719,6 +2947,350 @@ function isSetSquareTool(value) {
|
|
|
2719
2947
|
function isToolNode(value) {
|
|
2720
2948
|
return ToolNodeSchema.safeParse(value).success;
|
|
2721
2949
|
}
|
|
2950
|
+
const CHART_TYPES = ["bar", "pictograph"];
|
|
2951
|
+
const CHART_AXIS_DEFAULTS = { min: 0, max: 100, step: 10, visible: true };
|
|
2952
|
+
const CHART_MAX_AXIS_TICKS = 200;
|
|
2953
|
+
const CHART_MAX_PICTO_SLOTS = 60;
|
|
2954
|
+
const ChartAxisSchema = z.object({
|
|
2955
|
+
type: z.literal(AST_TYPES.CHART_X_AXIS).or(z.literal(AST_TYPES.CHART_Y_AXIS)),
|
|
2956
|
+
min: z.number().optional(),
|
|
2957
|
+
max: z.number().optional(),
|
|
2958
|
+
step: z.number().positive().optional(),
|
|
2959
|
+
/** When false, the entire axis is hidden. Defaults to true. */
|
|
2960
|
+
visible: z.boolean().optional(),
|
|
2961
|
+
/** Inline elements used as the axis title label */
|
|
2962
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
2963
|
+
}).superRefine((axis, ctx) => {
|
|
2964
|
+
if (axis.min !== void 0 && axis.max !== void 0 && axis.min >= axis.max) {
|
|
2965
|
+
ctx.addIssue({
|
|
2966
|
+
code: z.ZodIssueCode.custom,
|
|
2967
|
+
message: `AXIS: min (${axis.min}) must be strictly less than max (${axis.max})`,
|
|
2968
|
+
path: ["min"]
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
function createChartAxis(axisType, opts) {
|
|
2973
|
+
return ChartAxisSchema.parse({
|
|
2974
|
+
type: axisType,
|
|
2975
|
+
min: opts.min,
|
|
2976
|
+
max: opts.max,
|
|
2977
|
+
step: opts.step,
|
|
2978
|
+
visible: opts.visible,
|
|
2979
|
+
inlines: opts.inlines ?? []
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
function isChartAxis(value) {
|
|
2983
|
+
return ChartAxisSchema.safeParse(value).success;
|
|
2984
|
+
}
|
|
2985
|
+
const ChartPictoSchema = z.object({
|
|
2986
|
+
type: z.literal(AST_TYPES.CHART_PICTO),
|
|
2987
|
+
id: z.string().min(1),
|
|
2988
|
+
value: z.number().default(0),
|
|
2989
|
+
answer: z.number().optional(),
|
|
2990
|
+
image: z.string().min(1).refine(
|
|
2991
|
+
(s) => IMAGE_SRC_SCHEME_RE.test(s),
|
|
2992
|
+
{ message: "image must use http(s) or data:image scheme" }
|
|
2993
|
+
),
|
|
2994
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
2995
|
+
});
|
|
2996
|
+
function createChartPicto(id, opts) {
|
|
2997
|
+
return ChartPictoSchema.parse({
|
|
2998
|
+
type: AST_TYPES.CHART_PICTO,
|
|
2999
|
+
id,
|
|
3000
|
+
value: opts.value ?? 0,
|
|
3001
|
+
answer: opts.answer,
|
|
3002
|
+
image: opts.image,
|
|
3003
|
+
inlines: opts.inlines ?? []
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
function isChartPicto(value) {
|
|
3007
|
+
return ChartPictoSchema.safeParse(value).success;
|
|
3008
|
+
}
|
|
3009
|
+
const ChartBarSchema = z.object({
|
|
3010
|
+
type: z.literal(AST_TYPES.CHART_BAR),
|
|
3011
|
+
id: z.string().min(1),
|
|
3012
|
+
value: z.number().default(0),
|
|
3013
|
+
answer: z.number().optional(),
|
|
3014
|
+
color: z.string().optional(),
|
|
3015
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
3016
|
+
});
|
|
3017
|
+
function createChartBar(id, opts) {
|
|
3018
|
+
return ChartBarSchema.parse({
|
|
3019
|
+
type: AST_TYPES.CHART_BAR,
|
|
3020
|
+
id,
|
|
3021
|
+
value: opts.value ?? 0,
|
|
3022
|
+
answer: opts.answer,
|
|
3023
|
+
color: opts.color,
|
|
3024
|
+
inlines: opts.inlines ?? []
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
function isChartBar(value) {
|
|
3028
|
+
return ChartBarSchema.safeParse(value).success;
|
|
3029
|
+
}
|
|
3030
|
+
const ChartBlockSchema = z.object({
|
|
3031
|
+
type: z.literal(AST_TYPES.CHART_BLOCK),
|
|
3032
|
+
name: z.string().optional(),
|
|
3033
|
+
title: z.string().optional(),
|
|
3034
|
+
chartType: z.enum(CHART_TYPES),
|
|
3035
|
+
grid: z.boolean().default(false),
|
|
3036
|
+
scale: z.number().positive().default(1),
|
|
3037
|
+
snap: z.number().positive().optional(),
|
|
3038
|
+
xAxis: ChartAxisSchema.optional(),
|
|
3039
|
+
yAxis: ChartAxisSchema.optional(),
|
|
3040
|
+
bars: z.array(ChartBarSchema).default([]),
|
|
3041
|
+
pictos: z.array(ChartPictoSchema).default([])
|
|
3042
|
+
}).superRefine((block, ctx) => {
|
|
3043
|
+
validateChartBlock(block, ctx);
|
|
3044
|
+
});
|
|
3045
|
+
function resolveChartAxes(block) {
|
|
3046
|
+
const horizontal = axisHasNumericParams(block.xAxis) && !axisHasNumericParams(block.yAxis);
|
|
3047
|
+
const orientation = horizontal ? "horizontal" : "vertical";
|
|
3048
|
+
return {
|
|
3049
|
+
orientation,
|
|
3050
|
+
valueAxis: resolveNumericAxis(horizontal ? block.xAxis : block.yAxis),
|
|
3051
|
+
categoryAxis: horizontal ? block.yAxis : block.xAxis
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
function validateChartBlock(block, ctx) {
|
|
3055
|
+
if (hasAmbiguousValueAxes(block)) {
|
|
3056
|
+
ctx.addIssue({
|
|
3057
|
+
code: z.ZodIssueCode.custom,
|
|
3058
|
+
message: `CHART[type="${block.chartType}"]: both X_AXIS and Y_AXIS declare numeric params (min/max/step) — ambiguous orientation. Remove params from one axis: the axis with params becomes the value axis.`,
|
|
3059
|
+
path: ["xAxis"]
|
|
3060
|
+
});
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
const { valueAxis } = resolveChartAxes(block);
|
|
3064
|
+
validateValueAxisRange(valueAxis, ctx);
|
|
3065
|
+
if (block.chartType === "bar") {
|
|
3066
|
+
if (block.pictos.length > 0) {
|
|
3067
|
+
ctx.addIssue({
|
|
3068
|
+
code: z.ZodIssueCode.custom,
|
|
3069
|
+
message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO',
|
|
3070
|
+
path: ["pictos"]
|
|
3071
|
+
});
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
if (block.bars.length === 0) {
|
|
3075
|
+
ctx.addIssue({
|
|
3076
|
+
code: z.ZodIssueCode.custom,
|
|
3077
|
+
message: 'CHART[type="bar"]: at least one BAR is required',
|
|
3078
|
+
path: ["bars"]
|
|
3079
|
+
});
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
validateUniqueIds(block.bars, ctx);
|
|
3083
|
+
validateChartSnap(block.snap, valueAxis.step, ctx);
|
|
3084
|
+
validateBarAnswers(block.bars, valueAxis, block.snap, ctx);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (block.chartType === "pictograph") {
|
|
3088
|
+
if (block.bars.length > 0) {
|
|
3089
|
+
ctx.addIssue({
|
|
3090
|
+
code: z.ZodIssueCode.custom,
|
|
3091
|
+
message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR',
|
|
3092
|
+
path: ["bars"]
|
|
3093
|
+
});
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
if (block.pictos.length === 0) {
|
|
3097
|
+
ctx.addIssue({
|
|
3098
|
+
code: z.ZodIssueCode.custom,
|
|
3099
|
+
message: 'CHART[type="pictograph"]: at least one PICTO is required',
|
|
3100
|
+
path: ["pictos"]
|
|
3101
|
+
});
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (block.snap !== void 0) {
|
|
3105
|
+
ctx.addIssue({
|
|
3106
|
+
code: z.ZodIssueCode.custom,
|
|
3107
|
+
message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
|
|
3108
|
+
path: ["snap"]
|
|
3109
|
+
});
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
validateUniqueIds(block.pictos, ctx);
|
|
3113
|
+
if (!axisHasNumericParams(valueAxis.axis)) {
|
|
3114
|
+
ctx.addIssue({
|
|
3115
|
+
code: z.ZodIssueCode.custom,
|
|
3116
|
+
message: 'CHART[type="pictograph"]: a value axis with min/max/step is required (e.g. Y_AXIS[min=0, max=8, step=2]) — without it the chart defaults to a 0–100 range and renders 100 icons per column.',
|
|
3117
|
+
path: ["yAxis"]
|
|
3118
|
+
});
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
const slotCount = Math.round(valueAxis.max - valueAxis.min);
|
|
3122
|
+
if (slotCount > CHART_MAX_PICTO_SLOTS) {
|
|
3123
|
+
ctx.addIssue({
|
|
3124
|
+
code: z.ZodIssueCode.custom,
|
|
3125
|
+
message: `CHART[type="pictograph"]: the value axis spans ${slotCount} units, so each column would render ${slotCount} icons — over the ${CHART_MAX_PICTO_SLOTS} limit. Narrow the range.`,
|
|
3126
|
+
path: ["yAxis"]
|
|
3127
|
+
});
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
validatePictoAnswers(block.pictos, valueAxis, ctx);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
ctx.addIssue({
|
|
3134
|
+
code: z.ZodIssueCode.custom,
|
|
3135
|
+
message: `CHART: unsupported chartType "${block.chartType}" — add a validation branch to validateChartBlock`,
|
|
3136
|
+
path: ["chartType"]
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
function axisHasNumericParams(axis) {
|
|
3140
|
+
return axis?.min !== void 0 || axis?.max !== void 0 || axis?.step !== void 0;
|
|
3141
|
+
}
|
|
3142
|
+
function resolveNumericAxis(axis) {
|
|
3143
|
+
return {
|
|
3144
|
+
axis,
|
|
3145
|
+
min: axis?.min ?? CHART_AXIS_DEFAULTS.min,
|
|
3146
|
+
max: axis?.max ?? CHART_AXIS_DEFAULTS.max,
|
|
3147
|
+
step: axis?.step ?? CHART_AXIS_DEFAULTS.step
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
function hasAmbiguousValueAxes(block) {
|
|
3151
|
+
return axisHasNumericParams(block.xAxis) && axisHasNumericParams(block.yAxis);
|
|
3152
|
+
}
|
|
3153
|
+
function validateUniqueIds(items, ctx) {
|
|
3154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3155
|
+
items.forEach((item, index) => {
|
|
3156
|
+
if (seen.has(item.id)) {
|
|
3157
|
+
ctx.addIssue({
|
|
3158
|
+
code: z.ZodIssueCode.custom,
|
|
3159
|
+
message: `CHART: duplicate id "${item.id}" — ids must be unique within the chart`,
|
|
3160
|
+
path: [index, "id"]
|
|
3161
|
+
});
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
seen.add(item.id);
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
function validateValueAxisRange(axis, ctx) {
|
|
3168
|
+
if (axis.min >= axis.max) {
|
|
3169
|
+
ctx.addIssue({
|
|
3170
|
+
code: z.ZodIssueCode.custom,
|
|
3171
|
+
message: `CHART: value axis min (${axis.min}) must be strictly less than max (${axis.max})`
|
|
3172
|
+
});
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
const tickCount = Math.floor((axis.max - axis.min) / axis.step) + 1;
|
|
3176
|
+
if (tickCount > CHART_MAX_AXIS_TICKS) {
|
|
3177
|
+
ctx.addIssue({
|
|
3178
|
+
code: z.ZodIssueCode.custom,
|
|
3179
|
+
message: `CHART: value axis would render ${tickCount} ticks ((${axis.max} − ${axis.min}) / step ${axis.step}), over the ${CHART_MAX_AXIS_TICKS} limit — widen the step or narrow the range.`
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function validateChartSnap(snap, step, ctx) {
|
|
3184
|
+
if (snap === void 0) return;
|
|
3185
|
+
if (snap > step) {
|
|
3186
|
+
ctx.addIssue({
|
|
3187
|
+
code: z.ZodIssueCode.custom,
|
|
3188
|
+
message: `CHART: snap (${snap}) must be ≤ value-axis step (${step}).`,
|
|
3189
|
+
path: ["snap"]
|
|
3190
|
+
});
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
const remainder = step % snap;
|
|
3194
|
+
if (remainder > 1e-9 && remainder < snap - 1e-9) {
|
|
3195
|
+
ctx.addIssue({
|
|
3196
|
+
code: z.ZodIssueCode.custom,
|
|
3197
|
+
message: `CHART: snap (${snap}) must evenly divide the value-axis step (${step}) — otherwise bars snap to positions between tick marks.`,
|
|
3198
|
+
path: ["snap"]
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
function validateBarAnswers(bars, valueAxis, snap, ctx) {
|
|
3203
|
+
const gridStep = snap ?? valueAxis.step;
|
|
3204
|
+
bars.forEach((bar, index) => {
|
|
3205
|
+
if (bar.value < valueAxis.min || bar.value > valueAxis.max) {
|
|
3206
|
+
ctx.addIssue({
|
|
3207
|
+
code: z.ZodIssueCode.custom,
|
|
3208
|
+
message: `CHART: BAR "${bar.id}" value (${bar.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the bar will not render correctly`,
|
|
3209
|
+
path: ["bars", index, "value"]
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
if (bar.answer === void 0) return;
|
|
3213
|
+
if (bar.answer < valueAxis.min || bar.answer > valueAxis.max) {
|
|
3214
|
+
ctx.addIssue({
|
|
3215
|
+
code: z.ZodIssueCode.custom,
|
|
3216
|
+
message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
|
|
3217
|
+
path: ["bars", index, "answer"]
|
|
3218
|
+
});
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const offGrid = Math.abs(bar.answer - valueAxis.min) % gridStep;
|
|
3222
|
+
if (offGrid > 1e-9 && offGrid < gridStep - 1e-9) {
|
|
3223
|
+
ctx.addIssue({
|
|
3224
|
+
code: z.ZodIssueCode.custom,
|
|
3225
|
+
message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is not on the drag grid (min ${valueAxis.min} + multiples of ${gridStep}) — the student can never land on it. Align the answer to the step/snap grid.`,
|
|
3226
|
+
path: ["bars", index, "answer"]
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
function isOnPictoGrid(value, axisMin) {
|
|
3232
|
+
const offset = value - axisMin;
|
|
3233
|
+
return Math.abs(offset - Math.round(offset)) <= 1e-9;
|
|
3234
|
+
}
|
|
3235
|
+
function validatePictoAnswers(pictos, valueAxis, ctx) {
|
|
3236
|
+
pictos.forEach((picto, index) => {
|
|
3237
|
+
if (picto.value < valueAxis.min || picto.value > valueAxis.max) {
|
|
3238
|
+
ctx.addIssue({
|
|
3239
|
+
code: z.ZodIssueCode.custom,
|
|
3240
|
+
message: `CHART: PICTO "${picto.id}" value (${picto.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}]`,
|
|
3241
|
+
path: ["pictos", index, "value"]
|
|
3242
|
+
});
|
|
3243
|
+
} else if (!isOnPictoGrid(picto.value, valueAxis.min)) {
|
|
3244
|
+
ctx.addIssue({
|
|
3245
|
+
code: z.ZodIssueCode.custom,
|
|
3246
|
+
message: `CHART: PICTO "${picto.id}" value (${picto.value}) is not a whole number of icons above the axis minimum (${valueAxis.min}) — icons can't be drawn in fractions.`,
|
|
3247
|
+
path: ["pictos", index, "value"]
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
if (picto.answer === void 0) return;
|
|
3251
|
+
if (picto.answer < valueAxis.min || picto.answer > valueAxis.max) {
|
|
3252
|
+
ctx.addIssue({
|
|
3253
|
+
code: z.ZodIssueCode.custom,
|
|
3254
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
|
|
3255
|
+
path: ["pictos", index, "answer"]
|
|
3256
|
+
});
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
if (!isOnPictoGrid(picto.answer, valueAxis.min)) {
|
|
3260
|
+
ctx.addIssue({
|
|
3261
|
+
code: z.ZodIssueCode.custom,
|
|
3262
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is not a whole number of icons above the axis minimum (${valueAxis.min}) — the student can never land on it.`,
|
|
3263
|
+
path: ["pictos", index, "answer"]
|
|
3264
|
+
});
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
if (picto.answer < picto.value) {
|
|
3268
|
+
ctx.addIssue({
|
|
3269
|
+
code: z.ZodIssueCode.custom,
|
|
3270
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is below its prefilled value (${picto.value}) — prefilled icons can't be removed, so the exercise is unsolvable.`,
|
|
3271
|
+
path: ["pictos", index, "answer"]
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
function createChartBlock(chartType, opts = {}) {
|
|
3277
|
+
return ChartBlockSchema.parse({
|
|
3278
|
+
type: AST_TYPES.CHART_BLOCK,
|
|
3279
|
+
name: opts.name,
|
|
3280
|
+
title: opts.title,
|
|
3281
|
+
chartType,
|
|
3282
|
+
grid: opts.grid ?? false,
|
|
3283
|
+
scale: opts.scale ?? 1,
|
|
3284
|
+
snap: opts.snap,
|
|
3285
|
+
xAxis: opts.xAxis,
|
|
3286
|
+
yAxis: opts.yAxis,
|
|
3287
|
+
bars: opts.bars ?? [],
|
|
3288
|
+
pictos: opts.pictos ?? []
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
function isChartBlock(value) {
|
|
3292
|
+
return ChartBlockSchema.safeParse(value).success;
|
|
3293
|
+
}
|
|
2722
3294
|
const RadioOptionsBlockSchema = z.object({
|
|
2723
3295
|
type: z.literal(AST_TYPES.RADIO_OPTIONS),
|
|
2724
3296
|
name: z.string(),
|
|
@@ -2778,7 +3350,7 @@ function isMixedValidation(value) {
|
|
|
2778
3350
|
function isValidationNode(value) {
|
|
2779
3351
|
return ValidationNodeSchema.safeParse(value).success;
|
|
2780
3352
|
}
|
|
2781
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema]);
|
|
3353
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema, ChartBlockSchema]);
|
|
2782
3354
|
const ExerciseDocumentSchema = z.object({
|
|
2783
3355
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
2784
3356
|
question: TextBlockSchema.optional(),
|
|
@@ -2884,6 +3456,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2884
3456
|
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
2885
3457
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2886
3458
|
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
3459
|
+
if (ctx.chartBlock) return this.visit(ctx.chartBlock);
|
|
2887
3460
|
if (ctx.line) return this.visit(ctx.line);
|
|
2888
3461
|
throw new Error("Unknown content block");
|
|
2889
3462
|
}
|
|
@@ -3362,6 +3935,70 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3362
3935
|
return createInlineDragOption(value);
|
|
3363
3936
|
}
|
|
3364
3937
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3938
|
+
// Chart Block
|
|
3939
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3940
|
+
chartBlock(ctx) {
|
|
3941
|
+
const params = this.visit(ctx.parameterList);
|
|
3942
|
+
const chartType = params.type;
|
|
3943
|
+
if (!chartType) throw new Error('CHART: type is required ("bar" | "pictograph")');
|
|
3944
|
+
if (!CHART_TYPES.includes(chartType)) {
|
|
3945
|
+
throw new Error(`CHART: type must be one of ${CHART_TYPES.join(", ")} (got "${chartType}")`);
|
|
3946
|
+
}
|
|
3947
|
+
const name = params.name;
|
|
3948
|
+
const title = params.title;
|
|
3949
|
+
const grid = params.grid === true || params.grid === "true";
|
|
3950
|
+
const scale = params.scale !== void 0 ? Number(params.scale) : 1;
|
|
3951
|
+
const snap = params.snap !== void 0 ? Number(params.snap) : void 0;
|
|
3952
|
+
if (ctx.chartXAxis && ctx.chartXAxis.length > 1) {
|
|
3953
|
+
throw new Error("CHART: only one X_AXIS is allowed");
|
|
3954
|
+
}
|
|
3955
|
+
if (ctx.chartYAxis && ctx.chartYAxis.length > 1) {
|
|
3956
|
+
throw new Error("CHART: only one Y_AXIS is allowed");
|
|
3957
|
+
}
|
|
3958
|
+
const xAxis = ctx.chartXAxis ? this.visit(ctx.chartXAxis) : void 0;
|
|
3959
|
+
const yAxis = ctx.chartYAxis ? this.visit(ctx.chartYAxis) : void 0;
|
|
3960
|
+
const bars = ctx.chartBarItem ? ctx.chartBarItem.map((b) => this.visit(b)) : [];
|
|
3961
|
+
const pictos = ctx.chartPictoItem ? ctx.chartPictoItem.map((p) => this.visit(p)) : [];
|
|
3962
|
+
return createChartBlock(chartType, { bars, pictos, name, title, grid, scale, snap, xAxis, yAxis });
|
|
3963
|
+
}
|
|
3964
|
+
/** X_AXIS and Y_AXIS carry identical params — only the produced AST type differs.
|
|
3965
|
+
* Chevrotain needs two separate grammar rules, but one visitor body serves both. */
|
|
3966
|
+
buildChartAxis(ctx, astType) {
|
|
3967
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
3968
|
+
const min = params.min !== void 0 ? Number(params.min) : void 0;
|
|
3969
|
+
const max = params.max !== void 0 ? Number(params.max) : void 0;
|
|
3970
|
+
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
3971
|
+
const visible = params.visible !== void 0 ? params.visible === true || params.visible === "true" : void 0;
|
|
3972
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
3973
|
+
return createChartAxis(astType, { min, max, step, visible, inlines });
|
|
3974
|
+
}
|
|
3975
|
+
chartXAxis(ctx) {
|
|
3976
|
+
return this.buildChartAxis(ctx, AST_TYPES.CHART_X_AXIS);
|
|
3977
|
+
}
|
|
3978
|
+
chartYAxis(ctx) {
|
|
3979
|
+
return this.buildChartAxis(ctx, AST_TYPES.CHART_Y_AXIS);
|
|
3980
|
+
}
|
|
3981
|
+
chartBarItem(ctx) {
|
|
3982
|
+
const params = this.visit(ctx.parameterList);
|
|
3983
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
3984
|
+
return createChartBar(params.id, {
|
|
3985
|
+
value: params.value !== void 0 ? Number(params.value) : void 0,
|
|
3986
|
+
answer: params.answer !== void 0 ? Number(params.answer) : void 0,
|
|
3987
|
+
color: params.color,
|
|
3988
|
+
inlines
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
chartPictoItem(ctx) {
|
|
3992
|
+
const params = this.visit(ctx.parameterList);
|
|
3993
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
3994
|
+
return createChartPicto(params.id, {
|
|
3995
|
+
value: params.value !== void 0 ? Number(params.value) : void 0,
|
|
3996
|
+
answer: params.answer !== void 0 ? Number(params.answer) : void 0,
|
|
3997
|
+
image: params.image,
|
|
3998
|
+
inlines
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3365
4002
|
// Tools
|
|
3366
4003
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3367
4004
|
tool(ctx) {
|
|
@@ -3548,7 +4185,6 @@ const BoldTextSchema = z.object({
|
|
|
3548
4185
|
value: z.string(),
|
|
3549
4186
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
3550
4187
|
});
|
|
3551
|
-
const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
|
|
3552
4188
|
const ImageItemSchema = z.object({
|
|
3553
4189
|
id: z.string(),
|
|
3554
4190
|
type: z.literal(JSON_TYPES.IMAGE),
|
|
@@ -3617,7 +4253,8 @@ const LayoutItemSchema = z.lazy(
|
|
|
3617
4253
|
TableItemSchema,
|
|
3618
4254
|
NumberLineItemSchema,
|
|
3619
4255
|
VerticalOperationItemSchema,
|
|
3620
|
-
ClockItemSchema
|
|
4256
|
+
ClockItemSchema,
|
|
4257
|
+
ChartItemSchema
|
|
3621
4258
|
])
|
|
3622
4259
|
);
|
|
3623
4260
|
const GroupItemSchema = z.object({
|
|
@@ -3790,6 +4427,97 @@ const VerticalOperationItemSchema = z.object({
|
|
|
3790
4427
|
cols: z.number().int().positive(),
|
|
3791
4428
|
items: z.array(z.union([VopRowItemSchema, VopCarryRowItemSchema, VopSeparatorItemSchema]))
|
|
3792
4429
|
});
|
|
4430
|
+
const ChartAxisJSONSchema = z.object({
|
|
4431
|
+
min: z.number().optional(),
|
|
4432
|
+
max: z.number().optional(),
|
|
4433
|
+
step: z.number().positive().optional(),
|
|
4434
|
+
visible: z.boolean().default(true),
|
|
4435
|
+
title: z.array(LayoutItemSchema).optional()
|
|
4436
|
+
}).superRefine((axis, ctx) => {
|
|
4437
|
+
const numericFields = [axis.min, axis.max, axis.step];
|
|
4438
|
+
const numericFieldCount = numericFields.filter((value) => value !== void 0).length;
|
|
4439
|
+
if (numericFieldCount > 0 && numericFieldCount < numericFields.length) {
|
|
4440
|
+
ctx.addIssue({
|
|
4441
|
+
code: z.ZodIssueCode.custom,
|
|
4442
|
+
message: "AXIS: min, max, and step must be provided together for a numeric axis"
|
|
4443
|
+
});
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
if (axis.min !== void 0 && axis.max !== void 0 && axis.min >= axis.max) {
|
|
4447
|
+
ctx.addIssue({
|
|
4448
|
+
code: z.ZodIssueCode.custom,
|
|
4449
|
+
message: `AXIS: min (${axis.min}) must be strictly less than max (${axis.max})`,
|
|
4450
|
+
path: ["min"]
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
});
|
|
4454
|
+
const ChartBarJSONSchema = z.discriminatedUnion("interactive", [
|
|
4455
|
+
z.object({
|
|
4456
|
+
id: z.string(),
|
|
4457
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4458
|
+
interactive: z.literal(false),
|
|
4459
|
+
value: z.number(),
|
|
4460
|
+
color: z.string().optional(),
|
|
4461
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4462
|
+
}),
|
|
4463
|
+
z.object({
|
|
4464
|
+
id: z.string(),
|
|
4465
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4466
|
+
interactive: z.literal(true),
|
|
4467
|
+
value: z.number(),
|
|
4468
|
+
widgetId: z.string(),
|
|
4469
|
+
color: z.string().optional(),
|
|
4470
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4471
|
+
})
|
|
4472
|
+
]);
|
|
4473
|
+
const ChartPictoJSONSchema = z.object({
|
|
4474
|
+
id: z.string(),
|
|
4475
|
+
type: z.literal(JSON_TYPES.CHART_PICTO),
|
|
4476
|
+
value: z.number(),
|
|
4477
|
+
answer: z.number().optional(),
|
|
4478
|
+
image: z.string().min(1).refine(
|
|
4479
|
+
(s) => IMAGE_SRC_SCHEME_RE.test(s),
|
|
4480
|
+
{ message: "image must use http(s) or data:image scheme" }
|
|
4481
|
+
),
|
|
4482
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4483
|
+
});
|
|
4484
|
+
const ChartItemSchema = z.object({
|
|
4485
|
+
id: z.string(),
|
|
4486
|
+
type: z.literal(JSON_TYPES.CHART),
|
|
4487
|
+
name: z.string().optional(),
|
|
4488
|
+
title: z.string().optional(),
|
|
4489
|
+
chartType: z.enum(CHART_TYPES),
|
|
4490
|
+
orientation: z.enum(["vertical", "horizontal"]).default("vertical"),
|
|
4491
|
+
grid: z.boolean().default(false),
|
|
4492
|
+
scale: z.number().positive().default(1),
|
|
4493
|
+
snap: z.number().positive().optional(),
|
|
4494
|
+
xAxis: ChartAxisJSONSchema.optional(),
|
|
4495
|
+
yAxis: ChartAxisJSONSchema.optional(),
|
|
4496
|
+
bars: z.array(ChartBarJSONSchema).default([]),
|
|
4497
|
+
pictos: z.array(ChartPictoJSONSchema).default([])
|
|
4498
|
+
}).superRefine((item, ctx) => {
|
|
4499
|
+
if (item.chartType === "bar" && item.bars.length === 0) {
|
|
4500
|
+
ctx.addIssue({
|
|
4501
|
+
code: z.ZodIssueCode.custom,
|
|
4502
|
+
message: 'CHART[type="bar"]: at least one BAR is required',
|
|
4503
|
+
path: ["bars"]
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
if (item.chartType === "pictograph" && item.pictos.length === 0) {
|
|
4507
|
+
ctx.addIssue({
|
|
4508
|
+
code: z.ZodIssueCode.custom,
|
|
4509
|
+
message: 'CHART[type="pictograph"]: at least one PICTO is required',
|
|
4510
|
+
path: ["pictos"]
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
if (item.chartType === "pictograph" && item.snap !== void 0) {
|
|
4514
|
+
ctx.addIssue({
|
|
4515
|
+
code: z.ZodIssueCode.custom,
|
|
4516
|
+
message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
|
|
4517
|
+
path: ["snap"]
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
});
|
|
3793
4521
|
const StringInputJSONSchema = z.object({
|
|
3794
4522
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
3795
4523
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
@@ -3848,6 +4576,14 @@ const PairMatcherWidgetJSONSchema = z.object({
|
|
|
3848
4576
|
z.array(MatcherItemContentSchema).min(1)
|
|
3849
4577
|
])
|
|
3850
4578
|
});
|
|
4579
|
+
const ChartBarWidgetJSONSchema = z.object({
|
|
4580
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4581
|
+
value: z.number()
|
|
4582
|
+
});
|
|
4583
|
+
const ChartPictoWidgetJSONSchema = z.object({
|
|
4584
|
+
type: z.literal(JSON_TYPES.CHART_PICTO),
|
|
4585
|
+
value: z.number()
|
|
4586
|
+
});
|
|
3851
4587
|
const VisualFractionJSONSchema = z.object({
|
|
3852
4588
|
type: z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
3853
4589
|
shape: z.enum(["bar", "circle", "triangle", "grid"]).default("bar"),
|
|
@@ -3918,7 +4654,9 @@ const WidgetJSONSchema = z.union([
|
|
|
3918
4654
|
GridSymmetryJSONSchema,
|
|
3919
4655
|
RulerWidgetSchema,
|
|
3920
4656
|
ProtractorWidgetSchema,
|
|
3921
|
-
SetSquareWidgetSchema
|
|
4657
|
+
SetSquareWidgetSchema,
|
|
4658
|
+
ChartBarWidgetJSONSchema,
|
|
4659
|
+
ChartPictoWidgetJSONSchema
|
|
3922
4660
|
]);
|
|
3923
4661
|
const ValidationJSONSchema = z.object({
|
|
3924
4662
|
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION, VALIDATION_MODES.MIXED]),
|
|
@@ -4012,6 +4750,9 @@ function isVerticalOperationItem(item) {
|
|
|
4012
4750
|
function isClockItem(item) {
|
|
4013
4751
|
return item.type === JSON_TYPES.CLOCK;
|
|
4014
4752
|
}
|
|
4753
|
+
function isChartItem(item) {
|
|
4754
|
+
return item.type === JSON_TYPES.CHART;
|
|
4755
|
+
}
|
|
4015
4756
|
function isInlineInputJSON(widget) {
|
|
4016
4757
|
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
4017
4758
|
}
|
|
@@ -4033,6 +4774,12 @@ function isFractionJSON(widget) {
|
|
|
4033
4774
|
function isGridSymmetryJSON(widget) {
|
|
4034
4775
|
return widget.type === JSON_TYPES.GRID_SYMMETRY;
|
|
4035
4776
|
}
|
|
4777
|
+
function isChartBarWidgetJSON(widget) {
|
|
4778
|
+
return widget.type === JSON_TYPES.CHART_BAR;
|
|
4779
|
+
}
|
|
4780
|
+
function isChartPictoWidgetJSON(widget) {
|
|
4781
|
+
return widget.type === JSON_TYPES.CHART_PICTO;
|
|
4782
|
+
}
|
|
4036
4783
|
function isRulerWidget(widget) {
|
|
4037
4784
|
return widget.type === JSON_TYPES.RULER;
|
|
4038
4785
|
}
|
|
@@ -8522,7 +9269,7 @@ lodash.exports;
|
|
|
8522
9269
|
function valuesIn(object) {
|
|
8523
9270
|
return object == null ? [] : baseValues(object, keysIn(object));
|
|
8524
9271
|
}
|
|
8525
|
-
function
|
|
9272
|
+
function clamp2(number, lower, upper) {
|
|
8526
9273
|
if (upper === undefined$1) {
|
|
8527
9274
|
upper = lower;
|
|
8528
9275
|
lower = undefined$1;
|
|
@@ -9199,7 +9946,7 @@ lodash.exports;
|
|
|
9199
9946
|
lodash2.camelCase = camelCase;
|
|
9200
9947
|
lodash2.capitalize = capitalize;
|
|
9201
9948
|
lodash2.ceil = ceil;
|
|
9202
|
-
lodash2.clamp =
|
|
9949
|
+
lodash2.clamp = clamp2;
|
|
9203
9950
|
lodash2.clone = clone;
|
|
9204
9951
|
lodash2.cloneDeep = cloneDeep;
|
|
9205
9952
|
lodash2.cloneDeepWith = cloneDeepWith;
|
|
@@ -9623,6 +10370,15 @@ function toJSON(ast) {
|
|
|
9623
10370
|
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
9624
10371
|
type: JSON_TYPES.LINEBREAK
|
|
9625
10372
|
});
|
|
10373
|
+
} else if (block.type === AST_TYPES.CHART_BLOCK) {
|
|
10374
|
+
const { item: chartItem, chartWidgets, chartAnswers } = transformChart(
|
|
10375
|
+
block,
|
|
10376
|
+
idGenerator,
|
|
10377
|
+
inlineDragIds
|
|
10378
|
+
);
|
|
10379
|
+
layout.push(chartItem);
|
|
10380
|
+
for (const [id, w] of Object.entries(chartWidgets)) widgets[id] = w;
|
|
10381
|
+
for (const [id, a] of Object.entries(chartAnswers)) answers[id] = a;
|
|
9626
10382
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
9627
10383
|
const radioBlock = block;
|
|
9628
10384
|
layout.push({
|
|
@@ -9771,12 +10527,18 @@ function toJSON(ast) {
|
|
|
9771
10527
|
}
|
|
9772
10528
|
}
|
|
9773
10529
|
}
|
|
10530
|
+
let validationSection = {};
|
|
10531
|
+
if (ast.validation) {
|
|
10532
|
+
validationSection = { validation: transformValidation(ast.validation, answers, widgets) };
|
|
10533
|
+
} else if (Object.keys(answers).length > 0) {
|
|
10534
|
+
validationSection = { validation: { mode: VALIDATION_MODES.SIMPLE, answers } };
|
|
10535
|
+
}
|
|
9774
10536
|
return {
|
|
9775
10537
|
version: "2.0",
|
|
9776
10538
|
// Natural DSL version
|
|
9777
10539
|
layout,
|
|
9778
10540
|
widgets,
|
|
9779
|
-
...
|
|
10541
|
+
...validationSection
|
|
9780
10542
|
};
|
|
9781
10543
|
}
|
|
9782
10544
|
function collectAllInlines(blocks) {
|
|
@@ -9809,10 +10571,100 @@ function collectAllInlines(blocks) {
|
|
|
9809
10571
|
for (const item of pm.groups.flat()) {
|
|
9810
10572
|
result.push(...item.inlines);
|
|
9811
10573
|
}
|
|
10574
|
+
} else if (block.type === AST_TYPES.CHART_BLOCK) {
|
|
10575
|
+
const chart = block;
|
|
10576
|
+
for (const bar of chart.bars) result.push(...bar.inlines);
|
|
10577
|
+
for (const picto of chart.pictos) result.push(...picto.inlines);
|
|
10578
|
+
if (chart.xAxis) result.push(...chart.xAxis.inlines);
|
|
10579
|
+
if (chart.yAxis) result.push(...chart.yAxis.inlines);
|
|
9812
10580
|
}
|
|
9813
10581
|
}
|
|
9814
10582
|
return result;
|
|
9815
10583
|
}
|
|
10584
|
+
function transformChart(block, idGenerator, inlineDragIds) {
|
|
10585
|
+
const { orientation, valueAxis, categoryAxis } = resolveChartAxes(block);
|
|
10586
|
+
function transformAxisTitle(axis) {
|
|
10587
|
+
return axis !== void 0 && axis.inlines.length > 0 ? axis.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds)) : void 0;
|
|
10588
|
+
}
|
|
10589
|
+
function transformNumericAxis(axis) {
|
|
10590
|
+
const title = transformAxisTitle(axis.axis);
|
|
10591
|
+
return {
|
|
10592
|
+
min: axis.min,
|
|
10593
|
+
max: axis.max,
|
|
10594
|
+
step: axis.step,
|
|
10595
|
+
visible: axis.axis?.visible ?? true,
|
|
10596
|
+
...title !== void 0 && { title }
|
|
10597
|
+
};
|
|
10598
|
+
}
|
|
10599
|
+
function transformCategoryAxis(axis) {
|
|
10600
|
+
const title = transformAxisTitle(axis);
|
|
10601
|
+
const visible = axis?.visible ?? true;
|
|
10602
|
+
if (!title && visible) return void 0;
|
|
10603
|
+
return { visible, ...title && { title } };
|
|
10604
|
+
}
|
|
10605
|
+
function toLabel(inlines) {
|
|
10606
|
+
return inlines.length > 0 ? inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds)) : void 0;
|
|
10607
|
+
}
|
|
10608
|
+
const xAxis = orientation === "horizontal" ? transformNumericAxis(valueAxis) : transformCategoryAxis(categoryAxis);
|
|
10609
|
+
const yAxis = orientation === "horizontal" ? transformCategoryAxis(categoryAxis) : transformNumericAxis(valueAxis);
|
|
10610
|
+
const chartWidgets = {};
|
|
10611
|
+
const chartAnswers = {};
|
|
10612
|
+
const bars = block.bars.map((bar) => {
|
|
10613
|
+
const label = toLabel(bar.inlines);
|
|
10614
|
+
if (bar.answer !== void 0) {
|
|
10615
|
+
chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
|
|
10616
|
+
chartAnswers[bar.id] = bar.answer;
|
|
10617
|
+
return {
|
|
10618
|
+
id: bar.id,
|
|
10619
|
+
type: JSON_TYPES.CHART_BAR,
|
|
10620
|
+
interactive: true,
|
|
10621
|
+
value: bar.value,
|
|
10622
|
+
widgetId: bar.id,
|
|
10623
|
+
...bar.color !== void 0 && { color: bar.color },
|
|
10624
|
+
...label !== void 0 && { label }
|
|
10625
|
+
};
|
|
10626
|
+
}
|
|
10627
|
+
return {
|
|
10628
|
+
id: bar.id,
|
|
10629
|
+
type: JSON_TYPES.CHART_BAR,
|
|
10630
|
+
interactive: false,
|
|
10631
|
+
value: bar.value,
|
|
10632
|
+
...bar.color !== void 0 && { color: bar.color },
|
|
10633
|
+
...label !== void 0 && { label }
|
|
10634
|
+
};
|
|
10635
|
+
});
|
|
10636
|
+
const pictos = block.pictos.map((picto) => {
|
|
10637
|
+
const label = toLabel(picto.inlines);
|
|
10638
|
+
if (picto.answer !== void 0) {
|
|
10639
|
+
chartWidgets[picto.id] = { type: JSON_TYPES.CHART_PICTO, value: picto.value };
|
|
10640
|
+
chartAnswers[picto.id] = picto.answer;
|
|
10641
|
+
}
|
|
10642
|
+
return {
|
|
10643
|
+
id: picto.id,
|
|
10644
|
+
type: JSON_TYPES.CHART_PICTO,
|
|
10645
|
+
value: picto.value,
|
|
10646
|
+
image: picto.image,
|
|
10647
|
+
...picto.answer !== void 0 && { answer: picto.answer },
|
|
10648
|
+
...label !== void 0 && { label }
|
|
10649
|
+
};
|
|
10650
|
+
});
|
|
10651
|
+
const item = {
|
|
10652
|
+
id: idGenerator.generate(JSON_TYPES.CHART),
|
|
10653
|
+
type: JSON_TYPES.CHART,
|
|
10654
|
+
...block.name !== void 0 && { name: block.name },
|
|
10655
|
+
...block.title !== void 0 && { title: block.title },
|
|
10656
|
+
chartType: block.chartType,
|
|
10657
|
+
orientation,
|
|
10658
|
+
grid: block.grid,
|
|
10659
|
+
scale: block.scale,
|
|
10660
|
+
...block.snap !== void 0 && { snap: block.snap },
|
|
10661
|
+
...xAxis !== void 0 && { xAxis },
|
|
10662
|
+
...yAxis !== void 0 && { yAxis },
|
|
10663
|
+
bars,
|
|
10664
|
+
pictos
|
|
10665
|
+
};
|
|
10666
|
+
return { item, chartWidgets, chartAnswers };
|
|
10667
|
+
}
|
|
9816
10668
|
function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
|
|
9817
10669
|
const positions = numberLine.positions.map((pos) => {
|
|
9818
10670
|
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
|
|
@@ -10179,17 +11031,6 @@ function extractVariables$1(expression, validVariables) {
|
|
|
10179
11031
|
}
|
|
10180
11032
|
return uniqueVars;
|
|
10181
11033
|
}
|
|
10182
|
-
function normalize(value) {
|
|
10183
|
-
if (typeof value === "number") {
|
|
10184
|
-
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
10185
|
-
}
|
|
10186
|
-
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
10187
|
-
const asNum = Number(trimmed);
|
|
10188
|
-
if (trimmed !== "" && !isNaN(asNum)) {
|
|
10189
|
-
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
10190
|
-
}
|
|
10191
|
-
return trimmed;
|
|
10192
|
-
}
|
|
10193
11034
|
const NL_DRAG_PREFIX = "\0nl-drag\0";
|
|
10194
11035
|
const isNLDragEncoded = (v) => v.startsWith(NL_DRAG_PREFIX);
|
|
10195
11036
|
const encodeNLDragValue = (label, position) => `${NL_DRAG_PREFIX}${label}@${String(position)}`;
|
|
@@ -10338,16 +11179,6 @@ function validateSimple(exercise, userInputs) {
|
|
|
10338
11179
|
allCorrect = false;
|
|
10339
11180
|
}
|
|
10340
11181
|
}
|
|
10341
|
-
for (const widgetId of Object.keys(userInputs)) {
|
|
10342
|
-
if (!(widgetId in answers) && !(widgetId in details)) {
|
|
10343
|
-
details[widgetId] = {
|
|
10344
|
-
widgetId,
|
|
10345
|
-
userValue: userInputs[widgetId],
|
|
10346
|
-
isCorrect: true
|
|
10347
|
-
// No validation rule = assume correct
|
|
10348
|
-
};
|
|
10349
|
-
}
|
|
10350
|
-
}
|
|
10351
11182
|
return {
|
|
10352
11183
|
isCorrect: allCorrect,
|
|
10353
11184
|
details,
|
|
@@ -14521,7 +15352,9 @@ const DEFAULT_SOUNDS = {
|
|
|
14521
15352
|
"input-select": inputSelect,
|
|
14522
15353
|
"answer-correct": answerCorrect,
|
|
14523
15354
|
"answer-wrong": answerWrong,
|
|
14524
|
-
"answer-reveal": answerReveal
|
|
15355
|
+
"answer-reveal": answerReveal,
|
|
15356
|
+
"bar-step": buttonClick,
|
|
15357
|
+
"bar-release": dragDrop
|
|
14525
15358
|
};
|
|
14526
15359
|
function resolveSounds(overrides) {
|
|
14527
15360
|
return overrides ? { ...DEFAULT_SOUNDS, ...overrides } : DEFAULT_SOUNDS;
|
|
@@ -14806,27 +15639,41 @@ function useRotate(options) {
|
|
|
14806
15639
|
return { rotation, setRotation };
|
|
14807
15640
|
}
|
|
14808
15641
|
const REPEAT_DELAY_MS = 5e3;
|
|
14809
|
-
function useJiggle(
|
|
15642
|
+
function useJiggle(isDiscovered, options = {}) {
|
|
15643
|
+
const {
|
|
15644
|
+
ignoreBubbledAnimationEnd = false,
|
|
15645
|
+
repeatDelayMs = REPEAT_DELAY_MS
|
|
15646
|
+
} = options;
|
|
14810
15647
|
const [jiggling, setJiggling] = useState(true);
|
|
14811
15648
|
const [discovered, setDiscovered] = useState(false);
|
|
15649
|
+
const discoveredRef = useRef(false);
|
|
14812
15650
|
const timerRef = useRef(null);
|
|
14813
|
-
|
|
14814
|
-
if (
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
setJiggling(false);
|
|
14818
|
-
}
|
|
14819
|
-
}, [isDragging, discovered]);
|
|
14820
|
-
useEffect(() => () => {
|
|
14821
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
15651
|
+
const clearRepeatTimer = useCallback(() => {
|
|
15652
|
+
if (!timerRef.current) return;
|
|
15653
|
+
clearTimeout(timerRef.current);
|
|
15654
|
+
timerRef.current = null;
|
|
14822
15655
|
}, []);
|
|
14823
|
-
const
|
|
15656
|
+
const discover = useCallback(() => {
|
|
15657
|
+
discoveredRef.current = true;
|
|
15658
|
+
clearRepeatTimer();
|
|
15659
|
+
setDiscovered(true);
|
|
14824
15660
|
setJiggling(false);
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
}, [
|
|
14829
|
-
|
|
15661
|
+
}, [clearRepeatTimer]);
|
|
15662
|
+
useEffect(() => {
|
|
15663
|
+
if (isDiscovered) discover();
|
|
15664
|
+
}, [isDiscovered, discover]);
|
|
15665
|
+
useEffect(() => () => clearRepeatTimer(), [clearRepeatTimer]);
|
|
15666
|
+
const onAnimationEnd = useCallback((event) => {
|
|
15667
|
+
if (ignoreBubbledAnimationEnd && event && event.currentTarget !== event.target) return;
|
|
15668
|
+
setJiggling(false);
|
|
15669
|
+
if (discoveredRef.current) return;
|
|
15670
|
+
clearRepeatTimer();
|
|
15671
|
+
timerRef.current = setTimeout(() => {
|
|
15672
|
+
timerRef.current = null;
|
|
15673
|
+
if (!discoveredRef.current) setJiggling(true);
|
|
15674
|
+
}, repeatDelayMs);
|
|
15675
|
+
}, [clearRepeatTimer, ignoreBubbledAnimationEnd, repeatDelayMs]);
|
|
15676
|
+
return { jiggling: jiggling && !discovered, onAnimationEnd, discover };
|
|
14830
15677
|
}
|
|
14831
15678
|
const VW$2 = 320;
|
|
14832
15679
|
const VH$2 = 44;
|
|
@@ -16331,6 +17178,743 @@ const GridSymmetry = ({
|
|
|
16331
17178
|
}
|
|
16332
17179
|
) });
|
|
16333
17180
|
};
|
|
17181
|
+
function getChartClassName(item) {
|
|
17182
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17183
|
+
return [
|
|
17184
|
+
"chart",
|
|
17185
|
+
isHorizontal ? "chart--horizontal" : "",
|
|
17186
|
+
item.chartType === "pictograph" ? "chart--pictograph" : "",
|
|
17187
|
+
item.xAxis?.visible === false ? "chart--no-xaxis" : "",
|
|
17188
|
+
item.yAxis?.visible === false ? "chart--no-yaxis" : ""
|
|
17189
|
+
].filter(Boolean).join(" ");
|
|
17190
|
+
}
|
|
17191
|
+
function resolveItemValue(id, defaultValue2, values) {
|
|
17192
|
+
const raw = values[id];
|
|
17193
|
+
const parsed = parseFloat(raw ?? "");
|
|
17194
|
+
return isNaN(parsed) ? defaultValue2 : parsed;
|
|
17195
|
+
}
|
|
17196
|
+
function resolveValue(bar, values) {
|
|
17197
|
+
return bar.interactive ? resolveItemValue(bar.widgetId, bar.value, values) : bar.value;
|
|
17198
|
+
}
|
|
17199
|
+
function clamp(v, min, max) {
|
|
17200
|
+
return Math.min(max, Math.max(min, v));
|
|
17201
|
+
}
|
|
17202
|
+
function snapToStep(v, min, step) {
|
|
17203
|
+
const snapped = min + Math.round((v - min) / step) * step;
|
|
17204
|
+
return +snapped.toFixed(10);
|
|
17205
|
+
}
|
|
17206
|
+
function nextStepValue(v, axis, direction, snap) {
|
|
17207
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17208
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17209
|
+
const step = snap ?? axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17210
|
+
const raw = direction === 1 ? min + Math.ceil((v - min) / step + 1e-9) * step : min + Math.floor((v - min) / step - 1e-9) * step;
|
|
17211
|
+
return +clamp(raw, min, max).toFixed(10);
|
|
17212
|
+
}
|
|
17213
|
+
function toFill(v, axis) {
|
|
17214
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17215
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17216
|
+
return clamp((v - min) / (max - min), 0, 1);
|
|
17217
|
+
}
|
|
17218
|
+
function barClasses(base, validationStatus) {
|
|
17219
|
+
const statusClass = validationStatus === "correct" ? "chart-bar-correct" : validationStatus === "incorrect" ? "chart-bar-incorrect" : null;
|
|
17220
|
+
return [...base, ...statusClass ? [statusClass] : []].join(" ");
|
|
17221
|
+
}
|
|
17222
|
+
function buildAxisTicks(axis) {
|
|
17223
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17224
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17225
|
+
const step = axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17226
|
+
const ticks = [];
|
|
17227
|
+
for (let v = min; v <= max + 1e-9; v = +(v + step).toFixed(10)) {
|
|
17228
|
+
ticks.push(v);
|
|
17229
|
+
}
|
|
17230
|
+
if (ticks.length === 0 || Math.abs(ticks[ticks.length - 1] - max) > 1e-9) {
|
|
17231
|
+
ticks.push(max);
|
|
17232
|
+
}
|
|
17233
|
+
return ticks;
|
|
17234
|
+
}
|
|
17235
|
+
function axisTickPercent(value, axis) {
|
|
17236
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17237
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17238
|
+
return (value - min) / (max - min) * 100;
|
|
17239
|
+
}
|
|
17240
|
+
function buildAxisTickPositions(axis) {
|
|
17241
|
+
return buildAxisTicks(axis).map((value) => ({
|
|
17242
|
+
value,
|
|
17243
|
+
percent: axisTickPercent(value, axis)
|
|
17244
|
+
}));
|
|
17245
|
+
}
|
|
17246
|
+
function formatAxisTick(value) {
|
|
17247
|
+
return Number.isInteger(value) ? value : parseFloat(value.toPrecision(10));
|
|
17248
|
+
}
|
|
17249
|
+
const CHART_METRICS = {
|
|
17250
|
+
scaleMin: 0.6,
|
|
17251
|
+
scaleMax: 1.6,
|
|
17252
|
+
slotWidth: 5.2,
|
|
17253
|
+
gapWidth: 1,
|
|
17254
|
+
axisWidth: 2.35,
|
|
17255
|
+
barWidth: 3.35,
|
|
17256
|
+
fontSize: 0.9,
|
|
17257
|
+
labelFontSize: 0.98,
|
|
17258
|
+
handleSize: 1.15,
|
|
17259
|
+
chartHeight: 16
|
|
17260
|
+
};
|
|
17261
|
+
function computeBaseChartVars(scale, barCount) {
|
|
17262
|
+
const visualScale = clamp(scale, CHART_METRICS.scaleMin, CHART_METRICS.scaleMax);
|
|
17263
|
+
const slotWidth = CHART_METRICS.slotWidth * visualScale;
|
|
17264
|
+
const gapWidth = CHART_METRICS.gapWidth * visualScale;
|
|
17265
|
+
const trackLength = barCount * slotWidth + Math.max(0, barCount - 1) * gapWidth + gapWidth * 2;
|
|
17266
|
+
return {
|
|
17267
|
+
visualScale,
|
|
17268
|
+
trackLength,
|
|
17269
|
+
cssVars: {
|
|
17270
|
+
"--chart-axis-width": `${CHART_METRICS.axisWidth * visualScale}rem`,
|
|
17271
|
+
"--chart-bar-slot-width": `${slotWidth}rem`,
|
|
17272
|
+
"--chart-bar-width": `${CHART_METRICS.barWidth * visualScale}rem`,
|
|
17273
|
+
"--chart-bar-gap": `${gapWidth}rem`,
|
|
17274
|
+
"--chart-font-size": `${CHART_METRICS.fontSize * visualScale}rem`,
|
|
17275
|
+
"--chart-label-font-size": `${CHART_METRICS.labelFontSize * visualScale}rem`,
|
|
17276
|
+
"--chart-handle-size": `${CHART_METRICS.handleSize * visualScale}rem`
|
|
17277
|
+
}
|
|
17278
|
+
};
|
|
17279
|
+
}
|
|
17280
|
+
function computeChartStyle(scale, barCount) {
|
|
17281
|
+
const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
|
|
17282
|
+
return {
|
|
17283
|
+
...cssVars,
|
|
17284
|
+
"--chart-height": `${CHART_METRICS.chartHeight * visualScale}rem`,
|
|
17285
|
+
"--chart-content-width": `${trackLength}rem`
|
|
17286
|
+
};
|
|
17287
|
+
}
|
|
17288
|
+
function computePictographChartStyle(scale, columnCount, totalSlots, orientation = "vertical") {
|
|
17289
|
+
const slots = Math.max(0, totalSlots);
|
|
17290
|
+
const gaps = Math.max(0, slots - 1);
|
|
17291
|
+
const slotSpan = `calc(${slots} * var(--picto-cell-size) + ${gaps} * var(--picto-cell-gap))`;
|
|
17292
|
+
if (orientation === "horizontal") {
|
|
17293
|
+
return {
|
|
17294
|
+
...computeHorizontalChartStyle(scale, columnCount),
|
|
17295
|
+
"--picto-total-slots": slots,
|
|
17296
|
+
// Horizontal columns run along the width, so the value axis is the width here.
|
|
17297
|
+
// Both vars move together to keep the X-axis ticks aligned with the icons.
|
|
17298
|
+
"--chart-h-value-width": slotSpan,
|
|
17299
|
+
"--chart-content-width": slotSpan
|
|
17300
|
+
};
|
|
17301
|
+
}
|
|
17302
|
+
return {
|
|
17303
|
+
...computeChartStyle(scale, columnCount),
|
|
17304
|
+
"--picto-total-slots": slots,
|
|
17305
|
+
"--chart-height": slotSpan
|
|
17306
|
+
};
|
|
17307
|
+
}
|
|
17308
|
+
function computeHorizontalChartStyle(scale, barCount) {
|
|
17309
|
+
const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
|
|
17310
|
+
const valueWidth = CHART_METRICS.chartHeight * visualScale;
|
|
17311
|
+
return {
|
|
17312
|
+
...cssVars,
|
|
17313
|
+
"--chart-height": `${trackLength}rem`,
|
|
17314
|
+
"--chart-h-value-width": `${valueWidth}rem`,
|
|
17315
|
+
"--chart-content-width": `${valueWidth}rem`
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
17318
|
+
const YAxis = ({ axis }) => {
|
|
17319
|
+
const ticks = buildAxisTickPositions(axis);
|
|
17320
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-axis-y", children: ticks.map(({ value, percent }) => /* @__PURE__ */ jsx("span", { className: "chart-axis-tick-y", style: { bottom: `${percent}%` }, children: formatAxisTick(value) }, value)) });
|
|
17321
|
+
};
|
|
17322
|
+
const Grid = ({ axis, orientation = "vertical" }) => {
|
|
17323
|
+
const lines = buildAxisTickPositions(axis);
|
|
17324
|
+
return /* @__PURE__ */ jsx(Fragment, { children: lines.map(({ value, percent }) => {
|
|
17325
|
+
const style = orientation === "horizontal" ? { left: `${percent}%` } : { bottom: `${percent}%` };
|
|
17326
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-gridline", style }, value);
|
|
17327
|
+
}) });
|
|
17328
|
+
};
|
|
17329
|
+
const XAxis = ({ axis, title }) => {
|
|
17330
|
+
const ticks = buildAxisTickPositions(axis);
|
|
17331
|
+
return /* @__PURE__ */ jsxs("div", { className: "chart-axis-x-numeric", children: [
|
|
17332
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-x-numeric-ticks", children: ticks.map(({ value, percent }) => /* @__PURE__ */ jsx("span", { className: "chart-axis-tick-x-num", style: { left: `${percent}%` }, children: formatAxisTick(value) }, value)) }),
|
|
17333
|
+
title && /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x", children: title })
|
|
17334
|
+
] });
|
|
17335
|
+
};
|
|
17336
|
+
const DraggableBar = ({
|
|
17337
|
+
bar,
|
|
17338
|
+
currentValue,
|
|
17339
|
+
axis,
|
|
17340
|
+
snap,
|
|
17341
|
+
validationStatus,
|
|
17342
|
+
orientation = "vertical",
|
|
17343
|
+
onChange
|
|
17344
|
+
}) => {
|
|
17345
|
+
const trackRef = useRef(null);
|
|
17346
|
+
const dragging = useRef(false);
|
|
17347
|
+
const lastSnap = useRef(null);
|
|
17348
|
+
const preDragValue = useRef(0);
|
|
17349
|
+
const playSound = useSound();
|
|
17350
|
+
const exerciseLocked = useExerciseLocked();
|
|
17351
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
17352
|
+
const { jiggling, onAnimationEnd, discover } = useJiggle(isDragging, {
|
|
17353
|
+
ignoreBubbledAnimationEnd: true
|
|
17354
|
+
});
|
|
17355
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17356
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17357
|
+
const axisStep = axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17358
|
+
const snapStep = snap ?? axisStep;
|
|
17359
|
+
const fillFromPointer = useCallback(
|
|
17360
|
+
(clientY, clientX) => {
|
|
17361
|
+
const track = trackRef.current;
|
|
17362
|
+
if (!track) return;
|
|
17363
|
+
const rect = track.getBoundingClientRect();
|
|
17364
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
17365
|
+
let ratio;
|
|
17366
|
+
if (orientation === "horizontal") {
|
|
17367
|
+
ratio = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
17368
|
+
} else {
|
|
17369
|
+
ratio = clamp(1 - (clientY - rect.top) / rect.height, 0, 1);
|
|
17370
|
+
}
|
|
17371
|
+
const rawValue = axisMin + ratio * (axisMax - axisMin);
|
|
17372
|
+
const snapped = clamp(snapToStep(rawValue, axisMin, snapStep), axisMin, axisMax);
|
|
17373
|
+
if (snapped !== lastSnap.current) {
|
|
17374
|
+
lastSnap.current = snapped;
|
|
17375
|
+
playSound("bar-step");
|
|
17376
|
+
onChange(bar.widgetId, String(snapped));
|
|
17377
|
+
}
|
|
17378
|
+
},
|
|
17379
|
+
[axisMin, axisMax, snapStep, bar.widgetId, onChange, orientation, playSound]
|
|
17380
|
+
);
|
|
17381
|
+
const onPointerDown = useCallback(
|
|
17382
|
+
(e) => {
|
|
17383
|
+
if (exerciseLocked) return;
|
|
17384
|
+
e.preventDefault();
|
|
17385
|
+
discover();
|
|
17386
|
+
dragging.current = true;
|
|
17387
|
+
preDragValue.current = currentValue;
|
|
17388
|
+
lastSnap.current = clamp(snapToStep(currentValue, axisMin, snapStep), axisMin, axisMax);
|
|
17389
|
+
setIsDragging(true);
|
|
17390
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17391
|
+
fillFromPointer(e.clientY, e.clientX);
|
|
17392
|
+
},
|
|
17393
|
+
[fillFromPointer, currentValue, axisMin, axisMax, snapStep, discover, exerciseLocked]
|
|
17394
|
+
);
|
|
17395
|
+
const onPointerMove = useCallback(
|
|
17396
|
+
(e) => {
|
|
17397
|
+
if (!dragging.current) return;
|
|
17398
|
+
fillFromPointer(e.clientY, e.clientX);
|
|
17399
|
+
},
|
|
17400
|
+
[fillFromPointer]
|
|
17401
|
+
);
|
|
17402
|
+
const onPointerUp = useCallback(() => {
|
|
17403
|
+
if (!dragging.current) return;
|
|
17404
|
+
dragging.current = false;
|
|
17405
|
+
setIsDragging(false);
|
|
17406
|
+
playSound("bar-release");
|
|
17407
|
+
}, [playSound]);
|
|
17408
|
+
const onPointerCancel = useCallback(() => {
|
|
17409
|
+
if (!dragging.current) return;
|
|
17410
|
+
dragging.current = false;
|
|
17411
|
+
setIsDragging(false);
|
|
17412
|
+
onChange(bar.widgetId, String(preDragValue.current));
|
|
17413
|
+
}, [bar.widgetId, onChange]);
|
|
17414
|
+
const onKeyDown = useCallback(
|
|
17415
|
+
(e) => {
|
|
17416
|
+
if (exerciseLocked) return;
|
|
17417
|
+
const isInc = e.key === "ArrowUp" || e.key === "ArrowRight";
|
|
17418
|
+
const isDec = e.key === "ArrowDown" || e.key === "ArrowLeft";
|
|
17419
|
+
if (isInc) {
|
|
17420
|
+
e.preventDefault();
|
|
17421
|
+
playSound("bar-step");
|
|
17422
|
+
onChange(bar.widgetId, String(nextStepValue(currentValue, axis, 1, snap)));
|
|
17423
|
+
} else if (isDec) {
|
|
17424
|
+
e.preventDefault();
|
|
17425
|
+
playSound("bar-step");
|
|
17426
|
+
onChange(bar.widgetId, String(nextStepValue(currentValue, axis, -1, snap)));
|
|
17427
|
+
} else if (e.key === "Home") {
|
|
17428
|
+
e.preventDefault();
|
|
17429
|
+
playSound("bar-step");
|
|
17430
|
+
onChange(bar.widgetId, String(axisMin));
|
|
17431
|
+
} else if (e.key === "End") {
|
|
17432
|
+
e.preventDefault();
|
|
17433
|
+
playSound("bar-step");
|
|
17434
|
+
onChange(bar.widgetId, String(axisMax));
|
|
17435
|
+
}
|
|
17436
|
+
},
|
|
17437
|
+
[axisMin, axisMax, bar.widgetId, currentValue, axis, snap, onChange, playSound, exerciseLocked]
|
|
17438
|
+
);
|
|
17439
|
+
const fill = toFill(currentValue, axis);
|
|
17440
|
+
const isAtMin = currentValue <= axisMin;
|
|
17441
|
+
const ariaLabel = bar.label?.map((item) => "value" in item ? String(item.value) : "").filter(Boolean).join(" ") || bar.id;
|
|
17442
|
+
return /* @__PURE__ */ jsx(
|
|
17443
|
+
"div",
|
|
17444
|
+
{
|
|
17445
|
+
ref: trackRef,
|
|
17446
|
+
className: "chart-bar-track chart-bar-track--interactive",
|
|
17447
|
+
children: /* @__PURE__ */ jsx(
|
|
17448
|
+
"div",
|
|
17449
|
+
{
|
|
17450
|
+
role: "slider",
|
|
17451
|
+
tabIndex: 0,
|
|
17452
|
+
"aria-valuemin": axisMin,
|
|
17453
|
+
"aria-valuemax": axisMax,
|
|
17454
|
+
"aria-valuenow": currentValue,
|
|
17455
|
+
"aria-orientation": orientation === "horizontal" ? "horizontal" : "vertical",
|
|
17456
|
+
"aria-label": ariaLabel,
|
|
17457
|
+
"aria-disabled": exerciseLocked ? "true" : void 0,
|
|
17458
|
+
className: barClasses(
|
|
17459
|
+
[
|
|
17460
|
+
"chart-bar",
|
|
17461
|
+
orientation === "horizontal" ? "chart-bar--horizontal" : "chart-bar--vertical",
|
|
17462
|
+
"chart-bar-interactive",
|
|
17463
|
+
isAtMin ? "chart-bar-at-min" : "",
|
|
17464
|
+
jiggling && !isDragging && !exerciseLocked && validationStatus === null ? "chart-bar--peek" : ""
|
|
17465
|
+
],
|
|
17466
|
+
validationStatus
|
|
17467
|
+
),
|
|
17468
|
+
onAnimationEnd,
|
|
17469
|
+
onPointerDown,
|
|
17470
|
+
onPointerMove,
|
|
17471
|
+
onPointerUp,
|
|
17472
|
+
onPointerCancel,
|
|
17473
|
+
onKeyDown,
|
|
17474
|
+
style: {
|
|
17475
|
+
"--bar-fill": fill,
|
|
17476
|
+
"--chart-bar-instance-color": bar.color ?? "var(--chart-bar-color)"
|
|
17477
|
+
},
|
|
17478
|
+
children: /* @__PURE__ */ jsx("span", { className: "chart-bar-handle", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 10 10", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
17479
|
+
/* @__PURE__ */ jsx("path", { d: "M1.5 4.8 L5 1.5 L8.5 4.8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
17480
|
+
/* @__PURE__ */ jsx("path", { d: "M1.5 8.5 L5 5.2 L8.5 8.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
17481
|
+
] }) })
|
|
17482
|
+
}
|
|
17483
|
+
)
|
|
17484
|
+
}
|
|
17485
|
+
);
|
|
17486
|
+
};
|
|
17487
|
+
const StaticBar = ({
|
|
17488
|
+
bar,
|
|
17489
|
+
currentValue,
|
|
17490
|
+
axis,
|
|
17491
|
+
validationStatus,
|
|
17492
|
+
orientation = "vertical"
|
|
17493
|
+
}) => {
|
|
17494
|
+
const fill = toFill(currentValue, axis);
|
|
17495
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-track", children: /* @__PURE__ */ jsx(
|
|
17496
|
+
"div",
|
|
17497
|
+
{
|
|
17498
|
+
className: barClasses(
|
|
17499
|
+
[
|
|
17500
|
+
"chart-bar",
|
|
17501
|
+
orientation === "horizontal" ? "chart-bar--horizontal" : "chart-bar--vertical"
|
|
17502
|
+
],
|
|
17503
|
+
validationStatus
|
|
17504
|
+
),
|
|
17505
|
+
style: {
|
|
17506
|
+
"--bar-fill": fill,
|
|
17507
|
+
"--chart-bar-instance-color": bar.color ?? "var(--chart-bar-color)",
|
|
17508
|
+
backgroundColor: "var(--chart-bar-instance-color)"
|
|
17509
|
+
}
|
|
17510
|
+
}
|
|
17511
|
+
) });
|
|
17512
|
+
};
|
|
17513
|
+
const EMPTY_SET$2 = /* @__PURE__ */ new Set();
|
|
17514
|
+
const XAxisLabel = ({
|
|
17515
|
+
bar,
|
|
17516
|
+
widgets,
|
|
17517
|
+
values,
|
|
17518
|
+
validationDetails,
|
|
17519
|
+
onChange
|
|
17520
|
+
}) => {
|
|
17521
|
+
if (!bar.label || bar.label.length === 0) return /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x" });
|
|
17522
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x-inner", children: bar.label.map((subItem) => /* @__PURE__ */ jsx(
|
|
17523
|
+
LayoutRenderer,
|
|
17524
|
+
{
|
|
17525
|
+
item: subItem,
|
|
17526
|
+
widgets,
|
|
17527
|
+
values,
|
|
17528
|
+
droppedInputs: EMPTY_SET$2,
|
|
17529
|
+
validationDetails,
|
|
17530
|
+
onChange
|
|
17531
|
+
},
|
|
17532
|
+
subItem.id
|
|
17533
|
+
)) }) });
|
|
17534
|
+
};
|
|
17535
|
+
const EMPTY_SET$1 = /* @__PURE__ */ new Set();
|
|
17536
|
+
const BarChart = ({
|
|
17537
|
+
item,
|
|
17538
|
+
widgets,
|
|
17539
|
+
values,
|
|
17540
|
+
validationDetails,
|
|
17541
|
+
onChange
|
|
17542
|
+
}) => {
|
|
17543
|
+
const { bars, grid } = item;
|
|
17544
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17545
|
+
const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
|
|
17546
|
+
const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
|
|
17547
|
+
const chartClass = getChartClassName(item);
|
|
17548
|
+
const renderAxisTitle = (title) => {
|
|
17549
|
+
if (!title?.length) return null;
|
|
17550
|
+
return /* @__PURE__ */ jsx(Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsx(
|
|
17551
|
+
LayoutRenderer,
|
|
17552
|
+
{
|
|
17553
|
+
item: subItem,
|
|
17554
|
+
widgets,
|
|
17555
|
+
values,
|
|
17556
|
+
droppedInputs: EMPTY_SET$1,
|
|
17557
|
+
validationDetails,
|
|
17558
|
+
onChange
|
|
17559
|
+
},
|
|
17560
|
+
subItem.id
|
|
17561
|
+
)) });
|
|
17562
|
+
};
|
|
17563
|
+
const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
|
|
17564
|
+
const renderBar = (bar, orient = "vertical") => {
|
|
17565
|
+
const currentValue = resolveValue(bar, values);
|
|
17566
|
+
const vStatus = bar.interactive ? validationDetails?.[bar.widgetId] : void 0;
|
|
17567
|
+
const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
|
|
17568
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: bar.interactive ? /* @__PURE__ */ jsx(
|
|
17569
|
+
DraggableBar,
|
|
17570
|
+
{
|
|
17571
|
+
bar,
|
|
17572
|
+
currentValue,
|
|
17573
|
+
axis: fillAxis,
|
|
17574
|
+
snap: item.snap,
|
|
17575
|
+
validationStatus,
|
|
17576
|
+
orientation: orient,
|
|
17577
|
+
onChange
|
|
17578
|
+
}
|
|
17579
|
+
) : /* @__PURE__ */ jsx(
|
|
17580
|
+
StaticBar,
|
|
17581
|
+
{
|
|
17582
|
+
bar,
|
|
17583
|
+
currentValue,
|
|
17584
|
+
axis: fillAxis,
|
|
17585
|
+
validationStatus,
|
|
17586
|
+
orientation: orient
|
|
17587
|
+
}
|
|
17588
|
+
) }, bar.id);
|
|
17589
|
+
};
|
|
17590
|
+
if (isHorizontal) {
|
|
17591
|
+
const chartStyle2 = computeHorizontalChartStyle(item.scale, bars.length);
|
|
17592
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
|
|
17593
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17594
|
+
categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
|
|
17595
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-y--categories", children: bars.map((bar) => /* @__PURE__ */ jsx("div", { className: "chart-bar-y-label", children: bar.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17596
|
+
LayoutRenderer,
|
|
17597
|
+
{
|
|
17598
|
+
item: subItem,
|
|
17599
|
+
widgets,
|
|
17600
|
+
values,
|
|
17601
|
+
droppedInputs: EMPTY_SET$1,
|
|
17602
|
+
validationDetails,
|
|
17603
|
+
onChange
|
|
17604
|
+
},
|
|
17605
|
+
subItem.id
|
|
17606
|
+
)) }, bar.id)) }),
|
|
17607
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17608
|
+
grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis, orientation: "horizontal" }),
|
|
17609
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: bars.map((b) => renderBar(b, "horizontal")) })
|
|
17610
|
+
] }),
|
|
17611
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) })
|
|
17612
|
+
] }) }) });
|
|
17613
|
+
}
|
|
17614
|
+
const chartStyle = computeChartStyle(item.scale, bars.length);
|
|
17615
|
+
const hasAnyLabel = bars.some((b) => b.label && b.label.length > 0);
|
|
17616
|
+
const xAxisRow = hasAnyLabel ? /* @__PURE__ */ jsx("div", { className: "chart-axis-x", children: bars.map((bar) => /* @__PURE__ */ jsx(
|
|
17617
|
+
XAxisLabel,
|
|
17618
|
+
{
|
|
17619
|
+
bar,
|
|
17620
|
+
widgets,
|
|
17621
|
+
values,
|
|
17622
|
+
validationDetails,
|
|
17623
|
+
onChange
|
|
17624
|
+
},
|
|
17625
|
+
bar.id
|
|
17626
|
+
)) }) : null;
|
|
17627
|
+
const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
|
|
17628
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
|
|
17629
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17630
|
+
valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
|
|
17631
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(YAxis, { axis: valueAxis }),
|
|
17632
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17633
|
+
grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis }),
|
|
17634
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: bars.map((b) => renderBar(b)) })
|
|
17635
|
+
] }),
|
|
17636
|
+
xAxisRow,
|
|
17637
|
+
xAxisTitle
|
|
17638
|
+
] }) }) });
|
|
17639
|
+
};
|
|
17640
|
+
const clampSlotCount = (value, axisMin, totalSlots) => Math.round(Math.min(Math.max(value - axisMin, 0), totalSlots));
|
|
17641
|
+
const buildSelectedAnswerSlots = (totalSlots, baseFilledCount, selectedCount) => {
|
|
17642
|
+
const slots = Array.from({ length: totalSlots }, () => false);
|
|
17643
|
+
const end = Math.min(totalSlots, baseFilledCount + selectedCount);
|
|
17644
|
+
for (let i = baseFilledCount; i < end; i++) slots[i] = true;
|
|
17645
|
+
return slots;
|
|
17646
|
+
};
|
|
17647
|
+
const PictographColumn = ({
|
|
17648
|
+
picto,
|
|
17649
|
+
currentValue,
|
|
17650
|
+
axis,
|
|
17651
|
+
validationStatus,
|
|
17652
|
+
orientation = "vertical",
|
|
17653
|
+
onChange
|
|
17654
|
+
}) => {
|
|
17655
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17656
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17657
|
+
const isInteractive = picto.answer !== void 0;
|
|
17658
|
+
const totalSlots = Math.round(axisMax - axisMin);
|
|
17659
|
+
const baseFilledCount = clampSlotCount(picto.value, axisMin, totalSlots);
|
|
17660
|
+
const currentFilledCount = clampSlotCount(currentValue, axisMin, totalSlots);
|
|
17661
|
+
const answerSlotCount = Math.max(0, totalSlots - baseFilledCount);
|
|
17662
|
+
const selectedAnswerCount = Math.min(
|
|
17663
|
+
Math.max(currentFilledCount - baseFilledCount, 0),
|
|
17664
|
+
answerSlotCount
|
|
17665
|
+
);
|
|
17666
|
+
const [selectedSlots, setSelectedSlots] = useState(
|
|
17667
|
+
() => buildSelectedAnswerSlots(totalSlots, baseFilledCount, selectedAnswerCount)
|
|
17668
|
+
);
|
|
17669
|
+
const lastInternalValueRef = useRef(null);
|
|
17670
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
17671
|
+
const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasInteracted);
|
|
17672
|
+
const playSound = useSound();
|
|
17673
|
+
const exerciseLocked = useExerciseLocked();
|
|
17674
|
+
useEffect(() => {
|
|
17675
|
+
const nextValue = String(axisMin + baseFilledCount + selectedAnswerCount);
|
|
17676
|
+
if (nextValue === lastInternalValueRef.current) return;
|
|
17677
|
+
lastInternalValueRef.current = null;
|
|
17678
|
+
setSelectedSlots(buildSelectedAnswerSlots(totalSlots, baseFilledCount, selectedAnswerCount));
|
|
17679
|
+
}, [axisMin, baseFilledCount, selectedAnswerCount, totalSlots]);
|
|
17680
|
+
const handleCellClick = useCallback(
|
|
17681
|
+
(cellIndex) => {
|
|
17682
|
+
if (exerciseLocked || !isInteractive || cellIndex < baseFilledCount) return;
|
|
17683
|
+
if (!hasInteracted) setHasInteracted(true);
|
|
17684
|
+
const slotRelative = cellIndex - baseFilledCount;
|
|
17685
|
+
const isCurrentlyFilled = Boolean(selectedSlots[cellIndex]);
|
|
17686
|
+
const nextCount = isCurrentlyFilled ? slotRelative : slotRelative + 1;
|
|
17687
|
+
const nextSlots = buildSelectedAnswerSlots(totalSlots, baseFilledCount, nextCount);
|
|
17688
|
+
const nextValue = axisMin + baseFilledCount + nextCount;
|
|
17689
|
+
lastInternalValueRef.current = String(nextValue);
|
|
17690
|
+
setSelectedSlots(nextSlots);
|
|
17691
|
+
onChange(picto.id, String(nextValue));
|
|
17692
|
+
playSound("fraction-shade");
|
|
17693
|
+
},
|
|
17694
|
+
[exerciseLocked, isInteractive, baseFilledCount, totalSlots, selectedSlots, axisMin, onChange, picto.id, playSound, hasInteracted]
|
|
17695
|
+
);
|
|
17696
|
+
const statusClass = validationStatus === "correct" ? "picto-column--correct" : validationStatus === "incorrect" ? "picto-column--incorrect" : "";
|
|
17697
|
+
const cellCount = isInteractive ? totalSlots : currentFilledCount;
|
|
17698
|
+
const cells = Array.from({ length: cellCount }, (_, i) => {
|
|
17699
|
+
const isPrefilled = isInteractive ? i < baseFilledCount : i < currentFilledCount;
|
|
17700
|
+
const isSelectedAnswer = isInteractive && i >= baseFilledCount && Boolean(selectedSlots[i]);
|
|
17701
|
+
const canToggle = isInteractive && i >= baseFilledCount;
|
|
17702
|
+
const isActive = isPrefilled || isSelectedAnswer;
|
|
17703
|
+
const isInactiveFn = !isActive && isInteractive;
|
|
17704
|
+
const isLastInteractive = canToggle && i === cellCount - 1;
|
|
17705
|
+
const animClass = validationStatus === "correct" && isActive ? "picto-cell--pop" : validationStatus === "incorrect" && isActive ? "picto-cell--jiggle" : hinting && validationStatus === null && !exerciseLocked && canToggle && !isActive ? "picto-cell--hint" : "";
|
|
17706
|
+
const cellClass = [
|
|
17707
|
+
"picto-cell",
|
|
17708
|
+
isInactiveFn ? "picto-cell--inactive" : "",
|
|
17709
|
+
canToggle ? "picto-cell--interactive" : "",
|
|
17710
|
+
isInteractive && !canToggle ? "picto-cell--locked" : "",
|
|
17711
|
+
animClass
|
|
17712
|
+
].filter(Boolean).join(" ");
|
|
17713
|
+
const imgStyle = animClass === "picto-cell--pop" || animClass === "picto-cell--jiggle" ? { animationDelay: `${i * 50}ms` } : animClass === "picto-cell--hint" ? { animationDelay: `${(i - baseFilledCount) * 80}ms` } : void 0;
|
|
17714
|
+
return /* @__PURE__ */ jsx(
|
|
17715
|
+
"div",
|
|
17716
|
+
{
|
|
17717
|
+
className: cellClass,
|
|
17718
|
+
onClick: canToggle ? () => handleCellClick(i) : void 0,
|
|
17719
|
+
role: isInteractive ? "checkbox" : void 0,
|
|
17720
|
+
"aria-checked": isInteractive ? isActive : void 0,
|
|
17721
|
+
"aria-disabled": isInteractive && (!canToggle || exerciseLocked) ? true : void 0,
|
|
17722
|
+
"aria-label": isInteractive ? `${picto.id} icon ${i + 1}` : void 0,
|
|
17723
|
+
tabIndex: canToggle ? 0 : void 0,
|
|
17724
|
+
onKeyDown: canToggle ? (e) => {
|
|
17725
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
17726
|
+
e.preventDefault();
|
|
17727
|
+
handleCellClick(i);
|
|
17728
|
+
}
|
|
17729
|
+
} : void 0,
|
|
17730
|
+
children: /* @__PURE__ */ jsx(
|
|
17731
|
+
"img",
|
|
17732
|
+
{
|
|
17733
|
+
src: picto.image,
|
|
17734
|
+
alt: "",
|
|
17735
|
+
"aria-hidden": "true",
|
|
17736
|
+
style: imgStyle,
|
|
17737
|
+
onAnimationEnd: isLastInteractive && animClass === "picto-cell--hint" ? onHintEnd : void 0
|
|
17738
|
+
}
|
|
17739
|
+
)
|
|
17740
|
+
},
|
|
17741
|
+
i
|
|
17742
|
+
);
|
|
17743
|
+
});
|
|
17744
|
+
const columnClass = [
|
|
17745
|
+
"chart-picto-column-wrapper",
|
|
17746
|
+
orientation === "horizontal" ? "chart-picto-column-wrapper--h" : "",
|
|
17747
|
+
statusClass
|
|
17748
|
+
].filter(Boolean).join(" ");
|
|
17749
|
+
return /* @__PURE__ */ jsx("div", { className: columnClass, children: /* @__PURE__ */ jsx("div", { className: `chart-picto-cells${orientation === "horizontal" ? " chart-picto-cells--h" : ""}`, children: cells }) });
|
|
17750
|
+
};
|
|
17751
|
+
const PictographGrid = ({
|
|
17752
|
+
axis,
|
|
17753
|
+
columnCount,
|
|
17754
|
+
orientation = "vertical"
|
|
17755
|
+
}) => {
|
|
17756
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17757
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17758
|
+
const totalSlots = Math.round(axisMax - axisMin);
|
|
17759
|
+
if (orientation === "horizontal") {
|
|
17760
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17761
|
+
Array.from({ length: columnCount - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17762
|
+
"div",
|
|
17763
|
+
{
|
|
17764
|
+
className: "picto-gridline picto-gridline--row",
|
|
17765
|
+
style: {
|
|
17766
|
+
top: `calc(var(--chart-bar-gap) + ${i + 1} * (var(--chart-bar-slot-width) + var(--chart-bar-gap)))`
|
|
17767
|
+
}
|
|
17768
|
+
},
|
|
17769
|
+
`row-${i}`
|
|
17770
|
+
)),
|
|
17771
|
+
Array.from({ length: totalSlots - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17772
|
+
"div",
|
|
17773
|
+
{
|
|
17774
|
+
className: "picto-gridline picto-gridline--col",
|
|
17775
|
+
style: {
|
|
17776
|
+
left: `calc(${i + 1} * (var(--picto-cell-size) + var(--picto-cell-gap)))`
|
|
17777
|
+
}
|
|
17778
|
+
},
|
|
17779
|
+
`col-${i}`
|
|
17780
|
+
))
|
|
17781
|
+
] });
|
|
17782
|
+
}
|
|
17783
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17784
|
+
Array.from({ length: totalSlots - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17785
|
+
"div",
|
|
17786
|
+
{
|
|
17787
|
+
className: "picto-gridline picto-gridline--row",
|
|
17788
|
+
style: {
|
|
17789
|
+
bottom: `calc(${i + 1} * (var(--picto-cell-size) + var(--picto-cell-gap)))`
|
|
17790
|
+
}
|
|
17791
|
+
},
|
|
17792
|
+
`row-${i}`
|
|
17793
|
+
)),
|
|
17794
|
+
Array.from({ length: columnCount - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17795
|
+
"div",
|
|
17796
|
+
{
|
|
17797
|
+
className: "picto-gridline picto-gridline--col",
|
|
17798
|
+
style: {
|
|
17799
|
+
left: `calc(var(--chart-bar-gap) + ${i + 1} * (var(--chart-bar-slot-width) + var(--chart-bar-gap)))`
|
|
17800
|
+
}
|
|
17801
|
+
},
|
|
17802
|
+
`col-${i}`
|
|
17803
|
+
))
|
|
17804
|
+
] });
|
|
17805
|
+
};
|
|
17806
|
+
const EMPTY_SET = /* @__PURE__ */ new Set();
|
|
17807
|
+
const PictographChart = ({
|
|
17808
|
+
item,
|
|
17809
|
+
widgets,
|
|
17810
|
+
values,
|
|
17811
|
+
validationDetails,
|
|
17812
|
+
onChange
|
|
17813
|
+
}) => {
|
|
17814
|
+
const { pictos = [], grid } = item;
|
|
17815
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17816
|
+
const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
|
|
17817
|
+
const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
|
|
17818
|
+
const chartClass = getChartClassName(item);
|
|
17819
|
+
const renderAxisTitle = (title) => {
|
|
17820
|
+
if (!title?.length) return null;
|
|
17821
|
+
return /* @__PURE__ */ jsx(Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsx(
|
|
17822
|
+
LayoutRenderer,
|
|
17823
|
+
{
|
|
17824
|
+
item: subItem,
|
|
17825
|
+
widgets,
|
|
17826
|
+
values,
|
|
17827
|
+
droppedInputs: EMPTY_SET,
|
|
17828
|
+
validationDetails,
|
|
17829
|
+
onChange
|
|
17830
|
+
},
|
|
17831
|
+
subItem.id
|
|
17832
|
+
)) });
|
|
17833
|
+
};
|
|
17834
|
+
const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
|
|
17835
|
+
const totalSlots = Math.round(
|
|
17836
|
+
(fillAxis.max ?? CHART_AXIS_DEFAULTS.max) - (fillAxis.min ?? CHART_AXIS_DEFAULTS.min)
|
|
17837
|
+
);
|
|
17838
|
+
const renderColumn = (picto, orient = "vertical") => {
|
|
17839
|
+
const currentValue = resolveItemValue(picto.id, picto.value, values);
|
|
17840
|
+
const vStatus = picto.answer !== void 0 ? validationDetails?.[picto.id] : void 0;
|
|
17841
|
+
const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
|
|
17842
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: /* @__PURE__ */ jsx(
|
|
17843
|
+
PictographColumn,
|
|
17844
|
+
{
|
|
17845
|
+
picto,
|
|
17846
|
+
currentValue,
|
|
17847
|
+
axis: fillAxis,
|
|
17848
|
+
validationStatus,
|
|
17849
|
+
orientation: orient,
|
|
17850
|
+
onChange
|
|
17851
|
+
}
|
|
17852
|
+
) }, picto.id);
|
|
17853
|
+
};
|
|
17854
|
+
if (isHorizontal) {
|
|
17855
|
+
const chartStyle2 = computePictographChartStyle(item.scale, pictos.length, totalSlots, "horizontal");
|
|
17856
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
|
|
17857
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17858
|
+
categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
|
|
17859
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-y--categories", children: pictos.map((picto) => /* @__PURE__ */ jsx("div", { className: "chart-bar-y-label", children: picto.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17860
|
+
LayoutRenderer,
|
|
17861
|
+
{
|
|
17862
|
+
item: subItem,
|
|
17863
|
+
widgets,
|
|
17864
|
+
values,
|
|
17865
|
+
droppedInputs: EMPTY_SET,
|
|
17866
|
+
validationDetails,
|
|
17867
|
+
onChange
|
|
17868
|
+
},
|
|
17869
|
+
subItem.id
|
|
17870
|
+
)) }, picto.id)) }),
|
|
17871
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17872
|
+
grid && /* @__PURE__ */ jsx(PictographGrid, { axis: fillAxis, columnCount: pictos.length, orientation: "horizontal" }),
|
|
17873
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: pictos.map((p) => renderColumn(p, "horizontal")) })
|
|
17874
|
+
] }),
|
|
17875
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) })
|
|
17876
|
+
] }) }) });
|
|
17877
|
+
}
|
|
17878
|
+
const chartStyle = computePictographChartStyle(item.scale, pictos.length, totalSlots);
|
|
17879
|
+
const hasAnyLabel = pictos.some((p) => p.label && p.label.length > 0);
|
|
17880
|
+
const xAxisRow = hasAnyLabel ? /* @__PURE__ */ jsx("div", { className: "chart-axis-x", children: pictos.map((picto) => /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x-inner", children: picto.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17881
|
+
LayoutRenderer,
|
|
17882
|
+
{
|
|
17883
|
+
item: subItem,
|
|
17884
|
+
widgets,
|
|
17885
|
+
values,
|
|
17886
|
+
droppedInputs: EMPTY_SET,
|
|
17887
|
+
validationDetails,
|
|
17888
|
+
onChange
|
|
17889
|
+
},
|
|
17890
|
+
subItem.id
|
|
17891
|
+
)) }) }, picto.id)) }) : null;
|
|
17892
|
+
const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
|
|
17893
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
|
|
17894
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17895
|
+
valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
|
|
17896
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(YAxis, { axis: valueAxis }),
|
|
17897
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17898
|
+
grid && /* @__PURE__ */ jsx(PictographGrid, { axis: fillAxis, columnCount: pictos.length }),
|
|
17899
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: pictos.map((p) => renderColumn(p)) })
|
|
17900
|
+
] }),
|
|
17901
|
+
xAxisRow,
|
|
17902
|
+
xAxisTitle
|
|
17903
|
+
] }) }) });
|
|
17904
|
+
};
|
|
17905
|
+
const CHART_TYPE_COMPONENTS = {
|
|
17906
|
+
bar: BarChart,
|
|
17907
|
+
pictograph: PictographChart
|
|
17908
|
+
// line: LineChart,
|
|
17909
|
+
// pie: PieChart,
|
|
17910
|
+
};
|
|
17911
|
+
const Chart = (props) => {
|
|
17912
|
+
const ChartComponent = CHART_TYPE_COMPONENTS[props.item.chartType];
|
|
17913
|
+
if (!ChartComponent) {
|
|
17914
|
+
return /* @__PURE__ */ jsx("div", { className: "chart chart--unsupported", children: /* @__PURE__ */ jsx("span", { style: { color: "var(--exercise-incorrect, #ef4444)", fontSize: "0.85rem" }, children: `Chart type "${props.item.chartType}" is not yet supported.` }) });
|
|
17915
|
+
}
|
|
17916
|
+
return /* @__PURE__ */ jsx(ChartComponent, { ...props });
|
|
17917
|
+
};
|
|
16334
17918
|
const RadioOptions = ({
|
|
16335
17919
|
name,
|
|
16336
17920
|
options,
|
|
@@ -18457,11 +20041,14 @@ const NumberLineRenderer = ({
|
|
|
18457
20041
|
onChangeRef.current = onChange;
|
|
18458
20042
|
const valuesRef = useRef(values);
|
|
18459
20043
|
valuesRef.current = values;
|
|
20044
|
+
const prevCounterCountRef = useRef(null);
|
|
18460
20045
|
useEffect(() => {
|
|
18461
20046
|
if (counter === void 0) return;
|
|
18462
20047
|
const next = String(counterCount);
|
|
20048
|
+
const isSeed = prevCounterCountRef.current === null || prevCounterCountRef.current === counterCount;
|
|
20049
|
+
prevCounterCountRef.current = counterCount;
|
|
18463
20050
|
if (valuesRef.current[counter.id] !== next) {
|
|
18464
|
-
onChangeRef.current(counter.id, next);
|
|
20051
|
+
onChangeRef.current(counter.id, next, false, void 0, isSeed);
|
|
18465
20052
|
}
|
|
18466
20053
|
}, [counterCount, counter?.id]);
|
|
18467
20054
|
const cursorKeys = useMemo(
|
|
@@ -18475,7 +20062,7 @@ const NumberLineRenderer = ({
|
|
|
18475
20062
|
for (const cursor of cursors ?? []) {
|
|
18476
20063
|
const existing = valuesRef.current[cursor.id];
|
|
18477
20064
|
if (!isDefinitionChange && existing !== void 0 && existing !== "") continue;
|
|
18478
|
-
onChangeRef.current(cursor.id, String(cursor.value));
|
|
20065
|
+
onChangeRef.current(cursor.id, String(cursor.value), false, void 0, true);
|
|
18479
20066
|
}
|
|
18480
20067
|
}, [cursorKeys]);
|
|
18481
20068
|
const effectiveStep = useMemo(() => {
|
|
@@ -19217,6 +20804,20 @@ const LayoutRenderer = ({
|
|
|
19217
20804
|
}
|
|
19218
20805
|
);
|
|
19219
20806
|
}
|
|
20807
|
+
case JSON_TYPES.CHART: {
|
|
20808
|
+
const chartItem = item;
|
|
20809
|
+
return /* @__PURE__ */ jsx(
|
|
20810
|
+
Chart,
|
|
20811
|
+
{
|
|
20812
|
+
item: chartItem,
|
|
20813
|
+
widgets,
|
|
20814
|
+
values,
|
|
20815
|
+
validationDetails,
|
|
20816
|
+
onChange: (barId, val) => onChange(barId, val)
|
|
20817
|
+
},
|
|
20818
|
+
resetKey
|
|
20819
|
+
);
|
|
20820
|
+
}
|
|
19220
20821
|
default:
|
|
19221
20822
|
return null;
|
|
19222
20823
|
}
|
|
@@ -19230,18 +20831,30 @@ const FLIP_BTN_EXTRA = 44;
|
|
|
19230
20831
|
function buildInitialValues(json) {
|
|
19231
20832
|
const initial = {};
|
|
19232
20833
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
19233
|
-
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER)
|
|
19234
|
-
|
|
20834
|
+
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) {
|
|
20835
|
+
initial[id] = [];
|
|
20836
|
+
} else if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
19235
20837
|
initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
|
|
19236
20838
|
cell,
|
|
19237
20839
|
widget.rows,
|
|
19238
20840
|
widget.cols,
|
|
19239
20841
|
widget.interactiveArea
|
|
19240
20842
|
));
|
|
20843
|
+
} else if (widget.type === JSON_TYPES.CHART_BAR || widget.type === JSON_TYPES.CHART_PICTO) {
|
|
20844
|
+
initial[id] = String(widget.value);
|
|
19241
20845
|
}
|
|
19242
20846
|
}
|
|
19243
20847
|
return initial;
|
|
19244
20848
|
}
|
|
20849
|
+
function toBaseId(widgetId) {
|
|
20850
|
+
return parseFractionKey(widgetId)?.widgetId ?? widgetId;
|
|
20851
|
+
}
|
|
20852
|
+
function buildInitialValuesSignature(json) {
|
|
20853
|
+
return JSON.stringify({
|
|
20854
|
+
widgets: json.widgets,
|
|
20855
|
+
answers: json.validation?.answers ?? null
|
|
20856
|
+
});
|
|
20857
|
+
}
|
|
19245
20858
|
function buildRevealedValues(json) {
|
|
19246
20859
|
const revealed = {};
|
|
19247
20860
|
const validation = json.validation;
|
|
@@ -19289,9 +20902,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19289
20902
|
soundEnabled = true,
|
|
19290
20903
|
sounds
|
|
19291
20904
|
}, ref) => {
|
|
20905
|
+
const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
|
|
19292
20906
|
const [values, setValues] = useState(() => buildInitialValues(json));
|
|
19293
20907
|
const valuesRef = useRef(values);
|
|
19294
20908
|
valuesRef.current = values;
|
|
20909
|
+
const touchedRef = useRef(/* @__PURE__ */ new Set());
|
|
20910
|
+
const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
|
|
19295
20911
|
const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
|
|
19296
20912
|
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
19297
20913
|
const [validationDetails, setValidationDetails] = useState(null);
|
|
@@ -19300,6 +20916,16 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19300
20916
|
const [revealed, setRevealed] = useState(false);
|
|
19301
20917
|
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
19302
20918
|
const externallyLocked = useExerciseLocked();
|
|
20919
|
+
useLayoutEffect(() => {
|
|
20920
|
+
if (loadedValuesSignature === initialValuesSignature) return;
|
|
20921
|
+
setValues(buildInitialValues(json));
|
|
20922
|
+
setDroppedSources(/* @__PURE__ */ new Map());
|
|
20923
|
+
setFocusedInputId(null);
|
|
20924
|
+
setValidationDetails(null);
|
|
20925
|
+
setResetKey((k) => k + 1);
|
|
20926
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
20927
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
20928
|
+
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
19303
20929
|
const locked = validationDetails !== null || externallyLocked;
|
|
19304
20930
|
const lastResultRef = useRef(null);
|
|
19305
20931
|
const runSubmit = () => {
|
|
@@ -19337,8 +20963,11 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19337
20963
|
getValues: () => values,
|
|
19338
20964
|
getCompletion: () => {
|
|
19339
20965
|
const answerable = getAnswerableInputs(json);
|
|
19340
|
-
const
|
|
19341
|
-
const
|
|
20966
|
+
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
20967
|
+
for (const id of answerable) {
|
|
20968
|
+
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
20969
|
+
}
|
|
20970
|
+
const missing = [...missingSet];
|
|
19342
20971
|
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
19343
20972
|
return {
|
|
19344
20973
|
total: answerable.length,
|
|
@@ -19358,6 +20987,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19358
20987
|
setValidationDetails(null);
|
|
19359
20988
|
setResetKey((k) => k + 1);
|
|
19360
20989
|
setRevealed(false);
|
|
20990
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
20991
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
19361
20992
|
},
|
|
19362
20993
|
revealAnswers: () => {
|
|
19363
20994
|
const revealedValues = buildRevealedValues(json);
|
|
@@ -19366,6 +20997,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19366
20997
|
const details = {};
|
|
19367
20998
|
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
19368
20999
|
details[widgetId] = { widgetId, userValue, isCorrect: true };
|
|
21000
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
19369
21001
|
}
|
|
19370
21002
|
const next = { ...valuesRef.current, ...revealedValues };
|
|
19371
21003
|
valuesRef.current = next;
|
|
@@ -19385,10 +21017,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19385
21017
|
const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
|
|
19386
21018
|
target?.focus();
|
|
19387
21019
|
}
|
|
19388
|
-
}), [json, values, onSubmit, playSound, locked]);
|
|
19389
|
-
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
21020
|
+
}), [json, values, onSubmit, playSound, locked, initialValuesSignature]);
|
|
21021
|
+
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
19390
21022
|
if (locked) return;
|
|
19391
|
-
|
|
21023
|
+
if (!silent) {
|
|
21024
|
+
setRevealed(false);
|
|
21025
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
21026
|
+
}
|
|
19392
21027
|
const next = { ...valuesRef.current, [widgetId]: value };
|
|
19393
21028
|
valuesRef.current = next;
|
|
19394
21029
|
setValues(next);
|
|
@@ -19475,6 +21110,22 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19475
21110
|
});
|
|
19476
21111
|
continue;
|
|
19477
21112
|
}
|
|
21113
|
+
if (item.type === JSON_TYPES.CHART) {
|
|
21114
|
+
if (currentGroup.length > 0) {
|
|
21115
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
21116
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
21117
|
+
const subGroups = groupLineItems(currentGroup);
|
|
21118
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
21119
|
+
currentGroup = [];
|
|
21120
|
+
}
|
|
21121
|
+
groups.push({
|
|
21122
|
+
items: [item],
|
|
21123
|
+
hasArabic: false,
|
|
21124
|
+
hasRTLInlineWidget: false,
|
|
21125
|
+
subGroups: []
|
|
21126
|
+
});
|
|
21127
|
+
continue;
|
|
21128
|
+
}
|
|
19478
21129
|
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
19479
21130
|
if (currentGroup.length > 0) {
|
|
19480
21131
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
@@ -19658,10 +21309,24 @@ export {
|
|
|
19658
21309
|
AST_TYPES,
|
|
19659
21310
|
BoldTextInlineSchema,
|
|
19660
21311
|
BoldTextSchema,
|
|
21312
|
+
CHART_AXIS_DEFAULTS,
|
|
21313
|
+
CHART_MAX_AXIS_TICKS,
|
|
21314
|
+
CHART_MAX_PICTO_SLOTS,
|
|
21315
|
+
CHART_TYPES,
|
|
19661
21316
|
CLOCK_FORMATS,
|
|
19662
21317
|
CLOCK_MODES,
|
|
19663
21318
|
CLOCK_TIME_REGEX,
|
|
19664
21319
|
CLOCK_TYPES,
|
|
21320
|
+
ChartAxisJSONSchema,
|
|
21321
|
+
ChartAxisSchema,
|
|
21322
|
+
ChartBarJSONSchema,
|
|
21323
|
+
ChartBarSchema,
|
|
21324
|
+
ChartBarWidgetJSONSchema,
|
|
21325
|
+
ChartBlockSchema,
|
|
21326
|
+
ChartItemSchema,
|
|
21327
|
+
ChartPictoJSONSchema,
|
|
21328
|
+
ChartPictoSchema,
|
|
21329
|
+
ChartPictoWidgetJSONSchema,
|
|
19665
21330
|
ClockInlineSchema,
|
|
19666
21331
|
ClockItemSchema,
|
|
19667
21332
|
ColorTextInlineSchema,
|
|
@@ -19778,6 +21443,10 @@ export {
|
|
|
19778
21443
|
computeGridSymmetryCellsToFill,
|
|
19779
21444
|
computeGridSymmetryExpectedCells,
|
|
19780
21445
|
createBoldText,
|
|
21446
|
+
createChartAxis,
|
|
21447
|
+
createChartBar,
|
|
21448
|
+
createChartBlock,
|
|
21449
|
+
createChartPicto,
|
|
19781
21450
|
createColorText,
|
|
19782
21451
|
createDisplayClock,
|
|
19783
21452
|
createDragOptions,
|
|
@@ -19825,6 +21494,13 @@ export {
|
|
|
19825
21494
|
getMissingInputs,
|
|
19826
21495
|
isBoldText,
|
|
19827
21496
|
isCellInInteractiveArea,
|
|
21497
|
+
isChartAxis,
|
|
21498
|
+
isChartBar,
|
|
21499
|
+
isChartBarWidgetJSON,
|
|
21500
|
+
isChartBlock,
|
|
21501
|
+
isChartItem,
|
|
21502
|
+
isChartPicto,
|
|
21503
|
+
isChartPictoWidgetJSON,
|
|
19828
21504
|
isClock,
|
|
19829
21505
|
isClockItem,
|
|
19830
21506
|
isColorText,
|
|
@@ -19892,6 +21568,7 @@ export {
|
|
|
19892
21568
|
parseSafe,
|
|
19893
21569
|
reflectGridSymmetryCell,
|
|
19894
21570
|
renderExercise,
|
|
21571
|
+
resolveChartAxes,
|
|
19895
21572
|
toAST,
|
|
19896
21573
|
toJSON,
|
|
19897
21574
|
tokenize,
|