mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat(QuickChart Node): Add QuickChart node (#3572)
* ✨ Add basic QuickChart node * 🏷️ Fix up types * ❇️ Add Boxplot and Violin * ❇️ Add point styles * ❇️ Add horizontal charts * ⚡ Make possible to provide array of labels via expressions * ⚡ Improvements * ⚡ Improvements * 🎨 fix lint errors * ⚡️disable chart types we don't want to support in P0 * ⚡️support setting labels manually or using an array * ⚡️move Horizontal parameter into options * ⚡️ update "Put Output In Field" param description and hint * ⚡️ removed font color * ⚡️fix Device Pixel Ratio * ⚡️fix Polar Chart not working * ⚡️Show Fill param only for charts supporting it * ⚡️Show pointStyle param only for charts supporting it * ⚡️remove second "Chart Type" option * ⚡ updated error message, added json data, updated description * Add codex json file * ✅ add unit test * ✅ improve unit test * ⚡ removed any, added aliases --------- Co-authored-by: ricardo <ricardoespinoza105@gmail.com> Co-authored-by: Marcus <marcus@n8n.io> Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
6a8c9b7ccc
commit
233f1fa7ec
19
packages/nodes-base/nodes/QuickChart/QuickChart.node.json
Normal file
19
packages/nodes-base/nodes/QuickChart/QuickChart.node.json
Normal file
|
@ -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"]
|
||||
}
|
430
packages/nodes-base/nodes/QuickChart/QuickChart.node.ts
Normal file
430
packages/nodes-base/nodes/QuickChart/QuickChart.node.ts
Normal file
|
@ -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 <a href="https://quickchart.io/documentation/chart-types/" target="_blank">here</a>',
|
||||
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<INodeExecutionData[][]> {
|
||||
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<string[]>(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 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
30
packages/nodes-base/nodes/QuickChart/constants.ts
Normal file
30
packages/nodes-base/nodes/QuickChart/constants.ts
Normal file
|
@ -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'];
|
14
packages/nodes-base/nodes/QuickChart/quickChart.svg
Normal file
14
packages/nodes-base/nodes/QuickChart/quickChart.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="445" height="445" viewBox="0 0 445 445" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g id="icon" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-2.4722222222222285 -2.4722222222222285) scale(4.94 4.94)" >
|
||||
<path d="M 40.135 90 h -8.782 c -1.519 0 -2.75 -1.231 -2.75 -2.75 V 45.381 c 0 -1.519 1.231 -2.75 2.75 -2.75 h 8.782 c 1.519 0 2.75 1.231 2.75 2.75 V 87.25 C 42.885 88.769 41.654 90 40.135 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,195,110); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 58.647 90 h -8.782 c -1.519 0 -2.75 -1.231 -2.75 -2.75 V 31.449 c 0 -1.519 1.231 -2.75 2.75 -2.75 h 8.782 c 1.519 0 2.75 1.231 2.75 2.75 V 87.25 C 61.397 88.769 60.165 90 58.647 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(165,215,110); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 21.624 90 h -8.782 c -1.519 0 -2.75 -1.231 -2.75 -2.75 V 59.313 c 0 -1.519 1.231 -2.75 2.75 -2.75 h 8.782 c 1.519 0 2.75 1.231 2.75 2.75 V 87.25 C 24.374 88.769 23.142 90 21.624 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(210,85,90); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 77.158 90 h -8.782 c -1.519 0 -2.75 -1.231 -2.75 -2.75 V 17.517 c 0 -1.519 1.231 -2.75 2.75 -2.75 h 8.782 c 1.519 0 2.75 1.231 2.75 2.75 V 87.25 C 79.908 88.769 78.677 90 77.158 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(120,210,190); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 67.773 17.977 h 18.183 c 1.787 0 2.682 -2.258 1.418 -3.578 L 74.185 0.614 c -0.783 -0.819 -2.053 -0.819 -2.836 0 L 58.16 14.398 c -1.264 1.321 -0.369 3.578 1.418 3.578 L 67.773 17.977" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(120,210,190); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
12
packages/nodes-base/nodes/QuickChart/types.ts
Normal file
12
packages/nodes-base/nodes/QuickChart/types.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue