Skip to content

Commit

Permalink
add plugin to draw crosshair to track y at x
Browse files Browse the repository at this point in the history
and refactor js to use start using modules
  • Loading branch information
chungg committed Dec 18, 2023
1 parent 800ca92 commit 61cc94c
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 99 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,13 @@ 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/
- customise bulma via https://bulma.io/documentation/customize/
- worker processes? via ?
- optimised ci via [mergify](https://mergify.com/)
- https://unsuckjs.com/
- plugin lines disappear (probably because afterevent is drawn before rendering);
52 changes: 8 additions & 44 deletions app/static/js/utils.js → app/static/js/chart.plugins.arblines.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -71,7 +45,6 @@ const arbLines = {
ctx.lineTo(line.x2, line.y2);
ctx.strokeStyle = "grey";
ctx.stroke();
ctx.restore();
}
break;
case "mouseup":
Expand All @@ -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;
Expand All @@ -109,7 +83,7 @@ const arbLines = {
},
};

function getCoords(chartArea, line) {
function getLineCoords(chartArea, line) {
if (line.full === false) {
return {
x1: line.x,
Expand Down Expand Up @@ -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 };
78 changes: 78 additions & 0 deletions app/static/js/chart.plugins.crosshairs.js
Original file line number Diff line number Diff line change
@@ -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 };
20 changes: 20 additions & 0 deletions app/static/js/chart.utils.js
Original file line number Diff line number Diff line change
@@ -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 };
67 changes: 67 additions & 0 deletions app/static/js/listeners.js
Original file line number Diff line number Diff line change
@@ -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 };
28 changes: 12 additions & 16 deletions app/templates/analytics.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator_bulma.min.css">
<!--link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator_simple.min.css"-->
{% endblock %}
{% block head_scripts %}
<script src="https://unpkg.com/chart.js@4.4.0"></script>
<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>
<script src="../../static/js/utils.js"></script>
{% endblock %}

{% block body %}
<section class="section">
<div class="container">
Expand Down Expand Up @@ -59,16 +53,18 @@ <h1 class="title">
<sup>source: https://www150.statcan.gc.ca/n1/daily-quotidien/231127/t001b-eng.htm</sup>
</div>
</section>
{% endblock %}
{% block body_scripts %}
<script src="https://unpkg.com/chart.js@4.4.0"></script>
<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>
<script type="module">
import { arbLines } from "../../static/js/chart.plugins.arblines.js";
import { crosshairs } from "../../static/js/chart.plugins.crosshairs.js";
import { toggleView } from "../../static/js/chart.utils.js";
import { setupResponseHandlers } from "../../static/js/listeners.js";

<script>
// 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/
data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
addData(chart, data.datasets, data.labels);
document.getElementById(evt.detail.dataId).remove();
});
setupResponseHandlers();
window.toggleView = toggleView;

new Chart(document.getElementById("chartId"), {
type: "bar",
Expand All @@ -86,7 +82,7 @@ <h1 class="title">

new Chart(document.getElementById("lineChartId"), {
type: "line",
plugins: [arbLines],
plugins: [arbLines, crosshairs],
options: {
events: ["mousedown", "mouseup", "mousemove"],
responsive: true,
Expand Down
1 change: 1 addition & 0 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
})
</script>
{% block body_scripts %}{% endblock %}
</body>
</html>
Loading

0 comments on commit 61cc94c

Please sign in to comment.