Skip to content

Commit

Permalink
[0.7.0] Cumulative Probability Graphs (#12)
Browse files Browse the repository at this point in the history
# Changes

- Add Cumulative Probability Graphs to `Advanced Stats` screen
- Add Cumulative Probability Graphs to PDF

# UI

- Allow users to toggle the X Axis Syncing of graphs in the `Advanced Stats` screen
  • Loading branch information
damonhook authored Dec 26, 2019
1 parent 6ee607a commit 4b34801
Show file tree
Hide file tree
Showing 21 changed files with 484 additions and 88 deletions.
27 changes: 25 additions & 2 deletions api/controllers/statsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ export const compareUnits = ({ units }) => {
};
};

const buildCumulative = (probabilities, unitNames, metrics) => {
const maxDamage = Math.max(...Object.keys(probabilities));
const sums = unitNames.reduce((acc, name) => ({ ...acc, [name]: 0 }), {});
const cumulative = [...Array(maxDamage + 1)].map((_, damage) => {
const map = probabilities[damage] || {};
return unitNames.reduce((acc, name) => {
const val = map[name] || 0;
sums[name] += val;
if (sums[name] >= 100 || damage > metrics.max[name]) {
sums[name] = 100;
}
return { ...acc, [name]: Number(sums[name].toFixed(2)) };
}, { damage });
});
return [
...cumulative,
unitNames.reduce((acc, name) => ({ ...acc, [name]: 100 }), { damage: maxDamage + 1 }),
];
};

const buildProbability = ({ save, ...unitResults }) => {
const probabilities = {};
const metrics = { mean: {}, median: {}, max: {} };
Expand All @@ -35,9 +55,12 @@ const buildProbability = ({ save, ...unitResults }) => {
metrics.max[name] = unitResults[name].metrics.max;
});
const buckets = Object.keys(probabilities).sort((x, y) => x - y).map((damage) => ({
damage, ...probabilities[damage],
damage: Number(damage), ...probabilities[damage],
}));
return { save, buckets, metrics };
const cumulative = buildCumulative(probabilities, Object.keys(unitResults), metrics);
return {
save, buckets, cumulative, metrics,
};
};

export const simulateUnitsForSave = ({
Expand Down
2 changes: 2 additions & 0 deletions client/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REACT_APP_VERSION=$npm_package_version
REACT_APP_NAME=$npm_package_name
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "statshammer",
"version": "0.6.2",
"version": "0.7.0",
"private": true,
"proxy": "http://localhost:5000/",
"engines": {
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Drawer/Drawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ const Drawer = ({ open, onClose, page }) => {
<Divider className={classes.divider} variant="middle" />
<SocialItems />
<Divider className={classes.divider} variant="middle" />
<Typography variant="caption" className={classes.version}>v0.6.2</Typography>
{process.env.REACT_APP_VERSION && (
<Typography variant="caption" className={classes.version}>
{`v${process.env.REACT_APP_VERSION}`}
</Typography>
)}
</List>
</AppDrawer>
);
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/GraphTooltips/ProbabilityTooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ const useStyles = makeStyles((theme) => ({
/**
* A tooltip to display when you hover over a value in a graph
*/
const ProbabilityTooltip = ({ active, payload, label }) => {
const ProbabilityTooltip = ({
active, payload, label, cumulative,
}) => {
const classes = useStyles();
if (active) {
return (
<Paper className={classes.tooltip}>
<Typography variant="h6">{`Damage: ${label}`}</Typography>
<Typography variant="h6">
{`Damage: ${cumulative ? '<= ' : ''}${label}`}
</Typography>
{(payload || []).map(({ color, name, value }) => (
<Typography style={{ color }} key={name}>{`${name}: ${value}%`}</Typography>
))}
Expand All @@ -34,6 +38,7 @@ ProbabilityTooltip.defaultProps = {
active: false,
payload: [],
label: '',
cumulative: false,
};

ProbabilityTooltip.propTypes = {
Expand All @@ -43,6 +48,7 @@ ProbabilityTooltip.propTypes = {
payload: PropTypes.arrayOf(PropTypes.object),
/** The series label */
label: PropTypes.string,
cumulative: PropTypes.bool,
};

export default ProbabilityTooltip;
15 changes: 12 additions & 3 deletions client/src/containers/AdvancedStats/AdvancedStats.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { useMediaQuery } from '@material-ui/core';
import { useHistory } from 'react-router-dom';
import { useMapping } from 'hooks';
import { getResultsMapping, getProbabilitiesMapping, applyUnitNameMapping } from 'utils/mappers';
import BasicCurves from 'containers/ProbabilityCurves/BasicCurves';
import CumulativeCurves from 'containers/ProbabilityCurves/CumulativeCurves';
import MetricsTables from './MetricsTables';
import ProbabilityCurves from './ProbabilityCurves';
import ProbabilityTables from './ProbabilityTables';

const useStyles = makeStyles((theme) => ({
Expand Down Expand Up @@ -77,10 +78,18 @@ const AdvancedStats = React.memo(({
<div className={classes.container}>
<Tabbed
className={classes.tabs}
tabNames={['Graphs', 'Tables']}
tabNames={['Cumulative', 'Single', 'Tables']}
tabContent={[
<div className={classes.tab}>
<ProbabilityCurves
<CumulativeCurves
pending={simulations.pending}
error={simulations.error}
probabilities={probabilities}
unitNames={unitNames}
/>
</div>,
<div className={classes.tab}>
<BasicCurves
pending={simulations.pending}
error={simulations.error}
probabilities={probabilities}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import React, { useCallback, useState } from 'react';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { LineGraph } from 'components/Graphs';
import {
Grid, MenuItem, TextField, Typography,
} from '@material-ui/core';
import clsx from 'clsx';
import ListItem from 'components/ListItem';
import { GraphSkeleton } from 'components/Skeletons';
import { AdvancedStatsErrorCard } from 'components/ErrorCards';
import { Grid } from '@material-ui/core';
import _ from 'lodash';
import { ProbabilityTooltip } from 'components/GraphTooltips';
import { getMaxDamage, getMaxProbability, getTicks } from './probabilityUtils';
import clsx from 'clsx';
import ListItem from 'components/ListItem';
import {
getMaxDamage, getMaxProbability, getTicks, REFERENCE_LINE_OPTIONS,
} from './probabilityUtils';
import GraphControls from './GraphControls';
import Loadable from './Loadable';

const useStyles = makeStyles((theme) => ({
probabilityCurves: {},
content: {},
graphContainer: ({ numUnits }) => ({
height: numUnits >= 3 ? '350px' : '250px',
height: numUnits >= 3 ? '350px' : '300px',
marginBottom: theme.spacing(3),
flexBasis: '50%',
minWidth: '450px',
Expand All @@ -25,9 +25,6 @@ const useStyles = makeStyles((theme) => ({
minWidth: '100%',
},
}),
skeleton: {
padding: theme.spacing(2, 4, 5),
},
select: {
maxWidth: '100%',
display: 'flex',
Expand All @@ -47,90 +44,54 @@ const useStyles = makeStyles((theme) => ({
},
}));

const REFERENCE_LINE_OPTIONS = {
NONE: 'None',
MEAN: 'Mean',
MEDIAN: 'Median',
MAX: 'Max',
};

const Loadable = React.memo(({
children, loading, numUnits, error,
}) => {
const classes = useStyles({ numUnits });

if (error) {
return <AdvancedStatsErrorCard />;
}
if (loading) {
return (
<Grid container spacing={2}>
{[...Array(6)].map(() => (
<Grid item className={classes.graphContainer}>
<GraphSkeleton
series={5}
groups={2}
height={numUnits >= 3 ? 350 : 250}
className={classes.skeleton}
/>
</Grid>
))}
</Grid>
);
}
return children;
}, (prevProps, nextProps) => _.isEqual(prevProps, nextProps));

const ProbabilityCurves = React.memo(({
pending, probabilities, unitNames, className, error,
const BasicCurves = React.memo(({
probabilities, unitNames, className, error, pending,
}) => {
const classes = useStyles({ numUnits: unitNames.length });
const theme = useTheme();
const [activeReferenceLine, setActiveReferenceLine] = useState(REFERENCE_LINE_OPTIONS.NONE);
const [matchXAxis, setMatchXAxis] = useState(true);

let [maxDamage, maxProbability, ticks] = [0, 0, null];
let maxDamage = matchXAxis ? 0 : 'dataMax';
let [maxProbability, ticks] = [0, null];
if (probabilities && probabilities.length) {
maxDamage = getMaxDamage(probabilities);
if (matchXAxis) {
maxDamage = getMaxDamage(probabilities);
}
maxProbability = getMaxProbability(probabilities);
if (maxProbability) {
ticks = getTicks(maxProbability);
}
}

const yAxisLabel = useCallback((value) => `${value}%`, []);

let activeMetric = null;
if (activeReferenceLine !== REFERENCE_LINE_OPTIONS.NONE) {
activeMetric = activeReferenceLine.toLowerCase();
}

const handleReferenceLineChanged = (event) => {
setActiveReferenceLine(event.target.value);
const handleReferenceLineChanged = (value) => {
setActiveReferenceLine(value);
};

const handleSetMatchXAxisChanged = (value) => {
setMatchXAxis(value);
};

return (
<ListItem
className={clsx(classes.probabilityCurves, className)}
header="Probability Curves"
header="Base Probability Curves"
collapsible
loading={pending}
loaderDelay={0}
>
<div className={classes.select}>
<Typography className={classes.selectInfo}>Reference Lines:</Typography>
<TextField
select
variant="filled"
label="Metric"
className={classes.field}
value={activeReferenceLine}
onChange={handleReferenceLineChanged}
>
{Object.values(REFERENCE_LINE_OPTIONS).map((option) => (
<MenuItem value={option} key={option}>{option}</MenuItem>
))}
</TextField>
</div>
<GraphControls
matchXAxis={matchXAxis}
setMatchXAxis={handleSetMatchXAxisChanged}
activeReferenceLine={activeReferenceLine}
setActiveReferenceLine={handleReferenceLineChanged}
/>
<Loadable loading={pending} numUnits={unitNames.length} error={error}>
<Grid container spacing={2} className={classes.content}>
{probabilities.map(({ save, buckets, metrics }) => (
Expand Down Expand Up @@ -174,4 +135,4 @@ const ProbabilityCurves = React.memo(({
);
}, (prevProps, nextProps) => _.isEqual(prevProps, nextProps));

export default ProbabilityCurves;
export default BasicCurves;
Loading

0 comments on commit 4b34801

Please sign in to comment.