mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
233f1fa7ec
* ✨ 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>
431 lines
9.8 KiB
TypeScript
431 lines
9.8 KiB
TypeScript
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 },
|
|
},
|
|
]);
|
|
}
|
|
}
|