From bbb03ab5ad6e0958fd03f5644aec1db4b6991d4a Mon Sep 17 00:00:00 2001 From: gord chung Date: Fri, 8 Dec 2023 22:09:54 -0500 Subject: [PATCH] add ability to draw arbitrary lines sometimes you want ability to draw lines to make it seem like you know what you're doing and that the market behaves rationally. --- app/api/v1/data.py | 1 - app/static/js/utils.js | 125 +++++++++++++++++++++++++++++++++++ app/templates/analytics.html | 7 +- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/app/api/v1/data.py b/app/api/v1/data.py index fd4b104..b3227e0 100644 --- a/app/api/v1/data.py +++ b/app/api/v1/data.py @@ -55,7 +55,6 @@ def sales_data(): @bp.get('/data/deaths') def death_data(): - # https://www150.statcan.gc.ca/n1/daily-quotidien/231127/t001b-eng.htm table = pa_csv.read_csv('app/static/data/can-deaths.csv') data = {'data': table.to_pylist()} if request.headers.get('Hx-Request'): diff --git a/app/static/js/utils.js b/app/static/js/utils.js index 36b3ad5..9ecf726 100644 --- a/app/static/js/utils.js +++ b/app/static/js/utils.js @@ -27,3 +27,128 @@ function addData(chart, newData, labels) { } chart.update(); } + +const chartStates = new WeakMap(); +const arbLines = { + id: "chartjs-arb-lines", + + defaults: { + color: "black", + lineThreshold: 15, + lineWidth: 2, + enableKey: "Control", // draw only when keypressed + modifierKey: "Shift", // extends line to borders + }, + + beforeInit(chart) { + chartStates.set(chart, { + lines: [], + }); + }, + + afterEvent(chart, args, options) { + const { ctx, chartArea } = chart; + const state = chartStates.get(chart); + + switch (args.event.type) { + case "mousedown": + if (args.replay !== true && isKeyDown(options.enableKey)) { + // registers mousedown event twice if you "click", ignore one. + state.startXY = { x: args.event.x, y: args.event.y }; + } + break; + case "mousemove": + if (state.startXY) { + ctx.beginPath(); + ctx.lineWidth = options.lineWidth; + const line = getCoords(chartArea, { + ...state.startXY, + x2: args.event.x, + y2: args.event.y, + full: isKeyDown(options.modifierKey), + }); + ctx.moveTo(line.x1, line.y1); + ctx.lineTo(line.x2, line.y2); + ctx.strokeStyle = "grey"; + ctx.stroke(); + ctx.restore(); + } + break; + case "mouseup": + if ( + isKeyDown(options.enableKey) && + Math.abs(state.startXY.x - args.event.x) + Math.abs(state.startXY.y - args.event.y) > + options.lineThreshold + ) { + // don't draw tiny lines + state.lines.push({ + ...state.startXY, + x2: args.event.x, + y2: args.event.y, + full: isKeyDown(options.modifierKey), + }); + } + state.startXY = null; + break; + } + }, + + afterDatasetsDraw(chart, args, options) { + const { ctx, chartArea } = chart; + const state = chartStates.get(chart); + for (const line of state.lines) { + ctx.beginPath(); + ctx.lineWidth = options.lineWidth; + const drawLine = getCoords(chartArea, line); + ctx.moveTo(drawLine.x1, drawLine.y1); + ctx.lineTo(drawLine.x2, drawLine.y2); + ctx.strokeStyle = options.color; + ctx.closePath(); + ctx.stroke(); + } + }, +}; + +function getCoords(chartArea, line) { + if (line.full === false) { + return { + x1: line.x, + y1: line.y, + x2: line.x2, + y2: line.y2, + }; + } + // NOTE: i graduated highschool and i had to google how to y = mx + b ... + const slope = (line.y - line.y2) / (line.x - line.x2); + const intercept = line.y - slope * line.x; + let x1 = chartArea.left; + if (slope * chartArea.left + intercept < chartArea.top) { + x1 = (chartArea.top - intercept) / slope; + } else if (slope * chartArea.left + intercept > chartArea.bottom) { + x1 = (chartArea.bottom - intercept) / slope; + } + let x2 = chartArea.right; + if (slope * chartArea.right + intercept < chartArea.top) { + x2 = (chartArea.top - intercept) / slope; + } else if (slope * chartArea.right + intercept > chartArea.bottom) { + x2 = (chartArea.bottom - intercept) / slope; + } + return { + x1: x1, + y1: Math.min(Math.max(slope * x1 + intercept, chartArea.top), chartArea.bottom), + x2: x2, + y2: Math.min(Math.max(slope * x2 + intercept, chartArea.top), chartArea.bottom), + }; +} + +const isKeyDown = (() => { + // https://stackoverflow.com/a/48750898 + const state = {}; + + // biome-ignore lint: let it mod + window.addEventListener("keyup", (e) => (state[e.key] = false)); + // biome-ignore lint: let it mod + window.addEventListener("keydown", (e) => (state[e.key] = true)); + + return (key) => (Object.hasOwn(state, key) && state[key]) || false; +})(); diff --git a/app/templates/analytics.html b/app/templates/analytics.html index fd5771c..6a0457e 100644 --- a/app/templates/analytics.html +++ b/app/templates/analytics.html @@ -40,7 +40,7 @@

hx-swap="beforeend" hx-target="body" id="lineChartId"> - https://data.bts.gov/Research-and-Statistics/Auto-Sales/7n6a-n5tz + source: https://data.bts.gov/Research-and-Statistics/Auto-Sales/7n6a-n5tz @@ -52,7 +52,7 @@

@@ -66,6 +66,7 @@

+ source: https://www150.statcan.gc.ca/n1/daily-quotidien/231127/t001b-eng.htm
@@ -104,7 +105,9 @@

document.getElementById("lineChartId"), { type: "line", + plugins: [arbLines], options: { + events: ['mousedown', 'mouseup', 'mousemove'], responsive: true, plugins: { legend: {