├── AUTHORS ├── .gitignore ├── src ├── utils.ts ├── icons │ ├── line-chart.svg.ts │ ├── bubble-chart.svg.ts │ ├── pie-chart.svg.ts │ ├── doughnut-chart.svg.ts │ ├── radar-chart.svg.ts │ ├── bar-chart.svg.ts │ ├── polarArea-chart.svg.ts │ ├── index.ts │ └── scatter-chart.svg.ts ├── style.ts ├── locale │ └── en.js ├── loadBlocks.ts ├── chartjsLoader.js ├── index.ts ├── constants.ts ├── loadTraits.ts └── loadComponents.ts ├── biome.json ├── tsconfig.json ├── LICENSE ├── .release-it.json ├── package.json ├── CHANGELOG.md └── README.md /AUTHORS: -------------------------------------------------------------------------------- 1 | Andrea Fassina -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | private/ 4 | /locale 5 | node_modules/ 6 | *.log 7 | _index.html 8 | dist/ 9 | stats.json 10 | .vscode 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "grapesjs"; 2 | import { PLUGIN } from "./constants"; 3 | 4 | export const getI18nName = (editor: Editor, key: string): string => { 5 | return editor.I18n.t(`${PLUGIN}.${key}`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/icons/line-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/bubble-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/pie-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/doughnut-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/radar-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/bar-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/polarArea-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /src/icons/index.ts: -------------------------------------------------------------------------------- 1 | import bar from "./bar-chart.svg"; 2 | import bubble from "./bubble-chart.svg"; 3 | import doughnut from "./doughnut-chart.svg"; 4 | import line from "./line-chart.svg"; 5 | import pie from "./pie-chart.svg"; 6 | import polarArea from "./polarArea-chart.svg"; 7 | import radar from "./radar-chart.svg"; 8 | import scatter from "./scatter-chart.svg"; 9 | 10 | export default { bar, bubble, doughnut, line, pie, polarArea, radar, scatter }; 11 | -------------------------------------------------------------------------------- /src/icons/scatter-chart.svg.ts: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "sourceMap": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": false 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/style.ts: -------------------------------------------------------------------------------- 1 | const style = `.cjs-button-wrapper { 2 | padding: 5px 0px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | } 7 | 8 | .cjs-button-container { 9 | width: auto; 10 | } 11 | 12 | .cjs-button { 13 | color: var(--gjs-font-color); 14 | fill: var(--gjs-font-color); 15 | background: none; 16 | border: none; 17 | cursor: pointer; 18 | outline: none; 19 | position: static; 20 | opacity: 0.75; 21 | padding: 0; 22 | width: 18px; 23 | height: 18px; 24 | } 25 | 26 | .cjs-button-disabled { 27 | opacity: 0.2 !important; 28 | cursor: auto !important; 29 | }`; 30 | export default style; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-current Andrea Fassina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/locale/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "grapesjs-chartjs-plugin": { 3 | category: "Charts", 4 | blocks: { 5 | bar: "Bar Chart", 6 | line: "Line Chart", 7 | pie: "Pie Chart", 8 | doughnut: "Doughnut Chart", 9 | polarArea: "Polar Area Chart", 10 | radar: "Radar Chart", 11 | bubble: "Bubble Chart", 12 | scatter: "Scatter Chart", 13 | }, 14 | traits: { 15 | addBackgroundColor: "Add Background Color", 16 | addBorderColor: "Add Border Color", 17 | addColor: "Add Color", 18 | addDataset: "Add Dataset", 19 | addLineColor: "Add Line Color", 20 | addPointColor: "Add Point Color", 21 | addSegmentColor: "Add Segment Color", 22 | borderWidth: "Border Width", 23 | chartSettings: "Chart Settings", 24 | dataset: "Dataset", 25 | datasetData: "Values", 26 | datasetLabels: "Labels", 27 | datasetName: "Name", 28 | height: "Height", 29 | fill: "Fill", 30 | lineWidth: "Line Width", 31 | radius: "Radius", 32 | removeColor: "Remove Color", 33 | removeDataset: "Remove Dataset", 34 | tension: "Tension", 35 | title: "Title", 36 | subtitle: "Subtitle", 37 | width: "Width", 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore(release): grapesjs-chartjs-plugin@${version}", 4 | "tagName": "v${version}", 5 | "tagAnnotation": "Release v${version}", 6 | "requireCommits": true, 7 | "requireCleanWorkingDir": true 8 | }, 9 | "github": { 10 | "release": true, 11 | "releaseName": "v${version}", 12 | "commitArgs": ["-S"], 13 | "tagArgs": ["-s"] 14 | }, 15 | "npm": { 16 | "publish": true, 17 | "access": "public" 18 | }, 19 | "plugins": { 20 | "@release-it/conventional-changelog": { 21 | "header": "# Changelog", 22 | "infile": "CHANGELOG.md", 23 | "preset": { 24 | "name": "conventionalcommits", 25 | "types": [ 26 | { "type": "feat", "section": "Features" }, 27 | { "type": "fix", "section": "Bug Fixes" }, 28 | { "type": "chore", "hidden": true }, 29 | { "type": "docs", "hidden": true }, 30 | { "type": "refactor", "hidden": true }, 31 | { "type": "perf", "hidden": true }, 32 | { "type": "test", "hidden": true }, 33 | { "type": "style", "hidden": true } 34 | ] 35 | } 36 | } 37 | }, 38 | "hooks": { 39 | "before:init": "npm run lint", 40 | "after:bump": "npm run build" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/loadBlocks.ts: -------------------------------------------------------------------------------- 1 | import type { BlockCategoryProperties, Editor } from "grapesjs"; 2 | import type { ChartjsPluginOptions } from "."; 3 | import { CHARTS, CHART_TYPE } from "./constants"; 4 | import icons from "./icons"; 5 | import { getI18nName } from "./utils"; 6 | 7 | export default async ( 8 | editor: Editor, 9 | options: Required, 10 | ) => { 11 | const bm = editor.BlockManager; 12 | const categoryName = 13 | (options.category as BlockCategoryProperties).label === "category" // i18n key for category name 14 | ? getI18nName(editor, (options.category as BlockCategoryProperties).label) 15 | : ((options.category as BlockCategoryProperties).label ?? 16 | options.category); 17 | for (const chart of CHARTS) { 18 | const type = chart.type ?? "bar"; 19 | const blockType = `chartjs-${type}`; 20 | if (options.blocks?.includes(blockType)) { 21 | const icon = icons[chart.type]; 22 | bm.add(blockType, { 23 | label: getI18nName(editor, `blocks.${type}`), 24 | category: { 25 | id: (options.category as BlockCategoryProperties)?.id ?? "chartjs", 26 | label: categoryName, 27 | }, 28 | content: { 29 | tagName: "div", 30 | type: "chartjs", 31 | attributes: { 32 | "data-gjs-type": "chartjs", 33 | [CHART_TYPE]: type, 34 | }, 35 | }, 36 | media: icon, 37 | }); 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/chartjsLoader.js: -------------------------------------------------------------------------------- 1 | // this file should be in plain javascript to avoit strange behavior with webpack in editor canvas 2 | export function loadChartJs(props) { 3 | // biome-ignore lint/complexity/noUselessThisAlias: 4 | const el = this; 5 | const init = () => { 6 | if (el.firstChild?.$chartjs === undefined) { 7 | addCanvas(); 8 | const ctx = el.firstChild; 9 | window.loadedCharts[el.id] = new window.Chart(ctx); 10 | } 11 | updateChart(); 12 | }; 13 | 14 | const updateChart = () => { 15 | if (props.chartjsOptions) { 16 | if (props.chartjsOptions.options) { 17 | window.loadedCharts[el.id].options = props.chartjsOptions.options; 18 | } 19 | if (props.chartjsOptions.data) { 20 | window.loadedCharts[el.id].data = props.chartjsOptions.data; 21 | } 22 | window.loadedCharts[el.id].update(); 23 | } 24 | }; 25 | 26 | const addCanvas = () => { 27 | if (el.hasChildNodes()) el.innerHTML = ""; 28 | const canvas = document.createElement("canvas"); 29 | canvas.style.width = "100%"; 30 | canvas.style.height = "100%"; 31 | el.appendChild(canvas); 32 | }; 33 | 34 | if (window.Chart === undefined) { 35 | window.loadedCharts = {}; 36 | const script = document.createElement("script"); 37 | script.onload = init; 38 | script.src = "https://cdn.jsdelivr.net/npm/chart.js"; 39 | document.body.appendChild(script); 40 | } else { 41 | init(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-chartjs-plugin", 3 | "version": "0.2.0", 4 | "description": "GrapesJS Plugin to integrate Chart.js into your GrapesJS editor.", 5 | "main": "dist/index.js", 6 | "author": "Andrea Fassina ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:fasenderos/grapesjs-chartjs-plugin.git" 10 | }, 11 | "homepage": "https://github.com/fasenderos/grapesjs-chartjs-plugin", 12 | "bugs": { 13 | "url": "https://github.com/fasenderos/grapesjs-chartjs-plugin/issues" 14 | }, 15 | "scripts": { 16 | "build": "rm -rf ./dist && rm -rf ./locale && grapesjs-cli build --patch=false", 17 | "lint": "biome check ./src", 18 | "lint:fix": "biome check --write ./src", 19 | "release": "node --env-file=.env ./node_modules/release-it/bin/release-it.js --ci", 20 | "start": "grapesjs-cli serve" 21 | }, 22 | "keywords": [ 23 | "grapesjs chartjs plugin", 24 | "grapesjs chartjs", 25 | "grapesjs chart.js plugin", 26 | "grapesjs chart.js", 27 | "grapesjs charts", 28 | "grapesjs", 29 | "chartjs", 30 | "chart.js", 31 | "grapesjs plugin", 32 | "grapesjs plugin chartjs", 33 | "grapesjs plugin chart.js", 34 | "grapesjs plugin charts", 35 | "charts" 36 | ], 37 | "devDependencies": { 38 | "@biomejs/biome": "1.9.4", 39 | "@release-it/conventional-changelog": "^10.0.0", 40 | "chart.js": "^4.4.7", 41 | "grapesjs": "^0.22.5", 42 | "grapesjs-cli": "^4.1.3", 43 | "release-it": "^18.1.2" 44 | }, 45 | "license": "MIT", 46 | "files": [ 47 | "dist", 48 | "locale", 49 | "AUTHORS", 50 | "CHANGELOG.md", 51 | "LICENSE", 52 | "README.md", 53 | "package.json" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ChartOptions } from "chart.js"; 2 | import type { BlockProperties, Editor } from "grapesjs"; 3 | import { CHARTS } from "./constants"; 4 | import loadBlocks from "./loadBlocks"; 5 | import loadComponents from "./loadComponents"; 6 | import { loadTraits } from "./loadTraits"; 7 | import en from "./locale/en"; 8 | import style from "./style"; 9 | 10 | export type ChartjsPluginOptions = { 11 | /** 12 | * I18n object containing languages, [more info](https://grapesjs.com/docs/modules/I18n.html#configuration). 13 | * @default {} 14 | */ 15 | i18n?: Record; 16 | /** 17 | * This object will be passed directly to the underlying Chart.js `options`. 18 | * @see https://www.chartjs.org/docs/latest/configuration for more information 19 | * @default { maintainAspectRatio: false } 20 | */ 21 | chartjsOptions?: ChartOptions; 22 | /** 23 | * Which blocks to add. 24 | * @default [ "chartjs-bar", "chartjs-line", "chartjs-pie", "chartjs-doughnut", "chartjs-polarArea", "chartjs-radar", "chartjs-bubble", "chartjs-scatter" ] 25 | */ 26 | blocks?: string[]; 27 | /** 28 | * Category name for blocks. 29 | * @default { id: 'chartjs', label: 'category' } => The label is the i18n key By default the i18n category name will be Charts 30 | */ 31 | category?: BlockProperties["category"]; 32 | }; 33 | 34 | export default (editor: Editor, opts: ChartjsPluginOptions = {}) => { 35 | // Add ChartjsPlugin Style 36 | document.head.insertAdjacentHTML("beforeend", ``); 37 | const options: Required = { 38 | ...{ 39 | i18n: {}, 40 | chartjsOptions: { 41 | maintainAspectRatio: false, 42 | }, 43 | blocks: CHARTS.map((chart) => `chartjs-${chart.type}`), 44 | category: { id: "chartjs", label: "category" }, 45 | }, 46 | ...opts, 47 | }; 48 | // Load i18n files 49 | editor.I18n?.addMessages({ 50 | en, 51 | ...options.i18n, 52 | }); 53 | // Add traits 54 | loadTraits(editor); 55 | // Add components 56 | loadComponents(editor, options); 57 | // Add blocks 58 | loadBlocks(editor, options); 59 | }; 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.0](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.1.2...v0.2.0) (2025-03-19) 4 | 5 | ### Features 6 | 7 | * support placeholder as data ([7272f48](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/7272f481c2c9d4359c83aef99d24a3ce23044ab2)) 8 | 9 | ## [0.1.2](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.1.1...v0.1.2) (2025-02-10) 10 | 11 | ### Features 12 | 13 | * new blocks and category options ([4a05197](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/4a051978278433c8ed7c1db0d3099e9278f2689a)) 14 | 15 | ## [0.1.1](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.1.0...v0.1.1) (2025-02-09) 16 | 17 | ## [0.1.0](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.0.5...v0.1.0) (2025-01-29) 18 | 19 | ### Features 20 | 21 | * add i18n support ([dbc71be](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/dbc71bed253e68c048236316ca5a9b9a6460e4c0)) 22 | 23 | ## [0.0.5](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.0.4...v0.0.5) (2025-01-29) 24 | 25 | ### Bug Fixes 26 | 27 | * load chart.js in canvas ([e482eaa](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/e482eaa420aeea292350970ff1bc80be014c6b95)) 28 | 29 | ## [0.0.4](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.0.3...v0.0.4) (2025-01-26) 30 | 31 | ## [0.0.3](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.0.2...v0.0.3) (2025-01-25) 32 | 33 | ### Bug Fixes 34 | 35 | * avoid duplicate traits on component drag ([0e5e3bb](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/0e5e3bb92c915b46ce2dfa218db5548aab4f9a31)) 36 | 37 | ## [0.0.2](https://github.com/fasenderos/grapesjs-chartjs-plugin/compare/v0.0.1...v0.0.2) (2025-01-25) 38 | 39 | ### Features 40 | 41 | * avoid await icons import ([d3cfa0e](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/d3cfa0e85da9de93ba535eb4da1378574afb2989)) 42 | 43 | ## 0.0.1 (2025-01-25) 44 | 45 | ### Features 46 | 47 | * add custom trait for specific chart ([65cb7a8](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/65cb7a8e7dc707b8eaeffff6c744a1d163c9dfae)) 48 | * add support for xy and xyr data labels ([707dd17](https://github.com/fasenderos/grapesjs-chartjs-plugin/commit/707dd17fea03cd62041566f08fa7554f94c11d96)) 49 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { ChartOptions, ChartType } from "chart.js"; 2 | import type { TraitProperties } from "grapesjs"; 3 | 4 | export const PLUGIN = "grapesjs-chartjs-plugin"; 5 | 6 | export const ADD_DATASET = "cjs-add-dataset"; 7 | export const REMOVE_DATASET = "cjs-remove-dataset"; 8 | export const ADD_BACKGROUND = "cjs-add-background-color"; 9 | export const ADD_BORDER = "cjs-add-border-color"; 10 | 11 | export const CHART_TYPE = "cjs-chart-type"; 12 | export const CHART_LABELS = "cjs-chart-labels"; 13 | export const CHART_TITLE = "cjs-chart-title"; 14 | export const CHART_SUBTITLE = "cjs-chart-subtitle"; 15 | export const CHART_WIDTH = "cjs-chart-width"; 16 | export const CHART_HEIGHT = "cjs-chart-height"; 17 | export const DATASET_LABEL = "cjs-dataset-label"; 18 | export const DATASET_DATA = "cjs-dataset-data"; 19 | export const DATASET_BACKGROUND_COLOR = "cjs-dataset-background-color"; 20 | export const DATASET_BORDER_COLOR = "cjs-dataset-border-color"; 21 | export const DATASET_BORDER_WIDTH = "cjs-dataset-border-width"; 22 | export const DATASET_OPTIONAL_PROPERTY = "cjs-dataset-custom"; 23 | 24 | export const DEFAULT_OPTIONS = { 25 | data: "65, 59, 80, 81, 56", 26 | label: "My Dataset", 27 | labels: "Jan, Feb, Mar, Apr, May", 28 | width: 300, 29 | height: 300, 30 | title: undefined, 31 | subtitle: undefined, 32 | }; 33 | 34 | type ChartOptionalDatasetProperties = { 35 | property: string; 36 | type: "text" | "number" | "select" | "checkbox" | "color" | "button"; 37 | traitOptions?: Omit; 38 | }; 39 | 40 | export type DatasetType = "labels-data" | "x-y" | "x-y-r"; // default to "labels-data" 41 | 42 | export type ChartComponentOptions = { 43 | type: ChartType; 44 | datasetType?: DatasetType; 45 | defaultLabels?: string; 46 | defaultData?: string; 47 | chartjsOptions?: ChartOptions; 48 | optionalDatasetProperties?: ChartOptionalDatasetProperties[]; 49 | backgroundColor?: TraitProperties; 50 | borderColor?: TraitProperties; 51 | borderWidth?: TraitProperties; 52 | }; 53 | 54 | export const CHARTS: ChartComponentOptions[] = [ 55 | { type: "bar" }, 56 | { 57 | type: "line", 58 | backgroundColor: { label: "addPointColor" }, 59 | borderColor: { label: "addLineColor" }, 60 | borderWidth: { label: "lineWidth", placeholder: "1", value: 1 }, 61 | optionalDatasetProperties: [ 62 | { property: "fill", type: "checkbox" }, 63 | { 64 | property: "tension", 65 | type: "number", 66 | traitOptions: { 67 | min: 0, 68 | placeholder: "0.1", 69 | step: 0.1, 70 | }, 71 | }, 72 | ], 73 | }, 74 | { 75 | type: "pie", 76 | defaultLabels: "Jan, Feb, Mar", 77 | defaultData: "300, 50, 100", 78 | backgroundColor: { label: "addSegmentColor" }, 79 | borderWidth: { placeholder: "1", value: 1 }, 80 | }, 81 | { 82 | type: "doughnut", 83 | defaultLabels: "Jan, Feb, Mar", 84 | defaultData: "300, 50, 100", 85 | backgroundColor: { label: "addSegmentColor" }, 86 | borderWidth: { placeholder: "1", value: 1 }, 87 | }, 88 | { 89 | type: "polarArea", 90 | borderWidth: { placeholder: "1", value: 1 }, 91 | }, 92 | { 93 | type: "radar", 94 | defaultLabels: "Eating, Drinking, Sleeping, Running", 95 | defaultData: "70, 80, 90, 65", 96 | }, 97 | { 98 | type: "bubble", 99 | defaultLabels: "-10, 0, 10, 0.5", 100 | defaultData: "0, 10, 5, 5.5", 101 | datasetType: "x-y-r", 102 | optionalDatasetProperties: [ 103 | { 104 | property: "radial", 105 | type: "text", 106 | traitOptions: { value: "15, 5, 30, 10", label: "radius" }, 107 | }, 108 | ], 109 | }, 110 | { 111 | type: "scatter", 112 | defaultLabels: "-10, 0, 10, 0.5", 113 | defaultData: "0, 10, 5, 5.5", 114 | datasetType: "x-y", 115 | }, 116 | ]; 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | NPM Version 3 | Package License 4 | NPM Downloads 5 | Built with TypeScript 6 |

7 | 8 | # GrapesJS Chart.js Plugin 9 | 10 | This plugin integrates [Chart.js](https://www.chartjs.org/) into your GrapesJS editor :rocket::rocket:. You can add various types of charts :bar_chart: to your projects and customize them according to your needs. 11 | 12 | [DEMO](https://codesandbox.io/p/sandbox/grapesjs-chartjs-plugin-jxy3qk) 13 | 14 |

15 | :star: Star me on GitHub — it motivates me a lot! 16 |

17 | 18 |

19 | 20 |

21 | 22 |

23 | 24 | 25 |

26 | 27 | ## Summary 28 | 29 | - Plugin name: `grapesjs-chartjs-plugin` 30 | - Components 31 | - `chartjs` 32 | - Blocks 33 | - `chartjs-bar` 34 | - `chartjs-line` 35 | - `chartjs-pie` 36 | - `chartjs-doughnut` 37 | - `chartjs-polarArea` 38 | - `chartjs-radar` 39 | - `chartjs-bubble` 40 | - `chartjs-scatter` 41 | 42 | ## Options 43 | 44 | | Option | Description | Default | 45 | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | 46 | | `chartjsOptions` | This object will be passed directly to the underlying Chart.js `options`. [See here for more info](https://www.chartjs.org/docs/latest/configuration/). | `{ maintainAspectRatio: false }` | 47 | | `blocks` | Which blocks to add. | `[ "chartjs-bar", "chartjs-line", "chartjs-pie", "chartjs-doughnut", "chartjs-polarArea", "chartjs-radar", "chartjs-bubble", "chartjs-scatter" ]` | 48 | | `category` | Category name. By default the `category` value of the i18n [`en`](src/locale/en.js) locale file will be used. | `{ id: 'chartjs', label: "Charts" }` | 49 | | `i18n` | Object used to translate plugin properties. Check the `en` locale file and follow its inner path. | [`en`](src/locale/en.js) | 50 | 51 | ## Download 52 | 53 | - CDN 54 | - `https://unpkg.com/grapesjs-chartjs-plugin` 55 | - NPM 56 | - `npm i grapesjs-chartjs-plugin` 57 | - GIT 58 | - `git clone https://github.com/fasenderos/grapesjs-chartjs-plugin.git` 59 | 60 | ## Usage 61 | 62 | Directly in the browser 63 | 64 | ```html 65 | 69 | 70 | 71 | 72 |
73 | 74 | 86 | ``` 87 | 88 | Modern javascript 89 | 90 | ```js 91 | import grapesjs from 'grapesjs'; 92 | import plugin from 'grapesjs-chartjs-plugin'; 93 | import 'grapesjs/dist/css/grapes.min.css'; 94 | 95 | const editor = grapesjs.init({ 96 | container : '#gjs', 97 | // ... 98 | plugins: [plugin], 99 | pluginsOpts: { 100 | [plugin]: { /* options */ } 101 | } 102 | // or 103 | plugins: [ 104 | editor => plugin(editor, { /* options */ }), 105 | ], 106 | }); 107 | ``` 108 | 109 | ## Development 110 | 111 | Clone the repository 112 | 113 | ```sh 114 | $ git clone https://github.com/fasenderos/grapesjs-chartjs-plugin.git 115 | $ cd grapesjs-chartjs-plugin 116 | ``` 117 | 118 | Install dependencies 119 | 120 | ```sh 121 | $ npm i 122 | ``` 123 | 124 | Start the dev server 125 | 126 | ```sh 127 | $ npm start 128 | ``` 129 | 130 | Build the source 131 | 132 | ```sh 133 | $ npm run build 134 | ``` 135 | 136 | ## License 137 | 138 | MIT 139 | -------------------------------------------------------------------------------- /src/loadTraits.ts: -------------------------------------------------------------------------------- 1 | import type { Category, Component, Editor, Trait } from "grapesjs"; 2 | import { 3 | ADD_BACKGROUND, 4 | DATASET_BACKGROUND_COLOR, 5 | DATASET_BORDER_COLOR, 6 | } from "./constants"; 7 | import { getI18nName } from "./utils"; 8 | 9 | const loadTraits = (editor: Editor) => { 10 | const tm = editor.TraitManager; 11 | tm.addType("cjs-add-color-button", { 12 | noLabel: true, 13 | templateInput() { 14 | return ""; 15 | }, 16 | createInput({ trait, component }) { 17 | const label = 18 | trait.attributes.label ?? getI18nName(editor, "traits.addColor"); 19 | const el = document.createElement("div"); 20 | const handleRemoveColorTrait = () => { 21 | removeColorTrait(component, trait); 22 | }; 23 | const handleAddColorTrait = () => { 24 | addColorTrait(component, trait); 25 | }; 26 | el.innerHTML = `
27 | 28 | ${label} 29 | 30 |
31 | 34 | 37 |
38 |
`; 39 | const removeButton = el.getElementsByTagName("button")[0]; 40 | const addButton = el.getElementsByTagName("button")[1]; 41 | removeButton.addEventListener("click", handleRemoveColorTrait); 42 | addButton.addEventListener("click", handleAddColorTrait); 43 | return el.firstElementChild as HTMLDivElement; 44 | }, 45 | }); 46 | }; 47 | 48 | const getColorTraitName = (isBg: boolean, counter: number, id: string) => { 49 | return isBg 50 | ? `${DATASET_BACKGROUND_COLOR}-${counter}-${id}` 51 | : `${DATASET_BORDER_COLOR}-${counter}-${id}`; 52 | }; 53 | 54 | const addColorTrait = (component: Component, trait: Trait) => { 55 | const id = (trait.id as string).split("-").pop(); 56 | if (id && trait.category) { 57 | const isBg = (trait.id as string).includes(ADD_BACKGROUND); 58 | const traits = component.getTraits(); 59 | const { counter, lastIndex } = getFieldsCount(traits, isBg, trait.category); 60 | const last = 61 | lastIndex > 0 ? lastIndex : traits.findIndex((t) => trait.id === t.id); 62 | const at = last + 1; 63 | const name = getColorTraitName(isBg, counter, id); 64 | 65 | if (counter === 0) { 66 | const removeButton = getRemoveButton(trait); 67 | if (removeButton) removeButton.classList.remove("cjs-button-disabled"); 68 | } 69 | component.addTrait( 70 | [ 71 | { 72 | type: "color", 73 | label: false, 74 | name, 75 | category: (trait.category as Category).attributes as string, 76 | }, 77 | ], 78 | { at }, 79 | )[0]; 80 | } 81 | }; 82 | 83 | const removeColorTrait = (component: Component, trait: Trait) => { 84 | const id = (trait.id as string).split("-").pop(); 85 | const isBg = (trait.id as string).includes(ADD_BACKGROUND); 86 | const traits = component.getTraits(); 87 | if (trait.category) { 88 | const { counter } = getFieldsCount(traits, isBg, trait.category); 89 | if (counter === 1) { 90 | const removeButton = getRemoveButton(trait); 91 | if (removeButton) removeButton.classList.add("cjs-button-disabled"); 92 | } 93 | if (counter > 0 && id) { 94 | const traitId = getColorTraitName(isBg, counter - 1, id); 95 | const traitToBeRemoved = component.getTrait(traitId); 96 | traitToBeRemoved.setValue(""); 97 | component.removeTrait(traitId); 98 | } 99 | } 100 | }; 101 | 102 | const getFieldsCount = ( 103 | traits: Trait[], 104 | isBg: boolean, 105 | traitCategory: Category, 106 | ) => { 107 | const traitsLength = traits.length; 108 | let lastIndex = 0; 109 | let bgCount = 0; 110 | let borderCount = 0; 111 | 112 | for (let index = 0; index < traitsLength; index++) { 113 | if ( 114 | traits[index].attributes.type === "color" && 115 | traitCategory.id === traits[index].category?.id 116 | ) { 117 | const trait = traits[index]; 118 | if (isBg) { 119 | if ((trait.id as string).includes(DATASET_BACKGROUND_COLOR)) { 120 | lastIndex = index; 121 | bgCount++; 122 | } 123 | } else { 124 | if ((trait.id as string).includes(DATASET_BORDER_COLOR)) { 125 | lastIndex = index; 126 | borderCount++; 127 | } 128 | } 129 | } 130 | } 131 | const counter = isBg ? bgCount : borderCount; 132 | return { counter, lastIndex }; 133 | }; 134 | 135 | const getRemoveButton = (trait: Trait) => { 136 | const wrapper = document.getElementById(`${trait.id}-wrapper`); 137 | if (wrapper) { 138 | return wrapper.querySelector("[data-cjs-remove-color]"); 139 | } 140 | }; 141 | 142 | export { loadTraits, addColorTrait }; 143 | -------------------------------------------------------------------------------- /src/loadComponents.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CallbackOptions, 3 | Component, 4 | Editor, 5 | Trait, 6 | TraitProperties, 7 | } from "grapesjs"; 8 | import type { ChartjsPluginOptions } from "."; 9 | import { loadChartJs } from "./chartjsLoader"; 10 | import { 11 | ADD_BACKGROUND, 12 | ADD_BORDER, 13 | ADD_DATASET, 14 | CHARTS, 15 | CHART_HEIGHT, 16 | CHART_LABELS, 17 | CHART_SUBTITLE, 18 | CHART_TITLE, 19 | CHART_TYPE, 20 | CHART_WIDTH, 21 | type ChartComponentOptions, 22 | DATASET_BACKGROUND_COLOR, 23 | DATASET_BORDER_COLOR, 24 | DATASET_BORDER_WIDTH, 25 | DATASET_DATA, 26 | DATASET_LABEL, 27 | DATASET_OPTIONAL_PROPERTY, 28 | DEFAULT_OPTIONS, 29 | type DatasetType, 30 | REMOVE_DATASET, 31 | } from "./constants"; 32 | import { addColorTrait } from "./loadTraits"; 33 | import { getI18nName } from "./utils"; 34 | 35 | type UpdateChartDatasetBorderWidthProps = { 36 | borderWidth: number; 37 | index: number; 38 | }; 39 | 40 | type UpdateChartDatasetColorProps = { 41 | action: "update" | "unset"; 42 | backgroundColor?: string; 43 | borderColor?: string; 44 | index: number; 45 | colorIndex: number; 46 | }; 47 | 48 | export default (editor: Editor, options: Required) => { 49 | const domc = editor.DomComponents; 50 | const chartSettingCategory = { 51 | id: "cjs-common", 52 | label: getI18nName(editor, "traits.chartSettings"), 53 | }; 54 | 55 | domc.addType("chartjs", { 56 | model: { 57 | defaults: { 58 | script: loadChartJs, 59 | "script-props": ["chartjsOptions"], 60 | resizable: { 61 | ratioDefault: true, 62 | tc: false, 63 | bc: false, 64 | cl: false, 65 | cr: false, 66 | onEnd: (_ev: Event, { el }: CallbackOptions) => { 67 | const component = editor.getSelected(); 68 | const { offsetHeight: height, offsetWidth: width } = el; 69 | const traitWidth = component?.getTrait(CHART_WIDTH); 70 | const traitHeight = component?.getTrait(CHART_HEIGHT); 71 | traitWidth?.setValue(width); 72 | traitHeight?.setValue(height); 73 | }, 74 | }, 75 | unstylable: ["width", "height"], 76 | // @ts-ignore 77 | traits(component: Component) { 78 | const type = component.getAttributes()[CHART_TYPE]; 79 | const chartSettings = CHARTS.find((c) => c.type === type); 80 | const datasetType = chartSettings?.datasetType ?? "labels-data"; 81 | component.set("chartComponentOptions", chartSettings); 82 | 83 | const traits: TraitProperties[] = [ 84 | { 85 | type: "text", 86 | label: getI18nName(editor, "traits.title"), 87 | name: CHART_TITLE, 88 | category: chartSettingCategory, 89 | value: DEFAULT_OPTIONS.title, 90 | placeholder: DEFAULT_OPTIONS.title, 91 | }, 92 | { 93 | type: "text", 94 | label: getI18nName(editor, "traits.subtitle"), 95 | name: CHART_SUBTITLE, 96 | category: chartSettingCategory, 97 | value: DEFAULT_OPTIONS.subtitle, 98 | placeholder: DEFAULT_OPTIONS.title, 99 | }, 100 | { 101 | type: "number", 102 | label: getI18nName(editor, "traits.width"), 103 | name: CHART_WIDTH, 104 | category: chartSettingCategory, 105 | value: DEFAULT_OPTIONS.width, 106 | placeholder: "300", 107 | }, 108 | { 109 | type: "number", 110 | label: getI18nName(editor, "traits.height"), 111 | name: CHART_HEIGHT, 112 | category: chartSettingCategory, 113 | value: DEFAULT_OPTIONS.height, 114 | placeholder: "300", 115 | }, 116 | { 117 | type: "button", 118 | text: getI18nName(editor, "traits.addDataset"), 119 | full: true, 120 | name: ADD_DATASET, 121 | category: chartSettingCategory, 122 | command(editor: Editor) { 123 | const component = editor.getSelected(); 124 | // @ts-ignore 125 | component?.addNewDatasetTraitsGroup(); 126 | }, 127 | }, 128 | ]; 129 | 130 | if (datasetType === "labels-data") { 131 | traits.unshift({ 132 | type: "text", 133 | label: getI18nName(editor, "traits.datasetLabels"), 134 | name: CHART_LABELS, 135 | category: chartSettingCategory, 136 | value: chartSettings?.defaultLabels ?? DEFAULT_OPTIONS.labels, 137 | placeholder: 138 | chartSettings?.defaultLabels ?? DEFAULT_OPTIONS.labels, 139 | }); 140 | } 141 | return traits; 142 | }, 143 | }, 144 | init() { 145 | const alreadyLoaded = this.getTrait(`${DATASET_DATA}-1`); 146 | if (alreadyLoaded == null) { 147 | this.addNewDatasetTraitsGroup(); 148 | } else { 149 | const attributes = this.getAttributes(); 150 | let dataSetCount = 0; 151 | for (const key of Object.keys(attributes)) { 152 | const fieldId = key.split("-").pop(); 153 | if (fieldId) { 154 | const id = Number.parseInt(fieldId); 155 | if (id > dataSetCount) { 156 | this.addNewDatasetTraitsGroup(); 157 | dataSetCount++; 158 | } 159 | const parent = key.includes(DATASET_BACKGROUND_COLOR) 160 | ? ADD_BACKGROUND 161 | : key.includes(DATASET_BORDER_COLOR) 162 | ? ADD_BORDER 163 | : null; 164 | if (parent) { 165 | const trait = this.getTraits().find( 166 | (t) => t.id === `${parent}-${id}`, 167 | ); 168 | if (trait) { 169 | addColorTrait(this, trait); 170 | } 171 | } 172 | } 173 | } 174 | } 175 | this.on("change:attributes", this.handleAttrChange); 176 | }, 177 | handleAttrChange(component: Component) { 178 | // @ts-ignore 179 | component.view.updateChart(); 180 | }, 181 | addNewDatasetTraitsGroup() { 182 | const newTraitsGroup = 183 | this.getNewDatasetTraitsGroup() as TraitProperties[]; 184 | this.addTrait(newTraitsGroup); 185 | const newAttributes = newTraitsGroup.reduce( 186 | (acc, curr) => { 187 | if (curr.name && curr.value) { 188 | acc[curr.name] = curr.value; 189 | } 190 | return acc; 191 | }, 192 | {} as Record, 193 | ); 194 | if (Object.keys(newTraitsGroup).length) 195 | this.addAttributes(newAttributes); 196 | }, 197 | getNewDatasetTraitsGroup(): TraitProperties[] { 198 | const chartOptions = this.get( 199 | "chartComponentOptions", 200 | ) as ChartComponentOptions; 201 | const datasetType = chartOptions.datasetType ?? "labels-data"; 202 | 203 | let last = 0; 204 | for (const trait of this.getTraits() ?? []) { 205 | if (trait.category?.id && trait.category.id !== "cjs-common") { 206 | const categoryId = (trait.category.id as string).split("-").pop(); 207 | if (categoryId) { 208 | const id = Number.parseInt(categoryId); 209 | if (id > last) last = id; 210 | } 211 | } 212 | } 213 | const id = last + 1; 214 | const newTraitsGroup: TraitProperties[] = []; 215 | const category = { 216 | id: `cjs-dataset-options-${id}`, 217 | label: `#${id} ${getI18nName(editor, "traits.dataset")}`, 218 | open: id === 1, 219 | }; 220 | if (id > 1) { 221 | newTraitsGroup.push({ 222 | type: "button", 223 | text: getI18nName(editor, "traits.removeDataset"), 224 | full: true, 225 | name: `${REMOVE_DATASET}-${id}`, 226 | category, 227 | command() { 228 | const component = editor.getSelected(); 229 | if (component) { 230 | const traits = component 231 | .getTraits() 232 | .filter((t) => t.category?.id === category.id); 233 | for (const trait of traits) { 234 | trait.off("change:value"); 235 | trait.setValue(""); 236 | component.removeTrait(trait.id as string); 237 | } 238 | // @ts-ignore 239 | component.view?.removeDataset(id - 1); 240 | } 241 | }, 242 | }); 243 | } 244 | newTraitsGroup.push({ 245 | type: "text", 246 | label: getI18nName(editor, "traits.datasetName"), 247 | name: `${DATASET_LABEL}-${id}`, 248 | category, 249 | value: `#${id} ${DEFAULT_OPTIONS.label}`, 250 | placeholder: DEFAULT_OPTIONS.label, 251 | }); 252 | if (datasetType !== "labels-data") { 253 | newTraitsGroup.push({ 254 | type: "text", 255 | label: getI18nName(editor, "traits.datasetLabels"), 256 | name: `${CHART_LABELS}-${id}`, 257 | category, 258 | value: chartOptions.defaultLabels ?? DEFAULT_OPTIONS.labels, 259 | placeholder: chartOptions.defaultLabels ?? DEFAULT_OPTIONS.labels, 260 | }); 261 | } 262 | newTraitsGroup.push({ 263 | type: "text", 264 | label: getI18nName(editor, "traits.datasetData"), 265 | name: `${DATASET_DATA}-${id}`, 266 | category, 267 | value: chartOptions.defaultData ?? DEFAULT_OPTIONS.data, 268 | placeholder: chartOptions.defaultData ?? DEFAULT_OPTIONS.data, 269 | }); 270 | newTraitsGroup.push({ 271 | type: "cjs-add-color-button", 272 | name: `${ADD_BACKGROUND}-${id}`, 273 | label: getI18nName( 274 | editor, 275 | `traits.${chartOptions?.backgroundColor?.label ?? "addBackgroundColor"}`, 276 | ), 277 | category, 278 | }); 279 | newTraitsGroup.push({ 280 | type: "cjs-add-color-button", 281 | name: `${ADD_BORDER}-${id}`, 282 | label: getI18nName( 283 | editor, 284 | `traits.${chartOptions?.borderColor?.label ?? "addBorderColor"}`, 285 | ), 286 | category, 287 | }); 288 | newTraitsGroup.push({ 289 | type: "number", 290 | label: getI18nName( 291 | editor, 292 | `traits.${chartOptions?.borderWidth?.label ?? "borderWidth"}`, 293 | ), 294 | name: `${DATASET_BORDER_WIDTH}-${id}`, 295 | value: chartOptions?.borderWidth?.value ?? 0, 296 | placeholder: chartOptions?.borderWidth?.placeholder ?? "0", 297 | min: chartOptions?.borderWidth?.min ?? 0, 298 | category, 299 | }); 300 | 301 | if (chartOptions?.optionalDatasetProperties?.length) { 302 | for (const { 303 | property, 304 | type, 305 | traitOptions, 306 | ...rest 307 | } of chartOptions.optionalDatasetProperties) { 308 | newTraitsGroup.push({ 309 | ...traitOptions, 310 | type, 311 | label: getI18nName( 312 | editor, 313 | `traits.${traitOptions?.label ?? property}`, 314 | ), 315 | name: `${DATASET_OPTIONAL_PROPERTY}-${property}-${id}`, 316 | category, 317 | }); 318 | } 319 | } 320 | return newTraitsGroup; 321 | }, 322 | }, 323 | view: { 324 | init() { 325 | this.chart = { 326 | data: { 327 | labels: [], 328 | datasets: [], 329 | }, 330 | options: { 331 | ...(options.chartjsOptions ?? {}), 332 | }, 333 | }; 334 | this.updateChart(); 335 | }, 336 | getDatasetType() { 337 | const options = this.model.get( 338 | "chartComponentOptions", 339 | ) as ChartComponentOptions; 340 | return options?.datasetType ?? "labels-data"; 341 | }, 342 | updateChart() { 343 | const { 344 | id, 345 | [CHART_TYPE]: type, 346 | "data-gjs-type": componentName, 347 | ...restTraits 348 | } = this.model.getTraits().reduce( 349 | (acc, curr) => { 350 | acc[curr.getName()] = curr; 351 | return acc; 352 | }, 353 | {} as { [name: string]: Trait }, 354 | ); 355 | 356 | const datasetType = this.getDatasetType() as DatasetType; 357 | for (const name in restTraits) { 358 | if (Object.prototype.hasOwnProperty.call(restTraits, name)) { 359 | const trait = restTraits[name]; 360 | const value = trait.getValue(); 361 | const splitTrait = name.split("-") as string[]; 362 | const fieldNumber = Number.parseInt( 363 | splitTrait?.[splitTrait.length - 1] ?? "1", 364 | ); 365 | const traitName = [...splitTrait] 366 | ?.splice( 367 | 0, 368 | Number.isNaN(fieldNumber) 369 | ? splitTrait.length 370 | : splitTrait.length - 1, 371 | ) 372 | .join("-"); 373 | const index = fieldNumber - 1; 374 | switch (traitName) { 375 | case DATASET_DATA: { 376 | this.updateChartDatasetData(value, index); 377 | break; 378 | } 379 | case DATASET_LABEL: { 380 | this.updateChartDatasetLabel(value, index); 381 | break; 382 | } 383 | case CHART_LABELS: { 384 | this.updateChartLabels(value, index); 385 | break; 386 | } 387 | case CHART_TITLE: 388 | this.updateChartTitle(value); 389 | break; 390 | case CHART_SUBTITLE: { 391 | this.updateChartSubtitle(value); 392 | break; 393 | } 394 | case CHART_HEIGHT: { 395 | this.model.addStyle({ height: `${value ?? 0}px` }); 396 | break; 397 | } 398 | case CHART_WIDTH: { 399 | this.model.addStyle({ width: `${value ?? 0}px` }); 400 | break; 401 | } 402 | case DATASET_BORDER_WIDTH: { 403 | const payload: UpdateChartDatasetBorderWidthProps = { 404 | borderWidth: Number.parseInt(value), 405 | index, 406 | }; 407 | this.updateChartDatasetBorderWidth(payload); 408 | break; 409 | } 410 | default: { 411 | const colorIndex = Number.parseInt( 412 | splitTrait[splitTrait.length - 2], 413 | ); 414 | if (traitName?.includes(DATASET_BACKGROUND_COLOR)) { 415 | this.updateChartDatasetBackgroundColor( 416 | value, 417 | index, 418 | colorIndex, 419 | ); 420 | } else if (traitName?.includes(DATASET_BORDER_COLOR)) { 421 | this.updateChartDatasetBorderColor(value, index, colorIndex); 422 | } else if (traitName?.includes(DATASET_OPTIONAL_PROPERTY)) { 423 | if (datasetType === "labels-data") { 424 | this.updateOptionalDatasetProperty( 425 | trait, 426 | traitName, 427 | value, 428 | index, 429 | ); 430 | } else { 431 | const type = traitName.split("-").pop(); 432 | this.updateChartByDatasetType(type, value, index); 433 | } 434 | } 435 | break; 436 | } 437 | } 438 | } 439 | } 440 | // Update chart view 441 | this.updateChartView(); 442 | }, 443 | updateChartView() { 444 | this.model.set("chartjsOptions", this.chart); 445 | this.model.trigger("change:chartjsOptions"); 446 | }, 447 | addDataset() { 448 | if (!this.chart.data.datasets) this.chart.data.datasets = []; 449 | const options = this.model.get( 450 | "chartComponentOptions", 451 | ) as ChartComponentOptions; 452 | this.chart.data.datasets.push({ 453 | type: options.type, 454 | backgroundColor: ["rgba(54, 162, 235, 0.5)"], 455 | borderColor: ["rgb(54, 162, 235)"], 456 | data: [], 457 | }); 458 | }, 459 | removeDataset(index: number): void { 460 | if (this.chart.data.datasets[index] != null) { 461 | this.chart.data.datasets.splice(index, 1); 462 | this.updateChartView(); 463 | } 464 | }, 465 | updateChartDatasetData(value: string, index: number): void { 466 | if (!this.chart.data.datasets[index]) { 467 | const options = this.model.get( 468 | "chartComponentOptions", 469 | ) as ChartComponentOptions; 470 | this.chart.data.datasets[index] = { type: options.type }; 471 | } 472 | this.updateChartByDatasetType("data", value, index); 473 | }, 474 | updateChartDatasetLabel(value: string, index: number): void { 475 | if (!this.chart.data.datasets[index]) { 476 | const options = this.model.get( 477 | "chartComponentOptions", 478 | ) as ChartComponentOptions; 479 | this.chart.data.datasets[index] = { type: options.type }; 480 | } 481 | this.chart.data.datasets[index].label = value; 482 | }, 483 | updateChartDatasetBorderWidth({ 484 | borderWidth, 485 | index, 486 | }: UpdateChartDatasetBorderWidthProps): void { 487 | if (!this.chart.data.datasets[index].borderWidth) { 488 | this.chart.data.datasets[index].borderWidth = 0; 489 | } 490 | if (typeof borderWidth === "number" && borderWidth >= 0) { 491 | this.chart.data.datasets[index].borderWidth = borderWidth; 492 | } else { 493 | this.chart.data.datasets[index].borderWidth = 0; 494 | } 495 | }, 496 | updateChartDatasetBackgroundColor( 497 | value: string, 498 | index: number, 499 | colorIndex: number, 500 | ): void { 501 | const payload: UpdateChartDatasetColorProps = { 502 | action: "update", 503 | backgroundColor: value, 504 | index, 505 | colorIndex, 506 | }; 507 | this.updateChartDatasetColor(payload); 508 | }, 509 | updateChartDatasetBorderColor( 510 | value: string, 511 | index: number, 512 | colorIndex: number, 513 | ): void { 514 | const payload: UpdateChartDatasetColorProps = { 515 | action: "update", 516 | borderColor: value, 517 | index, 518 | colorIndex, 519 | }; 520 | this.updateChartDatasetColor(payload); 521 | }, 522 | updateChartDatasetColor({ 523 | action, 524 | backgroundColor, 525 | borderColor, 526 | index, 527 | colorIndex, 528 | }: UpdateChartDatasetColorProps): void { 529 | if (!this.chart.data.datasets[index]) { 530 | this.chart.data.datasets[index] = {}; 531 | } 532 | if (!this.chart.data.datasets[index].backgroundColor) { 533 | this.chart.data.datasets[index].backgroundColor = []; 534 | } 535 | if (!this.chart.data.datasets[index].borderColor) { 536 | this.chart.data.datasets[index].borderColor = []; 537 | } 538 | 539 | if (action === "unset") { 540 | this.chart.data.datasets[index].backgroundColor = [ 541 | "rgba(54, 162, 235, 0.5)", 542 | ]; 543 | this.chart.data.datasets[index].borderColor = ["rgb(54, 162, 235)"]; 544 | this.chart.data.datasets[index].borderWidth = 0; 545 | } else { 546 | if (backgroundColor != null) { 547 | if (backgroundColor === "") { 548 | this.chart.data.datasets[index].backgroundColor.splice( 549 | colorIndex, 550 | 1, 551 | ); 552 | if ( 553 | this.chart.data.datasets[index].backgroundColor.length === 0 554 | ) { 555 | this.chart.data.datasets[index].backgroundColor = [ 556 | "rgba(54, 162, 235, 0.5)", 557 | ]; 558 | } 559 | } else { 560 | this.chart.data.datasets[index].backgroundColor[colorIndex] = 561 | backgroundColor; 562 | } 563 | } 564 | 565 | if (borderColor != null) { 566 | if (borderColor === "") { 567 | this.chart.data.datasets[index].borderColor.splice(colorIndex, 1); 568 | if (this.chart.data.datasets[index].borderColor.length === 0) { 569 | this.chart.data.datasets[index].borderColor = [ 570 | "rgb(54, 162, 235)", 571 | ]; 572 | } 573 | } else { 574 | this.chart.data.datasets[index].borderColor[colorIndex] = 575 | borderColor; 576 | } 577 | } 578 | } 579 | }, 580 | updateChartLabels(value: string, index: number): void { 581 | this.updateChartByDatasetType("labels", value, index); 582 | }, 583 | updateChartTitle(value: string): void { 584 | this.chart.options.plugins = { 585 | ...(this.chart.options.plugins ?? {}), 586 | title: { display: value.length > 0, text: value }, 587 | }; 588 | }, 589 | updateChartSubtitle(value: string): void { 590 | this.chart.options.plugins = { 591 | ...(this.chart.options.plugins ?? {}), 592 | subtitle: { display: value.length > 0, text: value }, 593 | }; 594 | }, 595 | updateOptionalDatasetProperty( 596 | trait: Trait, 597 | traitName: string, 598 | value: unknown, 599 | index: number, 600 | ) { 601 | const property = traitName.split("-").pop(); 602 | if (property) { 603 | if (!this.chart.data.datasets[index]) { 604 | this.chart.data.datasets[index] = {}; 605 | } 606 | let newValue = value; 607 | // checkbox can comes as a target attributes with an empty string that means "true", true or false 608 | if (trait.attributes.type === "checkbox" && value !== false) { 609 | const attributes = trait.target.getAttributes(); 610 | if (attributes?.[trait.id] === "") newValue = true; 611 | } 612 | this.chart.data.datasets[index][property] = newValue; 613 | } 614 | }, 615 | updateChartByDatasetType( 616 | type: "labels" | "data" | "radial", 617 | value: string, 618 | index: number, 619 | ) { 620 | const datasetType = this.getDatasetType() as DatasetType; 621 | if (datasetType === "labels-data") { 622 | if (type === "labels") { 623 | this.chart.data.labels = value 624 | ? value.split(",").map((x) => x.trim()) 625 | : []; 626 | } else { 627 | this.chart.data.datasets[index].data = value 628 | ? value.split(",").map((x) => x.trim()) 629 | : []; 630 | } 631 | } else { 632 | // can be x-y or x-y-r 633 | // x => labels 634 | // y => data 635 | // r => radial 636 | const coordiantes = datasetType.split("-"); 637 | const axis = 638 | type === "labels" 639 | ? coordiantes[0] 640 | : type === "data" 641 | ? coordiantes[1] 642 | : coordiantes[2]; 643 | if (axis != null) { 644 | const oldValues = [...(this.chart.data.datasets[index].data ?? [])]; 645 | const newValues = value?.split(",").map((x) => x.trim()) ?? []; 646 | const maxLength = 647 | newValues.length >= oldValues.length 648 | ? newValues.length 649 | : oldValues.length; 650 | 651 | const results = []; 652 | for (let i = 0; i < maxLength; i++) { 653 | const data = { 654 | // copy old values 655 | ...(oldValues[i] != null ? oldValues[i] : {}), 656 | ...(newValues[i] != null ? { [axis]: newValues[i] } : {}), 657 | }; 658 | if (newValues[i] == null) delete data[axis]; 659 | results.push(data); 660 | } 661 | this.chart.data.datasets[index].data = [...results]; 662 | } 663 | } 664 | }, 665 | }, 666 | }); 667 | }; 668 | --------------------------------------------------------------------------------