Repository: kyndryl-design-system/shidoka-charts
Pull request #44: feat(box + violin plot): add new plot chart variations and documentation
Author: login: wbarbee
Labels: name: released
Created: 2025-05-07T15:35:21Z | Merged: 2025-05-13T15:53:09Z

## Summary

Addition of two new chart types -- violin plot and boxplot -- using an external library [@sgratzl/chartjs-chart-boxplot](https://www.sgratzl.com/chartjs-chart-boxplot/getting-started.html).

Fixes:

1. Dark mode x-/y-axis colors and general display in dark mode
2. Small formatting issue in the Legend.mdx docs

## ADO Story or GitHub Issue Link

[AB#2251496](https://dev.azure.com/Kyndryl/f7f7ab25-06ec-43dc-902d-dee2e157cebd/_workitems/edit/2251496)

## Figma

[Boxplot + Violin Figma Link](https://www.figma.com/design/VC8A7QxPdskgsXcfw9czLO/2.2-Oreo?node-id=7065-189&m=dev)

## Checklist

- [x] Used Conventional Commit messages as outlined in the contributing guide.
  - [ ] Noted breaking changes (if any).
- [x] Documented/updated all props, events, slots, parts, etc with JSDoc.
  - [x] Ran the `analyze` command to update Storybook docs.
- [x] Added/updated Stories with controls to cover all variants.
- [x] Ran `test` locally to address any failures.
- [ ] Added/updated the Figma link for the Story's Design tab.
- [x] Added any new component exports to the src/index.ts file

## Testing Instructions

New [`Boxplot`](https://deploy-preview-44--shidoka-charts.netlify.app/?path=/docs/third-party-charts-boxplot-violin-plot-boxplot--docs) and [`Violin Plot`](https://deploy-preview-44--shidoka-charts.netlify.app/?path=/docs/third-party-charts-boxplot-violin-plot-violin-plot--docs) stories are available for testing.

## Screenshots
#### Boxplot Default (dark mode)
<img width="1103" alt="Screenshot 2025-05-08 at 9 20 36 AM" src="https://github.com/user-attachments/assets/dd78bce3-7fdb-4a89-b0a7-4d3396dfa099" />

#### Violin Default (light mode)
<img width="1103" alt="Screenshot 2025-05-08 at 9 20 44 AM" src="https://github.com/user-attachments/assets/4d019131-7181-4eb7-920d-42dcafb7ee99" />



Diff:
diff --git a/custom-elements.json b/custom-elements.json
index 4f9ab827..6ce233db 100644
--- a/custom-elements.json
+++ b/custom-elements.json
@@ -17,6 +17,84 @@
         }
       ]
     },
+    {
+      "kind": "javascript-module",
+      "path": "src/common/helpers/boxplotViolinTableRenderer.ts",
+      "declarations": [
+        {
+          "kind": "function",
+          "name": "renderBoxplotViolinTableHeader"
+        },
+        {
+          "kind": "function",
+          "name": "renderBoxplotViolinTableRow",
+          "parameters": [
+            {
+              "name": "label",
+              "type": {
+                "text": "string"
+              }
+            },
+            {
+              "name": "values",
+              "type": {
+                "text": "number[]"
+              }
+            }
+          ]
+        },
+        {
+          "kind": "function",
+          "name": "renderBoxplotViolinTable",
+          "parameters": [
+            {
+              "name": "labels",
+              "type": {
+                "text": "string[]"
+              }
+            },
+            {
+              "name": "datasets",
+              "type": {
+                "text": "any[]"
+              }
+            },
+            {
+              "name": "axisLabel",
+              "type": {
+                "text": "string"
+              }
+            }
+          ]
+        }
+      ],
+      "exports": [
+        {
+          "kind": "js",
+          "name": "renderBoxplotViolinTableHeader",
+          "declaration": {
+            "name": "renderBoxplotViolinTableHeader",
+            "module": "src/common/helpers/boxplotViolinTableRenderer.ts"
+          }
+        },
+        {
+          "kind": "js",
+          "name": "renderBoxplotViolinTableRow",
+          "declaration": {
+            "name": "renderBoxplotViolinTableRow",
+            "module": "src/common/helpers/boxplotViolinTableRenderer.ts"
+          }
+        },
+        {
+          "kind": "js",
+          "name": "renderBoxplotViolinTable",
+          "declaration": {
+            "name": "renderBoxplotViolinTable",
+            "module": "src/common/helpers/boxplotViolinTableRenderer.ts"
+          }
+        }
+      ]
+    },
     {
       "kind": "javascript-module",
       "path": "src/common/helpers/helpers.ts",
@@ -165,6 +243,30 @@
         }
       ]
     },
+    {
+      "kind": "javascript-module",
+      "path": "src/common/plugins/htmlLegendPlugin.ts",
+      "declarations": [
+        {
+          "kind": "variable",
+          "name": "htmlLegendPlugin",
+          "type": {
+            "text": "object"
+          },
+          "default": "{\n  id: 'htmlLegend',\n  afterUpdate(chart: Chart, _: any, options: HtmlLegendPluginOptions) {\n    const container = document.getElementById(options.containerId);\n    if (!container) return;\n    renderHTMLLegend(chart, container, options);\n  },\n}"
+        }
+      ],
+      "exports": [
+        {
+          "kind": "js",
+          "name": "htmlLegendPlugin",
+          "declaration": {
+            "name": "htmlLegendPlugin",
+            "module": "src/common/plugins/htmlLegendPlugin.ts"
+          }
+        }
+      ]
+    },
     {
       "kind": "javascript-module",
       "path": "src/components/chart/chart.ts",
@@ -340,6 +442,38 @@
               "description": "Customizable text labels.",
               "attribute": "customLabels"
             },
+            {
+              "kind": "field",
+              "name": "useHtmlLegend",
+              "type": {
+                "text": "boolean"
+              },
+              "default": "false",
+              "description": "Use HTML legend instead of Chart.js built-in canvas legend.",
+              "privacy": "public",
+              "attribute": "useHtmlLegend"
+            },
+            {
+              "kind": "field",
+              "name": "htmlLegendMaxHeight",
+              "type": {
+                "text": "number"
+              },
+              "default": "100",
+              "description": "Max height for HTML legend scroll container (px).",
+              "attribute": "htmlLegendMaxHeight",
+              "reflects": true
+            },
+            {
+              "kind": "field",
+              "name": "htmlLegendOptions",
+              "type": {
+                "text": "HtmlLegendOptions"
+              },
+              "default": "{}",
+              "description": "Full set of legend customization options",
+              "attribute": "htmlLegendOptions"
+            },
             {
               "kind": "field",
               "name": "_themeObserver",
@@ -353,21 +487,15 @@
               "name": "_resizeChart",
               "privacy": "private"
             },
-            {
-              "kind": "field",
-              "name": "useHtmlLegend",
-              "type": {
-                "text": "boolean"
-              },
-              "default": "false",
-              "description": "Use HTML legend instead of Chart.js built-in canvas legend.",
-              "privacy": "public",
-              "attribute": "useHtmlLegend"
-            },
             {
               "kind": "method",
               "name": "generateScrollableLegend",
-              "privacy": "private"
+              "privacy": "private",
+              "return": {
+                "type": {
+                  "text": "void"
+                }
+              }
             },
             {
               "kind": "method",
@@ -415,6 +543,44 @@
                 }
               ]
             },
+            {
+              "kind": "method",
+              "name": "exportCanvasOnly",
+              "privacy": "private",
+              "parameters": [
+                {
+                  "name": "imgFormat",
+                  "type": {
+                    "text": "string"
+                  }
+                },
+                {
+                  "name": "fileExt",
+                  "type": {
+                    "text": "string"
+                  }
+                }
+              ]
+            },
+            {
+              "kind": "method",
+              "name": "exportSimpleCanvasOnly",
+              "privacy": "private",
+              "parameters": [
+                {
+                  "name": "imgFormat",
+                  "type": {
+                    "text": "string"
+                  }
+                },
+                {
+                  "name": "fileExt",
+                  "type": {
+                    "text": "string"
+                  }
+                }
+              ]
+            },
             {
               "kind": "method",
               "name": "handleDownloadCsv",
@@ -439,6 +605,14 @@
               "privacy": "private"
             }
           ],
+          "events": [
+            {
+              "name": "on-click",
+              "type": {
+                "text": "CustomEvent"
+              }
+            }
+          ],
           "attributes": [
             {
               "name": "chartTitle",
@@ -581,6 +755,24 @@
               "default": "false",
               "description": "Use HTML legend instead of Chart.js built-in canvas legend.",
               "fieldName": "useHtmlLegend"
+            },
+            {
+              "name": "htmlLegendMaxHeight",
+              "type": {
+                "text": "number"
+              },
+              "default": "100",
+              "description": "Max height for HTML legend scroll container (px).",
+              "fieldName": "htmlLegendMaxHeight"
+            },
+            {
+              "name": "htmlLegendOptions",
+              "type": {
+                "text": "HtmlLegendOptions"
+              },
+              "default": "{}",
+              "description": "Full set of legend customization options",
+              "fieldName": "htmlLegendOptions"
             }
           ],
           "superclass": {
diff --git a/package-lock.json b/package-lock.json
index fb8b36f8..84081f61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
       "dependencies": {
         "@kyndryl-design-system/shidoka-foundation": "^2.0.0",
         "@kyndryl-design-system/shidoka-icons": "^2.0.0",
+        "@sgratzl/chartjs-chart-boxplot": "^4.4.4",
         "chart.js": "^4.4.0",
         "chartjs-adapter-date-fns": "^3.0.0",
         "chartjs-chart-geo": "^4.2.4",
@@ -5792,6 +5793,22 @@
         "semantic-release": ">=18.0.0"
       }
     },
+    "node_modules/@sgratzl/boxplots": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@sgratzl/boxplots/-/boxplots-2.0.0.tgz",
+      "integrity": "sha512-XHQTNTk0OtDEZyT7v+BKNKt4OEXYhn4GtfI+iQ1eFiFsx8Aq/csz+mOJYbfpkiP8Popd/8Nky8vLh1zzkYn0gw=="
+    },
+    "node_modules/@sgratzl/chartjs-chart-boxplot": {
+      "version": "4.4.4",
+      "resolved": "https://registry.npmjs.org/@sgratzl/chartjs-chart-boxplot/-/chartjs-chart-boxplot-4.4.4.tgz",
+      "integrity": "sha512-yenMztcbRyiBm20FxYry5HiJZPVGL/+Tc0nj3SIU5JlruthH2Awl3qSyrXUOwTzAhye2Lc/45Tw3j/UTBMLcrQ==",
+      "dependencies": {
+        "@sgratzl/boxplots": "^2.0.0"
+      },
+      "peerDependencies": {
+        "chart.js": "^4.1.1"
+      }
+    },
     "node_modules/@sideway/address": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -28957,6 +28974,19 @@
         "p-reduce": "^2.0.0"
       }
     },
+    "@sgratzl/boxplots": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@sgratzl/boxplots/-/boxplots-2.0.0.tgz",
+      "integrity": "sha512-XHQTNTk0OtDEZyT7v+BKNKt4OEXYhn4GtfI+iQ1eFiFsx8Aq/csz+mOJYbfpkiP8Popd/8Nky8vLh1zzkYn0gw=="
+    },
+    "@sgratzl/chartjs-chart-boxplot": {
+      "version": "4.4.4",
+      "resolved": "https://registry.npmjs.org/@sgratzl/chartjs-chart-boxplot/-/chartjs-chart-boxplot-4.4.4.tgz",
+      "integrity": "sha512-yenMztcbRyiBm20FxYry5HiJZPVGL/+Tc0nj3SIU5JlruthH2Awl3qSyrXUOwTzAhye2Lc/45Tw3j/UTBMLcrQ==",
+      "requires": {
+        "@sgratzl/boxplots": "^2.0.0"
+      }
+    },
     "@sideway/address": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
diff --git a/package.json b/package.json
index 900cfcb3..589f2790 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
   "dependencies": {
     "@kyndryl-design-system/shidoka-foundation": "^2.0.0",
     "@kyndryl-design-system/shidoka-icons": "^2.0.0",
+    "@sgratzl/chartjs-chart-boxplot": "^4.4.4",
     "chart.js": "^4.4.0",
     "chartjs-adapter-date-fns": "^3.0.0",
     "chartjs-chart-geo": "^4.2.4",
diff --git a/src/common/config/chartTypes/boxplot.js b/src/common/config/chartTypes/boxplot.js
new file mode 100644
index 00000000..70f98aa5
--- /dev/null
+++ b/src/common/config/chartTypes/boxplot.js
@@ -0,0 +1,108 @@
+import { getComputedColorPalette } from '../colorPalettes';
+
+export const type = 'boxplot';
+
+export const options = (ctx) => {
+  const horizontal = ctx.options.indexAxis === 'y';
+
+  const defaultOptions = {
+    scales: {
+      x: {
+        grid: { display: horizontal },
+      },
+      y: {
+        grid: { display: !horizontal },
+      },
+    },
+    plugins: {
+      legend: {
+        display: true,
+        position: 'bottom',
+      },
+      tooltip: {
+        enabled: true,
+        callbacks: {
+          title: (items) => {
+            const axisLabel = horizontal
+              ? items[0].chart.options.scales.y.title?.text
+              : items[0].chart.options.scales.x.title?.text;
+            return axisLabel
+              ? axisLabel + ': ' + items[0].label
+              : items[0].label;
+          },
+        },
+      },
+    },
+  };
+
+  return {
+    ...defaultOptions,
+    ...(ctx.options || {}),
+  };
+};
+
+export const datasetOptions = (ctx, index) => {
+  const {
+    colorPalette = 'categorical',
+    backgroundAlpha = '80',
+    borderWidth = 1,
+    outlierStyle = 'circle',
+    outlierRadius = 3,
+    outlierBorderWidth = 1,
+    itemStyle = 'circle',
+    itemRadius = 0,
+    itemBorderWidth = 0,
+    lowerBackgroundAlpha = '80',
+    upperBackgroundAlpha = '80',
+    datasetOptionsOverride = {},
+  } = ctx.options;
+
+  const palette = getComputedColorPalette(colorPalette);
+  const cycles = Math.floor(index / (palette.length - 1));
+  const idx =
+    index > palette.length - 1 ? index - (palette.length - 1) * cycles : index;
+  const color = palette[idx];
+
+  return {
+    backgroundColor: color + backgroundAlpha,
+    borderColor: color,
+    borderWidth,
+    outlierStyle,
+    outlierRadius,
+    outlierBorderWidth,
+    outlierBackgroundColor: color + backgroundAlpha,
+    outlierBorderColor: color,
+    medianColor: color,
+    meanStyle: 'circle',
+    meanRadius: 3,
+    meanBorderWidth: borderWidth,
+    meanBackgroundColor: color + backgroundAlpha,
+    meanBorderColor: color,
+    itemStyle,
+    itemRadius,
+    itemBorderWidth,
+    lowerBackgroundColor: color + lowerBackgroundAlpha,
+    upperBackgroundColor: color + upperBackgroundAlpha,
+    ...datasetOptionsOverride,
+  };
+};
+
+export const generateRandomData = (count, min, max, outliers = 0) => {
+  const values = Array.from({ length: count }, () =>
+    Math.floor(Math.random() * (max - min) + min)
+  ).sort((a, b) => a - b);
+
+  if (outliers > 0) {
+    for (let i = 0; i < outliers; i++) {
+      if (Math.random() > 0.5) {
+        values.push(max + Math.floor(Math.random() * max * 0.5));
+      } else {
+        values.unshift(
+          Math.max(0, min - Math.floor(Math.random() * min * 0.5))
+        );
+      }
+    }
+  }
+
+  return values;
+};
diff --git a/src/common/config/chartTypes/matrix.js b/src/common/config/chartTypes/matrix.js
index 0d0778db..5fe074ca 100644
--- a/src/common/config/chartTypes/matrix.js
+++ b/src/common/config/chartTypes/matrix.js
@@ -303,7 +303,7 @@ export const options = (ctx) => {
   const isDivergent = paletteKey.toLowerCase().includes('divergent');
   const computedNeutral = isDivergent ? 0 : 50;
   const legendEnabled = ctx.options?.plugins?.gradientLegend?.display;
-  const legendPadding = legendEnabled ? { bottom: 30 } : { bottom: 0 };
+  const legendPadding = legendEnabled ? { bottom: 35 } : { bottom: 0 };
 
   return {
     responsive: false,
diff --git a/src/common/config/chartTypes/violin.js b/src/common/config/chartTypes/violin.js
new file mode 100644
index 00000000..e16e64c4
--- /dev/null
+++ b/src/common/config/chartTypes/violin.js
@@ -0,0 +1,104 @@
+import { getComputedColorPalette } from '../colorPalettes';
+
+export const type = 'violin';
+
+export const options = (ctx) => {
+  const horizontal = ctx.options.indexAxis === 'y';
+
+  const defaultOptions = {
+    scales: {
+      x: {
+        grid: { display: horizontal },
+      },
+      y: {
+        grid: { display: !horizontal },
+      },
+    },
+    plugins: {
+      legend: {
+        display: true,
+        position: 'bottom',
+      },
+      tooltip: {
+        enabled: true,
+        callbacks: {
+          title: (items) => {
+            const axisLabel = horizontal
+              ? items[0].chart.options.scales.y.title?.text
+              : items[0].chart.options.scales.x.title?.text;
+            return axisLabel
+              ? axisLabel + ': ' + items[0].label
+              : items[0].label;
+          },
+        },
+      },
+    },
+  };
+  return {
+    ...defaultOptions,
+    ...(ctx.options || {}),
+  };
+};
+
+export const datasetOptions = (ctx, index) => {
+  const {
+    colorPalette = 'categorical',
+    backgroundAlpha = '95',
+    borderWidth = 1,
+    outlierStyle = 'circle',
+    outlierRadius = 3,
+    outlierBorderWidth = 1,
+    meanStyle = 'circle',
+    meanRadius = 3,
+    meanBorderWidth = 1,
+    pointCount = 100,
+    violinWidth = 0.8,
+    datasetOptionsOverride = {},
+  } = ctx.options;
+
+  const palette = getComputedColorPalette(colorPalette);
+  const cycles = Math.floor(index / (palette.length - 1));
+  const idx =
+    index > palette.length - 1 ? index - (palette.length - 1) * cycles : index;
+  const color = palette[idx];
+
+  return {
+    backgroundColor: color + backgroundAlpha,
+    borderColor: color,
+    borderWidth,
+    outlierStyle,
+    outlierRadius,
+    outlierBorderWidth,
+    outlierBackgroundColor: color + backgroundAlpha,
+    outlierBorderColor: color,
+    medianColor: color,
+    meanStyle,
+    meanRadius,
+    meanBorderWidth,
+    meanBackgroundColor: color + backgroundAlpha,
+    meanBorderColor: color,
+    points: pointCount,
+    width: violinWidth,
+    ...datasetOptionsOverride,
+  };
+};
+
+export const generateRandomData = (count, min, max, outliers = 0) => {
+  const values = Array.from({ length: count }, () =>
+    Math.floor(Math.random() * (max - min) + min)
+  ).sort((a, b) => a - b);
+
+  if (outliers > 0) {
+    for (let i = 0; i < outliers; i++) {
+      if (Math.random() > 0.5) {
+        values.push(max + Math.floor(Math.random() * max * 0.5));
+      } else {
+        values.unshift(
+          Math.max(0, min - Math.floor(Math.random() * min * 0.5))
+        );
+      }
+    }
+  }
+
+  return values;
+};
diff --git a/src/common/config/globalOptions.js b/src/common/config/globalOptions.js
index d79e4d94..a4fb6e48 100644
--- a/src/common/config/globalOptions.js
+++ b/src/common/config/globalOptions.js
@@ -1,6 +1,7 @@
 import { getTokenThemeVal } from '@kyndryl-design-system/shidoka-foundation/common/helpers/color';
 
 const defaultConfig = (ctx) => {
+  const PrimaryTextColor = getTokenThemeVal('--kd-color-text-level-primary');
   const SecondaryTextColor = getTokenThemeVal(
     '--kd-color-text-level-secondary'
   );
@@ -21,6 +22,7 @@ const defaultConfig = (ctx) => {
         display: false,
         position: 'bottom',
         labels: {
+          color: PrimaryTextColor,
           boxWidth: 16,
           boxHeight: 16,
           borderRadius: 2,
@@ -73,7 +75,7 @@ export default defaultConfig;
  * @param legend - The `legend` parameter is the legend object of a chart. It contains information
  * about the chart's legend, such as the labels and colors of the legend items.
  */
-const handleLegendHover = (e, item, legend) => {
+export const handleLegendHover = (e, item, legend) => {
   const DatasetIndex = item.datasetIndex || 0;
   const Datasets = legend.chart.data.datasets;
   const Dataset = Datasets[DatasetIndex];
@@ -120,7 +122,7 @@ const handleLegendHover = (e, item, legend) => {
  * @param legend - The `legend` parameter is the legend object of a chart. It contains information
  * about the chart's legend, such as the labels and colors of the legend items.
  */
-const handleLegendLeave = (e, item, legend) => {
+export const handleLegendLeave = (e, item, legend) => {
   const DatasetIndex = item.datasetIndex || 0;
   const Datasets = legend.chart.data.datasets;
   const Dataset = Datasets[DatasetIndex];
diff --git a/src/common/config/globalOptionsNonRadial.js b/src/common/config/globalOptionsNonRadial.js
index c1bf717d..5abde3b1 100644
--- a/src/common/config/globalOptionsNonRadial.js
+++ b/src/common/config/globalOptionsNonRadial.js
@@ -2,6 +2,7 @@ import { getTokenThemeVal } from '@kyndryl-design-system/shidoka-foundation/comm
 
 const defaultConfig = (ctx) => {
   const GridLinesColor = getTokenThemeVal('--kd-color-border-variants-light');
+  const AxisTextColor = getTokenThemeVal('--kd-color-text-level-primary');
 
   const MultiAxis =
     ctx.options.scales && Object.keys(ctx.options.scales).length > 2;
@@ -13,10 +14,15 @@ const defaultConfig = (ctx) => {
     },
     ticks: {
       padding: 8,
+      color: AxisTextColor,
     },
     border: {
       display: false,
     },
+    title: {
+      color: AxisTextColor,
+      display: true,
+    },
   };
 
   const options = {
diff --git a/src/common/config/globalOptionsRadial.js b/src/common/config/globalOptionsRadial.js
index d172d279..f5e62d9f 100644
--- a/src/common/config/globalOptionsRadial.js
+++ b/src/common/config/globalOptionsRadial.js
@@ -2,40 +2,47 @@ import { getTokenThemeVal } from '@kyndryl-design-system/shidoka-foundation/comm
 
 const defaultConfig = (ctx) => {
   const ArcColor = getTokenThemeVal('--kd-color-background-page-default');
+  const AxisTextColor = getTokenThemeVal('--kd-color-text-level-primary');
+
+  const scales = {
+    x: {
+      title: { text: 'X Axis', color: AxisTextColor },
+      grid: { display: false },
+      ticks: { display: false, color: AxisTextColor },
+      border: { display: false },
+    },
+    y: {
+      title: { text: 'Y Axis', color: AxisTextColor },
+      grid: { display: false },
+      ticks: { display: false, color: AxisTextColor },
+      border: { display: false },
+    },
+  };
+
+  const chartType = ctx.type || ctx.chart?.config.type;
+  if (chartType === 'radar' || chartType === 'polarArea') {
+    scales.r = {
+      grid: { display: true },
+      angleLines: { display: true },
+      ticks: { color: AxisTextColor },
+      pointLabels: { color: AxisTextColor },
+    };
+  }
 
   return {
     elements: {
-      arc: {
-        borderColor: ArcColor,
-      },
-    },
-    scales: {
-      x: {
-        title: { text: 'X Axis ' },
-        grid: { display: false },
-        ticks: { display: false },
-        border: { display: false },
-      },
-      y: {
-        title: { text: 'Y Axis ' },
-        grid: { display: false },
-        ticks: { display: false },
-        border: { display: false },
-      },
+      arc: { borderColor: ArcColor },
     },
+    scales,
     plugins: {
       tooltip: {
         callbacks: {
-          labelColor: function (context) {
-            const PerDatapointColors = Array.isArray(
-              context.dataset.backgroundColor
-            );
-
+          labelColor(context) {
+            const bg = context.dataset.backgroundColor;
+            const perPoint = Array.isArray(bg);
             return {
               borderColor: context.dataset.borderColor,
-              backgroundColor: PerDatapointColors
-                ? context.dataset.backgroundColor[context.dataIndex]
-                : context.dataset.backgroundColor,
+              backgroundColor: perPoint ? bg[context.dataIndex] : bg,
               borderRadius: 2,
             };
           },
diff --git a/src/common/helpers/boxplotViolinTableRenderer.ts b/src/common/helpers/boxplotViolinTableRenderer.ts
new file mode 100644
index 00000000..155484d2
--- /dev/null
+++ b/src/common/helpers/boxplotViolinTableRenderer.ts
@@ -0,0 +1,193 @@
+import { html } from 'lit';
+
+export function renderBoxplotViolinTableHeader() {
+  return html`
+    <tr>
+      <th>Group</th>
+      <th>Minimum</th>
+      <th>Q1</th>
+      <th>Median</th>
+      <th>Q3</th>
+      <th>Maximum</th>
+      <th>IQR</th>
+      <th>Outlier(s)</th>
+    </tr>
+  `;
+}
+
+export function renderBoxplotViolinTableRow(label: string, values: number[]) {
+  if (!values || !values.length) {
+    return html`
+      <tr>
+        <td>${label}</td>
+        <td>–</td>
+        <td>–</td>
+        <td>–</td>
+        <td>–</td>
+        <td>–</td>
+        <td>–</td>
+        <td>–</td>
+      </tr>
+    `;
+  }
+
+  const sortedValues = [...values].sort((a, b) => a - b);
+  const len = sortedValues.length;
+
+  const min = sortedValues[0];
+  const max = sortedValues[len - 1];
+  const median =
+    len % 2 === 0
+      ? (sortedValues[len / 2 - 1] + sortedValues[len / 2]) / 2
+      : sortedValues[Math.floor(len / 2)];
+
+  const lowerHalf = sortedValues.slice(0, Math.floor(len / 2));
+  const upperHalf =
+    len % 2 === 0
+      ? sortedValues.slice(len / 2)
+      : sortedValues.slice(Math.floor(len / 2) + 1);
+
+  const q1 =
+    lowerHalf.length % 2 === 0
+      ? (lowerHalf[lowerHalf.length / 2 - 1] +
+          lowerHalf[lowerHalf.length / 2]) /
+        2
+      : lowerHalf[Math.floor(lowerHalf.length / 2)];
+
+  const q3 =
+    upperHalf.length % 2 === 0
+      ? (upperHalf[upperHalf.length / 2 - 1] +
+          upperHalf[upperHalf.length / 2]) /
+        2
+      : upperHalf[Math.floor(upperHalf.length / 2)];
+
+  const iqr = q3 - q1;
+  const lowerBound = q1 - 1.5 * iqr;
+  const upperBound = q3 + 1.5 * iqr;
+  const outliers = sortedValues.filter(
+    (value) => value < lowerBound || value > upperBound
+  );
+
+  return html`
+    <tr>
+      <td>${label}</td>
+      <td>${min}</td>
+      <td>${q1}</td>
+      <td>${median}</td>
+      <td>${q3}</td>
+      <td>${max}</td>
+      <td>${iqr.toFixed(2)}</td>
+      <td>${outliers.length ? outliers.join(', ') : '–'}</td>
+    </tr>
+  `;
+}
+
+export function renderBoxplotViolinTable(
+  labels: string[],
+  datasets: any[],
+  axisLabel: string
+) {
+  if (!datasets || !datasets.length || !labels || !labels.length) {
+    return html`
+      <thead>
+        <tr>
+          <th>No data available</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>No data available for display</td>
+        </tr>
+      </tbody>
+    `;
+  }
+
+  return html`
+    <thead>
+      <tr>
+        <th>${axisLabel}</th>
+        ${datasets.map(
+          (dataset) => html`
+            <th>${dataset.label || 'Dataset'} (Min)</th>
+            <th>${dataset.label || 'Dataset'} (Q1)</th>
+            <th>${dataset.label || 'Dataset'} (Median)</th>
+            <th>${dataset.label || 'Dataset'} (Q3)</th>
+            <th>${dataset.label || 'Dataset'} (Max)</th>
+            <th>${dataset.label || 'Dataset'} (IQR)</th>
+            <th>${dataset.label || 'Dataset'} (Outlier(s))</th>
+          `
+        )}
+      </tr>
+    </thead>
+    <tbody>
+      ${labels.map(
+        (label, i) => html`
+          <tr>
+            <td>${label}</td>
+            ${datasets.map((dataset) => {
+              const values = dataset.data[i];
+              if (!values || !values.length) {
+                return html`
+                  <td>-</td>
+                  <td>-</td>
+                  <td>-</td>
+                  <td>-</td>
+                  <td>-</td>
+                  <td>-</td>
+                  <td>-</td>
+                `;
+              }
+
+              const sortedValues = [...values].sort((a, b) => a - b);
+              const len = sortedValues.length;
+
+              const min = sortedValues[0];
+              const max = sortedValues[len - 1];
+              const median =
+                len % 2 === 0
+                  ? (sortedValues[len / 2 - 1] + sortedValues[len / 2]) / 2
+                  : sortedValues[Math.floor(len / 2)];
+
+              const lowerHalf = sortedValues.slice(0, Math.floor(len / 2));
+              const upperHalf =
+                len % 2 === 0
+                  ? sortedValues.slice(len / 2)
+                  : sortedValues.slice(Math.floor(len / 2) + 1);
+
+              const q1 =
+                lowerHalf.length % 2 === 0
+                  ? (lowerHalf[lowerHalf.length / 2 - 1] +
+                      lowerHalf[lowerHalf.length / 2]) /
+                    2
+                  : lowerHalf[Math.floor(lowerHalf.length / 2)];
+
+              const q3 =
+                upperHalf.length % 2 === 0
+                  ? (upperHalf[upperHalf.length / 2 - 1] +
+                      upperHalf[upperHalf.length / 2]) /
+                    2
+                  : upperHalf[Math.floor(upperHalf.length / 2)];
+
+              const iqr = q3 - q1;
+              const lowerBound = q1 - 1.5 * iqr;
+              const upperBound = q3 + 1.5 * iqr;
+              const outliers = sortedValues.filter(
+                (value) => value < lowerBound || value > upperBound
+              );
+
+              return html`
+                <td>${min}</td>
+                <td>${q1}</td>
+                <td>${median}</td>
+                <td>${q3}</td>
+                <td>${max}</td>
+                <td>${iqr.toFixed(2)}</td>
+                <td>${outliers.length ? outliers.join(', ') : '–'}</td>
+              `;
+            })}
+          </tr>
+        `
+      )}
+    </tbody>
+  `;
+}
diff --git a/src/components/chart/chart.ts b/src/components/chart/chart.ts
index 21f5a1bb..032b1f57 100644
--- a/src/components/chart/chart.ts
+++ b/src/components/chart/chart.ts
@@ -14,6 +14,12 @@ import {
 } from 'chartjs-chart-geo';
 import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
 import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
+import {
+  BoxPlotController,
+  BoxAndWiskers,
+  ViolinController,
+  Violin,
+} from '@sgratzl/chartjs-chart-boxplot';
 import canvasBackgroundPlugin from '../../common/plugins/canvasBackground';
 import doughnutLabelPlugin from '../../common/plugins/doughnutLabel';
 import meterGaugePlugin from '../../common/plugins/meterGaugeNeedle';
@@ -24,6 +30,7 @@ import a11yPlugin from 'chartjs-plugin-a11y-legend';
 import datalabelsPlugin from 'chartjs-plugin-datalabels';
 import annotationPlugin from 'chartjs-plugin-annotation';
 import { convertChartDataToCSV, debounce } from '../../common/helpers/helpers';
+import { renderBoxplotViolinTable } from '../../common/helpers/boxplotViolinTableRenderer';
 import ChartScss from './chart.scss';
 import globalOptions from '../../common/config/globalOptions';
 import globalOptionsNonRadial from '../../common/config/globalOptionsNonRadial';
@@ -47,6 +54,10 @@ Chart.register(
   TreemapElement,
   MatrixController,
   MatrixElement,
+  BoxPlotController,
+  BoxAndWiskers,
+  ViolinController,
+  Violin,
   annotationPlugin,
   datalabelsPlugin
 );
@@ -459,6 +470,12 @@ export class KDChart extends LitElement {
                           })}
                         </tbody>
                       `
+                    : ['boxplot', 'violin'].includes(this.type)
+                    ? renderBoxplotViolinTable(
+                        this.labels,
+                        this.datasets,
+                        this.getTableAxisLabel()
+                      )
                     : html`
                         <thead>
                           <tr>
diff --git a/src/stories/Boxplot.stories.js b/src/stories/Boxplot.stories.js
new file mode 100644
index 00000000..c1cbbcdb
--- /dev/null
+++ b/src/stories/Boxplot.stories.js
@@ -0,0 +1,261 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+/**
+ * Boxplot chart type is available through the integration of the
+ * [@sgratzl/chartjs-chart-boxplot](https://www.sgratzl.com/chartjs-chart-boxplot/getting-started.html) package.
+ *
+ * For detailed documentation on the availble, configurable options, refer to the [plugin's documentation](https://www.sgratzl.com/chartjs-chart-boxplot/api/interfaces/IBoxAndWhiskersOptions.html).
+ */
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+  },
+};
+
+const singleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+];
+
+const doubleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+  {
+    label: 'Dataset 2',
+    data: [
+      generateRandomData(30, 20, 120, 1),
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 110, 3),
+      generateRandomData(30, 40, 80, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 70, 0),
+    ],
+  },
+];
+
+const baseArgs = {
+  chartTitle: 'Boxplot Example',
+  description: 'Boxplot chart with two datasets',
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  datasets: doubleDataset,
+  colorPalette: 'categorical',
+  hideDescription: false,
+  hideCaptions: false,
+  hideHeader: false,
+  hideControls: false,
+  noBorder: false,
+  width: null,
+  height: null,
+  options: {
+    scales: {
+      x: {
+        title: { display: true, text: 'Categories' },
+      },
+      y: {
+        title: { display: true, text: 'Values' },
+      },
+    },
+  },
+};
+
+export const Default = {
+  args: baseArgs,
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
+          colorPalette: args.colorPalette,
+          plugins: {
+            legend: { display: true },
+            tooltip: { enabled: true },
+          },
+          scales: args.options?.scales || {
+            x: {
+              title: { display: true, text: 'Categories' },
+            },
+            y: {
+              title: { display: true, text: 'Values' },
+            },
+          },
+        }}
+        ?hideDescription=${args.hideDescription}
+        ?hideCaptions=${args.hideCaptions}
+        ?hideHeader=${args.hideHeader}
+        ?hideControls=${args.hideControls}
+        ?noBorder=${args.noBorder}
+        .width=${args.width}
+        .height=${args.height}
+      ></kd-chart>
+    `;
+  },
+};
+
+export const Horizontal = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Horizontal Boxplot Example',
+    description: 'Horizontal boxplot chart with two datasets',
+    options: {
+      scales: {
+        x: {
+          title: { display: true, text: 'Values' },
+        },
+        y: {
+          title: { display: true, text: 'Categories' },
+        },
+      },
+    },
+  },
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
+          colorPalette: args.colorPalette,
+          indexAxis: 'y',
+          plugins: {
+            legend: { display: true },
+            tooltip: { enabled: true },
+          },
+          scales: args.options?.scales || {
+            x: {
+              title: { display: true, text: 'Values' },
+            },
+            y: {
+              title: { display: true, text: 'Categories' },
+            },
+          },
+        }}
+        ?hideDescription=${args.hideDescription}
+        ?hideCaptions=${args.hideCaptions}
+        ?hideHeader=${args.hideHeader}
+        ?hideControls=${args.hideControls}
+        ?noBorder=${args.noBorder}
+        .width=${args.width}
+        .height=${args.height}
+      ></kd-chart>
+    `;
+  },
+};
+
+export const VerticalSingleDataset = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Vertical Boxplot Single Dataset Example',
+    description: 'Vertical boxplot chart with single dataset',
+    colorPalette: 'sequential01',
+    datasets: singleDataset,
+  },
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
+          colorPalette: args.colorPalette,
+          plugins: {
+            legend: { display: true },
+            tooltip: { enabled: true },
+          },
+          scales: args.options?.scales || {
+            x: {
+              title: { display: true, text: 'Categories' },
+            },
+            y: {
+              title: { display: true, text: 'Values' },
+            },
+          },
+        }}
+        ?hideDescription=${args.hideDescription}
+        ?hideCaptions=${args.hideCaptions}
+        ?hideHeader=${args.hideHeader}
+        ?hideControls=${args.hideControls}
+        ?noBorder=${args.noBorder}
+        .width=${args.width}
+        .height=${args.height}
+      ></kd-chart>
+    `;
+  },
+};
+
+export const WithoutLegend = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Boxplot Without Legend',
+    description: 'Boxplot chart with legend hidden',
+    colorPalette: 'rag03',
+  },
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
+          colorPalette: args.colorPalette,
+          plugins: {
+            legend: { display: false },
+            tooltip: { enabled: true },
+          },
+          scales: args.options?.scales || {
+            x: {
+              title: { display: true, text: 'Categories' },
+            },
+            y: {
+              title: { display: true, text: 'Values' },
+            },
+          },
+        }}
+        ?hideDescription=${args.hideDescription}
+        ?hideCaptions=${args.hideCaptions}
+        ?hideHeader=${args.hideHeader}
+        ?hideControls=${args.hideControls}
+        ?noBorder=${args.noBorder}
+        .width=${args.width}
+        .height=${args.height}
+      ></kd-chart>
+    `;
+  },
+};
diff --git a/src/stories/Legend.mdx b/src/stories/Legend.mdx
index 6fdc0b0d..279a10b8 100644
--- a/src/stories/Legend.mdx
+++ b/src/stories/Legend.mdx
@@ -35,22 +35,20 @@ This uses the default Chart.js canvas-rendered legend:
 
 The HTML legend utilizes the [chartjs HTML Legend](https://www.chartjs.org/docs/latest/samples/legend/html.html) and is enabled by setting the `useHtmlLegend` attribute to `true`. This renders the legend as a separate HTML element outside the canvas, allowing for more customization and styling options.
 
-````html
-
 > ⚠️ **Warning:** Implementing the HTML legend should be done with caution. Depending on your implementation approach, it may lead to breakages or inconsistencies in your chart's appearance or behavior. Always test thoroughly across different browsers and with various data configurations to ensure reliability.
 
 > **PNG/JPG Considerations:** Only the `<canvas>` itself is captured in PNG/JPG exports. Any HTML legend rendered outside the `<canvas>` will not appear in the exported image.
 >
 > **Layout Considerations:** Using the HTML legend changes the overall chart aspect ratio since it exists separate from the canvas. The library now includes automatic adjustments to maintain consistent chart dimensions between HTML and canvas legends. This behavior can be controlled with the following options:
->
-> ```javascript
-> plugins: {
->   htmlLegend: {
->     adjustChartHeight: true,  // enable/disable automatic height adjustment
->     reservedLegendHeight: 40, // base height reserved for legend (px)
->   }
-> }
-> ```
+
+```javascript
+plugins: {
+  htmlLegend: {
+    adjustChartHeight: true,  // enable/disable automatic height adjustment
+    reservedLegendHeight: 40, // base height reserved for legend (px)
+  }
+}
+```
 
 This uses an enhanced HTML-based legend with better styling and scrollability:
 
@@ -70,7 +68,7 @@ This uses an enhanced HTML-based legend with better styling and scrollability:
     }
   }}
 ></kd-chart>
-````
+```
 
 The HTML legend can be configured using the `plugins.htmlLegend` options in the chart configuration:
 
diff --git a/src/stories/Violin.stories.js b/src/stories/Violin.stories.js
new file mode 100644
index 00000000..be0f0e2f
--- /dev/null
+++ b/src/stories/Violin.stories.js
@@ -0,0 +1,253 @@
+import { html } from 'lit';
+import '../components/chart';
+import { generateRandomData } from '../common/config/chartTypes/violin';
+import argTypes from '../common/config/chartArgTypes';
+
+/**
+ * Violin plot chart type is available through the integration of the
+ * [@sgratzl/chartjs-chart-boxplot](https://www.sgratzl.com/chartjs-chart-boxplot/getting-started.html) package.
+ *
+ * For detailed documentation on the availble, configurable options, refer to the [plugin's documentation](https://github.com/sgratzl/chartjs-chart-boxplot/blob/f9f20dad666f1ceac89bccffe38908fdfb9a9146/src/elements/Violin.ts#L12).
+ *
+ */
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Violin Plot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+  },
+};
+
+const singleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+];
+
+const doubleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+  {
+    label: 'Dataset 2',
+    data: [
+      generateRandomData(30, 20, 120, 1),
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 110, 3),
+      generateRandomData(30, 40, 80, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 70, 0),
+    ],
+  },
+];
+
+const baseArgs = {
+  chartTitle: 'Violin Plot Example',
+  description: 'Violin plot chart with two datasets',
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  datasets: doubleDataset,
+  colorPalette: 'categorical',
+  hideDescription: false,
+  hideCaptions: false,
+  hideHeader: false,
+  hideControls: false,
+  noBorder: false,
+  width: null,
+  height: null,
+  options: {
+    scales: {
+      x: {
+        title: { display: true, text: 'Categories' },
+      },
+      y: {
+        title: { display: true, text: 'Values' },
+      },
+    },
+  },
+};
+
+export const Default = {
+  args: baseArgs,
+  render: (args) => html`
+    <kd-chart
+      type="violin"
+      .chartTitle=${args.chartTitle}
+      .description=${args.description}
+      .labels=${args.labels}
+      .datasets=${args.datasets}
+      .options=${{
+        colorPalette: args.colorPalette,
+        plugins: {
+          legend: { display: true },
+          tooltip: { enabled: true },
+        },
+        scales: args.options?.scales || {
+          x: {
+            title: { display: true, text: 'Categories' },
+          },
+          y: {
+            title: { display: true, text: 'Values' },
+          },
+        },
+      }}
+      ?hideDescription=${args.hideDescription}
+      ?hideCaptions=${args.hideCaptions}
+      ?hideHeader=${args.hideHeader}
+      ?hideControls=${args.hideControls}
+      ?noBorder=${args.noBorder}
+      .width=${args.width}
+      .height=${args.height}
+    ></kd-chart>
+  `,
+};
+
+export const Horizontal = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Horizontal Violin Plot Example',
+    description: 'Horizontal violin plot chart with two datasets',
+    options: {
+      scales: {
+        x: {
+          title: { display: true, text: 'Values' },
+        },
+        y: {
+          title: { display: true, text: 'Categories' },
+        },
+      },
+    },
+  },
+  render: (args) => html`
+    <kd-chart
+      type="violin"
+      .chartTitle=${args.chartTitle}
+      .description=${args.description}
+      .labels=${args.labels}
+      .datasets=${args.datasets}
+      .options=${{
+        colorPalette: args.colorPalette,
+        indexAxis: 'y',
+        plugins: {
+          legend: { display: true },
+          tooltip: { enabled: true },
+        },
+        scales: args.options?.scales || {
+          x: {
+            title: { display: true, text: 'Values' },
+          },
+          y: {
+            title: { display: true, text: 'Categories' },
+          },
+        },
+      }}
+      ?hideDescription=${args.hideDescription}
+      ?hideCaptions=${args.hideCaptions}
+      ?hideHeader=${args.hideHeader}
+      ?hideControls=${args.hideControls}
+      ?noBorder=${args.noBorder}
+      .width=${args.width}
+      .height=${args.height}
+    ></kd-chart>
+  `,
+};
+
+export const VerticalSingleDataset = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Vertical Violin Plot Single Dataset Example',
+    description: 'Vertical violin plot chart with single dataset',
+    colorPalette: 'sequential04',
+    datasets: singleDataset,
+  },
+  render: (args) => html`
+    <kd-chart
+      type="violin"
+      .chartTitle=${args.chartTitle}
+      .description=${args.description}
+      .labels=${args.labels}
+      .datasets=${args.datasets}
+      .options=${{
+        colorPalette: args.colorPalette,
+        plugins: {
+          legend: { display: true },
+          tooltip: { enabled: true },
+        },
+        scales: args.options?.scales || {
+          x: {
+            title: { display: true, text: 'Categories' },
+          },
+          y: {
+            title: { display: true, text: 'Values' },
+          },
+        },
+      }}
+      ?hideDescription=${args.hideDescription}
+      ?hideCaptions=${args.hideCaptions}
+      ?hideHeader=${args.hideHeader}
+      ?hideControls=${args.hideControls}
+      ?noBorder=${args.noBorder}
+      .width=${args.width}
+      .height=${args.height}
+    ></kd-chart>
+  `,
+};
+
+export const WithoutLegend = {
+  args: {
+    ...baseArgs,
+    chartTitle: 'Violin Plot Without Legend',
+    description: 'Violin plot chart with legend hidden',
+    colorPalette: 'categorical',
+  },
+  render: (args) => html`
+    <kd-chart
+      type="violin"
+      .chartTitle=${args.chartTitle}
+      .description=${args.description}
+      .labels=${args.labels}
+      .datasets=${args.datasets}
+      .options=${{
+        colorPalette: args.colorPalette,
+        plugins: {
+          legend: { display: false },
+          tooltip: { enabled: true },
+        },
+        scales: args.options?.scales || {
+          x: {
+            title: { display: true, text: 'Categories' },
+          },
+          y: {
+            title: { display: true, text: 'Values' },
+          },
+        },
+      }}
+      ?hideDescription=${args.hideDescription}
+      ?hideCaptions=${args.hideCaptions}
+      ?hideHeader=${args.hideHeader}
+      ?hideControls=${args.hideControls}
+      ?noBorder=${args.noBorder}
+      .width=${args.width}
+      .height=${args.height}
+    ></kd-chart>
+  `,
+};


Reviews:
user: login: brian-patrick-kyndryl
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T13:40:09Z
user: login: wbarbee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:23:40Z
user: login: wbarbee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:24:01Z
user: login: wbarbee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:24:29Z
user: login: brian-patrick-kyndryl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T17:57:06Z
user: login: brian-patrick-kyndryl
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T18:00:29Z
user: login: wbarbee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:21:26Z
user: login: kumarivijeta
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T05:30:05Z

Review comments:
path: src/stories/BoxplotViolinPlot.mdx
user: login: brian-patrick-kyndryl
body: Seems like there's a lot of documentation added here that could really just be linked out to the boxplot plugin docs page.
created_at: 2025-05-08T13:35:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,179 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+    chartOrientation: {
+      options: ['vertical', 'horizontal'],
+      control: { type: 'select' },
+      defaultValue: 'vertical',
+    },
+    showLegend: {
path: src/stories/Boxplot.stories.js
user: login: brian-patrick-kyndryl
body: 1. Most other chart types show the legend by default, we should keep it consistent.
2. I don't think we should be creating controls for specific chart.js options like this (and orientation another example), it could be misleading for devs who think it's a prop we offer. Just show it as a separate story and use the `options` prop.
created_at: 2025-05-08T13:37:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,179 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+    chartOrientation: {
+      options: ['vertical', 'horizontal'],
+      control: { type: 'select' },
+      defaultValue: 'vertical',
+    },
+    showLegend: {
+      control: { type: 'boolean' },
+      defaultValue: false,
+    },
+    showTooltip: {
+      control: { type: 'boolean' },
+      defaultValue: true,
+    },
+    xAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Categories',
+    },
+    yAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Values',
+    },
+    xAxisMin: {
+      control: { type: 'number' },
+    },
+    xAxisMax: {
+      control: { type: 'number' },
+    },
+    yAxisMin: {
+      control: { type: 'number' },
+    },
+    yAxisMax: {
+      control: { type: 'number' },
+    },
+  },
+};
+
+const singleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+];
+
+const doubleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+  {
+    label: 'Dataset 2',
+    data: [
+      generateRandomData(30, 20, 120, 1),
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 110, 3),
+      generateRandomData(30, 40, 80, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 70, 0),
+    ],
+  },
+];
+
+const args = {
+  chartTitle: 'Boxplot Example',
+  description: 'Boxplot chart with two datasets',
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  datasets: doubleDataset,
+  chartOrientation: 'vertical',
+  colorPalette: 'categorical',
+  showLegend: false,
+  showTooltip: true,
+  xAxisTitle: 'Categories',
+  yAxisTitle: 'Values',
+  xAxisMin: null,
+  xAxisMax: null,
+  yAxisMin: null,
+  yAxisMax: null,
+  hideDescription: false,
+  hideCaptions: false,
+  hideHeader: false,
+  hideControls: false,
+  noBorder: false,
+  width: null,
+  height: null,
+};
+
+export const Default = {
+  args,
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
path: src/stories/Boxplot.stories.js
user: login: brian-patrick-kyndryl
body: Include the `scales` (axis titles) in the options, should fix the table view error, but also might need a null check?
created_at: 2025-05-08T13:39:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,179 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+    chartOrientation: {
+      options: ['vertical', 'horizontal'],
+      control: { type: 'select' },
+      defaultValue: 'vertical',
+    },
+    showLegend: {
+      control: { type: 'boolean' },
+      defaultValue: false,
+    },
+    showTooltip: {
+      control: { type: 'boolean' },
+      defaultValue: true,
+    },
+    xAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Categories',
+    },
+    yAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Values',
+    },
+    xAxisMin: {
+      control: { type: 'number' },
+    },
+    xAxisMax: {
+      control: { type: 'number' },
+    },
+    yAxisMin: {
+      control: { type: 'number' },
+    },
+    yAxisMax: {
+      control: { type: 'number' },
+    },
+  },
+};
+
+const singleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+];
+
+const doubleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+  {
+    label: 'Dataset 2',
+    data: [
+      generateRandomData(30, 20, 120, 1),
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 110, 3),
+      generateRandomData(30, 40, 80, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 70, 0),
+    ],
+  },
+];
+
+const args = {
+  chartTitle: 'Boxplot Example',
+  description: 'Boxplot chart with two datasets',
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  datasets: doubleDataset,
+  chartOrientation: 'vertical',
+  colorPalette: 'categorical',
+  showLegend: false,
+  showTooltip: true,
+  xAxisTitle: 'Categories',
+  yAxisTitle: 'Values',
+  xAxisMin: null,
+  xAxisMax: null,
+  yAxisMin: null,
+  yAxisMax: null,
+  hideDescription: false,
+  hideCaptions: false,
+  hideHeader: false,
+  hideControls: false,
+  noBorder: false,
+  width: null,
+  height: null,
+};
+
+export const Default = {
+  args,
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
path: src/stories/Boxplot.stories.js
user: login: wbarbee
body: i had a peek at how a certain other design system was handling table view in boxplot and it was something like the following implementation:
<img width="1102" alt="Screenshot 2025-05-08 at 9 22 00 AM" src="https://github.com/user-attachments/assets/051a3053-c00f-4ff7-965b-c9fa92cb24ce" />

So i made a new `boxplotViolinTableRenderer.ts` that is used in `chart.js`, detecting chart type boxplot or violin and rendering this special, dynamic table. i added the scales in as well
created_at: 2025-05-08T16:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+    chartOrientation: {
+      options: ['vertical', 'horizontal'],
+      control: { type: 'select' },
+      defaultValue: 'vertical',
+    },
+    showLegend: {
path: src/stories/Boxplot.stories.js
user: login: wbarbee
body: ✅
created_at: 2025-05-08T16:24:01Z
author_association: CONTRIBUTOR
path: src/stories/BoxplotViolinPlot.mdx
user: login: wbarbee
body: i removed that extra documentation and instead point to the relevant files within in the individual boxplot and violin 'Docs' pages
created_at: 2025-05-08T16:24:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+export default {
+  title: 'Third Party Charts/Boxplot & Violin Plot/Boxplot',
+  component: 'kd-chart',
+  argTypes: {
+    ...argTypes,
+    chartOrientation: {
+      options: ['vertical', 'horizontal'],
+      control: { type: 'select' },
+      defaultValue: 'vertical',
+    },
+    showLegend: {
+      control: { type: 'boolean' },
+      defaultValue: false,
+    },
+    showTooltip: {
+      control: { type: 'boolean' },
+      defaultValue: true,
+    },
+    xAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Categories',
+    },
+    yAxisTitle: {
+      control: { type: 'text' },
+      defaultValue: 'Values',
+    },
+    xAxisMin: {
+      control: { type: 'number' },
+    },
+    xAxisMax: {
+      control: { type: 'number' },
+    },
+    yAxisMin: {
+      control: { type: 'number' },
+    },
+    yAxisMax: {
+      control: { type: 'number' },
+    },
+  },
+};
+
+const singleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+];
+
+const doubleDataset = [
+  {
+    label: 'Dataset 1',
+    data: [
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 80, 1),
+      generateRandomData(30, 30, 120, 3),
+      generateRandomData(30, 40, 90, 2),
+      generateRandomData(30, 20, 70, 1),
+      generateRandomData(30, 10, 60, 2),
+      generateRandomData(30, 30, 80, 1),
+    ],
+  },
+  {
+    label: 'Dataset 2',
+    data: [
+      generateRandomData(30, 20, 120, 1),
+      generateRandomData(30, 10, 100, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 110, 3),
+      generateRandomData(30, 40, 80, 2),
+      generateRandomData(30, 20, 90, 1),
+      generateRandomData(30, 30, 70, 0),
+    ],
+  },
+];
+
+const args = {
+  chartTitle: 'Boxplot Example',
+  description: 'Boxplot chart with two datasets',
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  datasets: doubleDataset,
+  chartOrientation: 'vertical',
+  colorPalette: 'categorical',
+  showLegend: false,
+  showTooltip: true,
+  xAxisTitle: 'Categories',
+  yAxisTitle: 'Values',
+  xAxisMin: null,
+  xAxisMax: null,
+  yAxisMin: null,
+  yAxisMax: null,
+  hideDescription: false,
+  hideCaptions: false,
+  hideHeader: false,
+  hideControls: false,
+  noBorder: false,
+  width: null,
+  height: null,
+};
+
+export const Default = {
+  args,
+  render: (args) => {
+    return html`
+      <kd-chart
+        type="boxplot"
+        .chartTitle=${args.chartTitle}
+        .description=${args.description}
+        .labels=${args.labels}
+        .datasets=${args.datasets}
+        .options=${{
path: src/stories/Boxplot.stories.js
user: login: brian-patrick-kyndryl
body: Oh wow that's definitely more complex than I realized. Good idea seeing as the table render logic was already getting messy.
created_at: 2025-05-08T17:57:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+/**
+ * Boxplot chart type is available through the integration of the
+ * [@sgratzl/chartjs-chart-boxplot](https://www.sgratzl.com/chartjs-chart-boxplot/getting-started.html) package.
path: src/stories/Boxplot.stories.js
user: login: brian-patrick-kyndryl
body: Oh neat I had no idea we could add content to the Autodocs pages through comments like this.
created_at: 2025-05-08T17:59:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+import { html } from 'lit';
+import { generateRandomData } from '../common/config/chartTypes/boxplot';
+import argTypes from '../common/config/chartArgTypes';
+
+import '../components/chart';
+
+/**
+ * Boxplot chart type is available through the integration of the
+ * [@sgratzl/chartjs-chart-boxplot](https://www.sgratzl.com/chartjs-chart-boxplot/getting-started.html) package.
path: src/stories/Boxplot.stories.js
user: login: wbarbee
body: yeah! i was looking for a way to include multiple links in the Docs section. works well
created_at: 2025-05-08T18:21:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-07T15:35:50Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *shidoka-charts* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 74ce7d861a491852a1215407facf7b2ec482bb30 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shidoka-charts/deploys/68236a147fc6da0008a60674 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-44--shidoka-charts.netlify.app](https://deploy-preview-44--shidoka-charts.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQ0LS1zaGlkb2thLWNoYXJ0cy5uZXRsaWZ5LmFwcCJ9.N8iDum-JF5I_oB8VYHXwc0cdV6SYMg3hsjpxVPXYwgQ)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [shidoka-charts Preview](https://deploy-preview-44--shidoka-charts.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/shidoka-charts/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: kumarivijeta
created_at: 2025-05-08T05:10:20Z
body: @wbarbee box-violin plot looks awesome. 
Few observation:
1. Toggle table is not working.
2. In dark theme , x-axis and y-axis text/values are having visibility issue in charts + downloaded png/jpg images.
    I think dark theme x&y axis visibility issue applicable for all the charts .
Works fine till https://github.com/kyndryl-design-system/shidoka-charts/pull/41.
author_association: CONTRIBUTOR
user: login: wbarbee
created_at: 2025-05-08T16:29:35Z
body: > @wbarbee box-violin plot looks awesome. Few observation:
> 
> 1. Toggle table is not working.
> 2. In dark theme , x-axis and y-axis text/values are having visibility issue in charts + downloaded png/jpg images.
>    I think dark theme x&y axis visibility issue applicable for all the charts .
>    Works fine till [feat(legend): more customizable, scrollable html legend #41](https://github.com/kyndryl-design-system/shidoka-charts/pull/41).

Thanks, @kumarivijeta. item 2 should be fixed now.

New violin chart:
<img width="1104" alt="Screenshot 2025-05-08 at 9 27 19 AM" src="https://github.com/user-attachments/assets/c3cae068-792d-4502-b2c5-d8aad2315ea8" />

HTML legend bar graph:
<img width="823" alt="Screenshot 2025-05-08 at 9 27 42 AM" src="https://github.com/user-attachments/assets/89f67635-2f6a-4276-8a54-c28048dfeb66" />

Canvas legend line graph:
<img width="834" alt="Screenshot 2025-05-08 at 9 28 56 AM" src="https://github.com/user-attachments/assets/3ab2278f-9ed1-4c08-a9af-26dfb04b6d85" />
author_association: CONTRIBUTOR
user: login: brian-patrick-kyndryl
created_at: 2025-05-13T15:57:54Z
body: :tada: This PR is included in version 2.4.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@kyndryl-design-system/shidoka-charts/v/2.4.0)
- [GitHub release](https://github.com/kyndryl-design-system/shidoka-charts/releases/tag/v2.4.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER