Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add plugin to draw crosshair to track y at x #10

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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