- Replace all hand-written CSS with Tailwind utility classes - Upgrade CodeMirror 5 to CodeMirror 6 (scoped @codemirror/* packages) - Redesign layout with responsive sidebar navigation using JSX/Mithril - Flatten admin routes (e.g. /admin/presets -> /presets) - Add "remember me" login option with 2-week persistent cookie - Add delete confirmation dialogs across admin pages - Upgrade esbuild from 0.21 to 0.27 and update browser targets - Add Inter and Roboto Mono variable fonts with subset script
149 lines
4.2 KiB
TypeScript
149 lines
4.2 KiB
TypeScript
import { ClosureComponent, Component, Children } from "mithril";
|
|
import { m } from "./components.ts";
|
|
import { stringify } from "../lib/common/expression/parser.ts";
|
|
import memoize from "../lib/common/memoize.ts";
|
|
import * as store from "./store.ts";
|
|
|
|
const memoizedStringify = memoize(stringify);
|
|
|
|
function drawChart(chartData): Children {
|
|
const slices = chartData.slices;
|
|
const total: number = Array.from(Object.values(chartData.slices)).reduce(
|
|
(a: number, s) => a + (s["count"]["value"] || 0),
|
|
0,
|
|
);
|
|
const legend = [];
|
|
const paths = [];
|
|
const links = [];
|
|
let currentProgressPercentage = 0;
|
|
let startX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100;
|
|
let startY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100;
|
|
let endX, endY;
|
|
|
|
for (const slice of Object.values(slices)) {
|
|
const percent = total > 0 ? (slice["count"]["value"] || 0) / total : 0;
|
|
legend.push(
|
|
m("tr", [
|
|
m(
|
|
"td",
|
|
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
|
|
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`,
|
|
}),
|
|
),
|
|
m("td.w-full", store.evaluateExpression(slice["label"], null)),
|
|
m(
|
|
"td.text-stone-500 text-right tabular-nums",
|
|
`${Math.round(percent * 100)}%`,
|
|
),
|
|
m(
|
|
"td.text-right tabular-nums",
|
|
m(
|
|
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
|
|
{
|
|
href: `#!/devices/?${m.buildQueryString({
|
|
filter: memoizedStringify(slice["filter"]),
|
|
})}`,
|
|
},
|
|
slice["count"]["value"] || 0,
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
|
|
if (percent > 0) {
|
|
currentProgressPercentage += percent;
|
|
endX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100;
|
|
endY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100;
|
|
const isBigArc = percent > 0.5 ? 1 : 0;
|
|
|
|
const sketch =
|
|
`M ${startX} ${startY} ` + // Move to the starting point
|
|
`A 100 100 0 ${isBigArc} 1 ${endX} ${endY} ` + // Draw an Arc from starting point to ending point
|
|
`L 0 0 z`; // complete the shape by drawing a line to the center of circle
|
|
|
|
startX = endX;
|
|
startY = endY;
|
|
|
|
paths.push(
|
|
m("path.stroke-white stroke-1", {
|
|
d: sketch,
|
|
fill: store.evaluateExpression(slice["color"], null),
|
|
}),
|
|
);
|
|
|
|
const percentageX =
|
|
Math.cos(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50;
|
|
const percentageY =
|
|
Math.sin(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50;
|
|
|
|
links.push(
|
|
m(
|
|
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
|
|
{
|
|
"xlink:href": `#!/devices/?${m.buildQueryString({
|
|
filter: memoizedStringify(slice["filter"]),
|
|
})}`,
|
|
},
|
|
[
|
|
m("path.stroke-cyan-500 stroke-1", {
|
|
d: sketch,
|
|
"fill-opacity": 0,
|
|
}),
|
|
m(
|
|
"text.opacity-40 font-medium fill-black",
|
|
{
|
|
x: percentageX,
|
|
y: percentageY,
|
|
"dominant-baseline": "middle",
|
|
"text-anchor": "middle",
|
|
},
|
|
`${Math.round(percent * 100)}%`,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
legend.push(
|
|
m(
|
|
"tr",
|
|
m("td", ""),
|
|
m("td", { colspan: 2 }, "Total"),
|
|
m("td.text-right tabular-nums", total),
|
|
),
|
|
);
|
|
|
|
return m(
|
|
"loading",
|
|
{
|
|
queries: Object.values(chartData.slices).map((s) => s["count"]),
|
|
},
|
|
m("div", [
|
|
m(
|
|
"svg.m-4",
|
|
{
|
|
// Adding 2 as padding; strokes must not be more than 2
|
|
viewBox: "-102 -102 204 204",
|
|
width: "204px",
|
|
height: "204px",
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
|
},
|
|
paths.concat(links),
|
|
),
|
|
m("table.mt-8 text-sm", legend),
|
|
]),
|
|
);
|
|
}
|
|
|
|
const component: ClosureComponent = (): Component => {
|
|
return {
|
|
view: (vnode) => {
|
|
return drawChart(vnode.attrs["chart"]);
|
|
},
|
|
};
|
|
};
|
|
|
|
export default component;
|