diff --git a/packages/nodes-base/nodes/QuickChart/QuickChart.node.json b/packages/nodes-base/nodes/QuickChart/QuickChart.node.json
new file mode 100644
index 0000000000000..364fbf6f4d22f
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/QuickChart.node.json
@@ -0,0 +1,19 @@
+{
+ "node": "n8n-nodes-base.quickChart",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["Marketing & Content"],
+ "resources": {
+ "credentialDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/credentials/quickchart"
+ }
+ ],
+ "primaryDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.quickchart/"
+ }
+ ]
+ },
+ "alias": ["image", "graph", "report", "chart", "diagram", "data", "visualize"]
+}
diff --git a/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts b/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts
new file mode 100644
index 0000000000000..9b36934cded67
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts
@@ -0,0 +1,430 @@
+import type {
+ IDataObject,
+ IExecuteFunctions,
+ IHttpRequestOptions,
+ IN8nHttpFullResponse,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+import { jsonParse, NodeOperationError } from 'n8n-workflow';
+
+import {
+ CHART_TYPE_OPTIONS,
+ Fill_CHARTS,
+ HORIZONTAL_CHARTS,
+ ITEM_STYLE_CHARTS,
+ POINT_STYLE_CHARTS,
+} from './constants';
+import type { IDataset } from './types';
+
+import _ from 'lodash';
+export class QuickChart implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'QuickChart',
+ name: 'quickChart',
+ icon: 'file:quickChart.svg',
+ group: ['output'],
+ description: 'Create a chart via QuickChart',
+ version: 1,
+ defaults: {
+ name: 'QuickChart',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ properties: [
+ {
+ displayName: 'Chart Type',
+ name: 'chartType',
+ type: 'options',
+ default: 'bar',
+ options: CHART_TYPE_OPTIONS,
+ description: 'The type of chart to create',
+ },
+ {
+ displayName: 'Add Labels',
+ name: 'labelsMode',
+ type: 'options',
+ options: [
+ {
+ name: 'Manually',
+ value: 'manually',
+ },
+ {
+ name: 'From Array',
+ value: 'array',
+ },
+ ],
+ default: 'manually',
+ },
+ {
+ displayName: 'Labels',
+ name: 'labelsUi',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ sortable: true,
+ },
+ default: {},
+ required: true,
+ description: 'Labels to use in the chart',
+ placeholder: 'Add Label',
+ options: [
+ {
+ name: 'labelsValues',
+ displayName: 'Labels',
+ values: [
+ {
+ displayName: 'Label',
+ name: 'label',
+ type: 'string',
+ default: '',
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ labelsMode: ['manually'],
+ },
+ },
+ },
+ {
+ displayName: 'Labels Array',
+ name: 'labelsArray',
+ type: 'string',
+ required: true,
+ default: '',
+ placeholder: 'e.g. ["Berlin", "Paris", "Rome", "New York"]',
+ displayOptions: {
+ show: {
+ labelsMode: ['array'],
+ },
+ },
+ description: 'The array of labels to be used in the chart',
+ },
+ {
+ displayName: 'Data',
+ name: 'data',
+ type: 'json',
+ default: '',
+ description:
+ 'Data to use for the dataset, documentation and examples here',
+ placeholder: 'e.g. [60, 10, 12, 20]',
+ required: true,
+ },
+ {
+ displayName: 'Put Output In Field',
+ name: 'output',
+ type: 'string',
+ default: 'data',
+ required: true,
+ description:
+ 'The binary data will be displayed in the Output panel on the right, under the Binary tab',
+ hint: 'The name of the output field to put the binary file data in',
+ },
+ {
+ displayName: 'Chart Options',
+ name: 'chartOptions',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ displayName: 'Background Color',
+ name: 'backgroundColor',
+ type: 'color',
+ typeOptions: {
+ showAlpha: true,
+ },
+ default: '',
+ description: 'Background color of the chart',
+ },
+ {
+ displayName: 'Device Pixel Ratio',
+ name: 'devicePixelRatio',
+ type: 'number',
+ default: 2,
+ typeOptions: {
+ minValue: 1,
+ maxValue: 2,
+ },
+ description: 'Pixel ratio of the chart',
+ },
+ {
+ displayName: 'Format',
+ name: 'format',
+ type: 'options',
+ default: 'png',
+ description: 'File format of the resulting chart',
+ options: [
+ {
+ name: 'PNG',
+ value: 'png',
+ },
+ {
+ name: 'PDF',
+ value: 'pdf',
+ },
+ {
+ name: 'SVG',
+ value: 'svg',
+ },
+ {
+ name: 'WebP',
+ value: 'webp',
+ },
+ ],
+ },
+ {
+ displayName: 'Height',
+ name: 'height',
+ type: 'number',
+ default: 300,
+ description: 'Height of the chart',
+ },
+ {
+ displayName: 'Horizontal',
+ name: 'horizontal',
+ type: 'boolean',
+ default: false,
+ description: 'Whether the chart should use its Y axis horizontal',
+ displayOptions: {
+ show: {
+ '/chartType': HORIZONTAL_CHARTS,
+ },
+ },
+ },
+ {
+ displayName: 'Width',
+ name: 'width',
+ type: 'number',
+ default: 500,
+ description: 'Width of the chart',
+ },
+ ],
+ },
+ {
+ displayName: 'Dataset Options',
+ name: 'datasetOptions',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ displayName: 'Background Color',
+ name: 'backgroundColor',
+ type: 'color',
+ default: '',
+ typeOptions: {
+ showAlpha: true,
+ },
+ description:
+ 'Color used for the background the dataset (area of a line graph, fill of a bar chart, etc.)',
+ },
+ {
+ displayName: 'Border Color',
+ name: 'borderColor',
+ type: 'color',
+ typeOptions: {
+ showAlpha: true,
+ },
+ default: '',
+ description: 'Color used for lines of the dataset',
+ },
+ {
+ displayName: 'Fill',
+ name: 'fill',
+ type: 'boolean',
+ default: true,
+ description: 'Whether to fill area of the dataset',
+ displayOptions: {
+ show: {
+ '/chartType': Fill_CHARTS,
+ },
+ },
+ },
+ {
+ displayName: 'Label',
+ name: 'label',
+ type: 'string',
+ default: '',
+ description: 'The label of the dataset',
+ },
+ {
+ displayName: 'Point Style',
+ name: 'pointStyle',
+ type: 'options',
+ default: 'circle',
+ description: 'Style to use for points of the dataset',
+ options: [
+ {
+ name: 'Circle',
+ value: 'circle',
+ },
+ {
+ name: 'Cross',
+ value: 'cross',
+ },
+ {
+ name: 'CrossRot',
+ value: 'crossRot',
+ },
+ {
+ name: 'Dash',
+ value: 'dash',
+ },
+ {
+ name: 'Line',
+ value: 'line',
+ },
+ {
+ name: 'Rect',
+ value: 'rect',
+ },
+ {
+ name: 'Rect Rot',
+ value: 'rectRot',
+ },
+ {
+ name: 'Rect Rounded',
+ value: 'rectRounded',
+ },
+ {
+ name: 'Star',
+ value: 'star',
+ },
+ {
+ name: 'Triangle',
+ value: 'triangle',
+ },
+ ],
+ displayOptions: {
+ show: {
+ '/chartType': POINT_STYLE_CHARTS,
+ },
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const datasets: IDataset[] = [];
+ let chartType = '';
+
+ const labels: string[] = [];
+ const labelsMode = this.getNodeParameter('labelsMode', 0) as string;
+
+ if (labelsMode === 'manually') {
+ const labelsUi = this.getNodeParameter('labelsUi.labelsValues', 0, []) as IDataObject[];
+
+ if (labelsUi.length) {
+ for (const labelValue of labelsUi as [{ label: string[] | string }]) {
+ if (Array.isArray(labelValue.label)) {
+ labels?.push(...labelValue.label);
+ } else {
+ labels?.push(labelValue.label);
+ }
+ }
+ }
+ } else {
+ const labelsArray = this.getNodeParameter('labelsArray', 0, '') as string;
+
+ const errorMessage =
+ 'Labels Array is not a valid array, use valid JSON format, or specify it by expressions';
+
+ if (Array.isArray(labelsArray)) {
+ labels.push(...labelsArray);
+ } else {
+ const labelsArrayParsed = jsonParse(labelsArray, {
+ errorMessage,
+ });
+ if (!Array.isArray(labelsArrayParsed)) {
+ throw new NodeOperationError(this.getNode(), errorMessage);
+ }
+ labels.push(...labelsArrayParsed);
+ }
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ const data = this.getNodeParameter('data', i) as string;
+ const datasetOptions = this.getNodeParameter('datasetOptions', i) as IDataObject;
+
+ const backgroundColor = datasetOptions.backgroundColor as string;
+ const borderColor = datasetOptions.borderColor as string | undefined;
+ const fill = datasetOptions.fill as boolean | undefined;
+ const label = (datasetOptions.label as string) || 'Chart';
+ const pointStyle = datasetOptions.pointStyle as string | undefined;
+
+ chartType = this.getNodeParameter('chartType', i) as string;
+
+ if (HORIZONTAL_CHARTS.includes(chartType)) {
+ const horizontal = this.getNodeParameter('chartOptions.horizontal', i, false) as boolean;
+ if (horizontal) {
+ chartType =
+ 'horizontal' + chartType[0].toUpperCase() + chartType.substring(1, chartType.length);
+ }
+ }
+
+ // Boxplots and Violins are an addon that uses the name 'itemStyle'
+ // instead of 'pointStyle'.
+ let pointStyleName = 'pointStyle';
+ if (ITEM_STYLE_CHARTS.includes(chartType)) {
+ pointStyleName = 'itemStyle';
+ }
+
+ datasets.push({
+ label,
+ data,
+ backgroundColor,
+ borderColor,
+ type: chartType,
+ fill,
+ [pointStyleName]: pointStyle,
+ });
+ }
+
+ const output = this.getNodeParameter('output', 0) as string;
+ const chartOptions = this.getNodeParameter('chartOptions', 0) as IDataObject;
+
+ const chart = {
+ type: chartType,
+ data: {
+ labels,
+ datasets,
+ },
+ };
+
+ const options: IHttpRequestOptions = {
+ method: 'GET',
+ url: 'https://quickchart.io/chart',
+ qs: {
+ chart: JSON.stringify(chart),
+ ...chartOptions,
+ },
+ returnFullResponse: true,
+ encoding: 'arraybuffer',
+ json: false,
+ };
+
+ const response = (await this.helpers.httpRequest(options)) as IN8nHttpFullResponse;
+ let mimeType = response.headers['content-type'] as string | undefined;
+ mimeType = mimeType ? mimeType.split(';').find((value) => value.includes('/')) : undefined;
+
+ return this.prepareOutputData([
+ {
+ binary: {
+ [output]: await this.helpers.prepareBinaryData(
+ response.body as Buffer,
+ undefined,
+ mimeType,
+ ),
+ },
+ json: { chart },
+ },
+ ]);
+ }
+}
diff --git a/packages/nodes-base/nodes/QuickChart/constants.ts b/packages/nodes-base/nodes/QuickChart/constants.ts
new file mode 100644
index 0000000000000..20d93ddd5796d
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/constants.ts
@@ -0,0 +1,30 @@
+import type { INodePropertyOptions } from 'n8n-workflow';
+
+// Disable some charts that use different datasets for now
+export const CHART_TYPE_OPTIONS: INodePropertyOptions[] = [
+ {
+ name: 'Bar Chart',
+ value: 'bar',
+ },
+ {
+ name: 'Doughnut Chart',
+ value: 'doughnut',
+ },
+ {
+ name: 'Line Chart',
+ value: 'line',
+ },
+ {
+ name: 'Pie Chart',
+ value: 'pie',
+ },
+ {
+ name: 'Polar Chart',
+ value: 'polarArea',
+ },
+];
+
+export const HORIZONTAL_CHARTS = ['bar', 'boxplot', 'violin'];
+export const ITEM_STYLE_CHARTS = ['boxplot', 'horizontalBoxplot', 'violin', 'horizontalViolin'];
+export const Fill_CHARTS = ['line'];
+export const POINT_STYLE_CHARTS = ['line'];
diff --git a/packages/nodes-base/nodes/QuickChart/quickChart.svg b/packages/nodes-base/nodes/QuickChart/quickChart.svg
new file mode 100644
index 0000000000000..0f2ae092a660c
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/quickChart.svg
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/packages/nodes-base/nodes/QuickChart/test/QuickChart.node.test.ts b/packages/nodes-base/nodes/QuickChart/test/QuickChart.node.test.ts
new file mode 100644
index 0000000000000..77f125fd51bc8
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/test/QuickChart.node.test.ts
@@ -0,0 +1,110 @@
+/* eslint-disable @typescript-eslint/no-loop-func */
+import * as Helpers from '../../../test/nodes/Helpers';
+import type { WorkflowTestData } from '../../../test/nodes/types';
+import { executeWorkflow } from '../../../test/nodes/ExecuteWorkflow';
+import nock from 'nock';
+
+describe('Test QuickChart Node', () => {
+ beforeEach(async () => {
+ await Helpers.initBinaryDataManager();
+ nock.disableNetConnect();
+ nock('https://quickchart.io')
+ .persist()
+ .get(/chart.*/)
+ .reply(200, { success: true });
+ });
+
+ afterEach(() => {
+ nock.restore();
+ });
+
+ const workflow = Helpers.readJsonFileSync('nodes/QuickChart/test/QuickChart.workflow.json');
+
+ const tests: WorkflowTestData[] = [
+ {
+ description: 'nodes/QuickChart/test/QuickChart.workflow.json',
+ input: {
+ workflowData: workflow,
+ },
+ output: {
+ nodeData: {
+ BarChart: [
+ [
+ {
+ json: {
+ chart: {
+ type: 'horizontalBar',
+ data: {
+ labels: ['Q1', 'Q2', 'Q3', 'Q4'],
+ datasets: [
+ {
+ label: 'Free Users',
+ data: [50, 60, 70, 180],
+ backgroundColor: '#121d6d77',
+ borderColor: '#e81010',
+ type: 'horizontalBar',
+ },
+ {
+ label: 'Paid Users',
+ data: [30, 10, 14, 25],
+ backgroundColor: '#0c0d0d96',
+ borderColor: '#e81010',
+ type: 'horizontalBar',
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ ],
+ Doughnut: [
+ [
+ {
+ json: {
+ chart: {
+ type: 'doughnut',
+ data: {
+ labels: ['Q1', 'Q2', 'Q3', 'Q4'],
+ datasets: [
+ {
+ label: 'Free Users',
+ data: [50, 60, 70, 180],
+ backgroundColor: '#121d6d77',
+ borderColor: '#e81010',
+ type: 'doughnut',
+ },
+ {
+ label: 'Paid Users',
+ data: [30, 10, 14, 25],
+ backgroundColor: '#0c0d0d96',
+ borderColor: '#e81010',
+ type: 'doughnut',
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ];
+
+ const nodeTypes = Helpers.setup(tests);
+
+ for (const testData of tests) {
+ test(testData.description, async () => {
+ const { result } = await executeWorkflow(testData, nodeTypes);
+
+ const resultNodeData = Helpers.getResultNodeData(result, testData);
+ resultNodeData.forEach(({ nodeName, resultData }) => {
+ delete resultData[0]![0].binary;
+ expect(resultData).toEqual(testData.output.nodeData[nodeName]);
+ });
+ expect(result.finished).toEqual(true);
+ });
+ }
+});
diff --git a/packages/nodes-base/nodes/QuickChart/test/QuickChart.workflow.json b/packages/nodes-base/nodes/QuickChart/test/QuickChart.workflow.json
new file mode 100644
index 0000000000000..73fbcc3ead1f0
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/test/QuickChart.workflow.json
@@ -0,0 +1,132 @@
+{
+ "meta": {
+ "instanceId": "104a4d08d8897b8bdeb38aaca515021075e0bd8544c983c2bb8c86e6a8e6081c"
+ },
+ "nodes": [
+ {
+ "parameters": {},
+ "id": "5ebe2f65-45db-4b22-bd2b-43993c20806f",
+ "name": "When clicking \"Execute Workflow\"",
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [740, 320]
+ },
+ {
+ "parameters": {
+ "jsCode": "return [\n {\n label: 'Free Users',\n labels: [\"Berlin\", \"Paris\", \"Rome\", \"New York\"],\n data: [50, 60, 70, 180],\n backgroundColor: '#121d6d77',\n chartType: 'line'\n },\n {\n label: 'Paid Users',\n labels: [\"Berlin\", \"Paris\", \"Rome\", \"New York\"],\n data: [30, 10, 14, 25],\n backgroundColor: '#0c0d0d96',\n chartType: 'bar'\n },\n]"
+ },
+ "id": "2e81f78c-41a5-48de-80c4-74abf163cd57",
+ "name": "Code",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 1,
+ "position": [980, 320]
+ },
+ {
+ "parameters": {
+ "labelsUi": {
+ "labelsValues": [
+ {
+ "label": "Q1"
+ },
+ {
+ "label": "Q2"
+ },
+ {
+ "label": "Q3"
+ },
+ {
+ "label": "Q4"
+ }
+ ]
+ },
+ "data": "={{ $json.data }}",
+ "chartOptions": {
+ "backgroundColor": "#f93636ff",
+ "devicePixelRatio": 2,
+ "format": "png",
+ "height": 300,
+ "horizontal": true,
+ "width": 500
+ },
+ "datasetOptions": {
+ "backgroundColor": "={{ $json[\"backgroundColor\"] }}",
+ "borderColor": "#e81010",
+ "label": "={{ $json[\"label\"] }}"
+ }
+ },
+ "name": "BarChart",
+ "type": "n8n-nodes-base.quickChart",
+ "typeVersion": 1,
+ "position": [1220, 200],
+ "id": "9f6c9d1c-2732-473f-a357-5766265cd0db"
+ },
+ {
+ "parameters": {
+ "chartType": "doughnut",
+ "labelsUi": {
+ "labelsValues": [
+ {
+ "label": "Q1"
+ },
+ {
+ "label": "Q2"
+ },
+ {
+ "label": "Q3"
+ },
+ {
+ "label": "Q4"
+ }
+ ]
+ },
+ "data": "={{ $json.data }}",
+ "chartOptions": {
+ "backgroundColor": "#f93636ff",
+ "devicePixelRatio": 2,
+ "format": "png",
+ "height": 300,
+ "width": 500
+ },
+ "datasetOptions": {
+ "backgroundColor": "={{ $json[\"backgroundColor\"] }}",
+ "borderColor": "#e81010",
+ "label": "={{ $json[\"label\"] }}"
+ }
+ },
+ "name": "Doughnut",
+ "type": "n8n-nodes-base.quickChart",
+ "typeVersion": 1,
+ "position": [1220, 400],
+ "id": "6c8e1463-c384-4f5c-9de3-d7e052b02b0a"
+ }
+ ],
+ "connections": {
+ "When clicking \"Execute Workflow\"": {
+ "main": [
+ [
+ {
+ "node": "Code",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Code": {
+ "main": [
+ [
+ {
+ "node": "BarChart",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Doughnut",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ }
+}
diff --git a/packages/nodes-base/nodes/QuickChart/types.ts b/packages/nodes-base/nodes/QuickChart/types.ts
new file mode 100644
index 0000000000000..11c1b07e15053
--- /dev/null
+++ b/packages/nodes-base/nodes/QuickChart/types.ts
@@ -0,0 +1,12 @@
+import type { IDataObject } from 'n8n-workflow';
+
+export interface IDataset {
+ label?: string;
+ data: string | IDataObject;
+ backgroundColor?: string;
+ borderColor?: string;
+ color?: string;
+ type?: string;
+ fill?: boolean;
+ pointStyle?: string;
+}
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index c5c3887511db8..d638228998dca 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -621,6 +621,7 @@
"dist/nodes/QuestDb/QuestDb.node.js",
"dist/nodes/QuickBase/QuickBase.node.js",
"dist/nodes/QuickBooks/QuickBooks.node.js",
+ "dist/nodes/QuickChart/QuickChart.node.js",
"dist/nodes/RabbitMQ/RabbitMQ.node.js",
"dist/nodes/RabbitMQ/RabbitMQTrigger.node.js",
"dist/nodes/Raindrop/Raindrop.node.js",