diff --git a/static/dashboard.js b/static/dashboard.js index ad71280..e4d6ff3 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -28,6 +28,10 @@ const IND_NAME = { adx: "ADX", roc: "Rate of change", keltner: "Keltner channel", divergence: "Divergence", candlestick: "Candlestick pattern" }; +// Divergence and candlestick only exist on a scan that found a pattern +// (engine.py); for them an absent entry means "no pattern", not a failure. +const PATTERN_IND = new Set(["divergence", "candlestick"]); +const absentWord = name => PATTERN_IND.has(name) ? "no pattern" : "no reading"; const VOTE = { STRONG_BUY: 2, BUY: 1, NEUTRAL: 0, SELL: -1, STRONG_SELL: -2 }; const SIG_ICON = { STRONG_BUY: "fa-angles-up", BUY: "fa-arrow-trend-up", NEUTRAL: "fa-arrows-left-right", @@ -275,11 +279,41 @@ function indChart(tf) { barThickness: 7 }] }, + // A neutral vote and a missing reading both have no bar; this marks + // them on the zero line (grey dot = neutral, faint tag = nothing found). + plugins: [{ + id: "indMarkers", + afterDatasetsDraw(chart) { + const cells = lastCells[tf]; + if (!cells) return; + const { ctx, scales } = chart; + const x0 = scales.x.getPixelForValue(0); + ctx.save(); + cells.forEach((cell, i) => { + const y = scales.y.getPixelForValue(i); + if (cell.present && cell.v === 0) { + ctx.fillStyle = C.flat; + ctx.beginPath(); + ctx.arc(x0, y, 3, 0, Math.PI * 2); + ctx.fill(); + } else if (!cell.present) { + ctx.fillStyle = "rgba(255,255,255,.32)"; + ctx.font = '9px "IBM Plex Mono", monospace'; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(absentWord(IND_ORDER[i]), x0 + 6, y); + } + }); + ctx.restore(); + } + }], options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, layout: { padding: { right: 4 } }, + // Hover the whole row: a zero-length bar cannot be intersected. + interaction: { mode: "index", axis: "y", intersect: false }, scales: { x: { min: -2, max: 2, @@ -291,8 +325,8 @@ function indChart(tf) { grid: { display: false }, border: { display: false }, ticks: { - // An indicator with no reading is not a neutral vote: both draw - // no bar, so the dimmed label is what tells them apart. + // An indicator with no reading is not a neutral vote: the label + // dims and the indMarkers plugin tags the row instead of a dot. color: ctx => (lastCells[tf] && lastCells[tf][ctx.index] && !lastCells[tf][ctx.index].present) ? "rgba(255,255,255,.32)" : C.axis, font: { family: '"IBM Plex Mono", monospace', size: 10 }, @@ -307,7 +341,10 @@ function indChart(tf) { title: items => IND_NAME[IND_ORDER[items[0].dataIndex]], label: item => { const cell = (lastCells[tf] || indCells(currentReport(tf)))[item.dataIndex]; - if (!cell || !cell.present) return "No reading on this scan"; + if (!cell || !cell.present) { + const w = absentWord(IND_ORDER[item.dataIndex]); + return `${w[0].toUpperCase()}${w.slice(1)} on this scan`; + } return `${sigWord(cell.sig)}, confidence ${Math.round(cell.conf)}%`; } } @@ -687,9 +724,12 @@ function indTable(src) { const i = src && src[name]; const k = `