diff --git a/package-lock.json b/package-lock.json
index 6f56374..2745f4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,10 @@
"name": "fancyscribe",
"version": "0.0.0",
"dependencies": {
+ "chart.js": "^4.4.8",
"jszip": "^3.10.1",
"react": "^19.0.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
@@ -742,6 +744,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1058,6 +1066,18 @@
}
]
},
+ "node_modules/chart.js": {
+ "version": "4.4.8",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
+ "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2063,6 +2083,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
+ "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
diff --git a/package.json b/package.json
index b06c14f..0ae56a1 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "chart.js": "^4.4.8",
"jszip": "^3.10.1",
"react": "^19.0.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
diff --git a/src/10th/Roster.jsx b/src/10th/Roster.jsx
index 850e5cc..c011ae4 100644
--- a/src/10th/Roster.jsx
+++ b/src/10th/Roster.jsx
@@ -1,4 +1,4 @@
-import { useRef, useState, useEffect, Fragment } from "react";
+import React, { useRef, useState, useEffect, Fragment } from "react";
import factionBackground from "../assets/factionBackground.png";
import keywordsBackground from "../assets/keywordsBackground.png";
import cardBackground from "../assets/cardBackground.png";
@@ -13,8 +13,14 @@ import { Weapons, hasDifferentProfiles } from "./Weapons";
import { useIndexedDB } from "../helpers/useIndexedDB"; // New hook for IndexedDB
import { ImgEditor } from "./ImgEditor";
import { trySettingLocalStorage } from "../helpers/useLocalStorage";
+import { ShortSummaryTable } from "./ShortSummaryTable";
-export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
+export const Roster = ({
+ roster,
+ onePerPage,
+ colorUserChoice,
+ primaryColor,
+}) => {
if (!roster) {
return null;
}
@@ -26,7 +32,7 @@ export const Roster = ({ roster, onePerPage, colorUserChoice }) => {
}}
>
{
marginBottom: "12px",
}}
>
- {name} [{cost.commandPoints} CP, {cost.points} pts]
+ {name}
+ {cost.points} pts
+
{forces.map((force, index) => (
-
+
+
+
+
))}
);
diff --git a/src/10th/ShortSummaryTable.jsx b/src/10th/ShortSummaryTable.jsx
new file mode 100644
index 0000000..e2ab6fe
--- /dev/null
+++ b/src/10th/ShortSummaryTable.jsx
@@ -0,0 +1,248 @@
+import React from "react";
+import { Bar } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Tooltip,
+ Legend,
+ Title,
+} from "chart.js";
+import cardBackground from "../assets/cardBackground.png";
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Tooltip,
+ Legend,
+ Title,
+);
+export const ShortSummaryTable = ({ force, primaryColor }) => {
+ const { units, factionRules, rules, catalog } = force;
+
+ const sortedUnits = units.sort((a, b) => {
+ // sort by points cost desc first, then by name
+ if (a.cost.points < b.cost.points) return 1;
+ if (a.cost.points > b.cost.points) return -1;
+ return a.name.localeCompare(b.name);
+ });
+
+ // Prepare data for the bar chart, group by toughness and sum points cost
+ const groupedByToughness = units.reduce((acc, unit) => {
+ const toughness = unit.modelStats?.[0]?.toughness || 0;
+ if (!acc[toughness]) {
+ acc[toughness] = { totalPoints: 0, unitNames: [] };
+ }
+ acc[toughness].totalPoints += unit.cost.points;
+ if (!acc[toughness].unitNames.includes(unit.name)) {
+ acc[toughness].unitNames.push(unit.name);
+ }
+ return acc;
+ }, {});
+
+ const groupedChartDataToughness = Object.entries(groupedByToughness)
+ .map(([toughness, { totalPoints, unitNames }]) => ({
+ toughness: parseInt(toughness, 10),
+ totalPoints,
+ unitNames,
+ }))
+ .sort((a, b) => a.toughness - b.toughness);
+
+ // Prepare data for the bar chart, group by movement and sum points cost
+ const groupedByMovement = units.reduce((acc, unit) => {
+ const movement = unit.modelStats?.[0]?.move || 0;
+ if (!acc[movement]) {
+ acc[movement] = { totalPoints: 0, unitNames: [] };
+ }
+ acc[movement].totalPoints += unit.cost.points;
+ if (!acc[movement].unitNames.includes(unit.name)) {
+ acc[movement].unitNames.push(unit.name);
+ }
+ return acc;
+ }, {});
+
+ const groupedChartDataMovement = Object.entries(groupedByMovement)
+ .map(([movement, { totalPoints, unitNames }]) => ({
+ movement: parseInt(movement, 10),
+ totalPoints,
+ unitNames,
+ }))
+ .sort((a, b) => a.movement - b.movement);
+
+ return (
+
+
+
+
+
+ Unit Name
+
+
+ Cost (pts)
+
+
+
+
+ {sortedUnits.map((unit, index) => {
+ let { name, cost, models } = unit;
+ const count = models?.[0]?.count || 1;
+ return (
+
+
+ {count > 1 ? count + "x " : ""}
+ {name}
+
+
+ {cost.points} pts
+
+
+ );
+ })}
+
+
+
+
+
+ data.movement + '"',
+ ),
+ datasets: [
+ {
+ label: "Total Cost (pts)",
+ data: groupedChartDataMovement.map(
+ (data) => data.totalPoints,
+ ),
+ backgroundColor: primaryColor,
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: "MOVEMENT OF YOUR UNITS",
+ font: {
+ size: 14,
+ weight: "bold",
+ family: "Noto Sans",
+ },
+ color: "#000",
+ align: "start",
+ },
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ displayColors: false,
+ callbacks: {
+ title: function (context) {
+ return "";
+ },
+ label: function (context) {
+ const movement = context.label;
+ const unitNames = groupedChartDataMovement.find(
+ (data) => data.movement === parseInt(movement, 10),
+ )?.unitNames;
+ return unitNames;
+ },
+ },
+ },
+ },
+ }}
+ />
+
+
+
+ data.toughness),
+ datasets: [
+ {
+ label: "Total Cost (pts)",
+ data: groupedChartDataToughness.map(
+ (data) => data.totalPoints,
+ ),
+ backgroundColor: primaryColor,
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true,
+ },
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: "TOUGHNESS OF YOUR UNITS",
+ font: {
+ size: 14,
+ weight: "bold",
+ family: "Noto Sans",
+ },
+ color: "#000",
+ align: "start",
+ },
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ displayColors: false,
+ callbacks: {
+ title: function (context) {
+ return "";
+ },
+ label: function (context) {
+ const toughness = context.label;
+ const unitNames = groupedChartDataToughness.find(
+ (data) => data.toughness === parseInt(toughness, 10),
+ )?.unitNames;
+ return unitNames;
+ },
+ },
+ },
+ },
+ }}
+ />
+
+
+
+ );
+};
diff --git a/src/App.jsx b/src/App.jsx
index d4c1925..d83bb43 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -214,6 +214,7 @@ function App() {
return (
)}