diff --git a/module/Apps/StatsViewer.mjs b/module/Apps/StatsViewer.mjs index 66777dc..e7c5c3a 100644 --- a/module/Apps/StatsViewer.mjs +++ b/module/Apps/StatsViewer.mjs @@ -1,5 +1,7 @@ +import { Chart } from "chart.js"; import { filePath } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { smallToLarge } from "../utils/sorters/smallToLarge.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -19,7 +21,7 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { }, position: { width: 475, - height: 315, + height: 400, }, actions: {}, }; @@ -55,6 +57,11 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { for (const input of elements) { input.addEventListener(`change`, this.#bindListener.bind(this)); }; + + if (options.parts.includes(`graph`)) { + const canvas = this.element.querySelector(`canvas`); + new Chart( canvas, this._graphData ); + }; }; async _preparePartContext(partId) { @@ -126,22 +133,76 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { _graphData = {}; async #prepareGraphContext(_ctx) { - const datasets = CONFIG.StatsDatabase.getRows( + const userData = CONFIG.StatsDatabase.getRows( `${this._selectedTable}/${this._selectedSubtable}`, this._selectedUsers, ); - Logger.log(datasets); + const data = {}; + const allBuckets = new Set(); - const buckets = {}; - for (const row of datasets[game.user.id] ?? []) { - buckets[row.value] ??= 0; - buckets[row.value] += 1; + /* + When we're displaying data for a table within the Dice namespace, we want to + include all values that any user might not have rolled, just for completeness + since we do know exactly what labels to display, this might eventually be + replaced with a per-table configuration setting to determine what values are + populated within the graph and nothing else / prevent non-accepted values + from being added to the table in the first place. + */ + if (this._selectedTable === `Dice`) { + const size = Number.parseInt(this._selectedSubtable.slice(1)); + for (let i = 1; i <= size; i++) { + allBuckets.add(i); + }; }; - const sorted = Object.entries(buckets).sort(([v1], [v2]) => Math.sign(v1 - v2)); + for (const user of this._selectedUsers) { + const buckets = {}; + for (const row of userData[user] ?? []) { + buckets[row.value] ??= 0; + buckets[row.value] += 1; + allBuckets.add(row.value); + }; + data[user] = buckets; + } - Logger.log(sorted); + const sortedBucketNames = Array.from(allBuckets).sort(smallToLarge); + const datasets = {}; + for (const bucket of sortedBucketNames) { + for (const user of this._selectedUsers) { + datasets[user] ??= []; + datasets[user].push(data[user][bucket]); + }; + }; + + this._graphData = { + type: `bar`, + options: { + scales: { + y: { + stacked: true, + }, + x: { + stacked: true, + }, + }, + }, + data: { + labels: sortedBucketNames, + datasets: Object.entries(datasets).map(([userID, values]) => { + const user = game.users.get(userID); + return { + label: user.name, + data: values, + borderColor: user.color.css, + backgroundColor: user.color.toRGBA(0.5), + borderWidth: 2, + borderRadius: 4, + borderSkipped: false, + }; + }), + }, + }; }; /** @@ -160,17 +221,5 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) { Logger.log(`updating ${binding} value to ${target.value}`); this[binding] = target.value; this.render(); - // this.#updatePartContainingElement(target); - }; - - /** - * @param { HTMLElement } element - */ - #updatePartContainingElement(element) { - const partRoot = element.closest(`[data-application-part]`); - if (!partRoot) { return }; - const data = partRoot.dataset; - const partId = data.applicationPart; - this.render({ parts: [partId] }); }; }; diff --git a/module/utils/databases/Memory.mjs b/module/utils/databases/Memory.mjs index a40b341..74c80ba 100644 --- a/module/utils/databases/Memory.mjs +++ b/module/utils/databases/Memory.mjs @@ -52,13 +52,17 @@ export class MemoryDatabase { if (!subtable) { continue; } + const size = Number.parseInt(subtable.slice(1)); const rows = []; for (let i = 1; i <= size; i++) { const count = getNormalDistributionHeight(i, size / 2, size); - console.table({ count, i }); - const temp = new Array(count).fill(null).map(() => generateRow(i)); + const temp = new Array(count) + .fill(null) + .map(() => generateRow( + game.user.id == user ? i : Math.ceil(Math.random() * size), + )); rows.push(...temp); }; diff --git a/module/utils/sorters/smallToLarge.mjs b/module/utils/sorters/smallToLarge.mjs new file mode 100644 index 0000000..e2c5da2 --- /dev/null +++ b/module/utils/sorters/smallToLarge.mjs @@ -0,0 +1,19 @@ +/** + * + * @param {string | number} a + * @param {string | number} b + */ +export function smallToLarge(a, b) { + const aInt = Number.parseInt(a); + const bInt = Number.parseInt(b); + + const aIsInvalid = Number.isNaN(aInt); + const bIsInvalid = Number.isNaN(bInt); + if (aIsInvalid && bIsInvalid) { + return a > b; + } else if (aIsInvalid || bIsInvalid) { + return aIsInvalid ? -1 : 1; + }; + + return Math.sign(aInt - bInt); +}; diff --git a/public/styles/Apps/StatsViewer.css b/public/styles/Apps/StatsViewer.css index 550b309..02a364e 100644 --- a/public/styles/Apps/StatsViewer.css +++ b/public/styles/Apps/StatsViewer.css @@ -17,5 +17,7 @@ [data-application-part="graph"] { flex-grow: 1; + justify-items: center; + position: relative; } } diff --git a/public/templates/Apps/StatsViewer/graph.hbs b/public/templates/Apps/StatsViewer/graph.hbs index c5730e1..73d52af 100644 --- a/public/templates/Apps/StatsViewer/graph.hbs +++ b/public/templates/Apps/StatsViewer/graph.hbs @@ -1,3 +1,3 @@