Make pipeline, parameters & product interactive

feature/gui
Alinson S. Xavier 4 years ago
parent 0e53a4334e
commit 524299a3c2
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -1,22 +1,22 @@
import styles from './Button.module.css'
import styles from './Button.module.css';
const Button = (props) => {
let className = styles.Button
let className = styles.Button;
if (props.kind === "inline") {
className += " " + styles.inline
className += " " + styles.inline;
}
let tooltip = "";
if (props.tooltip != undefined) {
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
}
return (
<button className={className}>
<button className={className} onClick={props.onClick}>
{tooltip}
{props.label}
</button>
)
}
);
};
export default Button;

@ -1,7 +1,7 @@
import styles from './ButtonRow.module.css'
import styles from './ButtonRow.module.css';
const ButtonRow = (props) => {
return <div className={styles.ButtonRow}>{props.children}</div>
}
return <div className={styles.ButtonRow}>{props.children}</div>;
};
export default ButtonRow;

@ -1,7 +1,7 @@
import styles from './Card.module.css'
import styles from './Card.module.css';
const Card = (props) => {
return (<div className={styles.Card}>{props.children}</div>)
}
return (<div className={styles.Card}>{props.children}</div>);
};
export default Card;

@ -1,27 +1,37 @@
import form_styles from './Form.module.css'
import Button from './Button'
import { useState } from 'react';
import form_styles from './Form.module.css';
import Button from './Button';
const DictInputRow = (props) => {
const dict = { ...props.value };
if (!props.disableKeys) {
dict[""] = "";
}
let unit = "";
if (props.unit) {
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
}
let tooltip = "";
if (props.tooltip != undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
}
let value = {}
if (props.value != undefined) {
value = props.value;
}
if (props.disableKeys === undefined) {
value[""] = "";
}
const onChangeValue = (key, v) => {
const newDict = { ...dict };
newDict[key] = v;
props.onChange(newDict);
};
const form = []
Object.keys(value).forEach((key, index) => {
const onChangeKey = (prevKey, newKey) => {
const newDict = renameKey(dict, prevKey, newKey);
if (!("" in newDict)) newDict[""] = "";
props.onChange(newDict);
};
const form = [];
Object.keys(dict).forEach((key, index) => {
let label = <span>{props.label} {unit}</span>;
if (index > 0) {
label = "";
@ -35,21 +45,33 @@ const DictInputRow = (props) => {
value={key}
placeholder={props.keyPlaceholder}
disabled={props.disableKeys}
onChange={e => onChangeKey(key, e.target.value)}
/>
<input
type="text"
data-index={index}
value={value[key]}
value={dict[key]}
placeholder={props.valuePlaceholder}
onChange={e => onChangeValue(key, e.target.value)}
/>
{tooltip}
</div>
);
});
return <>
{form}
</>;
return <>{form}</>;
};
function renameKey(obj, prevKey, newKey) {
const keys = Object.keys(obj);
return keys.reduce((acc, val) => {
if (val === prevKey) {
acc[newKey] = obj[prevKey];
} else {
acc[val] = obj[val];
}
return acc;
}, {});
}
export default DictInputRow;

@ -1,11 +1,11 @@
import form_styles from './Form.module.css'
import Button from './Button'
import form_styles from './Form.module.css';
import Button from './Button';
const FileInputRow = (props) => {
let tooltip = "";
if (props.tooltip != undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
}
return <div className={form_styles.FormRow}>
@ -16,6 +16,6 @@ const FileInputRow = (props) => {
<Button label="Template" kind="inline" />
{tooltip}
</div>;
}
};
export default FileInputRow;

@ -1,10 +1,10 @@
import styles from './Footer.module.css'
import styles from './Footer.module.css';
const Footer = () => {
return <div className={styles.Footer}>
<p>RELOG: Reverse Logistics Optimization</p>
<p>Copyright &copy; 2020&mdash;2022, UChicago Argonne, LLC. All Rights Reserved.</p>
</div>
}
</div>;
};
export default Footer;

@ -1,5 +1,5 @@
const Form = (props) => {
return <>{props.children}</>;
}
};
export default Form;

@ -1,4 +1,4 @@
import styles from './Header.module.css'
import styles from './Header.module.css';
const Header = () => {
return (
@ -7,7 +7,7 @@ const Header = () => {
<h1>RELOG</h1>
</div>
</div>
)
}
);
};
export default Header;

@ -1,4 +1,5 @@
import React from 'react';
import React, { useState } from 'react';
import './index.css';
import PipelineBlock from './PipelineBlock';
import ParametersBlock from './ParametersBlock';
@ -7,20 +8,249 @@ import PlantBlock from './PlantBlock';
import ButtonRow from './ButtonRow';
import Button from './Button';
const defaultData = {
parameters: {
"time horizon (years)": "1",
"building period (years)": "[1]",
"annual inflation rate (%)": "0.0",
},
products: {
},
plants: {
}
};
const defaultProduct = {
"acquisition cost ($/tonne)": "0.00",
"disposal cost ($/tonne)": "0.00",
"disposal limit (tonne)": "0",
"transportation cost ($/km/tonne)": "0.00",
"transportation energy (J/km/tonne)": "0",
"transportation emissions (J/km/tonne)": {
"CO2": 0,
"NH2": 0,
}
};
const randomPosition = () => {
return Math.round(Math.random() * 30) * 15;
};
const InputPage = () => {
let [data, setData] = useState(defaultData);
// onAdd
// ------------------------------------------------------------------------
const promptName = (prevData) => {
const name = prompt("Name");
if (!name || name.length == 0) return;
if (name in prevData.products || name in prevData.plants) return;
return name;
};
const onAddPlant = () => {
setData((prevData) => {
const name = promptName(prevData);
if (name === undefined) return prevData;
const newData = { ...prevData };
newData.plants[name] = {
x: randomPosition(),
y: randomPosition(),
outputs: {},
};
return newData;
});
};
const onAddProduct = () => {
setData((prevData) => {
const name = promptName(prevData);
if (name === undefined) return prevData;
const newData = { ...prevData };
newData.products[name] = {
...defaultProduct,
x: randomPosition(),
y: randomPosition(),
};
return newData;
});
};
// onRename
// ------------------------------------------------------------------------
const onRenamePlant = (prevName, newName) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[newName] = newData.plants[prevName];
delete newData.plants[prevName];
return newData;
});
};
const onRenameProduct = (prevName, newName) => {
setData((prevData) => {
const newData = { ...prevData };
newData.products[newName] = newData.products[prevName];
delete newData.products[prevName];
for (const [plantName, plant] of Object.entries(newData.plants)) {
if (plant.input == prevName) {
plant.input = newName;
}
let outputFound = false;
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
if (outputName == prevName) outputFound = true;
}
if (outputFound) {
plant.outputs[newName] = plant.outputs[prevName];
delete plant.outputs[prevName];
}
}
return newData;
});
};
// onMove
// ------------------------------------------------------------------------
const onMovePlant = (plantName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].x = x;
newData.plants[plantName].y = y;
return newData;
});
};
const onMoveProduct = (productName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.products[productName].x = x;
newData.products[productName].y = y;
return newData;
});
};
// onRemove
// ------------------------------------------------------------------------
const onRemovePlant = (plantName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.plants[plantName];
return newData;
});
};
const onRemoveProduct = (productName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.products[productName];
for (const [plantName, plant] of Object.entries(newData.plants)) {
if (plant.input == productName) {
delete plant.input;
}
let outputFound = false;
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
if (outputName == productName) outputFound = true;
}
if (outputFound) {
delete plant.outputs[productName];
}
}
return newData;
});
};
// Inputs & Outputs
// ------------------------------------------------------------------------
const onSetPlantInput = (plantName, productName) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].input = productName;
return newData;
});
};
const onAddPlantOutput = (plantName, productName) => {
setData((prevData) => {
if (productName in prevData.plants[plantName].outputs) {
return prevData;
}
const newData = { ...prevData };
newData.plants[plantName].outputs[productName] = 0;
return newData;
});
};
// onSave
// ------------------------------------------------------------------------
const onSave = () => {
console.log(data);
};
// onChange
// ------------------------------------------------------------------------
const onChangeParameters = (val) => {
setData(prevData => {
const newData = { ...prevData };
newData.parameters = val;
return newData;
});
};
const onChangeProduct = (prodName, val) => {
setData(prevData => {
const newData = { ...prevData };
newData.products[prodName] = val;
return newData;
});
};
// ------------------------------------------------------------------------
let productComps = [];
for (const [prodName, prod] of Object.entries(data.products)) {
productComps.push(
<ProductBlock
key={prodName}
name={prodName}
value={prod}
onChange={v => onChangeProduct(prodName, v)}
/>
);
}
let plantComps = [];
for (const [plantName, plant] of Object.entries(data.plants)) {
plantComps.push(
<PlantBlock key={plantName} name={plantName} />
);
}
return <>
<PipelineBlock />
<ParametersBlock />
<ProductBlock name="Battery" />
<ProductBlock name="Nickel" />
<ProductBlock name="Metal casing" />
<PlantBlock name="Battery Recycling Plant" />
<PipelineBlock
onAddPlant={onAddPlant}
onAddPlantOutput={onAddPlantOutput}
onAddProduct={onAddProduct}
onMovePlant={onMovePlant}
onMoveProduct={onMoveProduct}
onRenamePlant={onRenamePlant}
onRenameProduct={onRenameProduct}
onSetPlantInput={onSetPlantInput}
onRemovePlant={onRemovePlant}
onRemoveProduct={onRemoveProduct}
plants={data.plants}
products={data.products}
/>
<ParametersBlock
value={data.parameters}
onChange={onChangeParameters}
/>
{productComps}
{plantComps}
<ButtonRow>
<Button label="Load" />
<Button label="Save" />
<Button label="Save" onClick={onSave} />
</ButtonRow>
</>
}
</>;
};
export default InputPage;

@ -1,9 +1,13 @@
import Section from './Section'
import Card from './Card'
import Form from './Form'
import TextInputRow from './TextInputRow'
import Section from './Section';
import Card from './Card';
import Form from './Form';
import TextInputRow from './TextInputRow';
const ParametersBlock = () => {
const ParametersBlock = (props) => {
const onChangeField = (field, val) => {
props.value[field] = val;
props.onChange(props.value);
};
return (
<>
<Section title="Parameters" />
@ -13,24 +17,27 @@ const ParametersBlock = () => {
label="Time horizon"
unit="years"
tooltip="Number of years in the simulation."
default="1"
value={props.value["time horizon (years)"]}
onChange={v => onChangeField("time horizon (years)", v)}
/>
<TextInputRow
label="Building period"
unit="years"
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
default="[1]"
value={props.value["building period (years)"]}
onChange={v => onChangeField("building period (years)", v)}
/>
<TextInputRow
label="Annual inflation rate"
unit="%"
tooltip="Rate of inflation applied to all costs."
default="0"
value={props.value["annual inflation rate (%)"]}
onChange={v => onChangeField("annual inflation rate (%)", v)}
/>
</Form>
</Card>
</>
)
}
);
};
export default ParametersBlock;

@ -5,83 +5,138 @@ import Card from './Card';
import Button from './Button';
import styles from './PipelineBlock.module.css';
const elements = [
{
id: '1',
data: { label: 'Battery' },
sourcePosition: 'right',
targetPosition: 'left',
position: { x: 100, y: 200 },
className: styles.ProductNode,
},
{
id: '2',
data: { label: "Battery Recycling Plant" },
sourcePosition: 'right',
targetPosition: 'left',
position: { x: 500, y: 150 },
className: styles.PlantNode,
},
{
id: '3',
data: { label: 'Nickel' },
sourcePosition: 'right',
targetPosition: 'left',
position: { x: 900, y: 100 },
className: styles.ProductNode,
},
{
id: '4',
data: { label: 'Metal casing' },
sourcePosition: 'right',
targetPosition: 'left',
position: { x: 900, y: 300 },
className: styles.ProductNode,
},
{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
selectable: false,
style: { stroke: "black" },
},
{
id: 'e2-3',
source: '2',
target: '3',
animated: true,
selectable: false,
style: { stroke: "black" },
},
{
id: 'e2-4',
source: '2',
target: '4',
animated: true,
selectable: false,
style: { stroke: "black" },
},
];
const PipelineBlock = (props) => {
let elements = [];
let mapNameToType = {};
for (const [productName, product] of Object.entries(props.products)) {
mapNameToType[productName] = "product";
elements.push({
id: productName,
data: { label: productName, type: 'product' },
position: { x: product.x, y: product.y },
sourcePosition: 'right',
targetPosition: 'left',
className: styles.ProductNode,
});
}
for (const [plantName, plant] of Object.entries(props.plants)) {
mapNameToType[plantName] = "plant";
elements.push({
id: plantName,
data: { label: plantName, type: 'plant' },
position: { x: plant.x, y: plant.y },
sourcePosition: 'right',
targetPosition: 'left',
className: styles.PlantNode,
});
if (plant.input != undefined) {
elements.push({
id: `${plant.input}-${plantName}`,
source: plant.input,
target: plantName,
animated: true,
style: { stroke: "black" },
selectable: false,
});
}
for (const [productName, amount] of Object.entries(plant.outputs)) {
elements.push({
id: `${plantName}-${productName}`,
source: plantName,
target: productName,
animated: true,
style: { stroke: "black" },
selectable: false,
});
}
}
const onNodeDoubleClick = (ev, node) => {
const oldName = node.data.label;
const newName = window.prompt("Enter new name", oldName);
if (newName == undefined || newName.length == 0) return;
if (newName in mapNameToType) return;
if (node.data.type == "plant") {
props.onRenamePlant(oldName, newName);
} else {
props.onRenameProduct(oldName, newName);
}
};
const onElementsRemove = (elements) => {
elements.forEach(el => {
if (!(el.id in mapNameToType)) return;
if (el.data.type == "plant") {
props.onRemovePlant(el.data.label);
} else {
props.onRemoveProduct(el.data.label);
}
});
};
const onNodeDragStop = (ev, node) => {
if (node.data.type == "plant") {
props.onMovePlant(node.data.label, node.position.x, node.position.y);
} else {
props.onMoveProduct(node.data.label, node.position.x, node.position.y);
}
};
const onConnect = (args) => {
const sourceType = mapNameToType[args.source];
const targetType = mapNameToType[args.target];
if (sourceType === "product" && targetType === "plant") {
props.onSetPlantInput(args.target, args.source);
} else if (sourceType === "plant" && targetType === "product") {
props.onAddPlantOutput(args.source, args.target);
}
};
const PipelineBlock = () => {
return (
<>
<Section title="Pipeline" />
<Card>
<div className={styles.PipelineBlock}>
<ReactFlow elements={elements}>
<ReactFlow
elements={elements}
onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStop={onNodeDragStop}
onConnect={onConnect}
onElementsRemove={onElementsRemove}
deleteKeyCode={46}
maxZoom={1}
minZoom={1}
snapToGrid={true}
preventScrolling={false}
>
<Background />
</ReactFlow>
</div>
<div style={{ textAlign: 'center' }}>
<Button label="Add product" kind="inline" />
<Button label="Add plant" kind="inline" />
<Button
label="Add product"
kind="inline"
onClick={props.onAddProduct}
/>
<Button
label="Add plant"
kind="inline"
onClick={props.onAddPlant}
/>
<Button
label="?"
kind="inline"
tooltip="Drag from one connector to another to create links between products and plants. Double click to rename an element. Press [Delete] to remove an element."
/>
</div>
</Card>
</>
)
}
);
};
export default PipelineBlock;

@ -1,5 +1,8 @@
.PipelineBlock {
height: 600px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius);
margin-bottom: 12px;
}
.PlantNode, .ProductNode {

@ -1,20 +1,20 @@
import Section from './Section'
import Card from './Card'
import Form from './Form'
import TextInputRow from './TextInputRow'
import FileInputRow from './FileInputRow'
import DictInputRow from './DictInputRow'
import Section from './Section';
import Card from './Card';
import Form from './Form';
import TextInputRow from './TextInputRow';
import FileInputRow from './FileInputRow';
import DictInputRow from './DictInputRow';
const PlantBlock = (props) => {
const emissions = {
"CO2": "0.05",
"CH4": "0.01",
"N2O": "0.04",
}
};
const output = {
"Nickel": "0.5",
"Metal casing": "0.35",
}
};
return (
<>
<Section title={props.name} />
@ -136,7 +136,7 @@ const PlantBlock = (props) => {
</Form>
</Card>
</>
)
}
);
};
export default PlantBlock;

@ -1,10 +1,18 @@
import Section from './Section'
import Card from './Card'
import Form from './Form'
import TextInputRow from './TextInputRow'
import FileInputRow from './FileInputRow'
import { useState } from 'react';
import Section from './Section';
import Card from './Card';
import Form from './Form';
import TextInputRow from './TextInputRow';
import FileInputRow from './FileInputRow';
import DictInputRow from './DictInputRow';
const ProductBlock = (props) => {
const onChange = (field, val) => {
const newProduct = { ...props.value };
newProduct[field] = val;
props.onChange(newProduct);
};
return (
<>
<Section title={props.name} />
@ -19,7 +27,8 @@ const ProductBlock = (props) => {
label="Acquisition cost"
unit="$/tonne"
tooltip="The cost to acquire one tonne of this product from collection centers. Does not apply to plant outputs."
default="0.00"
value={props.value["acquisition cost ($/tonne)"]}
onChange={v => onChange("acquisition cost ($/tonne)", v)}
/>
<h1>Disposal</h1>
@ -27,13 +36,15 @@ const ProductBlock = (props) => {
label="Disposal cost"
unit="$/tonne"
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing. Does not apply to plant outputs."
default="0"
value={props.value["disposal cost ($/tonne)"]}
onChange={v => onChange("disposal cost ($/tonne)", v)}
/>
<TextInputRow
label="Disposal limit"
unit="tonne"
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing."
default="0"
value={props.value["disposal limit (tonne)"]}
onChange={v => onChange("disposal limit (tonne)", v)}
/>
<h1>Transportation</h1>
@ -41,24 +52,29 @@ const ProductBlock = (props) => {
label="Transportation cost"
unit="$/km/tonne"
tooltip="The cost to transport this product."
default="0.00"
value={props.value["transportation cost ($/km/tonne)"]}
onChange={v => onChange("transportation cost ($/km/tonne)", v)}
/>
<TextInputRow
label="Transportation energy"
unit="J/km/tonne"
default="0"
tooltip="The energy required to transport this product."
value={props.value["transportation energy (J/km/tonne)"]}
onChange={v => onChange("transportation energy (J/km/tonne)", v)}
/>
<TextInputRow
<DictInputRow
label="Transportation emissions"
unit="J/km/tonne"
tooltip="A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes)."
default="0"
keyPlaceholder="Emission name"
valuePlaceholder="0"
value={props.value["transportation emissions (J/km/tonne)"]}
onChange={v => onChange("transportation emissions (J/km/tonne)", v)}
/>
</Form>
</Card>
</>
)
}
);
};
export default ProductBlock;

@ -1,7 +1,7 @@
import styles from './Section.module.css'
import styles from './Section.module.css';
const Section = (props) => {
return <h2 className={styles.Section}>{props.title}</h2>
}
return <h2 className={styles.Section}>{props.title}</h2>;
};
export default Section;

@ -1,15 +1,15 @@
import form_styles from './Form.module.css'
import Button from './Button'
import form_styles from './Form.module.css';
import Button from './Button';
const TextInputRow = (props) => {
let unit = "";
if (props.unit) {
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
}
let tooltip = "";
if (props.tooltip != undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
}
return <div className={form_styles.FormRow}>
@ -21,9 +21,10 @@ const TextInputRow = (props) => {
placeholder={props.default}
disabled={props.disabled}
value={props.value}
onChange={e => props.onChange(e.target.value)}
/>
{tooltip}
</div>;
}
};
export default TextInputRow;

@ -39,6 +39,10 @@ body {
border: 1px solid black !important;
}
.react-flow__handle:hover {
background-color: black !important;
}
.react-flow__handle-right {
right: -5px !important;
}

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import './index.css';
import Header from './Header';
import InputPage from './InputPage';
import Footer from './Footer'
import Footer from './Footer';
ReactDOM.render(
<React.StrictMode>

Loading…
Cancel
Save