chartjs-plugin-chart2music 0.6.1 → 0.7.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.
package/dist/plugin.mjs CHANGED
@@ -84,7 +84,8 @@ const chartjs_c2m_converter = {
84
84
  boxplot: "box",
85
85
  radar: "bar",
86
86
  wordCloud: "bar",
87
- scatter: "scatter"
87
+ scatter: "scatter",
88
+ matrix: "matrix"
88
89
  };
89
90
  const processChartType = (chart) => {
90
91
  const topLevelType = chart.config.type;
@@ -134,13 +135,13 @@ const generateAxisInfo = (chartAxisInfo, chart) => {
134
135
  }
135
136
  return axis;
136
137
  };
137
- const generateAxes = (chart) => {
138
+ const generateAxes = (chart, options) => {
138
139
  const axes = {
139
140
  x: {
140
141
  ...generateAxisInfo(chart.options?.scales?.x, chart),
141
142
  },
142
143
  y: {
143
- format: (value) => value.toLocaleString(),
144
+ format: options?.axes?.y?.format || ((value) => value.toLocaleString()),
144
145
  ...generateAxisInfo(chart.options?.scales?.y, chart),
145
146
  }
146
147
  };
@@ -159,6 +160,55 @@ const whichDataStructure = (data) => {
159
160
  }
160
161
  return data;
161
162
  };
163
+ const labelIndex = (labels, value) => {
164
+ let index = labels.indexOf(value);
165
+ if (index === -1) {
166
+ labels.push(value);
167
+ index = labels.length - 1;
168
+ }
169
+ return index;
170
+ };
171
+ const processMatrixDataPoints = (data, datasetIndex, xLabels, yLabels) => {
172
+ return data.map((point, index) => {
173
+ const x = typeof point.x === "string" ? labelIndex(xLabels, point.x) : point.x;
174
+ const y = typeof point.y === "string" ? labelIndex(yLabels, point.y) : point.y;
175
+ return {
176
+ ...point,
177
+ x,
178
+ y,
179
+ custom: {
180
+ ...point.custom,
181
+ group: datasetIndex,
182
+ datasetIndex,
183
+ index
184
+ }
185
+ };
186
+ });
187
+ };
188
+ const processMatrixData = (data) => {
189
+ const xLabels = [];
190
+ const yLabels = [];
191
+ // Chart2Music represents matrix rows as groups and columns as points within
192
+ // each group. This preserves two-dimensional keyboard navigation.
193
+ const result = {};
194
+ data.datasets.forEach((obj, index) => {
195
+ const points = processMatrixDataPoints(obj.data, index, xLabels, yLabels);
196
+ points.forEach((point) => {
197
+ const rowLabel = typeof point.y === "number" && yLabels[point.y] !== undefined
198
+ ? yLabels[point.y]
199
+ : String(point.y);
200
+ const groupName = data.datasets.length === 1
201
+ ? rowLabel
202
+ : `${obj.label ?? `Group ${index + 1}`}: ${rowLabel}`;
203
+ if (!result[groupName]) {
204
+ result[groupName] = [];
205
+ }
206
+ result[groupName].push(point);
207
+ });
208
+ });
209
+ Object.values(result).forEach((row) => row.sort((a, b) => a.x - b.x));
210
+ return { groups: Object.keys(result), data: result, xLabels, yLabels };
211
+ };
162
212
  const scrubX = (data) => {
163
213
  const blackboard = JSON.parse(JSON.stringify(data));
164
214
  let labels = [];
@@ -173,11 +223,18 @@ const scrubX = (data) => {
173
223
  });
174
224
  return { labels, data: blackboard };
175
225
  }
226
+ else {
227
+ // Grouped
228
+ return undefined;
229
+ }
176
230
  };
177
231
  const processData = (data, c2m_types) => {
178
232
  if (c2m_types === "box") {
179
233
  return processBoxData(data);
180
234
  }
235
+ if (c2m_types === "matrix") {
236
+ return processMatrixData(data);
237
+ }
181
238
  let groups = [];
182
239
  if (data.datasets.length === 1) {
183
240
  return {
@@ -209,26 +266,36 @@ const determineCCElement = (canvas, provided) => {
209
266
  canvas.insertAdjacentElement("afterend", cc);
210
267
  return cc;
211
268
  };
269
+ const createDataSnapshot = (chart) => {
270
+ return JSON.stringify({
271
+ datasets: chart.data.datasets.map(ds => ds.data),
272
+ labels: chart.data.labels
273
+ });
274
+ };
212
275
  const displayPoint = (chart) => {
213
276
  if (!chartStates.has(chart)) {
214
277
  return;
215
278
  }
216
- const { c2m: ref, visible_groups } = chartStates.get(chart);
279
+ const { c2m: ref } = chartStates.get(chart);
217
280
  const { point, index } = ref.getCurrent();
281
+ // Use Chart2Music's internal visible group tracking
282
+ // @ts-ignore - accessing internal Chart2Music property
283
+ const visibleGroupIndices = ref._visible_group_indices?.slice(1) || [];
218
284
  try {
219
285
  const highlightElements = [];
220
286
  if ("custom" in point) {
287
+ const customPoint = point;
221
288
  highlightElements.push({
222
- // @ts-ignore
223
- datasetIndex: point.custom.group,
224
- // @ts-ignore
225
- index: point.custom.index
289
+ datasetIndex: customPoint.custom.datasetIndex ?? customPoint.custom.group,
290
+ index: customPoint.custom.index
226
291
  });
227
292
  }
228
293
  else {
229
- visible_groups.forEach((datasetIndex) => {
294
+ // For stacked charts, Chart2Music includes an "All" group at index 0,
295
+ // so we subtract 1 to get the actual dataset index
296
+ visibleGroupIndices.forEach((groupIndex) => {
230
297
  highlightElements.push({
231
- datasetIndex,
298
+ datasetIndex: groupIndex - 1,
232
299
  index
233
300
  });
234
301
  });
@@ -244,11 +311,10 @@ const displayPoint = (chart) => {
244
311
  const generateChart = (chart, options) => {
245
312
  const { valid, c2m_types, invalidType } = processChartType(chart);
246
313
  if (!valid) {
247
- // @ts-ignore
248
314
  options.errorCallback?.(`Unable to connect chart2music to chart. The chart is of type "${invalidType}", which is not one of the supported chart types for this plugin. This plugin supports: ${Object.keys(chartjs_c2m_converter).join(", ")}`);
249
315
  return;
250
316
  }
251
- let axes = generateAxes(chart);
317
+ let axes = generateAxes(chart, options);
252
318
  if (chart.config.type === "wordCloud") {
253
319
  delete axes.x.minimum;
254
320
  delete axes.x.maximum;
@@ -263,8 +329,20 @@ const generateChart = (chart, options) => {
263
329
  }
264
330
  // Generate CC element
265
331
  const cc = determineCCElement(chart.canvas, options.cc);
266
- const { data, groups } = processData(chart.data, c2m_types);
332
+ const processedData = processData(chart.data, c2m_types);
333
+ const { data } = processedData;
267
334
  // lastDataObj = JSON.stringify(data);
335
+ if (c2m_types === "matrix") {
336
+ if (processedData.xLabels?.length > 0) {
337
+ axes.x.valueLabels = processedData.xLabels.slice(0);
338
+ }
339
+ if (processedData.yLabels?.length > 0) {
340
+ // Category labels provide the Matrix Plot's Y-axis formatter.
341
+ // The general numeric formatter would otherwise announce row indexes.
342
+ delete axes.y.format;
343
+ axes.y.valueLabels = processedData.yLabels.slice(0);
344
+ }
345
+ }
268
346
  let scrub = scrubX(data);
269
347
  if (scrub?.labels && scrub?.labels?.length > 0) { // Something was scrubbed
270
348
  if (!chart.data.labels || chart.data.labels.length === 0) {
@@ -286,6 +364,13 @@ const generateChart = (chart, options) => {
286
364
  ...(options.axes?.y)
287
365
  },
288
366
  };
367
+ // Start with plugin's internal onFocusCallback
368
+ const pluginOnFocusCallback = () => {
369
+ displayPoint(chart);
370
+ };
371
+ // Merge user's options, wrapping onFocusCallback if provided
372
+ const userOptions = options.options || {};
373
+ const userOnFocusCallback = userOptions.onFocusCallback;
289
374
  const c2mOptions = {
290
375
  cc,
291
376
  element: chart.canvas,
@@ -294,10 +379,13 @@ const generateChart = (chart, options) => {
294
379
  title: determineChartTitle(chart.options),
295
380
  axes,
296
381
  options: {
297
- // @ts-ignore
298
- onFocusCallback: () => {
299
- displayPoint(chart);
300
- }
382
+ ...userOptions,
383
+ onFocusCallback: userOnFocusCallback
384
+ ? (point) => {
385
+ pluginOnFocusCallback();
386
+ userOnFocusCallback(point);
387
+ }
388
+ : pluginOnFocusCallback
301
389
  }
302
390
  };
303
391
  if (Array.isArray(c2mOptions.data)) {
@@ -306,8 +394,9 @@ const generateChart = (chart, options) => {
306
394
  return {
307
395
  ...point,
308
396
  custom: {
309
- group: 0,
310
- index
397
+ ...point.custom,
398
+ group: point.custom?.group ?? 0,
399
+ index: point.custom?.index ?? index
311
400
  }
312
401
  };
313
402
  });
@@ -326,10 +415,11 @@ const generateChart = (chart, options) => {
326
415
  }
327
416
  }
328
417
  else {
329
- const groups = Object.keys(c2mOptions.data);
418
+ const dataObj = c2mOptions.data;
419
+ const groups = Object.keys(dataObj);
330
420
  groups.forEach((groupName, groupNumber) => {
331
- if (!isNaN(c2mOptions.data[groupName][0])) {
332
- c2mOptions.data[groupName] = c2mOptions.data[groupName].map((num, index) => {
421
+ if (!isNaN(dataObj[groupName][0])) {
422
+ dataObj[groupName] = dataObj[groupName].map((num, index) => {
333
423
  return {
334
424
  x: index,
335
425
  y: num,
@@ -341,26 +431,25 @@ const generateChart = (chart, options) => {
341
431
  });
342
432
  }
343
433
  else {
344
- c2mOptions.data[groupName] = c2mOptions.data[groupName].map((point, index) => {
434
+ dataObj[groupName] = dataObj[groupName].map((point, index) => {
345
435
  return {
346
436
  ...point,
347
437
  custom: {
348
- group: groupNumber,
349
- index
438
+ ...point.custom,
439
+ group: point.custom?.group ?? groupNumber,
440
+ index: point.custom?.index ?? index
350
441
  }
351
442
  };
352
443
  });
353
444
  }
354
445
  });
355
446
  }
356
- // @ts-ignore
357
447
  if (chart.config.options?.scales?.x?.stacked) {
358
- // @ts-ignore
359
- c2mOptions.options.stack = true;
448
+ if (c2mOptions.options) {
449
+ c2mOptions.options.stack = true;
450
+ }
360
451
  }
361
- // @ts-ignore
362
452
  if (options.audioEngine) {
363
- // @ts-ignore
364
453
  c2mOptions.audioEngine = options.audioEngine;
365
454
  }
366
455
  if (c2mOptions.data.length === 0) {
@@ -372,7 +461,6 @@ const generateChart = (chart, options) => {
372
461
  const { err, data: c2m } = c2mChart(c2mOptions);
373
462
  /* istanbul-ignore-next */
374
463
  if (err) {
375
- // @ts-ignore
376
464
  options.errorCallback?.(err);
377
465
  return;
378
466
  }
@@ -381,12 +469,32 @@ const generateChart = (chart, options) => {
381
469
  }
382
470
  chartStates.set(chart, {
383
471
  c2m,
384
- visible_groups: groups?.map((g, i) => i) ?? []
472
+ lastDataSnapshot: createDataSnapshot(chart)
385
473
  });
474
+ if (c2m_types === "matrix") {
475
+ const matrixKeydown = (event) => {
476
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") {
477
+ return;
478
+ }
479
+ event.preventDefault();
480
+ event.stopImmediatePropagation();
481
+ const actions = c2m._availableActions;
482
+ if (event.key === "ArrowUp") {
483
+ actions.next_category();
484
+ }
485
+ else {
486
+ actions.previous_category();
487
+ }
488
+ };
489
+ chart.canvas.addEventListener("keydown", matrixKeydown, true);
490
+ const state = chartStates.get(chart);
491
+ state.matrixKeydown = matrixKeydown;
492
+ state.matrixKeydownTarget = chart.canvas;
493
+ }
386
494
  };
387
495
  const plugin = {
388
496
  id: "chartjs2music",
389
- afterInit: (chart, args, options) => {
497
+ afterInit: (chart, _args, options) => {
390
498
  if (!chartStates.has(chart)) {
391
499
  generateChart(chart, options);
392
500
  // Remove tooltip when the chart blurs
@@ -413,19 +521,17 @@ const plugin = {
413
521
  if (!chartStates.has(chart)) {
414
522
  generateChart(chart, options);
415
523
  }
416
- const { c2m: ref, visible_groups } = chartStates.get(chart);
524
+ const { c2m: ref } = chartStates.get(chart);
417
525
  if (!ref) {
418
526
  return;
419
527
  }
420
- // @ts-ignore
421
- const groups = ref._groups.slice(0);
422
- // @ts-ignore
423
- if (ref._options.stack) {
528
+ const refInternal = ref;
529
+ const groups = refInternal._groups.slice(0);
530
+ if (refInternal._options.stack) {
424
531
  groups.shift();
425
532
  }
426
533
  if (args.mode === "hide") {
427
534
  const err = ref.setCategoryVisibility(groups[args.index], false);
428
- visible_groups.splice(args.index, 1);
429
535
  if (err) {
430
536
  console.error(err);
431
537
  }
@@ -433,24 +539,49 @@ const plugin = {
433
539
  }
434
540
  if (args.mode === "show") {
435
541
  const err = ref.setCategoryVisibility(groups[args.index], true);
436
- visible_groups.push(args.index);
437
542
  if (err) {
438
543
  console.error(err);
439
544
  }
440
545
  return;
441
546
  }
442
547
  },
548
+ afterDatasetsUpdate: (chart, _args, options) => {
549
+ const state = chartStates.get(chart);
550
+ if (!state?.c2m)
551
+ return;
552
+ // Check if data has changed
553
+ const currentSnapshot = createDataSnapshot(chart);
554
+ if (currentSnapshot === state.lastDataSnapshot) {
555
+ return; // No data change, skip update
556
+ }
557
+ // Get chart type
558
+ const { valid, c2m_types } = processChartType(chart);
559
+ if (!valid)
560
+ return;
561
+ // Process data and generate axes
562
+ const { data } = processData(chart.data, c2m_types);
563
+ const axes = generateAxes(chart, options);
564
+ // Update Chart2Music with new data
565
+ state.c2m.setData(data, axes);
566
+ // Update snapshot
567
+ state.lastDataSnapshot = currentSnapshot;
568
+ },
443
569
  afterDestroy: (chart) => {
444
- const { c2m: ref } = chartStates.get(chart);
445
- if (!ref) {
570
+ const state = chartStates.get(chart);
571
+ if (!state?.c2m) {
446
572
  return;
447
573
  }
574
+ const { c2m: ref } = state;
575
+ if (state?.matrixKeydown && state.matrixKeydownTarget) {
576
+ state.matrixKeydownTarget.removeEventListener("keydown", state.matrixKeydown, true);
577
+ }
448
578
  ref.cleanUp();
449
579
  },
450
580
  defaults: {
451
581
  cc: null,
452
582
  audioEngine: null,
453
- errorCallback: null
583
+ errorCallback: null,
584
+ options: {}
454
585
  }
455
586
  };
456
587
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-plugin-chart2music",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Chart.js plugin for Chart2Music. Turns chart.js charts into music so the blind can hear data.",
6
6
  "main": "dist/plugin.js",
@@ -34,7 +34,9 @@
34
34
  },
35
35
  "license": "MIT",
36
36
  "scripts": {
37
- "start": "vite",
37
+ "start": "storybook dev -p 6006",
38
+ "storybook": "storybook dev -p 6006",
39
+ "build-storybook": "storybook build",
38
40
  "build": "rollup -c rollup.config.js --silent && yarn build-cjs",
39
41
  "build-cjs": "babel ./dist/plugin.mjs --out-file ./dist/plugin.cjs",
40
42
  "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
@@ -43,32 +45,59 @@
43
45
  "prepack": "npm run clean && npm run build"
44
46
  },
45
47
  "devDependencies": {
46
- "@babel/cli": "7.27.0",
47
- "@babel/core": "7.26.10",
48
- "@babel/plugin-transform-modules-commonjs": "7.26.3",
49
- "@babel/preset-env": "7.26.9",
50
- "@rollup/plugin-typescript": "12.1.2",
51
- "@sgratzl/chartjs-chart-boxplot": "4.4.4",
52
- "@types/jest": "29.5.14",
53
- "canvas": "3.1.0",
48
+ "@babel/cli": "^8.0.4",
49
+ "@babel/core": "^8.0.1",
50
+ "@babel/plugin-transform-modules-commonjs": "^8.0.1",
51
+ "@babel/preset-env": "^8.0.2",
52
+ "@rollup/plugin-typescript": "12.3.0",
53
+ "@storybook/html-vite": "10.5.0",
54
+ "@sgratzl/chartjs-chart-boxplot": "4.4.5",
55
+ "@types/jest": "^30.0.0",
56
+ "canvas": "^3.2.3",
54
57
  "chartjs-adapter-luxon": "1.3.1",
55
- "chartjs-chart-wordcloud": "4.4.4",
56
- "chartjs-plugin-a11y-legend": "0.2.1",
57
- "cross-env": "7.0.3",
58
+ "chartjs-chart-matrix": "3.0.5",
59
+ "chartjs-chart-wordcloud": "4.4.5",
60
+ "chartjs-plugin-a11y-legend": "0.2.2",
61
+ "cross-env": "^10.1.0",
58
62
  "depcheck": "1.4.7",
59
- "jest": "29.7.0",
60
- "jest-environment-jsdom": "29.7.0",
61
- "luxon": "3.6.1",
62
- "rimraf": "6.0.1",
63
- "rollup": "4.40.0",
63
+ "jest": "^30.4.2",
64
+ "jest-environment-jsdom": "^30.4.1",
65
+ "luxon": "^3.7.2",
66
+ "rimraf": "^6.1.3",
67
+ "rollup": "^4.62.2",
64
68
  "rollup-plugin-copy": "3.5.0",
65
- "ts-jest": "29.3.2",
69
+ "storybook": "10.5.0",
70
+ "ts-jest": "^29.4.11",
66
71
  "tslib": "2.8.1",
67
- "typescript": "5.8.3",
68
- "vite": "6.2.6"
72
+ "typescript": "5.9.3",
73
+ "vite": "^8.1.4"
69
74
  },
70
75
  "dependencies": {
71
- "chart.js": "4.4.8",
72
- "chart2music": "1.18.1"
76
+ "chart.js": "4.5.1",
77
+ "chart2music": "1.20.0"
78
+ },
79
+ "resolutions": {
80
+ "@babel/core": "^8.0.1",
81
+ "brace-expansion": "^1.1.13",
82
+ "@babel/cli/**/brace-expansion": "^1.1.13",
83
+ "@babel/cli/**/minimatch": "^3.1.4",
84
+ "@babel/cli/**/picomatch": "^2.3.2",
85
+ "@rollup/plugin-typescript/**/picomatch": "^4.0.4",
86
+ "canvas/**/tar-fs": "^2.1.3",
87
+ "depcheck/**/brace-expansion": "^2.0.3",
88
+ "depcheck/**/js-yaml": "^3.15.0",
89
+ "depcheck/**/lodash": "^4.18.1",
90
+ "depcheck/**/minimatch": "^7.4.8",
91
+ "depcheck/**/picomatch": "^2.3.2",
92
+ "depcheck/**/postcss": "^8.5.10",
93
+ "jest/**/brace-expansion": "^1.1.13",
94
+ "jest/**/js-yaml": "^3.15.0",
95
+ "jest/**/minimatch": "^3.1.4",
96
+ "jest/**/picomatch": "^2.3.2",
97
+ "rollup-plugin-copy/**/brace-expansion": "^1.1.13",
98
+ "rollup-plugin-copy/**/minimatch": "^3.1.4",
99
+ "rollup-plugin-copy/**/picomatch": "^2.3.2",
100
+ "**/ws": "^8.21.0",
101
+ "**/yaml": "^1.10.3"
73
102
  }
74
103
  }