node-red-contrib-prib-functions 0.23.3 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.github/copilot-instructions.md +36 -0
  2. package/README.md +15 -0
  3. package/columnar/columnar.html +258 -0
  4. package/columnar/columnar.js +1055 -0
  5. package/columnar/icons/columnar.svg +38 -0
  6. package/fileSystem/filesystem.html +299 -0
  7. package/fileSystem/filesystem.js +170 -0
  8. package/gitlab/gitlab.html +191 -0
  9. package/gitlab/gitlab.js +248 -0
  10. package/gitlab/icons/gitlab.svg +17 -0
  11. package/lib/typedInput.js +18 -2
  12. package/logisticRegression/icons/logisticregression.svg +22 -0
  13. package/logisticRegression/logisticRegression.html +136 -0
  14. package/logisticRegression/logisticRegression.js +83 -0
  15. package/package.json +19 -8
  16. package/test/columnar.js +509 -0
  17. package/test/data/.config.nodes.json +114 -70
  18. package/test/data/.config.nodes.json.backup +104 -71
  19. package/test/data/.config.runtime.json +2 -1
  20. package/test/data/.config.runtime.json.backup +2 -1
  21. package/test/data/.config.users.json +3 -2
  22. package/test/data/.config.users.json.backup +3 -2
  23. package/test/data/.flow.json.backup +561 -5
  24. package/test/data/flow.json +571 -2
  25. package/test/data/package-lock.json +1 -1
  26. package/test/data/shares/.config.nodes.json +74 -52
  27. package/test/data/shares/.config.nodes.json.backup +589 -0
  28. package/test/data/shares/.config.runtime.json +2 -1
  29. package/test/data/shares/.config.runtime.json.backup +2 -1
  30. package/test/data/shares/.config.users.json +3 -2
  31. package/test/data/shares/.config.users.json.backup +5 -1
  32. package/test/dataAnalysisExtensions.js +93 -93
  33. package/test/logisticRegression.js +379 -0
  34. package/test/transform.js +11 -11
  35. package/test/transformConfluence.js +4 -2
  36. package/test/transformNumPy.js +3 -1
  37. package/test/transformXLSX.js +4 -2
  38. package/test/transformXML.js +4 -2
  39. package/test-runner.js +400 -0
  40. package/test.parq +0 -0
  41. package/test_select.js +37 -0
@@ -0,0 +1,191 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('gitlab', {
3
+ category: 'function',
4
+ color: '#FC6D26',
5
+ defaults: {
6
+ name: {value:"", required:false},
7
+ action: {value:"getRepo", required:true},
8
+ gitlabUrl: {value:"https://gitlab.com", required:true},
9
+ accessToken: {value:"", required:true},
10
+ projectId: {value:"", required:true},
11
+ filePath: {value:"", required:false}
12
+ },
13
+ inputs: 1,
14
+ inputLabels: "in",
15
+ outputs:1,
16
+ outputLabels: ["Result"],
17
+ paletteLabel: "GitLab",
18
+ icon: "gitlab.svg",
19
+ label: function() {
20
+ return this.name || (this.action + " GitLab");
21
+ },
22
+ labelStyle: function() {
23
+ return "node_label_italic";
24
+ },
25
+ oneditprepare: function() {
26
+ const node = this;
27
+ $("#node-input-action").change(function() {
28
+ const action = $(this).val();
29
+ if (action === 'getFile') {
30
+ $("#file-path-row").show();
31
+ } else {
32
+ $("#file-path-row").hide();
33
+ }
34
+ });
35
+ $("#node-input-action").change(); // trigger on load
36
+ }
37
+ });
38
+ </script>
39
+
40
+ <script type="text/x-red" data-template-name="gitlab">
41
+ <div class="form-row">
42
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
43
+ <input type="text" id="node-input-name" placeholder="Name">
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-action"><i class="fa fa-cogs"></i> Action</label>
47
+ <select type="text" id="node-input-action">
48
+ <option value="getRepo">Get Repository Info</option>
49
+ <option value="listIssues">List Issues</option>
50
+ <option value="createIssue">Create Issue</option>
51
+ <option value="getMergeRequests">Get Merge Requests</option>
52
+ <option value="createMergeRequest">Create Merge Request</option>
53
+ <option value="getFile">Get File</option>
54
+ <option value="runPipeline">Run Pipeline</option>
55
+ <option value="createCommit">Create Commit</option>
56
+ <option value="getCommits">Get Commits</option>
57
+ <option value="getCommit">Get Commit</option>
58
+ </select>
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-gitlabUrl">GitLab URL</label>
62
+ <input type="text" id="node-input-gitlabUrl" placeholder="https://gitlab.com">
63
+ </div>
64
+ <div class="form-row">
65
+ <label for="node-input-accessToken">Access Token</label>
66
+ <input type="password" id="node-input-accessToken" placeholder="GitLab personal access token">
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-projectId">Project ID</label>
70
+ <input type="text" id="node-input-projectId" placeholder="project-id or namespace/project-name">
71
+ </div>
72
+ <div class="form-row" id="file-path-row" style="display:none;">
73
+ <label for="node-input-filePath">File Path</label>
74
+ <input type="text" id="node-input-filePath" placeholder="path/to/file.txt">
75
+ </div>
76
+ </script>
77
+
78
+ <script type="text/x-red" data-help-name="gitlab">
79
+ <p>GitLab API access node for performing various operations on GitLab repositories.</p>
80
+
81
+ <h3>Configuration</h3>
82
+ <ul>
83
+ <li><b>GitLab URL:</b> The GitLab instance URL (default: https://gitlab.com)</li>
84
+ <li><b>Access Token:</b> GitLab personal access token with appropriate permissions</li>
85
+ <li><b>Project ID:</b> The project identifier (numeric ID or namespace/project-name)</li>
86
+ <li><b>File Path:</b> Path to file in repository (only shown for Get File action)</li>
87
+ </ul>
88
+
89
+ <h3>Actions</h3>
90
+ <ul>
91
+ <li><b>Get Repository Info:</b> Retrieves basic information about the repository</li>
92
+ <li><b>List Issues:</b> Lists issues in the repository. Optional filters in msg.payload: state, labels, assignee_id</li>
93
+ <li><b>Create Issue:</b> Creates a new issue. Required in msg.payload: title. Optional: description, labels, assignee_ids</li>
94
+ <li><b>Get Merge Requests:</b> Lists merge requests. Optional filters in msg.payload: state, source_branch, target_branch</li>
95
+ <li><b>Create Merge Request:</b> Creates a new merge request. Required in msg.payload: title, source_branch, target_branch. Optional: description, assignee_id, labels</li>
96
+ <li><b>Get File:</b> Retrieves file content from repository. File path can be in msg.payload.file_path or configured in node. Optional: ref (branch/tag/commit)</li>
97
+ <li><b>Run Pipeline:</b> Triggers a CI/CD pipeline. Optional in msg.payload: ref (default: main), variables (array of {key, value})</li>
98
+ <li><b>Create Commit:</b> Creates a new commit with file changes. Required in msg.payload: branch, commit_message, actions (array of file operations)</li>
99
+ <li><b>Get Commits:</b> Lists commits in repository. Optional filters in msg.payload: ref_name, since, until, path, author, per_page</li>
100
+ <li><b>Get Commit:</b> Retrieves details of a specific commit. Required in msg.payload: commit_sha</li>
101
+ </ul>
102
+
103
+ <h3>Authentication</h3>
104
+ <p>You need a GitLab personal access token with appropriate scopes:
105
+ <ul>
106
+ <li>api - for full API access</li>
107
+ <li>read_repository - for read operations</li>
108
+ <li>write_repository - for write operations</li>
109
+ </ul>
110
+ Create tokens at: GitLab Profile → Access Tokens</p>
111
+
112
+ <h3>Project ID</h3>
113
+ <p>The project ID can be found in the repository URL or project settings. It can be:
114
+ <ul>
115
+ <li>Numeric ID (e.g., 12345)</li>
116
+ <li>URL-encoded path (e.g., namespace%2Fproject-name)</li>
117
+ </ul></p>
118
+
119
+ <h3>Examples</h3>
120
+
121
+ <h4>Get Repository Info</h4>
122
+ <pre>Input: (any)
123
+ Output: Repository object with id, name, description, etc.</pre>
124
+
125
+ <h4>Create Issue</h4>
126
+ <pre>Input:
127
+ {
128
+ "title": "Bug report",
129
+ "description": "Found a critical bug",
130
+ "labels": ["bug", "critical"],
131
+ "assignee_ids": [123]
132
+ }
133
+ Output: Created issue object</pre>
134
+
135
+ <h4>Get File</h4>
136
+ <pre>Input:
137
+ {
138
+ "file_path": "README.md",
139
+ "ref": "main"
140
+ }
141
+ Output: File object with content, encoding, size, etc.</pre>
142
+
143
+ <h4>Run Pipeline</h4>
144
+ <pre>Input:
145
+ {
146
+ "ref": "develop",
147
+ "variables": [
148
+ {"key": "ENV", "value": "staging"}
149
+ ]
150
+ }
151
+ Output: Pipeline object with id, status, etc.</pre>
152
+
153
+ <h4>Create Commit</h4>
154
+ <pre>Input:
155
+ {
156
+ "branch": "main",
157
+ "commit_message": "Update README.md",
158
+ "actions": [
159
+ {
160
+ "action": "update",
161
+ "file_path": "README.md",
162
+ "content": "Updated content here..."
163
+ }
164
+ ],
165
+ "author_email": "user@example.com",
166
+ "author_name": "User Name"
167
+ }
168
+ Output: Commit object with id, short_id, title, etc.</pre>
169
+
170
+ <h4>Get Commits</h4>
171
+ <pre>Input:
172
+ {
173
+ "ref_name": "main",
174
+ "per_page": 10,
175
+ "author": "user@example.com"
176
+ }
177
+ Output: Array of commit objects</pre>
178
+
179
+ <h4>Get Commit</h4>
180
+ <pre>Input:
181
+ {
182
+ "commit_sha": "abc123..."
183
+ }
184
+ Output: Detailed commit object with stats, files, etc.</pre>
185
+
186
+ <h3>Output</h3>
187
+ <p>Results are returned in msg.result. The structure depends on the action performed.</p>
188
+
189
+ <h3>Error Handling</h3>
190
+ <p>Errors from the GitLab API are caught and reported. Check msg.error for details.</p>
191
+ </script>
@@ -0,0 +1,248 @@
1
+ const logger = new (require("node-red-contrib-logger"))("GitLab Access");
2
+ logger.sendInfo("Copyright 2025 Jaroslav Peter Prib");
3
+
4
+ const axios = require('axios');
5
+
6
+ const actions = {
7
+ getRepo: async (RED, node, msg) => {
8
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}`;
9
+ const response = await axios.get(url, {
10
+ headers: {
11
+ 'Private-Token': node.accessToken,
12
+ 'User-Agent': 'Node-RED-GitLab-Node'
13
+ }
14
+ });
15
+ return response.data;
16
+ },
17
+
18
+ listIssues: async (RED, node, msg) => {
19
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/issues`;
20
+ const params = {};
21
+ if (msg.payload && msg.payload.state) params.state = msg.payload.state;
22
+ if (msg.payload && msg.payload.labels) params.labels = msg.payload.labels;
23
+ if (msg.payload && msg.payload.assignee_id) params.assignee_id = msg.payload.assignee_id;
24
+
25
+ const response = await axios.get(url, {
26
+ headers: {
27
+ 'Private-Token': node.accessToken,
28
+ 'User-Agent': 'Node-RED-GitLab-Node'
29
+ },
30
+ params
31
+ });
32
+ return response.data;
33
+ },
34
+
35
+ createIssue: async (RED, node, msg) => {
36
+ if (!msg.payload || !msg.payload.title) {
37
+ throw new Error("msg.payload must contain title for issue creation");
38
+ }
39
+
40
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/issues`;
41
+ const data = {
42
+ title: msg.payload.title,
43
+ description: msg.payload.description || '',
44
+ labels: msg.payload.labels || [],
45
+ assignee_ids: msg.payload.assignee_ids || []
46
+ };
47
+
48
+ const response = await axios.post(url, data, {
49
+ headers: {
50
+ 'Private-Token': node.accessToken,
51
+ 'User-Agent': 'Node-RED-GitLab-Node'
52
+ }
53
+ });
54
+ return response.data;
55
+ },
56
+
57
+ getMergeRequests: async (RED, node, msg) => {
58
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/merge_requests`;
59
+ const params = {};
60
+ if (msg.payload && msg.payload.state) params.state = msg.payload.state;
61
+ if (msg.payload && msg.payload.source_branch) params.source_branch = msg.payload.source_branch;
62
+ if (msg.payload && msg.payload.target_branch) params.target_branch = msg.payload.target_branch;
63
+
64
+ const response = await axios.get(url, {
65
+ headers: {
66
+ 'Private-Token': node.accessToken,
67
+ 'User-Agent': 'Node-RED-GitLab-Node'
68
+ },
69
+ params
70
+ });
71
+ return response.data;
72
+ },
73
+
74
+ createMergeRequest: async (RED, node, msg) => {
75
+ if (!msg.payload || !msg.payload.title || !msg.payload.source_branch || !msg.payload.target_branch) {
76
+ throw new Error("msg.payload must contain title, source_branch, and target_branch for MR creation");
77
+ }
78
+
79
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/merge_requests`;
80
+ const data = {
81
+ title: msg.payload.title,
82
+ description: msg.payload.description || '',
83
+ source_branch: msg.payload.source_branch,
84
+ target_branch: msg.payload.target_branch,
85
+ assignee_id: msg.payload.assignee_id,
86
+ labels: msg.payload.labels || []
87
+ };
88
+
89
+ const response = await axios.post(url, data, {
90
+ headers: {
91
+ 'Private-Token': node.accessToken,
92
+ 'User-Agent': 'Node-RED-GitLab-Node'
93
+ }
94
+ });
95
+ return response.data;
96
+ },
97
+
98
+ getFile: async (RED, node, msg) => {
99
+ const filePath = msg.payload && msg.payload.file_path ? msg.payload.file_path : node.filePath;
100
+ if (!filePath) {
101
+ throw new Error("file_path must be provided in msg.payload or configured in node");
102
+ }
103
+
104
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/repository/files/${encodeURIComponent(filePath)}`;
105
+ const params = {};
106
+ if (msg.payload && msg.payload.ref) params.ref = msg.payload.ref;
107
+ else params.ref = 'main';
108
+
109
+ const response = await axios.get(url, {
110
+ headers: {
111
+ 'Private-Token': node.accessToken,
112
+ 'User-Agent': 'Node-RED-GitLab-Node'
113
+ },
114
+ params
115
+ });
116
+ return {
117
+ file_path: response.data.file_path,
118
+ file_name: response.data.file_name,
119
+ size: response.data.size,
120
+ encoding: response.data.encoding,
121
+ content: Buffer.from(response.data.content, response.data.encoding).toString('utf8'),
122
+ ref: response.data.ref,
123
+ blob_id: response.data.blob_id,
124
+ commit_id: response.data.commit_id,
125
+ last_commit_id: response.data.last_commit_id
126
+ };
127
+ },
128
+
129
+ runPipeline: async (RED, node, msg) => {
130
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/pipeline`;
131
+ const data = {
132
+ ref: msg.payload && msg.payload.ref ? msg.payload.ref : 'main',
133
+ variables: msg.payload && msg.payload.variables ? msg.payload.variables : []
134
+ };
135
+
136
+ const response = await axios.post(url, data, {
137
+ headers: {
138
+ 'Private-Token': node.accessToken,
139
+ 'User-Agent': 'Node-RED-GitLab-Node'
140
+ }
141
+ });
142
+ return response.data;
143
+ },
144
+
145
+ createCommit: async (RED, node, msg) => {
146
+ if (!msg.payload || !msg.payload.branch || !msg.payload.commit_message || !msg.payload.actions) {
147
+ throw new Error("msg.payload must contain branch, commit_message, and actions for commit creation");
148
+ }
149
+
150
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/repository/commits`;
151
+ const data = {
152
+ branch: msg.payload.branch,
153
+ commit_message: msg.payload.commit_message,
154
+ actions: msg.payload.actions,
155
+ author_email: msg.payload.author_email,
156
+ author_name: msg.payload.author_name
157
+ };
158
+
159
+ const response = await axios.post(url, data, {
160
+ headers: {
161
+ 'Private-Token': node.accessToken,
162
+ 'User-Agent': 'Node-RED-GitLab-Node'
163
+ }
164
+ });
165
+ return response.data;
166
+ },
167
+
168
+ getCommits: async (RED, node, msg) => {
169
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/repository/commits`;
170
+ const params = {};
171
+ if (msg.payload && msg.payload.ref_name) params.ref_name = msg.payload.ref_name;
172
+ if (msg.payload && msg.payload.since) params.since = msg.payload.since;
173
+ if (msg.payload && msg.payload.until) params.until = msg.payload.until;
174
+ if (msg.payload && msg.payload.path) params.path = msg.payload.path;
175
+ if (msg.payload && msg.payload.author) params.author = msg.payload.author;
176
+ params.per_page = msg.payload && msg.payload.per_page ? msg.payload.per_page : 20;
177
+
178
+ const response = await axios.get(url, {
179
+ headers: {
180
+ 'Private-Token': node.accessToken,
181
+ 'User-Agent': 'Node-RED-GitLab-Node'
182
+ },
183
+ params
184
+ });
185
+ return response.data;
186
+ },
187
+
188
+ getCommit: async (RED, node, msg) => {
189
+ const commitSha = msg.payload && msg.payload.commit_sha ? msg.payload.commit_sha : '';
190
+ if (!commitSha) {
191
+ throw new Error("commit_sha must be provided in msg.payload");
192
+ }
193
+
194
+ const url = `${node.gitlabUrl}/api/v4/projects/${encodeURIComponent(node.projectId)}/repository/commits/${commitSha}`;
195
+
196
+ const response = await axios.get(url, {
197
+ headers: {
198
+ 'Private-Token': node.accessToken,
199
+ 'User-Agent': 'Node-RED-GitLab-Node'
200
+ }
201
+ });
202
+ return response.data;
203
+ }
204
+
205
+ module.exports = function (RED) {
206
+ function GitLabNode(config) {
207
+ RED.nodes.createNode(this, config);
208
+ const node = Object.assign(this, config, {
209
+ gitlabUrl: config.gitlabUrl || 'https://gitlab.com',
210
+ accessToken: config.accessToken || '',
211
+ projectId: config.projectId || '',
212
+ filePath: config.filePath || ''
213
+ });
214
+
215
+ node.callFunction = actions[config.action];
216
+ if (!node.callFunction) {
217
+ node.error("Unknown action: " + config.action);
218
+ node.status({ fill: "red", shape: "ring", text: "Unknown action: " + config.action });
219
+ return;
220
+ }
221
+
222
+ if (!node.accessToken) {
223
+ node.error("GitLab access token is required");
224
+ node.status({ fill: "red", shape: "ring", text: "Access token required" });
225
+ return;
226
+ }
227
+
228
+ if (!node.projectId) {
229
+ node.error("GitLab project ID is required");
230
+ node.status({ fill: "red", shape: "ring", text: "Project ID required" });
231
+ return;
232
+ }
233
+
234
+ node.status({ fill: "green", shape: "dot", text: "Ready" });
235
+
236
+ node.on('input', async function (msg) {
237
+ try {
238
+ msg.result = await node.callFunction(RED, node, msg);
239
+ node.send(msg);
240
+ node.status({ fill: "green", shape: "dot", text: "Success" });
241
+ } catch (error) {
242
+ node.error(error.message, msg);
243
+ node.status({ fill: "red", shape: "ring", text: error.message.substring(0, 20) });
244
+ }
245
+ });
246
+ }
247
+ RED.nodes.registerType("gitlab", GitLabNode);
248
+ };
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <defs>
3
+ <linearGradient id="gitlabGradient" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#FC6D26;stop-opacity:1" />
5
+ <stop offset="100%" style="stop-color:#E24329;stop-opacity:1" />
6
+ </linearGradient>
7
+ </defs>
8
+
9
+ <!-- GitLab fox-like logo simplified -->
10
+ <path d="M16 2C8.3 2 2 8.3 2 16s6.3 14 14 14 14-6.3 14-14S23.7 2 16 2zm6.5 20.5c-.3.3-.7.3-1 0L16 17l-5.5 5.5c-.3.3-.7.3-1 0s-.3-.7 0-1L15 16l-5.5-5.5c-.3-.3-.3-.7 0-1s.7-.3 1 0L16 15l5.5-5.5c.3-.3.7-.3 1 0s.3.7 0 1L17 16l5.5 5.5c.3.3.3.7 0 1z" fill="url(#gitlabGradient)"/>
11
+
12
+ <!-- Simplified fox face -->
13
+ <circle cx="16" cy="12" r="2" fill="#FFFFFF"/>
14
+ <circle cx="13" cy="10" r="1" fill="#FFFFFF"/>
15
+ <circle cx="19" cy="10" r="1" fill="#FFFFFF"/>
16
+ <path d="M14 14c1-1 2-1 3 0" stroke="#FFFFFF" stroke-width="1" fill="none" stroke-linecap="round"/>
17
+ </svg>
package/lib/typedInput.js CHANGED
@@ -1,3 +1,4 @@
1
+ const { get } = require("http");
1
2
 
2
3
  const setGetFunction=(RED,node,propertyName)=>node["get"+propertyName[0].toUpperCase() + propertyName.slice(1)]=getFunction(RED,node,propertyName)
3
4
 
@@ -69,9 +70,24 @@ const setFunction=(RED,node,name)=>{
69
70
 
70
71
  const getValue=(RED,node,propertyName,defaultValue)=>propertyName in node?getFunction(RED,node,propertyName)():defaultValue
71
72
 
73
+ const getArgFunctions=(RED,node,msg,propertyNames)=>{
74
+ const args=[]
75
+ propertyNames.forEach(property => {
76
+ args.push(node["get"+property])
77
+ });
78
+ return args
79
+ }
80
+
81
+ const getArgs=(RED,node,msg,propertyNames,argFunctions=getArgFunctions(RED,node,propertyNames))=>
82
+ argFunctions.reduce((args,propertyFunction)=>propertyFunction(RED,node,msg),[])
83
+ const getArgsFromFunctions=(RED,node,msg,argFunctions)=>argFunctions.reduce((args,propertyFunction)=>propertyFunction(RED,node,msg),[])
84
+
72
85
  module.exports={
86
+ getArgs:getArgs,
87
+ getArgsFromFunctions:getArgsFromFunctions,
88
+ getArgFunctions:getArgFunctions,
73
89
  getFunction:getFunction,
90
+ getValue:getValue,
74
91
  setFunction:setFunction,
75
- setGetFunction:setGetFunction,
76
- getValue:getValue
92
+ setGetFunction:setGetFunction
77
93
  }
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <!-- Background -->
3
+ <rect width="32" height="32" fill="none"/>
4
+
5
+ <!-- Axes -->
6
+ <line x1="4" y1="24" x2="28" y2="24" stroke="#666" stroke-width="1"/>
7
+ <line x1="4" y1="4" x2="4" y2="24" stroke="#666" stroke-width="1"/>
8
+
9
+ <!-- Sigmoid curve (logistic function) -->
10
+ <path d="M 4 22 Q 8 20, 12 18 Q 16 14, 16 12 Q 16 10, 20 6 Q 24 4, 28 4"
11
+ fill="none" stroke="#0066cc" stroke-width="2" stroke-linecap="round"/>
12
+
13
+ <!-- Data points -->
14
+ <circle cx="8" cy="20" r="1.5" fill="#ff6600"/>
15
+ <circle cx="12" cy="16" r="1.5" fill="#ff6600"/>
16
+ <circle cx="16" cy="12" r="1.5" fill="#ff6600"/>
17
+ <circle cx="20" cy="8" r="1.5" fill="#ff6600"/>
18
+ <circle cx="24" cy="5" r="1.5" fill="#ff6600"/>
19
+
20
+ <!-- Horizontal dashed line at 0.5 threshold -->
21
+ <line x1="4" y1="14" x2="28" y2="14" stroke="#999" stroke-width="0.5" stroke-dasharray="2,2"/>
22
+ </svg>
@@ -0,0 +1,136 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('logisticRegression', {
3
+ category: 'function',
4
+ color: '#a6bbcf',
5
+ defaults: {
6
+ name: {value:"", required:false},
7
+ modelName: {value:"", required:true},
8
+ action: {value:"predict", required:true},
9
+ learningRate: {value:0.1, required:false},
10
+ iterations: {value:2000, required:false},
11
+ fitIntercept: {value:true, required:false},
12
+ l2: {value:0.0, required:false},
13
+ tolerance: {value:1e-7, required:false},
14
+ verbose: {value:false, required:false},
15
+ threshold: {value:0.5, required:false}
16
+ },
17
+ inputs: 1,
18
+ inputLabels: "in",
19
+ outputs:1,
20
+ outputLabels: ["Result"],
21
+ paletteLabel: "Logistic Regression",
22
+ icon: "logisticregression.svg",
23
+ label: function() {
24
+ return this.name || (this.action + " Logistic Regression");
25
+ },
26
+ labelStyle: function() {
27
+ return "node_label_italic";
28
+ },
29
+ oneditprepare: function() {
30
+ const node = this;
31
+ $("#node-input-action").change(function() {
32
+ const action = $(this).val();
33
+ if (action === 'fit') {
34
+ $("#fit-params").show();
35
+ $("#predict-params").hide();
36
+ } else {
37
+ $("#fit-params").hide();
38
+ $("#predict-params").show();
39
+ }
40
+ });
41
+ $("#node-input-action").change(); // trigger on load
42
+ }
43
+ });
44
+ </script>
45
+
46
+ <script type="text/x-red" data-template-name="logisticRegression">
47
+ <div class="form-row">
48
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
49
+ <input type="text" id="node-input-name" placeholder="Name">
50
+ </div>
51
+ <div class="form-row">
52
+ <label for="node-input-modelName"><i class="fa fa-database"></i> Model</label>
53
+ <input type="text" id="node-input-modelName" data-type="flow" data-value="">
54
+ </div>
55
+ <div class="form-row">
56
+ <label for="node-input-action"><i class="fa fa-cogs"></i> Action</label>
57
+ <select type="text" id="node-input-action">
58
+ <option value="fit">Fit Model</option>
59
+ <option value="predict">Predict Class</option>
60
+ <option value="predictProba">Predict Probability</option>
61
+ </select>
62
+ </div>
63
+ <div id="fit-params">
64
+ <div class="form-row">
65
+ <label for="node-input-learningRate">Learning Rate</label>
66
+ <input type="number" id="node-input-learningRate" step="0.01">
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-iterations">Iterations</label>
70
+ <input type="number" id="node-input-iterations">
71
+ </div>
72
+ <div class="form-row">
73
+ <label for="node-input-fitIntercept">Fit Intercept</label>
74
+ <input type="checkbox" id="node-input-fitIntercept" checked>
75
+ </div>
76
+ <div class="form-row">
77
+ <label for="node-input-l2">L2 Regularization</label>
78
+ <input type="number" id="node-input-l2" step="0.01">
79
+ </div>
80
+ <div class="form-row">
81
+ <label for="node-input-tolerance">Tolerance</label>
82
+ <input type="number" id="node-input-tolerance" step="1e-8">
83
+ </div>
84
+ <div class="form-row">
85
+ <label for="node-input-verbose">Verbose</label>
86
+ <input type="checkbox" id="node-input-verbose">
87
+ </div>
88
+ </div>
89
+ <div id="predict-params" style="display:none;">
90
+ <div class="form-row">
91
+ <label for="node-input-threshold">Threshold (for predict)</label>
92
+ <input type="number" id="node-input-threshold" step="0.01" min="0" max="1">
93
+ </div>
94
+ </div>
95
+ </script>
96
+
97
+ <script type="text/x-red" data-help-name="logisticRegression">
98
+ <p>Logistic Regression node for binary classification using gradient descent optimization.</p>
99
+
100
+ <h3>Actions</h3>
101
+ <ul>
102
+ <li><b>Fit Model:</b> Train the model with training data. Input msg.payload should be:
103
+ <pre>{X: [[feature1, feature2, ...], ...], y: [0, 1, 0, ...]}</pre>
104
+ where X is array of feature vectors and y is array of binary labels (0 or 1)</li>
105
+ <li><b>Predict Class:</b> Predict class labels (0 or 1) for input features. Input msg.payload should be array of feature vectors:
106
+ <pre>[[feature1, feature2, ...], ...]</pre></li>
107
+ <li><b>Predict Probability:</b> Predict probabilities for class 1. Same input format as Predict Class, returns array of probabilities between 0 and 1</li>
108
+ </ul>
109
+
110
+ <h3>Configuration Parameters</h3>
111
+ <ul>
112
+ <li><b>Learning Rate:</b> Step size for gradient descent optimization (default: 0.1). Smaller values lead to slower convergence, larger values may overshoot.</li>
113
+ <li><b>Iterations:</b> Number of gradient descent iterations for model training (default: 2000). More iterations allow better convergence.</li>
114
+ <li><b>Fit Intercept:</b> Whether to fit an intercept (bias) term in the model (default: true).</li>
115
+ <li><b>L2 Regularization:</b> Regularization parameter to prevent overfitting (default: 0.0). Higher values apply stronger regularization.</li>
116
+ <li><b>Tolerance:</b> Convergence tolerance for optimization (default: 1e-7). Algorithm stops when cost change is below this threshold.</li>
117
+ <li><b>Verbose:</b> Enable logging of training progress (default: false).</li>
118
+ <li><b>Threshold:</b> Decision threshold for class prediction (default: 0.5). Probabilities >= threshold predict class 1, otherwise class 0.</li>
119
+ </ul>
120
+
121
+ <h3>Model Storage</h3>
122
+ <p><b>Model:</b> Required name for the logistic regression model. For fit actions, this specifies where to store the trained model in the flow context. For predict actions, this selects which fitted model to use from the flow context. The dropdown shows all available flow context variables - select an existing fitted model or enter a new name to create one.</p>
123
+ <p>Models are stored in the flow context and can be shared between multiple Logistic Regression nodes in the same flow. Fit a model with one node, then use the same model name in predict nodes to make predictions.</p>
124
+
125
+ <h3>Example Payload</h3>
126
+ <p><b>For Fit:</b></p>
127
+ <pre>{
128
+ "X": [[0, 0], [0, 1], [1, 0], [1, 1]],
129
+ "y": [0, 1, 1, 1]
130
+ }</pre>
131
+ <p><b>For Predict:</b></p>
132
+ <pre>[[0.5, 0.5], [1, 1]]</pre>
133
+
134
+ <h3>Output</h3>
135
+ <p>Results are returned in msg.result with appropriate format (fitted model confirmation for fit, predictions array for predict/predictProba).</p>
136
+ </script>