From 159cd65bb4151cad8031fc6fbf4671f1067fb70c 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 6b82623..9d48840 100644 --- a/app/api/v1/data.py +++ b/app/api/v1/data.py @@ -53,7 +53,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..9a50686 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 (don't move mouse), 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 4e98ead..b46bcf4 100644 --- a/app/templates/analytics.html +++ b/app/templates/analytics.html @@ -35,7 +35,7 @@

- https://data.bts.gov/Research-and-Statistics/Auto-Sales/7n6a-n5tz + source: https://data.bts.gov/Research-and-Statistics/Auto-Sales/7n6a-n5tz @@ -44,7 +44,7 @@

-
@@ -57,6 +57,7 @@

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

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