-
+
Target @@ -178,12 +178,24 @@
- +
+ + +
+
+ +
+
+ +
+
+
= new Set(); const selectedCategories: Set = new Set(); const searchResults: Set = new Set(); + +const bigRadarDiv = document.querySelector("#bigRadar")! +const smallRadarsDiv = document.querySelector("#smallRadars")! const weaponSearchResults = document.querySelector("#weaponSearchResults")! const displayedWeapons = document.querySelector("#displayedWeapons")!; + function toId(str: string) { return str .replaceAll(" ", "_") @@ -43,95 +50,15 @@ function toId(str: string) { // Normalization will only occur for stat types that have a unit present in the provided normalizationStats. // This allows for selective normalization, like for bar charts where we wan't mostly raw data, except for // "speed" (or other inverse metrics) which only make sense as a normalized value -function chartData( - dataset: WeaponStats, - categories: Set, - normalizationStats: UnitStats, - setBgColor: boolean -): ChartData { - let sortedCategories = Array.from(categories); - sortedCategories.sort((a,b) => { - return Object.values(MetricLabel).indexOf(a) - Object.values(MetricLabel).indexOf(b); - }); - return { - labels: [...sortedCategories], - datasets: [...selectedWeapons].map((w) => { - return { - label: w.name, - data: [...sortedCategories].map((c) => { - const metric = dataset.get(w.name)!.get(c)!; - let value = metric.value.result; - const maybeUnitStats = normalizationStats.get(c); - if (maybeUnitStats) { - const unitMin = maybeUnitStats!.min; - const unitMax = maybeUnitStats!.max; - - // Normalize - return (value - unitMin) / (unitMax - unitMin); - } - return value; - }), - backgroundColor: setBgColor ? weaponColor(w, 0.6) : weaponColor(w, 0.1), - borderColor: weaponColor(w, 0.6), - borderDash: borderDash(w), - }; - }), - }; -} - -const radar: Chart = new Chart( - document.getElementById("radar") as HTMLCanvasElement, - { - type: "radar", - options: { - animation: false, - plugins: { - legend: { - display: false, - position: "bottom", - }, - }, - responsive: true, - maintainAspectRatio: true, - scales: { - radial: { - min: 0, - max: 1, - ticks: { - display: false, - maxTicksLimit: 2, - }, - }, - }, - }, - data: chartData(stats, selectedCategories, unitStats, false), - } -); +const relativeRadar: Chart = createRadarChart( + document.querySelector("#relativeRadar")!, + chartData(stats, Array.from(selectedWeapons), selectedCategories, unitStats, false, null), + {min: 0, max: 1} +) const bars = new Array(); - -function createBarChart(element: HTMLCanvasElement, category: MetricLabel) { - const barUnitStats: UnitStats = new Map(); - if(category.includes("Speed")) { - barUnitStats.set(category, unitStats.get(category)!); - } - - return new Chart(element as HTMLCanvasElement, { - type: "bar", - options: { - animation: false, - plugins: { - legend: { - display: false, - }, - }, - responsive: true, - maintainAspectRatio: false, - }, - data: chartData(stats, new Set([category]), barUnitStats, true), - }); -} +const absoluteRadars = new Array(); function redrawBars() { const barsElem = document.getElementById("bars")!; @@ -149,11 +76,11 @@ function redrawBars() { const elem = document.createElement("canvas"); outer.appendChild(elem); barsElem.appendChild(outer); - bars.push(createBarChart(elem, c)); + bars.push(createBarChart(elem, Array.from(selectedWeapons), c, stats, unitStats)); }); } -function redrawTable(dataset: WeaponStats, unitStats: UnitStats) { +function redrawTable() { let sortedCategories = Array.from(selectedCategories); sortedCategories.sort((a,b) => { return Object.values(MetricLabel).indexOf(a) - Object.values(MetricLabel).indexOf(b); @@ -201,7 +128,7 @@ function redrawTable(dataset: WeaponStats, unitStats: UnitStats) { table.appendChild(head); selectedWeapons.forEach(weapon => { - let weaponData = dataset.get(weapon.name)!; + let weaponData = stats.get(weapon.name)!; let row = document.createElement("tr"); @@ -267,7 +194,8 @@ function redraw() { radar.update(); redrawBars(); - redrawTable(stats, unitStats); + redrawTable(); + redrawRadars(); // Update content of location string so we can share const params = new URLSearchParams(); @@ -279,6 +207,84 @@ function redraw() { window.history.replaceState(null, "", `?${params.toString()}`); } +function redrawRadars() { + if(absoluteRadarsEnabled) { + absoluteRadarsButton.classList.add('active'); + relativeRadarsButton.classList.remove('active'); + relativeRadarsDiv.classList.add('d-none'); + absoluteRadarsDiv.classList.remove('d-none'); + } else { + relativeRadarsButton.classList.add('active'); + absoluteRadarsButton.classList.remove('active'); + absoluteRadarsDiv.classList.add('d-none'); + relativeRadarsDiv.classList.remove('d-none'); + } + + relativeRadar.data = chartData(stats, Array.from(selectedWeapons), selectedCategories, unitStats, false, null); + relativeRadar.update(); + + let categoryGroups = subsets(selectedCategories, labelGroup) + let cateoryGroupsKeys = Array.from(categoryGroups.keys()); + absoluteRadars.forEach(r => { + r.clear(); + r.destroy(); + }); + absoluteRadars.splice(0, absoluteRadars.length); + if(!cateoryGroupsKeys.includes(selectedRadar)) { + selectedRadar = cateoryGroupsKeys[0]; + } + + bigRadarDiv.innerHTML = ""; + smallRadarsDiv.innerHTML = ""; + + cateoryGroupsKeys.forEach(unit => { + let parent = unit === selectedRadar ? bigRadarDiv : smallRadarsDiv + + let label = + unit === Unit.SPEED ? "Speed*" : + unit === Unit.DAMAGE ? "Damage" : + unit === Unit.RANGE ? "Range" : + "Unknown" + + let newLabel = document.createElement("div"); + newLabel.classList.add("category-group"); + newLabel.classList.add("text-center"); + newLabel.innerHTML = label; + parent.append(newLabel); + + if(categoryGroups.get(unit)!.size < 3) { + let newMessage = document.createElement("div"); + newMessage.innerHTML = "Select 3 or more categories of this type for this chart to display."; + newMessage.classList.add("text-center"); + newMessage.classList.add("my-5"); + parent.append(newMessage); + } else { + let newCanvasDiv = document.createElement("div"); + newCanvasDiv.classList.add("col-xs-6") + newCanvasDiv.classList.add("col-md-12") + let newCanvas = document.createElement("canvas"); + + let viewport = getViewport(); + if(["xl", "lg", "md"].includes(viewport)) { + newCanvas.onclick = () => { selectedRadar = unit; redrawRadars(); } + } + newCanvasDiv.appendChild(newCanvas); + parent.appendChild(newCanvasDiv); + + absoluteRadars.push( + createRadarChart( + newCanvas, + chartData(stats, Array.from(selectedWeapons), categoryGroups.get(unit)!, new Map(), false, x => x.split(' - ')[1]), + { min: 0, max: unitStats.get(unit)!.max } + ) + ); + } + }); + + absoluteRadars.forEach(r => r.draw()); + +} + function addWeaponDiv(weapon: Weapon) { const div = document.createElement("div"); div.id = weapon.name; @@ -443,14 +449,29 @@ document.getElementById("random")!.onclick = random; document.getElementById("all")!.onclick = all; document.getElementById("reset")!.onclick = reset; +let absoluteRadarsButton = document.querySelector("#absoluteRadars")!, + relativeRadarsButton = document.querySelector("#relativeRadars")!, + absoluteRadarsDiv = document.querySelector("#absoluteRadarsDiv")!, + relativeRadarsDiv = document.querySelector("#relativeRadarsDiv")!; + +absoluteRadarsButton.onclick = () => { + absoluteRadarsEnabled = true; + redrawRadars(); +} + +relativeRadarsButton.onclick = () => { + absoluteRadarsEnabled = false; + redrawRadars(); +} + // Link up Share button document.getElementById("share")!.onclick = () => { navigator.clipboard.writeText(window.location.toString()); alert("Copied to clipboard!"); }; -let numberOfTargetsInput = document.querySelector("#numberOfTargets")!; -let numberOfTargetsOutput = document.getElementById("numberOfTargetsOutput")!; +let numberOfTargetsInput = document.querySelector("#numberOfTargets")!, + numberOfTargetsOutput = document.getElementById("numberOfTargetsOutput")!; numberOfTargetsInput.oninput = () => { numberOfTargetsOutput.innerHTML = numberOfTargetsInput.value diff --git a/src/metrics.ts b/src/metrics.ts index 8aaaca4b..1d87836b 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -35,7 +35,8 @@ export enum Unit { INDEX = "Index", SPEED = "Milliseconds", RANGE = "Jeoffreys", - DAMAGE = "Hitpoints" + DAMAGE = "Hitpoints", + UNCATEGORIZED = "???" } export enum MetricLabel { @@ -76,6 +77,19 @@ export enum MetricLabel { POLEHAMMER_INDEX = "Index - Polehammer" } +export function labelGroup(label: MetricLabel): Unit { + if(label.startsWith("Damage") || label.startsWith("Thrown Damage")) + return Unit.DAMAGE; + + if(label.startsWith("Range")) + return Unit.RANGE; + + if(label.startsWith("Speed")) + return Unit.SPEED; + + return Unit.UNCATEGORIZED; +} + export function unitGroup(path: MetricPath) { if (path.includes(".damage")) { return Unit.DAMAGE; @@ -84,7 +98,7 @@ export function unitGroup(path: MetricPath) { } else if (path.includes(".range") || path.includes(".altRange")) { return Unit.RANGE; } - throw `Invalid path: ${path}`; + return Unit.UNCATEGORIZED } export const DAMAGE_METRICS = Object.values(MetricPath).filter( @@ -178,7 +192,7 @@ export class InverseMetric extends Metric { calculate(weapon: Weapon): MetricResult { let rawResult = extractNumber(weapon, this.path); return { - result: 1/rawResult, + result: (1 / rawResult) * 1000, rawResult: rawResult } } @@ -204,7 +218,7 @@ export class AggregateInverseMetric extends Metric { ); return { - result: 1 / rawResult, + result: (1 / rawResult) * 1000, rawResult: rawResult, } } diff --git a/src/ui.ts b/src/ui.ts index 9fe9c62a..5ab9acf7 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,4 +1,7 @@ +import { Chart, ChartData } from "chart.js"; import ALL_WEAPONS from "./all_weapons"; +import { MetricLabel } from "./metrics"; +import { UnitStats, WeaponStats } from "./stats"; import { Weapon } from "./weapon"; const SATURATION = "85%"; @@ -51,3 +54,111 @@ export function borderDash(weapon: Weapon) { return [2, 2]; } } + +export function createBarChart(element: HTMLCanvasElement, weapons: Array, category: MetricLabel, stats: WeaponStats, unitStats: UnitStats) { + const barUnitStats: UnitStats = new Map(); + if(category.includes("Speed")) { + barUnitStats.set(category, unitStats.get(category)!); + } + + return new Chart(element as HTMLCanvasElement, { + type: "bar", + options: { + animation: false, + plugins: { + legend: { + display: false, + }, + }, + responsive: true, + maintainAspectRatio: false, + }, + data: chartData(stats, weapons, new Set([category]), barUnitStats, true, null), + }); +} + +export function createRadarChart(canvasElem: HTMLCanvasElement, data: ChartData, range: {min: number; max: number}|undefined = undefined): Chart { + return new Chart(canvasElem, { + type: "radar", + options: { + elements: { + line: { + tension: 0, + } + }, + animation: false, + plugins: { + legend: { + display: false, + position: "bottom", + }, + }, + responsive: true, + maintainAspectRatio: true, + scales: { + radial: { + min: range ? range.max : undefined, + max: range ? range.min : undefined, + ticks: { + display: false, + maxTicksLimit: 2, + }, + }, + }, + }, + data: data + }); +} + +export function chartData( + dataset: WeaponStats, + weapons: Array, + categories: Set, + normalizationStats: UnitStats, + setBgColor: boolean, + labelTransform: ((m:MetricLabel) => string)|null +): ChartData { + let sortedCategories = Array.from(categories); + let allLabels = Object.values(MetricLabel) + sortedCategories.sort((a,b) => { + return allLabels.indexOf(a) - allLabels.indexOf(b); + }); + + return { + labels: labelTransform ? [...sortedCategories.map(labelTransform)] : [...sortedCategories], + datasets: [...weapons].map((w) => { + return { + label: w.name, + data: [...sortedCategories].map((c) => { + const metric = dataset.get(w.name)!.get(c)!; + let value = metric.value.result; + const maybeUnitStats = normalizationStats.get(c); + if (maybeUnitStats) { + const unitMin = maybeUnitStats!.min; + const unitMax = maybeUnitStats!.max; + + // Normalize + return (value - unitMin) / (unitMax - unitMin); + } + return value; + }), + backgroundColor: setBgColor ? weaponColor(w, 0.6) : weaponColor(w, 0.1), + borderColor: weaponColor(w, 0.6), + borderDash: borderDash(w), + }; + }), + }; +} + +export function getViewport(): string { + // https://stackoverflow.com/a/8876069 + const width = Math.max( + document.documentElement.clientWidth, + window.innerWidth || 0 + ) + if (width <= 576) return 'xs' + if (width <= 768) return 'sm' + if (width <= 992) return 'md' + if (width <= 1200) return 'lg' + return 'xl' +} diff --git a/src/util.ts b/src/util.ts index b9146ab8..dde93def 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,3 +6,27 @@ export function shuffle(arr: T[]) { } return newArr; } + +export function filterSet(set: Set, f: (a: A) => boolean): Set { + let results: Array = []; + set.forEach(a => { + if(f(a)) + results.push(a) + }); + return new Set(results); +} + + +export function subsets(set: Set, f: (a: A) => B): Map> { + let results = new Map>(); + set.forEach(a => { + let k = f(a); + let current = new Set(); + if(results.has(k)) { + current = results.get(k)!; + } + current.add(a); + results.set(k, current); + }); + return results +}