Skip to content

Commit bf0dd6f

Browse files
committed
Merge branch 'beta' of https://github.com/circuitpython/web-editor into beta
2 parents 548528d + 9794dbf commit bf0dd6f

File tree

7 files changed

+269
-0
lines changed

7 files changed

+269
-0
lines changed

index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,21 @@
9797
<div id="serial-bar">
9898
<button class="purple-button btn-restart">Restart<i class="fa-solid fa-redo"></i></button>
9999
<button class="purple-button btn-clear">Clear<i class="fa-solid fa-broom"></i></button>
100+
<button class="purple-button btn-plotter">Plotter<i class="fa-solid fa-chart-line"></i></button>
100101
<div id="terminal-title"></div>
101102
</div>
103+
<div id="plotter" class="hidden">
104+
<label for="buffer-size">Buffer Size</label>
105+
<input type="number" id="buffer-size" value="20">
106+
<label for="plot-gridlines-select">Grid Lines</label>
107+
<select id="plot-gridlines-select">
108+
<option value="both">Both</option>
109+
<option value="x">X Only</option>
110+
<option value="y">Y Only</option>
111+
<option value="none">None</option>
112+
</select>
113+
<canvas id="plotter-canvas"></canvas>
114+
</div>
102115
<div id="terminal"></div>
103116
</div>
104117
</div>

js/common/plotter.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import Chart from "chart.js/auto";
2+
3+
let textLineBuffer = "";
4+
let textLine;
5+
6+
let defaultColors = ['#8888ff', '#ff8888', '#88ff88'];
7+
8+
/**
9+
* @name LineBreakTransformer
10+
* Helper to parse the incoming string messages into lines.
11+
*/
12+
class LineBreakTransformer {
13+
constructor() {
14+
// A container for holding stream data until a new line.
15+
this.container = '';
16+
}
17+
18+
transform(chunk, linesList) {
19+
this.container += chunk;
20+
const lines = this.container.split('\n');
21+
this.container = lines.pop();
22+
lines.forEach(line => linesList.push(line));
23+
}
24+
25+
}
26+
27+
let lineTransformer = new LineBreakTransformer()
28+
29+
export function plotValues(chartObj, serialMessage, bufferSize) {
30+
/*
31+
Given a string serialMessage, parse it into the plottable value(s) that
32+
it contains if any, and plot those values onto the given chartObj. If
33+
the serialMessage doesn't represent a complete textLine it will be stored
34+
into a buffer and combined with subsequent serialMessages until a full
35+
textLine is formed.
36+
*/
37+
let currentLines = []
38+
lineTransformer.transform(serialMessage, currentLines)
39+
40+
for (textLine of currentLines) {
41+
42+
textLine = textLine.replace("\r", "").replace("\n", "")
43+
if (textLine.length === 0) {
44+
continue;
45+
}
46+
47+
let valuesToPlot;
48+
49+
// handle possible tuple in textLine
50+
if (textLine.startsWith("(") && textLine.endsWith(")")) {
51+
textLine = "[" + textLine.substring(1, textLine.length - 1) + "]";
52+
console.log("after tuple conversion: " + textLine);
53+
}
54+
55+
// handle possible list in textLine
56+
if (textLine.startsWith("[") && textLine.endsWith("]")) {
57+
valuesToPlot = JSON.parse(textLine);
58+
for (let i = 0; i < valuesToPlot.length; i++) {
59+
valuesToPlot[i] = parseFloat(valuesToPlot[i])
60+
}
61+
62+
} else { // handle possible CSV in textLine
63+
valuesToPlot = textLine.split(",")
64+
for (let i = 0; i < valuesToPlot.length; i++) {
65+
valuesToPlot[i] = parseFloat(valuesToPlot[i])
66+
}
67+
}
68+
69+
if (valuesToPlot === undefined || valuesToPlot.length === 0) {
70+
continue;
71+
}
72+
73+
try {
74+
while (chartObj.data.labels.length > bufferSize) {
75+
chartObj.data.labels.shift();
76+
for (let i = 0; i < chartObj.data.datasets.length; i++) {
77+
while (chartObj.data.datasets[i].data.length > bufferSize) {
78+
chartObj.data.datasets[i].data.shift();
79+
}
80+
}
81+
}
82+
chartObj.data.labels.push("");
83+
84+
for (let i = 0; i < valuesToPlot.length; i++) {
85+
if (isNaN(valuesToPlot[i])) {
86+
continue;
87+
}
88+
if (i > chartObj.data.datasets.length - 1) {
89+
let curColor = '#000000';
90+
if (i < defaultColors.length) {
91+
curColor = defaultColors[i];
92+
}
93+
chartObj.data.datasets.push({
94+
label: i.toString(),
95+
data: [],
96+
borderColor: curColor,
97+
backgroundColor: curColor
98+
});
99+
}
100+
chartObj.data.datasets[i].data.push(valuesToPlot[i]);
101+
}
102+
103+
updatePlotterScales(chartObj);
104+
chartObj.update();
105+
} catch (e) {
106+
console.log("JSON parse error");
107+
// This line isn't a valid data value
108+
}
109+
}
110+
}
111+
112+
function updatePlotterScales(chartObj) {
113+
/*
114+
Update the scale of the plotter so that maximum and minimum values are sure
115+
to be shown within the plotter instead of going outside the visible range.
116+
*/
117+
let allData = []
118+
for (let i = 0; i < chartObj.data.datasets.length; i++) {
119+
allData = allData.concat(chartObj.data.datasets[i].data)
120+
}
121+
chartObj.options.scales.y.min = Math.min(...allData) - 10
122+
chartObj.options.scales.y.max = Math.max(...allData) + 10
123+
}
124+
125+
export async function setupPlotterChart(workflow) {
126+
/*
127+
Initialize the plotter chart and configure it.
128+
*/
129+
let initialData = []
130+
Chart.defaults.backgroundColor = '#444444';
131+
Chart.defaults.borderColor = '#000000';
132+
Chart.defaults.color = '#000000';
133+
Chart.defaults.aspectRatio = 3/2;
134+
workflow.plotterChart = new Chart(
135+
document.getElementById('plotter-canvas'),
136+
{
137+
type: 'line',
138+
options: {
139+
animation: false,
140+
scales: {
141+
y: {
142+
min: -1,
143+
max: 1,
144+
grid:{
145+
color: "#666"
146+
},
147+
border: {
148+
color: "#444"
149+
}
150+
},
151+
x:{
152+
grid: {
153+
display: true,
154+
color: "#666"
155+
},
156+
border: {
157+
color: "#444"
158+
}
159+
}
160+
}
161+
},
162+
data: {
163+
labels: initialData.map(row => row.timestamp),
164+
datasets: [
165+
{
166+
label: '0',
167+
data: initialData.map(row => row.value)
168+
}
169+
]
170+
}
171+
}
172+
);
173+
174+
// Set up a listener to respond to user changing the grid choice configuration
175+
// dropdown
176+
workflow.plotterGridLines.addEventListener('change', (event) => {
177+
let gridChoice = event.target.value;
178+
if (gridChoice === "x"){
179+
workflow.plotterChart.options.scales.x.grid.display = true;
180+
workflow.plotterChart.options.scales.y.grid.display = false;
181+
}else if (gridChoice === "y"){
182+
workflow.plotterChart.options.scales.y.grid.display = true;
183+
workflow.plotterChart.options.scales.x.grid.display = false;
184+
}else if (gridChoice === "both"){
185+
workflow.plotterChart.options.scales.y.grid.display = true;
186+
workflow.plotterChart.options.scales.x.grid.display = true;
187+
}else if (gridChoice === "none"){
188+
workflow.plotterChart.options.scales.y.grid.display = false;
189+
workflow.plotterChart.options.scales.x.grid.display = false;
190+
}
191+
workflow.plotterChart.update();
192+
});
193+
}

js/script.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ButtonValueDialog, MessageModal } from './common/dialogs.js';
2020
import { isLocal, switchUrl, getUrlParam } from './common/utilities.js';
2121
import { CONNTYPE } from './constants.js';
2222
import './layout.js'; // load for side effects only
23+
import {setupPlotterChart} from "./common/plotter.js";
2324
import { mainContent, showSerial } from './layout.js';
2425

2526
// Instantiate workflows
@@ -33,6 +34,7 @@ let unchanged = 0;
3334
let connectionPromise = null;
3435

3536
const btnRestart = document.querySelector('.btn-restart');
37+
const btnPlotter = document.querySelector('.btn-plotter');
3638
const btnClear = document.querySelector('.btn-clear');
3739
const btnConnect = document.querySelectorAll('.btn-connect');
3840
const btnNew = document.querySelectorAll('.btn-new');
@@ -42,6 +44,7 @@ const btnSaveAs = document.querySelectorAll('.btn-save-as');
4244
const btnSaveRun = document.querySelectorAll('.btn-save-run');
4345
const btnInfo = document.querySelector('.btn-info');
4446
const terminalTitle = document.getElementById('terminal-title');
47+
const serialPlotter = document.getElementById('plotter');
4548

4649
const messageDialog = new MessageModal("message");
4750
const connectionType = new ButtonValueDialog("connection-type");
@@ -130,9 +133,28 @@ btnRestart.addEventListener('click', async function(e) {
130133

131134
// Clear Button
132135
btnClear.addEventListener('click', async function(e) {
136+
if (workflow.plotterChart){
137+
workflow.plotterChart.data.datasets.forEach((dataSet, index) => {
138+
workflow.plotterChart.data.datasets[index].data = [];
139+
});
140+
workflow.plotterChart.data.labels = [];
141+
workflow.plotterChart.options.scales.y.min = -1;
142+
workflow.plotterChart.options.scales.y.max = 1;
143+
workflow.plotterChart.update();
144+
}
133145
state.terminal.clear();
134146
});
135147

148+
// Plotter Button
149+
btnPlotter.addEventListener('click', async function(e){
150+
serialPlotter.classList.toggle("hidden");
151+
if (!workflow.plotterEnabled){
152+
await setupPlotterChart(workflow);
153+
workflow.plotterEnabled = true;
154+
}
155+
state.fitter.fit();
156+
});
157+
136158
btnInfo.addEventListener('click', async function(e) {
137159
if (await checkConnected()) {
138160
await workflow.showInfo(getDocState());

js/workflows/workflow.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {FileHelper} from '../common/file.js';
44
import {UnsavedDialog} from '../common/dialogs.js';
55
import {FileDialog, FILE_DIALOG_OPEN, FILE_DIALOG_SAVE} from '../common/file_dialog.js';
66
import {CONNTYPE, CONNSTATE} from '../constants.js';
7+
import {plotValues} from '../common/plotter.js'
78

89
/*
910
* This class will encapsulate all of the common workflow-related functions
@@ -47,6 +48,8 @@ class Workflow {
4748
this._unsavedDialog = new UnsavedDialog("unsaved");
4849
this._fileDialog = new FileDialog("files", this.showBusy.bind(this));
4950
this.repl = new REPL();
51+
this.plotterEnabled = false;
52+
this.plotterChart = false;
5053
}
5154

5255
async init(params) {
@@ -59,6 +62,8 @@ class Workflow {
5962
this._loadFileContents = params.loadFileFunc;
6063
this._showMessage = params.showMessageFunc;
6164
this.loader = document.getElementById("loader");
65+
this.plotterBufferSize = document.getElementById('buffer-size');
66+
this.plotterGridLines = document.getElementById('plot-gridlines-select');
6267
if ("terminalTitle" in params) {
6368
this.terminalTitle = params.terminalTitle;
6469
}
@@ -159,6 +164,9 @@ class Workflow {
159164
}
160165

161166
writeToTerminal(data) {
167+
if (this.plotterEnabled) {
168+
plotValues(this.plotterChart, data, this.plotterBufferSize.value);
169+
}
162170
this.terminal.write(data);
163171
}
164172

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@xterm/addon-fit": "^0.10.0",
2121
"@xterm/addon-web-links": "^0.11.0",
2222
"@xterm/xterm": "^5.5.0",
23+
"chart.js": "^4.4.4",
2324
"codemirror": "^6.0.1",
2425
"file-saver": "^2.0.5",
2526
"focus-trap": "^7.5.4",

sass/layout/_layout.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@
7272
}
7373

7474
#serial-page {
75+
#plotter {
76+
flex: 2 1 0;
77+
background: #777;
78+
position: relative;
79+
width: 99%;
80+
overflow: hidden;
81+
padding: 10px 20px;
82+
83+
&.hidden{
84+
display: none;
85+
}
86+
}
7587
#terminal {
7688
flex: 1 1 0%;
7789
background: #333;
@@ -99,6 +111,9 @@
99111
}
100112
}
101113
}
114+
#buffer-size{
115+
width: 70px;
116+
}
102117
}
103118

104119
#ble-instructions,

0 commit comments

Comments
 (0)