From 61cc94c0dfe93dd64de09deda25f294a599a445c Mon Sep 17 00:00:00 2001 From: gord chung Date: Tue, 12 Dec 2023 15:30:32 -0500 Subject: [PATCH] add plugin to draw crosshair to track y at x and refactor js to use start using modules --- README.md | 3 + .../{utils.js => chart.plugins.arblines.js} | 52 ++----------- app/static/js/chart.plugins.crosshairs.js | 78 +++++++++++++++++++ app/static/js/chart.utils.js | 20 +++++ app/static/js/listeners.js | 67 ++++++++++++++++ app/templates/analytics.html | 28 +++---- app/templates/base.html | 1 + app/templates/yahoo.html | 67 +++++++--------- 8 files changed, 217 insertions(+), 99 deletions(-) rename app/static/js/{utils.js => chart.plugins.arblines.js} (70%) create mode 100644 app/static/js/chart.plugins.crosshairs.js create mode 100644 app/static/js/chart.utils.js create mode 100644 app/static/js/listeners.js diff --git a/README.md b/README.md index f86f446..5f4094c 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,8 @@ todo how to set up external providers - https://blog.ohheybrian.com/2023/06/smarter-templating-with-htmx-and-flask/ - https://www.advantch.com/blog/how-to-build-interactive-charts-in-python-using-htmx-and-echarts/ - https://github.com/Konfuzian/htmx-examples-with-flask +- js + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules # todo - more frontend via htmx https://htmx.org/docs/ @@ -156,3 +158,4 @@ todo how to set up external providers - worker processes? via ? - optimised ci via [mergify](https://mergify.com/) - https://unsuckjs.com/ +- plugin lines disappear (probably because afterevent is drawn before rendering); diff --git a/app/static/js/utils.js b/app/static/js/chart.plugins.arblines.js similarity index 70% rename from app/static/js/utils.js rename to app/static/js/chart.plugins.arblines.js index 9ecf726..cac3545 100644 --- a/app/static/js/utils.js +++ b/app/static/js/chart.plugins.arblines.js @@ -1,32 +1,4 @@ -let og_datasets; - -function toggleView(elem, chart) { - if (elem.checked) { - og_datasets = chart.data.datasets.map((dataset) => { - return dataset.data; - }); - const total = og_datasets.reduce((r, a) => r.map((b, i) => a[i] + b)); - for (const [index, data] of og_datasets.entries()) { - chart.data.datasets[index].data = data.map((a, i) => (a / total[i]) * 100); - } - } else { - for (const [index, data] of og_datasets.entries()) { - chart.data.datasets[index].data = data; - } - } - chart.update(); -} - -// https://www.chartjs.org/docs/latest/developers/updates.html -function addData(chart, newData, labels) { - for (const data of newData) { - chart.data.datasets.push(data); - } - if (labels != null) { - chart.data.labels = labels; - } - chart.update(); -} +import { isKeyDown } from "./listeners.js"; const chartStates = new WeakMap(); const arbLines = { @@ -46,6 +18,7 @@ const arbLines = { }); }, + // https://stackoverflow.com/a/77663018/23104322 afterEvent(chart, args, options) { const { ctx, chartArea } = chart; const state = chartStates.get(chart); @@ -59,9 +32,10 @@ const arbLines = { break; case "mousemove": if (state.startXY) { + ctx.setLineDash([]); ctx.beginPath(); ctx.lineWidth = options.lineWidth; - const line = getCoords(chartArea, { + const line = getLineCoords(chartArea, { ...state.startXY, x2: args.event.x, y2: args.event.y, @@ -71,7 +45,6 @@ const arbLines = { ctx.lineTo(line.x2, line.y2); ctx.strokeStyle = "grey"; ctx.stroke(); - ctx.restore(); } break; case "mouseup": @@ -96,10 +69,11 @@ const arbLines = { afterDatasetsDraw(chart, args, options) { const { ctx, chartArea } = chart; const state = chartStates.get(chart); + ctx.setLineDash([]); for (const line of state.lines) { ctx.beginPath(); ctx.lineWidth = options.lineWidth; - const drawLine = getCoords(chartArea, line); + const drawLine = getLineCoords(chartArea, line); ctx.moveTo(drawLine.x1, drawLine.y1); ctx.lineTo(drawLine.x2, drawLine.y2); ctx.strokeStyle = options.color; @@ -109,7 +83,7 @@ const arbLines = { }, }; -function getCoords(chartArea, line) { +function getLineCoords(chartArea, line) { if (line.full === false) { return { x1: line.x, @@ -141,14 +115,4 @@ function getCoords(chartArea, line) { }; } -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; -})(); +export { arbLines }; diff --git a/app/static/js/chart.plugins.crosshairs.js b/app/static/js/chart.plugins.crosshairs.js new file mode 100644 index 0000000..1e84d45 --- /dev/null +++ b/app/static/js/chart.plugins.crosshairs.js @@ -0,0 +1,78 @@ +const chartStates = new WeakMap(); +const crosshairs = { + id: "chartjs-crosshairs", + + defaults: { + color: "black", + lineWidth: 1, + }, + + beforeInit(chart) {}, + + // https://stackoverflow.com/a/77663018/23104322 + afterEvent(chart, args, options) { + const { ctx, chartArea } = chart; + + switch (args.event.type) { + case "mousemove": + drawCrosshair(chart, { x: args.event.x, y: args.event.y }); + break; + } + }, + + afterDatasetsDraw(chart, args, options) {}, +}; + +function drawCrosshair(chart, coord) { + const { canvas, ctx, chartArea, scales } = chart; + if (!scales.y) { + return; + } + if ( + chartArea.right >= coord.x && + coord.x >= chartArea.left && + chartArea.bottom >= coord.y && + coord.y >= chartArea.top + ) { + canvas.style.cursor = "crosshair"; + + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + + ctx.beginPath(); + ctx.moveTo(chartArea.left, coord.y); + ctx.lineTo(chartArea.right, coord.y); + ctx.stroke(); + ctx.closePath(); + + // draw y-value + const yTipHeight = 10; + ctx.beginPath(); + ctx.fillStyle = "grey"; + ctx.moveTo(chartArea.left, coord.y); + ctx.lineTo(chartArea.left - yTipHeight / 2, coord.y + yTipHeight); + ctx.lineTo(0, coord.y + yTipHeight); + ctx.lineTo(0, coord.y - yTipHeight); + ctx.lineTo(chartArea.left - yTipHeight / 2, coord.y - yTipHeight); + ctx.fill(); + ctx.closePath(); + ctx.fillStyle = "white"; + ctx.textBaseline = "middle"; + ctx.textAlign = "right"; + ctx.fillText( + scales.y.getValueForPixel(coord.y).toFixed(2), + chartArea.left - yTipHeight / 2, + coord.y, + ); + + ctx.beginPath(); + ctx.moveTo(coord.x, chartArea.top); + ctx.lineTo(coord.x, chartArea.bottom); + ctx.stroke(); + ctx.closePath(); + } else { + canvas.style.cursor = "default"; + } +} + +export { crosshairs }; diff --git a/app/static/js/chart.utils.js b/app/static/js/chart.utils.js new file mode 100644 index 0000000..8504359 --- /dev/null +++ b/app/static/js/chart.utils.js @@ -0,0 +1,20 @@ +let og_datasets; + +function toggleView(elem, chart) { + if (elem.checked) { + og_datasets = chart.data.datasets.map((dataset) => { + return dataset.data; + }); + const total = og_datasets.reduce((r, a) => r.map((b, i) => a[i] + b)); + for (const [index, data] of og_datasets.entries()) { + chart.data.datasets[index].data = data.map((a, i) => (a / total[i]) * 100); + } + } else { + for (const [index, data] of og_datasets.entries()) { + chart.data.datasets[index].data = data; + } + } + chart.update(); +} + +export { toggleView }; diff --git a/app/static/js/listeners.js b/app/static/js/listeners.js new file mode 100644 index 0000000..c05f22e --- /dev/null +++ b/app/static/js/listeners.js @@ -0,0 +1,67 @@ +// https://www.chartjs.org/docs/latest/developers/updates.html +function addData(chart, newData, labels) { + for (const data of newData) { + chart.data.datasets.push(data); + } + if (labels != null) { + chart.data.labels = labels; + } + chart.update(); +} + +function displayPrices(chart, table, data) { + const prices = data.indicators.quote[0].close; + // ideally, should check overlap of labels between datasets + table.updateOrAddData(data.timestamp.map((a, i) => ({ date: a, [data.meta.symbol]: prices[i] }))); + if (!table.columnManager.columns.length) { + table.addColumn({ title: "Date", field: "date", frozen: true }, true); + } + table.addColumn({ + title: data.meta.symbol, + field: data.meta.symbol, + hozAlign: "right", + headerSort: false, + formatter: "money", + formatterParams: { thousand: false }, + }); + chart.data.labels = data.timestamp; + // change prices to percentage so it's comparable + chart.data.datasets.push({ + label: data.meta.symbol, + data: prices.map((x) => ((x - prices[0]) / prices[0]) * 100), + }); + chart.update(); +} + +function setupResponseHandlers() { + document.body.addEventListener("displayPrices", function (evt) { + const chart = Chart.getChart(evt.detail.target); + const table = Tabulator.findTable("#priceTable")[0]; + const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent); + displayPrices(chart, table, data); + document.getElementById(evt.detail.dataId).remove(); + }); + + // chartjs + htmx with payload in HX-Trigger + document.body.addEventListener("drawChart", function (evt) { + const chart = Chart.getChart(evt.detail.target); + // https://www.reddit.com/r/htmx/comments/10sdk43/comment/j72m2j7/ + const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent); + addData(chart, data.datasets, data.labels); + document.getElementById(evt.detail.dataId).remove(); + }); +} + +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; +})(); + +export { isKeyDown, setupResponseHandlers }; diff --git a/app/templates/analytics.html b/app/templates/analytics.html index 36474b1..207db23 100644 --- a/app/templates/analytics.html +++ b/app/templates/analytics.html @@ -4,12 +4,6 @@ {% endblock %} -{% block head_scripts %} - - - -{% endblock %} - {% block body %}
@@ -59,16 +53,18 @@

source: https://www150.statcan.gc.ca/n1/daily-quotidien/231127/t001b-eng.htm

+{% endblock %} +{% block body_scripts %} + + + + {% block body_scripts %}{% endblock %} diff --git a/app/templates/yahoo.html b/app/templates/yahoo.html index 98638cf..f99ead6 100644 --- a/app/templates/yahoo.html +++ b/app/templates/yahoo.html @@ -3,11 +3,6 @@ {% block head_css %} {% endblock %} -{% block head_scripts %} - - - -{% endblock %} {% block body %}
@@ -39,20 +34,38 @@

+{% endblock %} +{% block body_scripts %} + + + - {% endblock %}