added inventory chart for bins

This commit is contained in:
Carter 2025-03-10 13:33:55 -06:00
parent cd972409b2
commit a0eabf50c6
4 changed files with 368 additions and 31 deletions

View file

@ -0,0 +1,213 @@
import { Box, Chip, useTheme } from "@mui/material";
import { GrainColour } from "grain";
import { pond } from "protobuf-ts/pond";
import { useEffect, useState } from "react";
import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
import { stringToMaterialColour } from "utils";
export interface GrainAmount {
grain: pond.Grain;
grainName: string;
bushelAmount: number;
bushelsPerTonne?: number;
}
interface Props {
inventory: GrainAmount[];
onClick: (grain: pond.Grain, grainName: string) => void;
activeGrain?: pond.Grain | null;
customLabel?: string;
customUnit?: string;
}
export default function BinInventoryChart(props: Props) {
const { inventory, onClick, activeGrain, customLabel, customUnit } = props;
const theme = useTheme();
const [activeIndex, setActiveIndex] = useState(0);
useEffect(() => {
let newIndex = inventory.findIndex(g => g.grain === activeGrain);
setActiveIndex(newIndex);
}, [inventory, activeGrain]);
const renderNoGrain = (props: any) => {
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill, payload } = props;
return (
<g>
<text x={cx} y={cy} dy={-2} textAnchor="middle" fill={fill}>
{payload.grainName}
</text>
<text
x={cx}
y={cy}
dy={12}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize="0.7rem">
Please make bin
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={innerRadius - 8}
outerRadius={innerRadius - 5}
fill={fill}
/>
</g>
);
};
const renderActiveGrain = (props: any) => {
const {
cx,
cy,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
percent
} = props;
return (
<g>
<text x={cx} y={cy} dy={-8} textAnchor="middle" fill={fill}>
{payload.grainName}
</text>
<text
x={cx}
y={cy}
dy={6}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize="0.7rem">
{`${(percent * 100).toFixed(2)}%`}
</text>
<text
x={cx}
y={cy}
dy={20}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize="0.7rem">
{payload.bushelAmount.toLocaleString() + (customUnit ? customUnit : " bu")}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={innerRadius - 8}
outerRadius={innerRadius - 5}
fill={fill}
/>
</g>
);
};
const pieChart = () => {
if (inventory.length > 0) {
return (
<PieChart>
{customLabel && (
<Legend
verticalAlign="top"
content={() => (
<Box textAlign="center" marginY={0.5}>
<Chip label={customLabel} />
</Box>
)}
/>
)}
<Pie
cursor="pointer"
isAnimationActive={false}
dataKey="bushelAmount"
data={inventory}
cx="50%"
cy="50%"
innerRadius="70%"
outerRadius="100%"
stroke="transparent"
paddingAngle={3}
labelLine={false}
activeIndex={activeIndex === -1 ? 0 : activeIndex}
activeShape={(props: any) => renderActiveGrain(props)}
onMouseEnter={(_data: any, index: number) => setActiveIndex(index)}
onClick={(data: any, _index: number) => {
onClick(data.grain as pond.Grain, data.grainName);
}}>
{inventory.map((g, index) => (
<Cell
key={`cell-${index}`}
fill={
g.grain === pond.Grain.GRAIN_CUSTOM
? stringToMaterialColour(g.grainName)
: GrainColour(g.grain)
}
/>
))}
</Pie>
</PieChart>
);
}
let inv: any = [{ grain: 0, bushelAmount: 1 }];
return (
<PieChart>
{customLabel && (
<Legend
verticalAlign="top"
content={() => (
<Box textAlign="center" marginY={0.5}>
<Chip label={customLabel} />
</Box>
)}
/>
)}
<Pie
cursor="pointer"
isAnimationActive={false}
dataKey="bushelAmount"
data={inv}
cx="50%"
cy="50%"
innerRadius="70%"
outerRadius="100%"
stroke="transparent"
paddingAngle={3}
labelLine={false}
activeIndex={0}
activeShape={(props: any) => renderNoGrain(props)}>
Please Create Bin
</Pie>
</PieChart>
);
};
return (
<Box height={1} width={1}>
<ResponsiveContainer>{pieChart()}</ResponsiveContainer>
</Box>
);
}

View file

@ -0,0 +1,121 @@
import { Box, CardActionArea, Chip, useTheme } from "@mui/material";
import { GrainColour, grainName } from "grain";
import { pond } from "protobuf-ts/pond";
import { Label, Legend, Pie, PieChart, ResponsiveContainer, Text } from "recharts";
interface Props {
bushelAmount: number;
bushelCapacity: number;
grain: pond.Grain;
customLabel?: string;
customColour?: string;
onClick: () => void;
grainActive?: boolean;
customUnit?: string;
}
export default function BinUtilizationChart(props: Props) {
const {
bushelAmount,
bushelCapacity,
grain,
onClick,
grainActive,
customLabel,
customColour,
customUnit
} = props;
const grainLabel = customLabel ? customLabel : grainName(grain);
const theme = useTheme();
const grainColour = customColour ? customColour : GrainColour(grain);
return (
<Box height={1} width={1}>
<CardActionArea
onClick={onClick}
style={{ height: "100%", borderRadius: theme.shape.borderRadius }}>
<ResponsiveContainer>
<PieChart>
<Legend
verticalAlign="bottom"
content={() => (
<Box textAlign="center" marginY={0.5}>
<Chip
label={grainLabel}
variant={grainActive === true ? "filled" : "outlined"}
/>
</Box>
)}
/>
<Pie
dataKey="value"
cursor="pointer"
isAnimationActive={false}
data={[
{
name: "Amount",
value: bushelAmount,
fill: grainColour
},
{
name: "Capacity",
value: bushelCapacity - bushelAmount,
fill: grainColour,
opacity: theme.palette.mode === "light" ? 0.25 : 0.1
}
]}
cx="50%"
cy="50%"
innerRadius="70%"
outerRadius="100%"
startAngle={270}
endAngle={-90}
stroke="transparent">
<Label
position="center"
fill={theme.palette.text.secondary}
fontSize={"0.725rem"}
content={props => {
const { cx, cy } = props.viewBox as any;
return (
<g>
<Text
x={cx}
y={cy}
dy={-8}
textAnchor="middle"
verticalAnchor="middle"
fill={props.fill}
fontSize={props.fontSize}>
{bushelAmount.toLocaleString() + (customUnit ? customUnit : " bu")}
</Text>
<Text
x={cx}
y={cy}
dy={0}
textAnchor="middle"
verticalAnchor="end"
fill={props.fill}
fontSize={props.fontSize}>
_______
</Text>
<Text
x={cx}
y={cy}
dy={12}
textAnchor="middle"
verticalAnchor="middle"
fill={props.fill}
fontSize={props.fontSize}>
{bushelCapacity.toLocaleString() + (customUnit ? customUnit : " bu")}
</Text>
</g>
);
}}
/>
</Pie>
</PieChart>
</ResponsiveContainer>
</CardActionArea>
</Box>
);
}

View file

@ -115,7 +115,7 @@ export default function SideNavigator(props: Props) {
<ListItemIcon> <ListItemIcon>
<BinsIcon /> <BinsIcon />
</ListItemIcon> </ListItemIcon>
{open && <ListItemText primary="Devices" />} {open && <ListItemText primary="Bins" />}
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
<Tooltip title="Devices" placement="right"> <Tooltip title="Devices" placement="right">

View file

@ -17,6 +17,8 @@ import {
// GridList, // GridList,
// GridListTile, // GridListTile,
IconButton, IconButton,
ImageList,
ImageListItem,
InputLabel, InputLabel,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
@ -45,8 +47,8 @@ import {
// import BinSettings from "bin/BinSettings"; // import BinSettings from "bin/BinSettings";
// import BinsList from "bin/BinsList"; // import BinsList from "bin/BinsList";
// import BinYard from "bin/BinYard"; // import BinYard from "bin/BinYard";
// import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart"; import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart";
// import BinUtilizationChart from "charts/BinUtilizationChart"; import BinUtilizationChart from "charts/BinUtilizationChart";
// import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator"; // import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator";
import GrainDescriber, { grainName } from "grain/GrainDescriber"; import GrainDescriber, { grainName } from "grain/GrainDescriber";
// import GrainBagList from "grainBag/grainBagList"; // import GrainBagList from "grainBag/grainBagList";
@ -167,7 +169,7 @@ export default function Bins(props: Props) {
const [fertilizerBins, setFertilizerBins] = useState<Bin[]>([]); const [fertilizerBins, setFertilizerBins] = useState<Bin[]>([]);
const [emptyBins, setEmptyBins] = useState<Bin[]>([]); const [emptyBins, setEmptyBins] = useState<Bin[]>([]);
// const [grainBags, setGrainBags] = useState<GrainBag[]>([]); // const [grainBags, setGrainBags] = useState<GrainBag[]>([]);
// const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]); const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]);
// const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]); // const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]);
const [carouselIndex, setCarouselIndex] = useState(0); const [carouselIndex, setCarouselIndex] = useState(0);
const [addBagOpen, setAddBagOpen] = useState(false); const [addBagOpen, setAddBagOpen] = useState(false);
@ -338,23 +340,23 @@ export default function Bins(props: Props) {
binsTotal: resp.data.total binsTotal: resp.data.total
}); });
let metrics = pond.BinMetrics.fromObject(resp.data.metrics ?? {}); let metrics = pond.BinMetrics.fromObject(resp.data.metrics ?? {});
// let inventory: GrainAmount[] = []; let inventory: GrainAmount[] = [];
// let fertInventory: GrainAmount[] = []; // let fertInventory: GrainAmount[] = [];
if (metrics) { if (metrics) {
metrics.grainInventory.forEach(grain => { metrics.grainInventory.forEach(grain => {
// inventory.push({ inventory.push({
// grain: grain.grainType, grain: grain.grainType,
// bushelAmount: grain.bushelAmount, bushelAmount: grain.bushelAmount,
// grainName: grainName(grain.grainType) grainName: grainName(grain.grainType)
// }); });
}); });
metrics.customInventory.forEach(invObject => { metrics.customInventory.forEach(invObject => {
if (invObject.storageType === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) { if (invObject.storageType === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
// inventory.push({ inventory.push({
// grain: pond.Grain.GRAIN_CUSTOM, grain: pond.Grain.GRAIN_CUSTOM,
// bushelAmount: invObject.amount, bushelAmount: invObject.amount,
// grainName: invObject.name grainName: invObject.name
// }); });
} else if (invObject.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) { } else if (invObject.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
// fertInventory.push({ // fertInventory.push({
// grain: pond.Grain.GRAIN_CUSTOM, // grain: pond.Grain.GRAIN_CUSTOM,
@ -365,7 +367,7 @@ export default function Bins(props: Props) {
}); });
} }
setBinMetrics(metrics); setBinMetrics(metrics);
// setDisplayedInventory(inventory); setDisplayedInventory(inventory);
// setDisplayedFertilizer(fertInventory); // setDisplayedFertilizer(fertInventory);
if (yards.length === 0) { if (yards.length === 0) {
let y: pond.BinYardSettings[] = []; let y: pond.BinYardSettings[] = [];
@ -821,7 +823,7 @@ export default function Bins(props: Props) {
<React.Fragment> <React.Fragment>
<Grid container direction="row" alignContent="center" alignItems="center"> <Grid container direction="row" alignContent="center" alignItems="center">
<Grid style={{ height: "200px" }} item xs={6}> <Grid style={{ height: "200px" }} item xs={6}>
{/* <BinInventoryChart <BinInventoryChart
customLabel="Grain" customLabel="Grain"
inventory={displayedInventory} inventory={displayedInventory}
onClick={(grain: pond.Grain, grainName: string) => { onClick={(grain: pond.Grain, grainName: string) => {
@ -835,7 +837,7 @@ export default function Bins(props: Props) {
loadMoreBins(filter); loadMoreBins(filter);
}} }}
//activeGrain={grainFilter} //activeGrain={grainFilter}
/> */} />
</Grid> </Grid>
<Grid style={{ height: "200px" }} item xs={6}> <Grid style={{ height: "200px" }} item xs={6}>
{/* <BinInventoryChart {/* <BinInventoryChart
@ -902,24 +904,25 @@ export default function Bins(props: Props) {
<Typography variant="caption" color="textSecondary"> <Typography variant="caption" color="textSecondary">
Click a chart to filter bins by grain Click a chart to filter bins by grain
</Typography> </Typography>
{/* <GridList <ImageList
className={classes.gridList} className={classes.gridList}
cols={ cols={
props.insert && !isMobile props.insert && !isMobile
? 3.25 ? 3
: width === "xs" : width === "xs"
? 3.25 ? 3
: width === "sm" : width === "sm"
? 5.5 ? 5
: width === "md" : width === "md"
? 6.5 ? 6
: width === "lg" : width === "lg"
? 7.5 ? 7
: 8.5 : 8
}> }
>
{binMetrics && {binMetrics &&
binMetrics.grainInventory.map((inv, key) => ( binMetrics.grainInventory.map((inv, key) => (
<GridListTile key={key}> <ImageListItem key={key}>
<BinUtilizationChart <BinUtilizationChart
grain={inv.grainType} grain={inv.grainType}
customUnit={useWeight ? " mT" : " bu"} customUnit={useWeight ? " mT" : " bu"}
@ -939,7 +942,7 @@ export default function Bins(props: Props) {
}} }}
grainActive={contentFilter === pond.Grain[inv.grainType]} grainActive={contentFilter === pond.Grain[inv.grainType]}
/> />
</GridListTile> </ImageListItem>
))} ))}
{binMetrics && {binMetrics &&
binMetrics.customInventory.map((inv, key) => { binMetrics.customInventory.map((inv, key) => {
@ -959,7 +962,7 @@ export default function Bins(props: Props) {
} }
return ( return (
<GridListTile key={key}> <ImageListItem key={key}>
<BinUtilizationChart <BinUtilizationChart
grain={pond.Grain.GRAIN_CUSTOM} grain={pond.Grain.GRAIN_CUSTOM}
bushelAmount={amount} bushelAmount={amount}
@ -973,10 +976,10 @@ export default function Bins(props: Props) {
customLabel={inv.name} customLabel={inv.name}
customColour={stringToMaterialColour(inv.name)} customColour={stringToMaterialColour(inv.name)}
/> />
</GridListTile> </ImageListItem>
); );
})} })}
</GridList> */} </ImageList>
</Box> </Box>
) : ( ) : (
<Typography color="textSecondary" style={{ margin: "2rem", marginTop: "0rem" }}> <Typography color="textSecondary" style={{ margin: "2rem", marginTop: "0rem" }}>