RayMelius Claude Sonnet 4.6 commited on
Commit
53c4fc5
·
1 Parent(s): aeb86d9

Chart: volume dots, price dots, multi-symbol volumes; FIX UI back button

Browse files

Price chart:
- Volume bars narrowed to 25% slot width, capped at 5px
- Close-price overlay replaced with orange dots per candle (not a line)
- Legend updated to dot symbol instead of line

Multi-symbol chart:
- Volume data now passed from loadAllHistory (live + candle modes)
- drawMultiChart splits into 70% price / 30% volume when volumes present
- Aggregated volume bars (sum across symbols per 60s bucket) shown below
- Volume Y-axis labels on right side (0, 50%, max)

FIX UI:
- Added "← Dashboard" back button in header (links to /)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

dashboard/templates/index.html CHANGED
@@ -781,7 +781,7 @@
781
  const n = points.length;
782
  const slotW = W / n;
783
  const bodyW = Math.max(1, Math.floor(slotW * 0.65));
784
- const volBarW = Math.max(1, Math.floor(slotW * 0.35));
785
 
786
  // Price grid
787
  ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
@@ -831,14 +831,14 @@
831
  ctx.fillRect(x, bodyTop, bodyW, bodyH);
832
  });
833
 
834
- // Close-price line overlay (orange)
835
- ctx.strokeStyle = "rgba(255,152,0,0.85)"; ctx.lineWidth = 1.5;
836
- ctx.beginPath();
837
  points.forEach((p, i) => {
838
  const x = pad.left + i * slotW + slotW / 2;
839
- i === 0 ? ctx.moveTo(x, toY(p.close)) : ctx.lineTo(x, toY(p.close));
 
 
840
  });
841
- ctx.stroke();
842
 
843
  // Time labels (up to 5 evenly spaced)
844
  ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
@@ -856,8 +856,8 @@
856
  ctx.fillStyle = "#26a69a"; ctx.fillText("▲", pad.left, 12);
857
  ctx.fillStyle = "#ef5350"; ctx.fillText("▼", pad.left + 14, 12);
858
  ctx.fillStyle = "#888"; ctx.fillText("candle", pad.left + 24, 12);
859
- ctx.strokeStyle = "rgba(255,152,0,0.85)"; ctx.lineWidth = 1.5;
860
- ctx.beginPath(); ctx.moveTo(pad.left + 66, 9); ctx.lineTo(pad.left + 78, 9); ctx.stroke();
861
  ctx.fillStyle = "#ff9800"; ctx.fillText("close", pad.left + 80, 12);
862
  if (labelSuffix) { ctx.fillStyle = "#bbb"; ctx.fillText(labelSuffix, pad.left + 114, 12); }
863
  }
@@ -1180,9 +1180,15 @@
1180
  return;
1181
  }
1182
 
1183
- const pad = { top: 18, right: 48, bottom: 32, left: 46 };
1184
- const W = canvas.width - pad.left - pad.right;
1185
- const H = canvas.height - pad.top - pad.bottom;
 
 
 
 
 
 
1186
 
1187
  // Normalise each series to % change from its first point
1188
  const norm = active.map(s => {
@@ -1202,10 +1208,10 @@
1202
  const maxTs = Math.max(...allTs);
1203
  const tsRng = maxTs - minTs || 1;
1204
 
1205
- const toX = ts => pad.left + ((ts - minTs) / tsRng) * W;
1206
- const toY = pct => pad.top + H - ((pct - minPct) / (maxPct - minPct)) * H;
1207
 
1208
- // Grid + Y labels
1209
  ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
1210
  for (let i = 0; i <= 4; i++) {
1211
  const pct = minPct + (maxPct - minPct) * i / 4;
@@ -1236,6 +1242,44 @@
1236
  ctx.stroke();
1237
  });
1238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
  // Time labels (up to 4)
1240
  ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
1241
  for (let i = 0; i <= 3; i++) {
@@ -1277,7 +1321,7 @@
1277
  if (period === "live") {
1278
  const series = symbols.map(sym => {
1279
  let trades = state.trades.filter(t => t.symbol === sym).slice(0, 100).reverse();
1280
- return { symbol: sym, points: trades.map(t => ({ price: t.price || 0, ts: t.timestamp || Date.now()/1000 })) };
1281
  });
1282
  drawMultiChart(series, `${symbols.length} symbols`);
1283
  statusEl.textContent = `${symbols.length} symbols`;
@@ -1291,7 +1335,7 @@
1291
  );
1292
  const series = symbols.map((sym, i) => ({
1293
  symbol: sym,
1294
- points: (results[i].candles || []).map(c => ({ price: c.c, ts: c.t }))
1295
  }));
1296
  const filled = series.filter(s => s.points.length > 0);
1297
  drawMultiChart(series, `${filled.length} symbols`);
 
781
  const n = points.length;
782
  const slotW = W / n;
783
  const bodyW = Math.max(1, Math.floor(slotW * 0.65));
784
+ const volBarW = Math.max(1, Math.min(Math.floor(slotW * 0.25), 5));
785
 
786
  // Price grid
787
  ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
 
831
  ctx.fillRect(x, bodyTop, bodyW, bodyH);
832
  });
833
 
834
+ // Close-price dots (orange) — one point per candle
835
+ ctx.fillStyle = "rgba(255,152,0,0.9)";
 
836
  points.forEach((p, i) => {
837
  const x = pad.left + i * slotW + slotW / 2;
838
+ ctx.beginPath();
839
+ ctx.arc(x, toY(p.close), 2, 0, Math.PI * 2);
840
+ ctx.fill();
841
  });
 
842
 
843
  // Time labels (up to 5 evenly spaced)
844
  ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
 
856
  ctx.fillStyle = "#26a69a"; ctx.fillText("▲", pad.left, 12);
857
  ctx.fillStyle = "#ef5350"; ctx.fillText("▼", pad.left + 14, 12);
858
  ctx.fillStyle = "#888"; ctx.fillText("candle", pad.left + 24, 12);
859
+ ctx.fillStyle = "rgba(255,152,0,0.9)";
860
+ ctx.beginPath(); ctx.arc(pad.left + 72, 9, 2.5, 0, Math.PI * 2); ctx.fill();
861
  ctx.fillStyle = "#ff9800"; ctx.fillText("close", pad.left + 80, 12);
862
  if (labelSuffix) { ctx.fillStyle = "#bbb"; ctx.fillText(labelSuffix, pad.left + 114, 12); }
863
  }
 
1180
  return;
1181
  }
1182
 
1183
+ const hasVolume = active.some(s => s.points.some(p => (p.volume || 0) > 0));
1184
+ const pad = { top: 18, right: 48, bottom: 32, left: 46 };
1185
+ const W = canvas.width - pad.left - pad.right;
1186
+ const H = canvas.height - pad.top - pad.bottom;
1187
+ const priceH = hasVolume ? H * 0.70 : H;
1188
+ const gapH = hasVolume ? H * 0.03 : 0;
1189
+ const volH = hasVolume ? H * 0.27 : 0;
1190
+ const priceY = pad.top;
1191
+ const volY = pad.top + priceH + gapH;
1192
 
1193
  // Normalise each series to % change from its first point
1194
  const norm = active.map(s => {
 
1208
  const maxTs = Math.max(...allTs);
1209
  const tsRng = maxTs - minTs || 1;
1210
 
1211
+ const toX = ts => pad.left + ((ts - minTs) / tsRng) * W;
1212
+ const toY = pct => priceY + priceH - ((pct - minPct) / (maxPct - minPct)) * priceH;
1213
 
1214
+ // Price grid + Y labels
1215
  ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
1216
  for (let i = 0; i <= 4; i++) {
1217
  const pct = minPct + (maxPct - minPct) * i / 4;
 
1242
  ctx.stroke();
1243
  });
1244
 
1245
+ // Volume section — aggregate all symbols by time bucket
1246
+ if (hasVolume) {
1247
+ const volMap = {};
1248
+ active.forEach(s => {
1249
+ s.points.forEach(p => {
1250
+ const bucket = Math.round(p.ts / 60) * 60;
1251
+ volMap[bucket] = (volMap[bucket] || 0) + (p.volume || 0);
1252
+ });
1253
+ });
1254
+ const volPoints = Object.entries(volMap)
1255
+ .map(([ts, vol]) => ({ ts: +ts, volume: vol }))
1256
+ .sort((a, b) => a.ts - b.ts);
1257
+ const maxVol = Math.max(...volPoints.map(v => v.volume), 1);
1258
+
1259
+ // Divider
1260
+ ctx.strokeStyle = "#e0e0e0"; ctx.lineWidth = 1;
1261
+ ctx.beginPath(); ctx.moveTo(pad.left, volY); ctx.lineTo(canvas.width - pad.right, volY); ctx.stroke();
1262
+
1263
+ // Bars
1264
+ const volSlotW = W / Math.max(volPoints.length, 1);
1265
+ const volBarW = Math.max(1, Math.min(Math.floor(volSlotW * 0.25), 5));
1266
+ volPoints.forEach(v => {
1267
+ const x = toX(v.ts) - volBarW / 2;
1268
+ const bh = (v.volume / maxVol) * volH;
1269
+ ctx.fillStyle = "rgba(96,125,139,0.5)";
1270
+ ctx.fillRect(x, volY + volH - bh, volBarW, bh);
1271
+ });
1272
+
1273
+ // Volume Y-axis labels (right side)
1274
+ ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "left";
1275
+ [1.0, 0.5, 0.0].forEach(frac => {
1276
+ const val = Math.round(maxVol * frac);
1277
+ const y = volY + volH * (1 - frac);
1278
+ const lbl = val >= 1000 ? (val / 1000).toFixed(1) + "k" : val.toString();
1279
+ ctx.fillText(lbl, canvas.width - pad.right + 3, y + 3);
1280
+ });
1281
+ }
1282
+
1283
  // Time labels (up to 4)
1284
  ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
1285
  for (let i = 0; i <= 3; i++) {
 
1321
  if (period === "live") {
1322
  const series = symbols.map(sym => {
1323
  let trades = state.trades.filter(t => t.symbol === sym).slice(0, 100).reverse();
1324
+ return { symbol: sym, points: trades.map(t => ({ price: t.price || 0, ts: t.timestamp || Date.now()/1000, volume: t.quantity || t.qty || 0 })) };
1325
  });
1326
  drawMultiChart(series, `${symbols.length} symbols`);
1327
  statusEl.textContent = `${symbols.length} symbols`;
 
1335
  );
1336
  const series = symbols.map((sym, i) => ({
1337
  symbol: sym,
1338
+ points: (results[i].candles || []).map(c => ({ price: c.c, ts: c.t, volume: c.v || 0 }))
1339
  }));
1340
  const filled = series.filter(s => s.points.length > 0);
1341
  drawMultiChart(series, `${filled.length} symbols`);
fix-ui-client/templates/index.html CHANGED
@@ -125,6 +125,7 @@
125
  <span class="dot"></span>
126
  <span>{{ 'CONNECTED' if connected else 'DISCONNECTED' }}</span>
127
  </span>
 
128
  </h1>
129
 
130
  <div class="container">
 
125
  <span class="dot"></span>
126
  <span>{{ 'CONNECTED' if connected else 'DISCONNECTED' }}</span>
127
  </span>
128
+ <a href="/" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none;">← Dashboard</a>
129
  </h1>
130
 
131
  <div class="container">