chartjs-plugin-chart2music 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -5,6 +5,8 @@ chartjs plugin for chart2music.
5
5
 
6
6
  Turns your chart.js charts into music so the blind can hear data. This plugin will automatically add Chart2Music, an interactive sonification library, to your chart.js charts. The contents of the chart element will be modified to best support screen reader users, and the interactions will be visually synchronized to provide support for keyboard-only users.
7
7
 
8
+ [Check out our CodePen collection of examples using the plugin.](https://codepen.io/collection/VYEvEQ)
9
+
8
10
  ## Getting started
9
11
 
10
12
  Add the chartjs2music plugin to your existing chart.js code like this:
@@ -39,7 +41,9 @@ new Chart(canvasElement, {
39
41
  This plugin is currently in beta, so not all of the chart.js features are currently supported.
40
42
 
41
43
  A quick list of chart.js features we currently support includes:
42
- * Chart types: bar, line, pie, doughnut, polar, and combinations therein.
44
+ * Chart types: bar, line, pie, doughnut, polar, radar, and combinations therein.
45
+ * Boxplots using the `@sgratzl/chartjs-chart-boxplot` plugin (only support boxplots when there are no other chart types present)
46
+ * Wordclouds using the `chartjs-chart-wordcloud` plugin
43
47
  * Axes options: `title`, `min`, `max`, `type="linear"`, `type="logarithmic"`.
44
48
  * Chart title
45
49
  * Most data structures (not including `parsing` or non-standard axes identifiers)
@@ -55,4 +59,10 @@ Things we plan to support in the future:
55
59
  * Locale
56
60
  * Dataset visibility (when you show/hide a category from the legend)
57
61
  * Radar charts
58
- * Custom formatting for axis tick values
62
+ * Custom formatting for axis tick values
63
+
64
+ Plugins we plan to support in the future:
65
+ * [chartjs-char-error-bars](https://www.npmjs.com/package/chartjs-chart-error-bars)
66
+ * [chartjs-chart-matrix](https://www.npmjs.com/package/chartjs-chart-matrix)
67
+ * [chartjs-chart-pcp](https://www.npmjs.com/package/chartjs-chart-pcp)
68
+ * [chartjs-plugin-zoom](https://www.npmjs.com/package/chartjs-plugin-zoom)
package/dist/plugin.js CHANGED
@@ -1,13 +1,91 @@
1
1
  var chartjs2music = (function (c2mChart) {
2
2
  'use strict';
3
3
 
4
+ const calcMedian = (nums) => (nums.length % 2) ? nums[Math.floor(nums.length / 2)] : (nums[nums.length / 2] + nums[nums.length / 2 - 1]) / 2;
5
+ const fiveNumberSummary = (nums) => {
6
+ const sortedNumbers = nums.sort();
7
+ let min, max, q1, q3;
8
+ const datamin = Math.min(...sortedNumbers);
9
+ const datamax = Math.max(...sortedNumbers);
10
+ const median = calcMedian(sortedNumbers);
11
+ const length = sortedNumbers.length;
12
+ if (length % 2) {
13
+ q1 = calcMedian(sortedNumbers.slice(0, Math.floor(length / 2)));
14
+ q3 = calcMedian(sortedNumbers.slice(Math.ceil(length % 2)));
15
+ }
16
+ else {
17
+ q1 = calcMedian(sortedNumbers.slice(0, length / 2 - 1));
18
+ q3 = calcMedian(sortedNumbers.slice(length / 2));
19
+ }
20
+ const iqr = q3 - q1;
21
+ if (datamax < q3 + iqr) {
22
+ max = datamax;
23
+ }
24
+ else {
25
+ max = sortedNumbers.reverse().find((num) => num < datamax) ?? datamax;
26
+ }
27
+ if (datamin < q1 - iqr) {
28
+ min = datamin;
29
+ }
30
+ else {
31
+ min = sortedNumbers.reverse().find((num) => num < datamin) ?? datamin;
32
+ }
33
+ const outlier = sortedNumbers.filter((num) => num < min || num > max);
34
+ return {
35
+ low: min,
36
+ high: max,
37
+ median,
38
+ q1,
39
+ q3,
40
+ ...(outlier.length > 0 ? { outlier } : {})
41
+ };
42
+ };
43
+ const whichBoxData = (data) => {
44
+ return data.map((row, index) => {
45
+ if (typeof row === "object" && "min" in row) {
46
+ return {
47
+ ...row,
48
+ low: row.min,
49
+ high: row.max,
50
+ x: index,
51
+ ...("outliers" in row ? { outlier: row.outliers } : {})
52
+ };
53
+ }
54
+ if (Array.isArray(row)) {
55
+ return {
56
+ ...fiveNumberSummary(row),
57
+ x: index
58
+ };
59
+ }
60
+ });
61
+ };
62
+ const processBoxData = (data) => {
63
+ if (data.datasets.length === 1) {
64
+ return {
65
+ data: whichBoxData(data.datasets[0].data)
66
+ };
67
+ }
68
+ const groups = [];
69
+ const result = {};
70
+ data.datasets.forEach((obj, index) => {
71
+ const groupName = obj.label ?? `Group ${index + 1}`;
72
+ groups.push(groupName);
73
+ result[groupName] = whichBoxData(obj.data);
74
+ });
75
+ return { groups, data: result };
76
+ };
77
+
4
78
  // let lastDataObj = "";
5
79
  const chartjs_c2m_converter = {
6
80
  bar: "bar",
7
81
  line: "line",
8
82
  pie: "pie",
9
83
  polarArea: "bar",
10
- doughnut: "pie"
84
+ doughnut: "pie",
85
+ boxplot: "box",
86
+ radar: "bar",
87
+ wordCloud: "bar",
88
+ scatter: "scatter"
11
89
  };
12
90
  const processChartType = (chart) => {
13
91
  const topLevelType = chart.config.type;
@@ -94,7 +172,10 @@ var chartjs2music = (function (c2mChart) {
94
172
  return { labels, data: blackboard };
95
173
  }
96
174
  };
97
- const processData = (data) => {
175
+ const processData = (data, c2m_types) => {
176
+ if (c2m_types === "box") {
177
+ return processBoxData(data);
178
+ }
98
179
  let groups = [];
99
180
  if (data.datasets.length === 1) {
100
181
  return {
@@ -135,9 +216,21 @@ var chartjs2music = (function (c2mChart) {
135
216
  return;
136
217
  }
137
218
  const axes = generateAxes(chart);
219
+ if (chart.config.type === "wordCloud") {
220
+ delete axes.x.minimum;
221
+ delete axes.x.maximum;
222
+ delete axes.y.minimum;
223
+ delete axes.y.maximum;
224
+ if (!axes.x.label) {
225
+ axes.x.label = "Word";
226
+ }
227
+ if (!axes.y.label) {
228
+ axes.y.label = "Emphasis";
229
+ }
230
+ }
138
231
  // Generate CC element
139
232
  const cc = determineCCElement(chart.canvas, options.cc);
140
- const { data, groups } = processData(chart.data);
233
+ const { data, groups } = processData(chart.data, c2m_types);
141
234
  // lastDataObj = JSON.stringify(data);
142
235
  let scrub = scrubX(data);
143
236
  if (scrub?.labels && scrub?.labels?.length > 0) { // Something was scrubbed
@@ -145,6 +238,10 @@ var chartjs2music = (function (c2mChart) {
145
238
  axes.x.valueLabels = scrub.labels.slice(0);
146
239
  }
147
240
  }
241
+ if (c2m_types === "scatter") {
242
+ delete scrub?.data;
243
+ delete axes.x.valueLabels;
244
+ }
148
245
  const c2mOptions = {
149
246
  cc,
150
247
  element: chart.canvas,
@@ -154,12 +251,23 @@ var chartjs2music = (function (c2mChart) {
154
251
  axes,
155
252
  options: {
156
253
  // @ts-ignore
157
- onFocusCallback: ({ slice, index }) => {
254
+ onFocusCallback: ({ point, index }) => {
158
255
  try {
159
- const highlightElements = [{
160
- datasetIndex: groups?.indexOf(slice) ?? 0,
161
- index
162
- }];
256
+ const highlightElements = [];
257
+ if ("custom" in point) {
258
+ highlightElements.push({
259
+ datasetIndex: point.custom.group,
260
+ index: point.custom.index
261
+ });
262
+ }
263
+ else {
264
+ for (let i = 0; i < (groups?.length ?? 1); i++) {
265
+ highlightElements.push({
266
+ datasetIndex: i,
267
+ index
268
+ });
269
+ }
270
+ }
163
271
  chart?.setActiveElements(highlightElements);
164
272
  chart?.tooltip?.setActiveElements(highlightElements, {});
165
273
  chart?.update();
@@ -170,6 +278,62 @@ var chartjs2music = (function (c2mChart) {
170
278
  }
171
279
  }
172
280
  };
281
+ if (Array.isArray(c2mOptions.data)) {
282
+ if (isNaN(c2mOptions.data[0])) {
283
+ c2mOptions.data = c2mOptions.data.map((point, index) => {
284
+ return {
285
+ ...point,
286
+ custom: {
287
+ group: 0,
288
+ index
289
+ }
290
+ };
291
+ });
292
+ }
293
+ else {
294
+ c2mOptions.data = c2mOptions.data.map((num, index) => {
295
+ return {
296
+ x: index,
297
+ y: num,
298
+ custom: {
299
+ group: 0,
300
+ index
301
+ }
302
+ };
303
+ });
304
+ }
305
+ }
306
+ else {
307
+ const groups = Object.keys(c2mOptions.data);
308
+ groups.forEach((groupName, groupNumber) => {
309
+ if (!isNaN(c2mOptions.data[groupName][0])) {
310
+ c2mOptions.data[groupName] = c2mOptions.data[groupName].map((num, index) => {
311
+ return {
312
+ x: index,
313
+ y: num,
314
+ custom: {
315
+ group: groupNumber,
316
+ index
317
+ }
318
+ };
319
+ });
320
+ }
321
+ else {
322
+ c2mOptions.data[groupName] = c2mOptions.data[groupName].map((point, index) => {
323
+ return {
324
+ ...point,
325
+ custom: {
326
+ group: groupNumber,
327
+ index
328
+ }
329
+ };
330
+ });
331
+ }
332
+ });
333
+ }
334
+ if (chart.config.options?.scales?.x?.stacked) {
335
+ c2mOptions.options.stack = true;
336
+ }
173
337
  if (options.audioEngine) {
174
338
  // @ts-ignore
175
339
  c2mOptions.audioEngine = options.audioEngine;
package/dist/plugin.mjs CHANGED
@@ -1,12 +1,90 @@
1
1
  import c2mChart from 'chart2music';
2
2
 
3
+ const calcMedian = (nums) => (nums.length % 2) ? nums[Math.floor(nums.length / 2)] : (nums[nums.length / 2] + nums[nums.length / 2 - 1]) / 2;
4
+ const fiveNumberSummary = (nums) => {
5
+ const sortedNumbers = nums.sort();
6
+ let min, max, q1, q3;
7
+ const datamin = Math.min(...sortedNumbers);
8
+ const datamax = Math.max(...sortedNumbers);
9
+ const median = calcMedian(sortedNumbers);
10
+ const length = sortedNumbers.length;
11
+ if (length % 2) {
12
+ q1 = calcMedian(sortedNumbers.slice(0, Math.floor(length / 2)));
13
+ q3 = calcMedian(sortedNumbers.slice(Math.ceil(length % 2)));
14
+ }
15
+ else {
16
+ q1 = calcMedian(sortedNumbers.slice(0, length / 2 - 1));
17
+ q3 = calcMedian(sortedNumbers.slice(length / 2));
18
+ }
19
+ const iqr = q3 - q1;
20
+ if (datamax < q3 + iqr) {
21
+ max = datamax;
22
+ }
23
+ else {
24
+ max = sortedNumbers.reverse().find((num) => num < datamax) ?? datamax;
25
+ }
26
+ if (datamin < q1 - iqr) {
27
+ min = datamin;
28
+ }
29
+ else {
30
+ min = sortedNumbers.reverse().find((num) => num < datamin) ?? datamin;
31
+ }
32
+ const outlier = sortedNumbers.filter((num) => num < min || num > max);
33
+ return {
34
+ low: min,
35
+ high: max,
36
+ median,
37
+ q1,
38
+ q3,
39
+ ...(outlier.length > 0 ? { outlier } : {})
40
+ };
41
+ };
42
+ const whichBoxData = (data) => {
43
+ return data.map((row, index) => {
44
+ if (typeof row === "object" && "min" in row) {
45
+ return {
46
+ ...row,
47
+ low: row.min,
48
+ high: row.max,
49
+ x: index,
50
+ ...("outliers" in row ? { outlier: row.outliers } : {})
51
+ };
52
+ }
53
+ if (Array.isArray(row)) {
54
+ return {
55
+ ...fiveNumberSummary(row),
56
+ x: index
57
+ };
58
+ }
59
+ });
60
+ };
61
+ const processBoxData = (data) => {
62
+ if (data.datasets.length === 1) {
63
+ return {
64
+ data: whichBoxData(data.datasets[0].data)
65
+ };
66
+ }
67
+ const groups = [];
68
+ const result = {};
69
+ data.datasets.forEach((obj, index) => {
70
+ const groupName = obj.label ?? `Group ${index + 1}`;
71
+ groups.push(groupName);
72
+ result[groupName] = whichBoxData(obj.data);
73
+ });
74
+ return { groups, data: result };
75
+ };
76
+
3
77
  // let lastDataObj = "";
4
78
  const chartjs_c2m_converter = {
5
79
  bar: "bar",
6
80
  line: "line",
7
81
  pie: "pie",
8
82
  polarArea: "bar",
9
- doughnut: "pie"
83
+ doughnut: "pie",
84
+ boxplot: "box",
85
+ radar: "bar",
86
+ wordCloud: "bar",
87
+ scatter: "scatter"
10
88
  };
11
89
  const processChartType = (chart) => {
12
90
  const topLevelType = chart.config.type;
@@ -93,7 +171,10 @@ const scrubX = (data) => {
93
171
  return { labels, data: blackboard };
94
172
  }
95
173
  };
96
- const processData = (data) => {
174
+ const processData = (data, c2m_types) => {
175
+ if (c2m_types === "box") {
176
+ return processBoxData(data);
177
+ }
97
178
  let groups = [];
98
179
  if (data.datasets.length === 1) {
99
180
  return {
@@ -134,9 +215,21 @@ const plugin = {
134
215
  return;
135
216
  }
136
217
  const axes = generateAxes(chart);
218
+ if (chart.config.type === "wordCloud") {
219
+ delete axes.x.minimum;
220
+ delete axes.x.maximum;
221
+ delete axes.y.minimum;
222
+ delete axes.y.maximum;
223
+ if (!axes.x.label) {
224
+ axes.x.label = "Word";
225
+ }
226
+ if (!axes.y.label) {
227
+ axes.y.label = "Emphasis";
228
+ }
229
+ }
137
230
  // Generate CC element
138
231
  const cc = determineCCElement(chart.canvas, options.cc);
139
- const { data, groups } = processData(chart.data);
232
+ const { data, groups } = processData(chart.data, c2m_types);
140
233
  // lastDataObj = JSON.stringify(data);
141
234
  let scrub = scrubX(data);
142
235
  if (scrub?.labels && scrub?.labels?.length > 0) { // Something was scrubbed
@@ -144,6 +237,10 @@ const plugin = {
144
237
  axes.x.valueLabels = scrub.labels.slice(0);
145
238
  }
146
239
  }
240
+ if (c2m_types === "scatter") {
241
+ delete scrub?.data;
242
+ delete axes.x.valueLabels;
243
+ }
147
244
  const c2mOptions = {
148
245
  cc,
149
246
  element: chart.canvas,
@@ -153,12 +250,23 @@ const plugin = {
153
250
  axes,
154
251
  options: {
155
252
  // @ts-ignore
156
- onFocusCallback: ({ slice, index }) => {
253
+ onFocusCallback: ({ point, index }) => {
157
254
  try {
158
- const highlightElements = [{
159
- datasetIndex: groups?.indexOf(slice) ?? 0,
160
- index
161
- }];
255
+ const highlightElements = [];
256
+ if ("custom" in point) {
257
+ highlightElements.push({
258
+ datasetIndex: point.custom.group,
259
+ index: point.custom.index
260
+ });
261
+ }
262
+ else {
263
+ for (let i = 0; i < (groups?.length ?? 1); i++) {
264
+ highlightElements.push({
265
+ datasetIndex: i,
266
+ index
267
+ });
268
+ }
269
+ }
162
270
  chart?.setActiveElements(highlightElements);
163
271
  chart?.tooltip?.setActiveElements(highlightElements, {});
164
272
  chart?.update();
@@ -169,6 +277,62 @@ const plugin = {
169
277
  }
170
278
  }
171
279
  };
280
+ if (Array.isArray(c2mOptions.data)) {
281
+ if (isNaN(c2mOptions.data[0])) {
282
+ c2mOptions.data = c2mOptions.data.map((point, index) => {
283
+ return {
284
+ ...point,
285
+ custom: {
286
+ group: 0,
287
+ index
288
+ }
289
+ };
290
+ });
291
+ }
292
+ else {
293
+ c2mOptions.data = c2mOptions.data.map((num, index) => {
294
+ return {
295
+ x: index,
296
+ y: num,
297
+ custom: {
298
+ group: 0,
299
+ index
300
+ }
301
+ };
302
+ });
303
+ }
304
+ }
305
+ else {
306
+ const groups = Object.keys(c2mOptions.data);
307
+ groups.forEach((groupName, groupNumber) => {
308
+ if (!isNaN(c2mOptions.data[groupName][0])) {
309
+ c2mOptions.data[groupName] = c2mOptions.data[groupName].map((num, index) => {
310
+ return {
311
+ x: index,
312
+ y: num,
313
+ custom: {
314
+ group: groupNumber,
315
+ index
316
+ }
317
+ };
318
+ });
319
+ }
320
+ else {
321
+ c2mOptions.data[groupName] = c2mOptions.data[groupName].map((point, index) => {
322
+ return {
323
+ ...point,
324
+ custom: {
325
+ group: groupNumber,
326
+ index
327
+ }
328
+ };
329
+ });
330
+ }
331
+ });
332
+ }
333
+ if (chart.config.options?.scales?.x?.stacked) {
334
+ c2mOptions.options.stack = true;
335
+ }
172
336
  if (options.audioEngine) {
173
337
  // @ts-ignore
174
338
  c2mOptions.audioEngine = options.audioEngine;
package/package.json CHANGED
@@ -1,55 +1,59 @@
1
- {
2
- "name": "chartjs-plugin-chart2music",
3
- "version": "0.0.3",
4
- "type": "module",
5
- "description": "Chart.js plugin for Chart2Music. Turns chart.js charts into music so the blind can hear data.",
6
- "main": "dist/plugin.js",
7
- "module": "dist/plugin.mjs",
8
- "exports": {
9
- "import": "./dist/plugin.mjs"
10
- },
11
- "files": ["dist/*"],
12
- "keywords": [
13
- "a11y",
14
- "accessibility",
15
- "chart.js",
16
- "chart",
17
- "dataviz",
18
- "screen reader",
19
- "sonification"
20
- ],
21
- "repository": {
22
- "type": "git",
23
- "url": "https://github.com/julianna-langston/chartjs2music"
24
- },
25
- "bugs": {
26
- "url": "https://github.com/julianna-langston/chartjs2music/issues"
27
- },
28
- "license": "MIT",
29
- "scripts": {
30
- "start": "vite",
31
- "build": "rollup -c rollup.config.js --silent",
32
- "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
33
- "depcheck": "depcheck",
34
- "clean": "rimraf dist coverage"
35
- },
36
- "devDependencies": {
37
- "@rollup/plugin-typescript": "^11.0.0",
38
- "@types/jest": "^29.4.0",
39
- "canvas": "^2.11.0",
40
- "cross-env": "^7.0.3",
41
- "depcheck": "^1.4.3",
42
- "jest": "^29.4.2",
43
- "jest-environment-jsdom": "^29.4.2",
44
- "rimraf": "^4.1.2",
45
- "rollup": "^3.15.0",
46
- "ts-jest": "^29.0.5",
47
- "tslib": "^2.5.0",
48
- "typescript": "^4.9.3",
49
- "vite": "^4.1.0"
50
- },
51
- "dependencies": {
52
- "chart.js": "^4.2.1",
53
- "chart2music": "^1.8.2"
54
- }
55
- }
1
+ {
2
+ "name": "chartjs-plugin-chart2music",
3
+ "version": "0.0.5",
4
+ "type": "module",
5
+ "description": "Chart.js plugin for Chart2Music. Turns chart.js charts into music so the blind can hear data.",
6
+ "main": "dist/plugin.js",
7
+ "module": "dist/plugin.mjs",
8
+ "exports": {
9
+ "import": "./dist/plugin.mjs"
10
+ },
11
+ "files": [
12
+ "dist/*"
13
+ ],
14
+ "keywords": [
15
+ "a11y",
16
+ "accessibility",
17
+ "chart.js",
18
+ "chart",
19
+ "dataviz",
20
+ "screen reader",
21
+ "sonification"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/julianna-langston/chartjs2music"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/julianna-langston/chartjs2music/issues"
29
+ },
30
+ "license": "MIT",
31
+ "scripts": {
32
+ "start": "vite",
33
+ "build": "rollup -c rollup.config.js --silent",
34
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
35
+ "depcheck": "depcheck",
36
+ "clean": "rimraf dist coverage"
37
+ },
38
+ "devDependencies": {
39
+ "@rollup/plugin-typescript": "^11.0.0",
40
+ "@sgratzl/chartjs-chart-boxplot": "^4.1.1",
41
+ "@types/jest": "^29.4.0",
42
+ "canvas": "^2.11.0",
43
+ "chartjs-chart-wordcloud": "^4.1.1",
44
+ "cross-env": "^7.0.3",
45
+ "depcheck": "^1.4.3",
46
+ "jest": "^29.4.2",
47
+ "jest-environment-jsdom": "^29.4.2",
48
+ "rimraf": "^4.1.2",
49
+ "rollup": "^3.15.0",
50
+ "ts-jest": "^29.0.5",
51
+ "tslib": "^2.5.0",
52
+ "typescript": "^4.9.3",
53
+ "vite": "^4.1.0"
54
+ },
55
+ "dependencies": {
56
+ "chart.js": "^4.2.1",
57
+ "chart2music": "^1.9.0"
58
+ }
59
+ }