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