@baravak/risloo-profile-cli 4.46.3 → 4.75.0

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.
Files changed (101) hide show
  1. package/.agents/skills/create-profile/SKILL.md +14 -0
  2. package/.claude/settings.local.json +1 -3
  3. package/.claude/skills/create-profile/SKILL.md +203 -0
  4. package/AGENTS.md +413 -0
  5. package/CLAUDE.md +2 -185
  6. package/package.json +1 -1
  7. package/src/cli-commands/Executor.js +8 -2
  8. package/src/cli-commands/ExtractExecutor.js +55 -21
  9. package/src/cli-commands/GiftExecutor.js +1 -0
  10. package/src/handlebars/importPartials.js +10 -44
  11. package/src/handlebars/init.js +17 -11
  12. package/src/publish/json/profiles/BSSI93.json +392 -0
  13. package/src/publish/json/profiles/FRHPT93.json +2 -2
  14. package/src/publish/json/profiles/ISKS93.json +288 -0
  15. package/src/publish/json/profiles/MCMI9A.json +1540 -0
  16. package/src/publish/json/profiles/NEO93.json +3858 -0
  17. package/src/publish/json/profiles/NEO9A.json +1159 -1
  18. package/src/publish/json/profiles/NEO9Q.json +3856 -0
  19. package/src/publish/json/profiles/NEO9V.json +1157 -0
  20. package/src/publish/json/profiles/RIASEC9A.json +2775 -0
  21. package/src/publish/json/profiles/RSI93.json +359 -0
  22. package/src/samples/AEQ93.js +1 -1
  23. package/src/samples/AMS93.js +1 -1
  24. package/src/samples/AMS9A.js +3 -1
  25. package/src/samples/BAOMEIS93.js +1 -1
  26. package/src/samples/BEQI93.js +1 -1
  27. package/src/samples/BSCT93.js +5 -0
  28. package/src/samples/BSSI93.js +93 -0
  29. package/src/samples/CAATS93.js +1 -1
  30. package/src/samples/CERQ93.js +1 -1
  31. package/src/samples/CRAAS93.js +1 -1
  32. package/src/samples/DSWLS93.js +1 -1
  33. package/src/samples/EMSS93.js +1 -1
  34. package/src/samples/EPQ9A.js +6 -1
  35. package/src/samples/FACES93.js +6 -0
  36. package/src/samples/FRHPT93.js +41 -13
  37. package/src/samples/GMIT93.js +1 -1
  38. package/src/samples/HPL93.js +1 -1
  39. package/src/samples/ISKS93.js +62 -0
  40. package/src/samples/IUS93.js +1 -1
  41. package/src/samples/JCSI93.js +1 -1
  42. package/src/samples/JPFQ93.js +5 -0
  43. package/src/samples/KJGI93.js +1 -1
  44. package/src/samples/LMIQ93.js +1 -1
  45. package/src/samples/MCMI9A.js +530 -0
  46. package/src/samples/MMFAD93.js +1 -1
  47. package/src/samples/MOCI93.js +1 -1
  48. package/src/samples/NEO93.js +279 -0
  49. package/src/samples/NEO9A.js +93 -60
  50. package/src/samples/NEO9Q.js +281 -0
  51. package/src/samples/NEO9V.js +218 -0
  52. package/src/samples/OBQ4493.js +1 -1
  53. package/src/samples/PIES93.js +1 -1
  54. package/src/samples/PIES9A.js +4 -0
  55. package/src/samples/PMCIEF93.js +7 -1
  56. package/src/samples/PSWQ93.js +1 -1
  57. package/src/samples/RIASEC93.js +1 -1
  58. package/src/samples/RIASEC9A.js +256 -0
  59. package/src/samples/RSI93.js +89 -0
  60. package/src/samples/SASQ93.js +1 -1
  61. package/src/samples/SDCAQ93.js +1 -1
  62. package/src/samples/STAIY93.js +1 -1
  63. package/src/samples/WAQ93.js +1 -1
  64. package/src/samples/YBOCS93.js +1 -1
  65. package/src/samples/YPI93.js +5 -1
  66. package/views/profiles/MCMI9A_facets_partial.hbs +96 -0
  67. package/views/profiles/MCMI9A_main_partial.hbs +262 -0
  68. package/views/profiles/MCMI9A_noteworthy_partial.hbs +66 -0
  69. package/views/profiles/MCMI9A_vertical_label_partial.hbs +8 -0
  70. package/views/profiles/NEO_long_facets.hbs +125 -0
  71. package/views/profiles/NEO_main.hbs +152 -0
  72. package/views/profiles/NEO_short_facets.hbs +82 -0
  73. package/views/profiles/samples/BSCT93.hbs +17 -17
  74. package/views/profiles/samples/BSSI93.hbs +102 -0
  75. package/views/profiles/samples/FRHPT93_1.hbs +568 -87
  76. package/views/profiles/samples/FRHPT93_2.hbs +90 -76
  77. package/views/profiles/samples/FRHPT93_3.hbs +2 -2
  78. package/views/profiles/samples/FRHPT93_4.hbs +2 -2
  79. package/views/profiles/samples/FRHPT93_5.hbs +103 -0
  80. package/views/profiles/samples/FRHPT9A.hbs +7 -7
  81. package/views/profiles/samples/ISKS93.hbs +38 -0
  82. package/views/profiles/samples/MCMI9A_1.hbs +3 -0
  83. package/views/profiles/samples/MCMI9A_2.hbs +3 -0
  84. package/views/profiles/samples/MCMI9A_3.hbs +3 -0
  85. package/views/profiles/samples/MCMI9A_4.hbs +3 -0
  86. package/views/profiles/samples/NEO93_1.hbs +14 -0
  87. package/views/profiles/samples/NEO93_2.hbs +15 -0
  88. package/views/profiles/samples/NEO93_3.hbs +15 -0
  89. package/views/profiles/samples/NEO9A.hbs +14 -29
  90. package/views/profiles/samples/NEO9Q_1.hbs +13 -0
  91. package/views/profiles/samples/NEO9Q_2.hbs +14 -0
  92. package/views/profiles/samples/NEO9Q_3.hbs +14 -0
  93. package/views/profiles/samples/NEO9V_1.hbs +13 -0
  94. package/views/profiles/samples/NEO9V_2.hbs +3 -0
  95. package/views/profiles/samples/RIASEC9A_1.hbs +38 -0
  96. package/views/profiles/samples/RIASEC9A_2.hbs +67 -0
  97. package/views/profiles/samples/RIASEC9A_3.hbs +93 -0
  98. package/views/profiles/samples/RSI93.hbs +101 -0
  99. package/a.json +0 -4805
  100. package/src/publish/json/profiles/BSCT93_t1.json +0 -512
  101. package/src/publish/json/profiles/BSCT93_t2.json +0 -512
@@ -0,0 +1,256 @@
1
+ const { Profile, Mappings } = require("../Profile");
2
+
3
+ class RIASEC9A extends Profile {
4
+ // Number of pages
5
+ static pages = 3;
6
+
7
+ // Labels of the sample
8
+ labels = {
9
+ L1_0: { eng: "realistic_raw", fr: "واقع‌گرا", short: "و", enShort:"R" },
10
+ L1_1: { eng: "realistic_percentage" },
11
+ L1_2: { eng: "realistic_rank" },
12
+ L1_3: { eng: "realistic_congruence" },
13
+ L1_4: { eng: "realistic_calibrated" },
14
+ L2_0: { eng: "investigative_raw", fr: "جستجوگر", short: "ج", enShort:"I" },
15
+ L2_1: { eng: "investigative_percentage" },
16
+ L2_2: { eng: "investigative_rank" },
17
+ L2_3: { eng: "investigative_congruence" },
18
+ L2_4: { eng: "investigative_calibrated" },
19
+ L3_0: { eng: "artistic_raw", fr: "هنری", short: "هـ", enShort:"A" },
20
+ L3_1: { eng: "artistic_percentage" },
21
+ L3_2: { eng: "artistic_rank" },
22
+ L3_3: { eng: "artistic_congruence" },
23
+ L3_4: { eng: "artistic_calibrated" },
24
+ L4_0: { eng: "social_raw", fr: "اجتماعی", short: "الف", enShort:"S" },
25
+ L4_1: { eng: "social_percentage" },
26
+ L4_2: { eng: "social_rank" },
27
+ L4_3: { eng: "social_congruence" },
28
+ L4_4: { eng: "social_calibrated" },
29
+ L5_0: { eng: "enterprising_raw", fr: "متهور", short: "م", enShort:"E" },
30
+ L5_1: { eng: "enterprising_percentage" },
31
+ L5_2: { eng: "enterprising_rank" },
32
+ L5_3: { eng: "enterprising_congruence" },
33
+ L5_4: { eng: "enterprising_calibrated" },
34
+ L6_0: { eng: "conventional_raw", fr: "قراردادی", short: "ق", enShort:"C" },
35
+ L6_1: { eng: "conventional_percentage" },
36
+ L6_2: { eng: "conventional_rank" },
37
+ L6_3: { eng: "conventional_congruence" },
38
+ L6_4: { eng: "conventional_calibrated" },
39
+ Llt: { eng: "code_letters_abbr" },
40
+
41
+ };
42
+
43
+ profileSpec = {
44
+ /* "sample" determines some important info about the sample and profile */
45
+ /* Default prerequisites: 1. gender, 2. age, 3. education */
46
+ /* "prerequisites" is synonym to "fields" in our program */
47
+ sample: {
48
+ name: "پرسشنامه رغبت‌سنج تحصیلی - شغلی هالند - ویرایش حوزه" /* Name of the sample */,
49
+ multiProfile: false /* Whether the sample has multiple profiles or not */,
50
+ questions: true /* Determines whether to get questions from inital dataset or not */,
51
+ defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
52
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
53
+ },
54
+ /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
55
+ /* calculating its dimensions carefully is of great importance */
56
+ profile: {
57
+ get dimensions() {
58
+ return [
59
+ {
60
+ width: 602 + 2 * this.padding[0].x,
61
+ height: 595 + 2 * this.padding[0].y,
62
+ },
63
+ {
64
+ width: 592 + 2 * this.padding[1].x,
65
+ height: 523 + 2 * this.padding[1].y,
66
+ },
67
+ {
68
+ width: 686 + 2 * this.padding[2].x,
69
+ height: 404 + 2 * this.padding[2].y,
70
+ },
71
+ ];
72
+ },
73
+ padding: [
74
+ {
75
+ x: 0,
76
+ y: 51,
77
+ },
78
+ {
79
+ x: 147,
80
+ y: 96,
81
+ },
82
+ {
83
+ x: 109,
84
+ y: 155,
85
+ },
86
+ ],
87
+ },
88
+ /* "items" is the general term used for independent data elements to be drawn in the profile */
89
+ items: {
90
+ offsetY: 32 /* Horizontal offset between two top items */,
91
+ get distanceY() {
92
+ return this.offsetY + this.rect.body.height;
93
+ } /* Horizontal distance between two top items */,
94
+ maxValue: 50 /* Maximum value of items */,
95
+ rect: {
96
+ base: {
97
+ height: 4,
98
+ },
99
+ body: {
100
+ height: 8,
101
+ brs: {
102
+ tl: 0,
103
+ tr: 4,
104
+ bl: 0,
105
+ br: 0,
106
+ },
107
+ },
108
+ widthCoeff: 500 / 56,
109
+ opacityMappings: new Mappings()
110
+ .addMapping("1-10", 0.6)
111
+ .addMapping("11-20", 0.7)
112
+ .addMapping("21-30", 0.8)
113
+ .addMapping("31-40", 0.9)
114
+ .addMapping("41-50", 1) /* Opacity mapping for marks */,
115
+ },
116
+ },
117
+ /* "labels" part which has to be provided for each profile */
118
+ labels: Object.values(this.labels),
119
+ };
120
+
121
+ constructor(dataset, options, config = {}) {
122
+ super();
123
+ this._init(dataset, options, config);
124
+ }
125
+
126
+ _calcContext() {
127
+ const {
128
+ spec: { parameters: spec },
129
+ dataset,
130
+ } = this;
131
+
132
+ // Deconstructing the Spec of the Profile
133
+ const { items: itemsSpec } = spec;
134
+ const Items = [];
135
+ for (let i = 0; i < 30; i += 5) {
136
+ const item = dataset.score[i];
137
+ item.percentage = dataset.score[i + 1].mark ?? 0;
138
+ item.rank = dataset.score[i + 2].mark ?? 0;
139
+ item.congruence = dataset.score[i + 3].mark ?? 0;
140
+ item.calibrated = dataset.score[i + 4].mark ?? 0;
141
+ Items.push(item);
142
+ }
143
+ const map = new Map(Items.map(i => [i.label.enShort, i.label.short]))
144
+ const reportMap = dataset.score[30].mark.split('-').map(r => {
145
+ return r.split(',').map(key => map.get(key)).join(' - ')
146
+ }).reverse()
147
+ // Gathering required info for page 1 items
148
+ const items = Items.map((data) => ({
149
+ label: data.label,
150
+ mark: data.mark,
151
+ width: data.mark * itemsSpec.rect.widthCoeff,
152
+ opacity: itemsSpec.rect.opacityMappings.map(data.mark),
153
+ percentage: data.percentage,
154
+ rank: data.rank,
155
+ congruence: data.congruence,
156
+ calibrated: data.calibrated,
157
+ }));
158
+ const questionItems = [
159
+ {
160
+ label: "فعالیت‌ها",
161
+ marks: Array(6)
162
+ .fill(11)
163
+ .map((n, index) =>
164
+ dataset.questions
165
+ .slice(index * n, (index + 1) * n)
166
+ .reduce((sum, { user_answered }) => sum + (+user_answered !== 2 ? 1 : 0), 0)
167
+ ),
168
+ },
169
+ {
170
+ label: "تجربه‌ها",
171
+ marks: Array(6)
172
+ .fill(11)
173
+ .map((n, index) =>
174
+ dataset.questions
175
+ .slice(66 + index * n, 66 + (index + 1) * n)
176
+ .reduce((sum, { user_answered }) => sum + (+user_answered !== 2 ? 1 : 0), 0)
177
+ ),
178
+ },
179
+ {
180
+ label: "مشاغل",
181
+ marks: Array(6)
182
+ .fill(14)
183
+ .map((n, index) =>
184
+ dataset.questions
185
+ .slice(132 + index * n, 132 + (index + 1) * n)
186
+ .reduce((sum, { user_answered }) => sum + (+user_answered !== 2 ? 1 : 0), 0)
187
+ ),
188
+ },
189
+ {
190
+ label: "خودسنجی ۱",
191
+ marks: dataset.questions.slice(216, 216 + 6).map((q) => +q.user_answered),
192
+ },
193
+ {
194
+ label: "خودسنجی ۲",
195
+ marks: dataset.questions.slice(222, 222 + 6).map((q) => +q.user_answered),
196
+ },
197
+ ];
198
+ const itemObject = {};
199
+ let last = undefined;
200
+ const list = items
201
+ .map((item) => item.mark)
202
+ .sort((a, b) => b - a)
203
+ .filter((mark, i) => {
204
+ if (i <= 2) {
205
+ last = mark;
206
+ return true;
207
+ }
208
+ if (mark === last) {
209
+ return true;
210
+ } else {
211
+ return false;
212
+ }
213
+ });
214
+ items.forEach((item) => (itemObject[item.label.eng.slice(0, -4)] = item));
215
+ const itemRadians = [
216
+ itemObject.artistic,
217
+ itemObject.social,
218
+ itemObject.enterprising,
219
+ itemObject.conventional,
220
+ itemObject.realistic,
221
+ itemObject.investigative,
222
+ ].map((item, i) => {
223
+ const mark = ((item.mark ?? 0) * 207) / 56;
224
+ item.rad = pointsToCoordinates(mark - 14, i * 60);
225
+ item.bold = list.includes(item.mark);
226
+ item.percentageText = Math.floor(item.percentage * 100);
227
+ return item;
228
+ });
229
+ return [
230
+ {
231
+ items: [...items].sort((item1, item2) => item2.mark - item1.mark),
232
+ questionItems,
233
+ titleAppend: " - ۱",
234
+ },
235
+ {
236
+ itemRadians,
237
+ titleAppend: " - ۲",
238
+ },
239
+ {
240
+ items: [...items].reverse(),
241
+ reportMap,
242
+ titleAppend: " - ۳",
243
+ },
244
+ ];
245
+ }
246
+ }
247
+
248
+ function pointsToCoordinates(mark, angle) {
249
+ angle = (angle * Math.PI) / 180;
250
+ const start = [296 + 31 * Math.cos(angle), 261.5 + 31 * Math.sin(angle)];
251
+ const end = [start[0] + mark * Math.cos(angle), start[1] + mark * Math.sin(angle)];
252
+ const value = [end[0] + 12 * Math.cos(angle), end[1] + 12 * Math.sin(angle)];
253
+ return { start, end, value };
254
+ }
255
+
256
+ module.exports = RIASEC9A;
@@ -0,0 +1,89 @@
1
+ const { Profile } = require("../Profile");
2
+
3
+ class RSI93 extends Profile {
4
+ static pages = 1;
5
+
6
+ // RSI93 — "تصور از خدا" (خسروی). Chart has two halves:
7
+ // • Left : two mirrored donut-ring gauges (pink = negative image, green = positive image).
8
+ // • Right : two horizontal bars (god_in_life, god_cares) — the two sub-factors whose
9
+ // sum is the positive-image score (per the designer).
10
+ // Maxes = items × 5 (scoring/RSI93.py → round(raw / (count·5), 2)):
11
+ // negative_image 6→30, positive_image 21→105, god_in_life 11→55, god_cares 10→50.
12
+ // Each factor exposes _raw + _percentage; we read raw for the "X از Y" fraction and the
13
+ // 0–1 percentage for the fill sweep + the "٪ NN" label (designer: "محاسبه با درصد").
14
+ labels = {
15
+ L1_1: { eng: "negative_image_raw", max: 30 },
16
+ L1_2: { eng: "negative_image_percentage" },
17
+
18
+ L2_1: { eng: "positive_image_raw", max: 105 },
19
+ L2_2: { eng: "positive_image_percentage" },
20
+
21
+ L3_1: { eng: "god_in_life_raw", max: 55, title: "حضور خدا در زندگی" },
22
+ L3_2: { eng: "god_in_life_percentage" },
23
+
24
+ L4_1: { eng: "god_cares_raw", max: 50, title: "مراقبت خداوند" },
25
+ L4_2: { eng: "god_cares_percentage" },
26
+ };
27
+
28
+ profileSpec = {
29
+ sample: {
30
+ name: "پرسشنامه تصور از خدا (خسروی)",
31
+ multiProfile: false,
32
+ questions: false,
33
+ defaultFields: true,
34
+ fields: [],
35
+ },
36
+ profile: {
37
+ get dimensions() {
38
+ return {
39
+ width: 740 + 2 * this.padding.x, // Chart layer 740 wide → 943 page
40
+ height: 318 + 2 * this.padding.y, // Chart layer 318 tall → 754 page
41
+ };
42
+ },
43
+ padding: {
44
+ x: 101.5, // (943 − 740) / 2
45
+ y: 218, // (754 − 318) / 2
46
+ },
47
+ },
48
+ labels: Object.values(this.labels),
49
+ };
50
+
51
+ constructor(dataset, options, config = {}) {
52
+ super();
53
+ this._init(dataset, options, config);
54
+ }
55
+
56
+ _calcContext() {
57
+ const { dataset } = this;
58
+ const s = dataset.score;
59
+
60
+ const neg = packItem(s[0], s[1]); // negative image → left ring (pink)
61
+ const pos = packItem(s[2], s[3]); // positive image → right ring (green)
62
+
63
+ // Ring fill sweep. Both rings: 0 % at the bottom base (90°), 100 % at the top.
64
+ // • Left ring fills clockwise up the LEFT (increasing θ): zeta = π/2 + p·π (direction false).
65
+ // • Right ring fills counter-cw up the RIGHT (decreasing θ): zeta = π/2 − p·π (direction true).
66
+ neg.zeta = Math.PI / 2 + neg.p * Math.PI;
67
+ pos.zeta = Math.PI / 2 - pos.p * Math.PI;
68
+
69
+ const bars = [
70
+ packItem(s[4], s[5]), // god_in_life
71
+ packItem(s[6], s[7]), // god_cares
72
+ ];
73
+
74
+ return [{ neg, pos, bars }];
75
+ }
76
+ }
77
+
78
+ // Merge a factor's _raw and _percentage score entries into one drawing item.
79
+ function packItem(raw, percentage) {
80
+ return {
81
+ label: raw.label,
82
+ max: raw.label.max,
83
+ mark: raw.mark ?? 0,
84
+ percentage: Math.round((percentage.mark ?? 0) * 100),
85
+ p: percentage.mark ?? 0,
86
+ };
87
+ }
88
+
89
+ module.exports = RSI93;
@@ -19,7 +19,7 @@ class SASQ93 extends Profile {
19
19
  multiProfile: false /* Whether the sample has multiple profiles or not */,
20
20
  questions: false /* Determines whether to get questions from inital dataset or not */,
21
21
  defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
22
- fields: ["marital_status"] /* In case you want to get some additional fields and show in the profile */,
22
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
23
23
  },
24
24
  /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
25
25
  /* calculating its dimensions carefully is of great importance */
@@ -23,7 +23,7 @@ class SDCAQ93 extends Profile {
23
23
  multiProfile: false /* Whether the sample has multiple profiles or not */,
24
24
  questions: false /* Determines whether to get questions from inital dataset or not */,
25
25
  defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
26
- fields: ["marital_status"] /* In case you want to get some additional fields and show in the profile */,
26
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
27
27
  },
28
28
  /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
29
29
  /* calculating its dimensions carefully is of great importance */
@@ -19,7 +19,7 @@ class STAIY93 extends Profile {
19
19
  multiProfile: false /* Whether the sample has multiple profiles or not */,
20
20
  questions: false /* Determines whether to get questions from inital dataset or not */,
21
21
  defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
22
- fields: ["marital_status"] /* In case you want to get some additional fields and show in the profile */,
22
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
23
23
  },
24
24
  /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
25
25
  /* calculating its dimensions carefully is of great importance */
@@ -25,7 +25,7 @@ class WAQ93 extends Profile {
25
25
  multiProfile: false /* Whether the sample has multiple profiles or not */,
26
26
  questions: false /* Determines whether to get questions from inital dataset or not */,
27
27
  defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
28
- fields: ["marital_status"] /* In case you want to get some additional fields and show in the profile */,
28
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
29
29
  },
30
30
  /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
31
31
  /* calculating its dimensions carefully is of great importance */
@@ -398,7 +398,7 @@ class YBOCS93 extends Profile {
398
398
  multiProfile: true /* Whether the sample has multiple profiles or not */,
399
399
  questions: true /* Determines whether to get questions from inital dataset or not */,
400
400
  defaultFields: true /* Determines whether to have default prerequisites in the profile or not */,
401
- fields: ["marital_status"] /* In case you want to get some additional fields and show in the profile */,
401
+ fields: [] /* In case you want to get some additional fields and show in the profile */,
402
402
  },
403
403
  /* "profile" determines the dimensions of the drawn profile (to be used in svg tag viewbox) */
404
404
  /* calculating its dimensions carefully is of great importance */
@@ -3,6 +3,10 @@ class YPI93 extends Profile {
3
3
  // Number of pages
4
4
  static pages = 2;
5
5
 
6
+ static partials = {
7
+ YPI93_factors: "samples/YPI93_factors.hbs",
8
+ };
9
+
6
10
  // Labels of the sample
7
11
  labels = {
8
12
  L1_fs: { eng: "ab_father_score", group: 1},
@@ -347,4 +351,4 @@ const Items = {
347
351
  60:f15,61:f15,62:f15,63:f15,64:f15,
348
352
  65:f16,66:f16,67:f16,68:f16,
349
353
  69:f17,70:f17,71:f17,72:f17
350
- }
354
+ }
@@ -0,0 +1,96 @@
1
+ <defs>
2
+ <linearGradient id="mcmGray" x1="0" y1="1" x2="0" y2="0">
3
+ <stop stop-color="#CBD5E1"/>
4
+ <stop offset="1" stop-color="#94A3B8"/>
5
+ </linearGradient>
6
+ <linearGradient id="mcmZinc" x1="0" y1="1" x2="0" y2="0">
7
+ <stop stop-color="#D4D4D8"/>
8
+ <stop offset="1" stop-color="#A1A1AA"/>
9
+ </linearGradient>
10
+ <linearGradient id="mcmYellow" x1="0" y1="1" x2="0" y2="0">
11
+ <stop stop-color="#FEF08A"/>
12
+ <stop offset="1" stop-color="#EAB308"/>
13
+ </linearGradient>
14
+ <linearGradient id="mcmOrange" x1="0" y1="1" x2="0" y2="0">
15
+ <stop stop-color="#FDBA74"/>
16
+ <stop offset="1" stop-color="#EA580C"/>
17
+ </linearGradient>
18
+ <linearGradient id="mcmRose" x1="0" y1="1" x2="0" y2="0">
19
+ <stop stop-color="#FDA4AF"/>
20
+ <stop offset="1" stop-color="#E11D48"/>
21
+ </linearGradient>
22
+ <filter id="mcmFacetBaseline" x="24" y="358" width="838" height="7" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
23
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
24
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
25
+ <feOffset dx="1"/>
26
+ <feGaussianBlur stdDeviation="1.5"/>
27
+ <feComposite in2="hardAlpha" operator="out"/>
28
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
29
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1"/>
30
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1" result="shape"/>
31
+ </filter>
32
+ </defs>
33
+
34
+ <g transform="translate({{padding.x}}, {{padding.y}})">
35
+ {{#each groups as |group|}}
36
+ {{#each group.facets as |item|}}
37
+ <g transform="translate({{item.x}}, 61)">
38
+ {{bar 16 300 (object tl=2 bl=0 tr=2 br=0) (toRad 0) fill=item.theme.track}}
39
+ </g>
40
+ {{/each}}
41
+ {{/each}}
42
+
43
+ <line x1="26.5" y1="61.5" x2="857.5" y2="61.5" stroke="#E5E7EB" stroke-linecap="round" stroke-dasharray="6 6"/>
44
+ <line x1="26.5" y1="136.5" x2="857.5" y2="136.5" stroke="#D1D5DB" stroke-linecap="round"/>
45
+ <line x1="26.5" y1="211.5" x2="857.5" y2="211.5" stroke="#E5E7EB" stroke-linecap="round" stroke-dasharray="6 6"/>
46
+ <line x1="26.5" y1="286.5" x2="857.5" y2="286.5" stroke="#E5E7EB" stroke-linecap="round" stroke-dasharray="6 6"/>
47
+
48
+ <text x="10" y="70.5" transform="rotate(-90 10 70.5)" direction="ltr" font-size="12" fill="#9CA3AF" text-anchor="middle">100</text>
49
+ <text x="10" y="136.5" transform="rotate(-90 10 136.5)" direction="ltr" font-size="12" font-weight="500" fill="#6B7280" text-anchor="middle">75</text>
50
+ <text x="10" y="211.5" transform="rotate(-90 10 211.5)" direction="ltr" font-size="12" fill="#9CA3AF" text-anchor="middle">50</text>
51
+ <text x="10" y="286.5" transform="rotate(-90 10 286.5)" direction="ltr" font-size="12" fill="#9CA3AF" text-anchor="middle">25</text>
52
+ <text x="10" y="361.5" transform="rotate(-90 10 361.5)" direction="ltr" font-size="12" fill="#9CA3AF" text-anchor="middle">BR</text>
53
+ <text x="10" y="0" transform="rotate(-90 10 0)" font-size="12" fill="#9CA3AF" text-anchor="start">نمره خام</text>
54
+ <path d="M3 48H10M7 51L10 48L7 45" fill="none" stroke="#9CA3AF" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
55
+
56
+ {{#each groups as |group|}}
57
+ {{#each group.facets as |item|}}
58
+ {{#if item.barHeight}}
59
+ <g transform="translate({{item.x}}, {{item.barTop}})" fill="url(#{{item.theme.gradient}})">
60
+ {{bar 16 item.barHeight (object tl=2 bl=0 tr=2 br=0) (toRad 0)}}
61
+ </g>
62
+ {{/if}}
63
+ <text x="{{item.center}}" y="{{item.scoreY}}" transform="rotate(-90 {{item.center}} {{item.scoreY}})" direction="ltr" font-size="15" font-weight="{{ternary item.inside '600' '400'}}" fill="{{ternary item.inside 'white' item.theme.dark}}" text-anchor="{{item.scoreAnchor}}" dy=".35em">{{item.br}}</text>
64
+ <rect x="{{item.x}}" y="16" width="16" height="36" rx="2" fill="{{item.theme.track}}"/>
65
+ <text x="{{item.center}}" y="34" transform="rotate(-90 {{item.center}} 34)" direction="ltr" font-size="13" fill="{{item.theme.dark}}" text-anchor="middle" dy=".35em">{{item.raw}}</text>
66
+ {{!--
67
+ The code is centered in an invisible 32 × 16 box at y=366..398.
68
+ Gaps: chart baseline → code box = 4, code box → dash = 8,
69
+ dash → Persian title = 8.
70
+ --}}
71
+ <text class="eng-font" x="{{item.center}}" y="382" transform="rotate(-90 {{item.center}} 382)" direction="ltr" font-size="13" font-weight="{{ternary item.inside '600' '400'}}" fill="{{item.theme.text}}" text-anchor="middle" dy=".35em">{{item.code}}</text>
72
+ <text x="{{item.center}}" y="408.5" transform="rotate(-90 {{item.center}} 408.5)" font-size="13" font-weight="{{ternary item.inside '600' '300'}}" fill="{{item.theme.text}}" text-anchor="middle" dy=".35em">-</text>
73
+ <text x="{{item.center}}" y="419.6" transform="rotate(-90 {{item.center}} 419.6)" font-size="13" font-weight="{{ternary item.inside '600' '400'}}" fill="{{item.theme.text}}" text-anchor="start" dy=".35em">{{item.fa}}</text>
74
+ {{/each}}
75
+
76
+ <rect x="{{group.cardX}}" y="615.5" width="79" height="47" rx="4" fill="{{group.theme.card}}" stroke="{{group.theme.stroke}}"/>
77
+ {{#if (boolean group.key "===" "6b")}}
78
+ <g transform="rotate(180 {{math group.cardX '+' 39.5}} 639)">
79
+ <text x="{{math group.cardX '+' 39.5}}" y="631" dy=".3em" font-size="13" font-weight="500" fill="{{group.theme.dark}}" text-anchor="middle">دیگرآزار</text>
80
+ <text x="{{math group.cardX '+' 39.5}}" y="647" dy=".3em" font-size="13" font-weight="500" fill="{{group.theme.dark}}" text-anchor="middle">(سادیستیک)</text>
81
+ </g>
82
+ {{else}}
83
+ <text x="{{math group.cardX '+' 39.5}}" y="639" transform="rotate(180 {{math group.cardX '+' 39.5}} 639)" dy=".3em" font-size="13" font-weight="500" fill="{{group.theme.dark}}" text-anchor="middle">{{group.fa}}</text>
84
+ {{/if}}
85
+ <g transform="translate({{math group.cardX '+' 3.5}}, 663)">
86
+ {{bar 72 36 (object tl=0 tr=0 bl=2 br=2) (toRad 0) fill=group.theme.card}}
87
+ </g>
88
+ <line x1="{{math group.cardX '+' 39.5}}" y1="666" x2="{{math group.cardX '+' 39.5}}" y2="696" stroke="{{group.theme.divider}}"/>
89
+ <text class="eng-font" x="{{math group.cardX '+' 19.5}}" y="681" transform="rotate(-90 {{math group.cardX '+' 19.5}} 681)" direction="ltr" font-size="13" font-weight="500" fill="{{group.theme.cardText}}" text-anchor="middle" dy=".35em">{{group.code}}</text>
90
+ <text x="{{math group.cardX '+' 59.5}}" y="681" transform="rotate(-90 {{math group.cardX '+' 59.5}} 681)" direction="ltr" font-size="13" font-weight="500" fill="{{group.theme.cardText}}" text-anchor="middle" dy=".35em">{{group.br}}</text>
91
+ {{/each}}
92
+
93
+ <g opacity="0.5" filter="url(#mcmFacetBaseline)">
94
+ <line x1="26.5" y1="361.5" x2="857.5" y2="361.5" stroke="white" stroke-linecap="round"/>
95
+ </g>
96
+ </g>