diff --git a/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.css b/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.css
index 80db7e185..19b9e44e8 100644
--- a/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.css
+++ b/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.css
@@ -3,3 +3,35 @@
font-size: 0.9rem;
}
+.ResponsiveContainer2 .recharts-legend-item-text {
+ color: inherit !important;
+}
+
+.ResponsiveContainer2 .recharts-default-legend {
+ line-height: 22px;
+}
+
+.ResponsiveContainer2 .recharts-legend-item {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.ResponsiveContainer2 .recharts-default-legend + .recharts-default-legend {
+ margin-top: 8px;
+}
+
+.trinket-tooltip {
+ background: #1b1b1b;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ padding: 8px 10px;
+}
+
+.trinket-tooltip-label {
+ color: #fff;
+ margin-bottom: 6px;
+}
+
+.trinket-tooltip-row.is-hovered {
+ font-weight: 700;
+}
+
diff --git a/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.js b/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.js
index bf9438359..8dffebaa8 100644
--- a/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.js
+++ b/src/General/Modules/TrinketAnalysis/Charts/VerticalChart.js
@@ -1,5 +1,5 @@
import React, { PureComponent } from "react";
-import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Legend, CartesianGrid, Tooltip } from "recharts";
+import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, CartesianGrid, Tooltip, ReferenceLine, Customized, Legend, DefaultLegendContent } from "recharts";
// import chroma from "chroma-js";
import { getItemIcon, getTranslatedItemName } from "../../../Engine/ItemUtilities";
import MuiTooltip from '@mui/material/Tooltip';
@@ -89,10 +89,150 @@ function getInitials(str) {
.map(word => word[0].toUpperCase())
.join('');
}
+
+const Indicator = {
+ EQUIPPED: { id: "equipped", color: "#4FC3F7", label: "Equipped" },
+ OWNED: { id: "owned", color: "#CE93D8", label: "In bags" },
+ VAULT: { id: "vault", color: "#00FFFF", label: "Great Vault" },
+ key(id, ilvl) {
+ return `${id}:${ilvl}`;
+ },
+ of(trinket, current) {
+ if (trinket.isEquipped) return this.EQUIPPED;
+ if (trinket.vaultItem && current !== this.EQUIPPED) return this.VAULT;
+ return current || this.OWNED;
+ },
+ fromPlayer(trinkets, itemLevels) {
+ const indicators = {};
+ (trinkets || []).forEach((t) => {
+ if (!t) return;
+ const key = this.key(t.id, snapIlvl(t.level, itemLevels));
+ indicators[key] = this.of(t, indicators[key]);
+ });
+ return indicators;
+ },
+ legend() {
+ return [this.EQUIPPED, this.OWNED, this.VAULT];
+ },
+};
+
+const snapIlvl = (level, itemLevels) => {
+ if (!itemLevels || !itemLevels.length) return level;
+ return itemLevels.reduce((best, ilvl) =>
+ Math.abs(ilvl - level) < Math.abs(best - level) ? ilvl : best
+ );
+};
+
+const sliceScore = (rows, id, ilvl) => {
+ const row = (rows || []).find((entry) => entry.id === id);
+ return row ? row["i" + ilvl] || 0 : 0;
+};
+
+const playerTrinkets = (player) => (player && player.getActiveItems("Trinket")) || [];
+
+const chartSignature = ({ itemLevels = [], data = [], theme = [], breakdown, player }) => {
+ const top = itemLevels[itemLevels.length - 1] || "";
+ return [
+ itemLevels.join(),
+ !!breakdown,
+ theme.join(),
+ playerTrinkets(player).map((t) => `${t.id}:${t.level}:${!!t.isEquipped}:${!!t.vaultItem}`).join(),
+ data.map((row) => `${row.id}:${row["i" + top] || 0}`).join(),
+ ].join("|");
+};
+
+function IndicatorOverlay({ formattedGraphicalItems, indicators, hoverSlice }) {
+ if (!formattedGraphicalItems) return null;
+ const overlays = [];
+ let hoverRect = null;
+ formattedGraphicalItems.forEach((entry) => {
+ const ilvl = entry.item && entry.item.props && entry.item.props.dataKey;
+ const rects = entry.props && entry.props.data;
+ if (ilvl == null || !rects) return;
+ rects.forEach((rect) => {
+ const id = rect.payload && rect.payload.name;
+ if (id == null || !(rect.width > 0) || !(rect.height > 0)) return;
+ const indicator = indicators[Indicator.key(id, ilvl)];
+ if (indicator) {
+ overlays.push(
+
+ );
+ }
+ if (hoverSlice && hoverSlice.id === id && hoverSlice.ilvl === ilvl) hoverRect = rect;
+ });
+ });
+ return (
+
+ {hoverRect ? (
+
+ ) : null}
+ {overlays}
+
+ );
+}
+
+function TrinketTooltip({ active, payload, label, hoverSlice, breakdown, data, currentLanguage }) {
+ if (!active || !payload || !payload.length) return null;
+ const itemId = payload[0].payload && payload[0].payload.name;
+ if (!breakdown && (!hoverSlice || hoverSlice.id !== itemId)) return null;
+ return (
+
+
{getTranslatedItemName(label, currentLanguage)}
+ {payload.map((entry) => {
+ const name = entry.dataKey;
+ const hovered = !breakdown && hoverSlice && hoverSlice.id === itemId && hoverSlice.ilvl == name;
+ let text;
+ if (entry.value <= 0) text = "Unobtainable";
+ else if (breakdown) text = Math.round(entry.value);
+ else text = data.filter((row) => row.id === itemId).map((row) => row["i" + name]).toString();
+ const displayName = breakdown ? (name === "passive" ? "Passive Stats" : "Effect") : name;
+ return (
+
+ {hovered ? "▸ " : ""}
+ {displayName} : {text}
+
+ );
+ })}
+
+ );
+}
+
+function IndicatorLegend({ items }) {
+ return (
+
+ {items.map(({ label, color }) => (
+ -
+
+ {label}
+
+ ))}
+
+ );
+}
export default class VerticalChart extends PureComponent {
constructor() {
super();
- this.state = { focusBar: null, mouseLeave: true, width: window.innerWidth, height: window.innerHeight };
+ this.state = { focusBar: null, mouseLeave: true, width: window.innerWidth, height: window.innerHeight, hoverSlice: null, compare: null };
}
updateDimensions = () => {
@@ -101,6 +241,11 @@ export default class VerticalChart extends PureComponent {
componentDidMount() {
window.addEventListener('resize', this.updateDimensions);
}
+ componentDidUpdate(prevProps) {
+ if (this.state.compare && chartSignature(prevProps) !== chartSignature(this.props)) {
+ this.setState({ compare: null, hoverSlice: null });
+ }
+ }
componentWillUnmount() {
window.removeEventListener('resize', this.updateDimensions);
}
@@ -120,6 +265,14 @@ export default class VerticalChart extends PureComponent {
const barColours = this.props.theme;
const breakdown = this.props.breakdown ?? false;
+ const indicators = breakdown ? {} : Indicator.fromPlayer(playerTrinkets(this.props.player), itemLevels);
+ const { hoverSlice, compare } = this.state;
+ const guide = compare || (hoverSlice && hoverSlice.indicator ? hoverSlice : null);
+ const readSlice = (entry, ilvl) => {
+ const id = entry && entry.payload && entry.payload.name;
+ if (id == null) return null;
+ return { id, ilvl, score: sliceScore(data, id, ilvl), indicator: indicators[Indicator.key(id, ilvl)] };
+ };
let arr = [];
let cleanedArray = [];
@@ -212,12 +365,11 @@ export default class VerticalChart extends PureComponent {
barCategoryGap="15%"
data={cleanedArray}
layout="vertical"
-
onMouseMove={(state) => {
if (state.isTooltipActive) {
this.setState({ focusBar: state.activeTooltipIndex, mouseLeave: false });
} else {
- this.setState({ focusBar: null, mouseLeave: true });
+ this.setState({ focusBar: null, mouseLeave: true, hoverSlice: null });
}
}}
>
@@ -225,46 +377,32 @@ export default class VerticalChart extends PureComponent {
getTranslatedItemName(timeStr, currentLanguage)}
- formatter={(value, name, props) => {
- if (value <= 0) return ["Unobtainable", name];
- if (breakdown) {
- const isPassive = name === "passive";
- return [Math.round(value), isPassive ? "Passive Stats" : "Effect"];
- }
- return [
- data
- .filter((filter) => filter.id === props["payload"].name)
- .map((key) => key["i" + name])
- .toString(),
- name,
- ];
- }}
+ wrapperStyle={{ pointerEvents: "none" }}
+ content={
+
+ }
+ />
+