From 58f1c4283ad5e4c2f1cff6c2310dacd55073bed8 Mon Sep 17 00:00:00 2001
From: Frank Sauerburger <frank@sauerburger.com>
Date: Wed, 20 Jan 2021 18:59:50 +0100
Subject: [PATCH] Implement data interface for stack and ratio

---
 uhepp-js/src/components/UheppHistUI.jsx       | 400 +++++++++++++++++-
 .../templates/uhepp_vault/home.html           |   2 +-
 2 files changed, 392 insertions(+), 10 deletions(-)

diff --git a/uhepp-js/src/components/UheppHistUI.jsx b/uhepp-js/src/components/UheppHistUI.jsx
index c4b8294..3082527 100644
--- a/uhepp-js/src/components/UheppHistUI.jsx
+++ b/uhepp-js/src/components/UheppHistUI.jsx
@@ -31,6 +31,9 @@ const varstyle = (updown) => {
 
 const makeVariationStacks = (uhepp, stackId, variation, updown) => {
   const stack = uhepp.stacks[stackId]
+  if (!stack) {
+    return []
+  }
   return  [{
             type: "step", 
             content: [{
@@ -45,6 +48,9 @@ const makeVariationStacks = (uhepp, stackId, variation, updown) => {
 
 const makeVariationRatio = (uhepp, stackId, variation, updown) => {
   const stack = uhepp.stacks[stackId]
+  if (!stack) {
+    return []
+  }
   return  [{
             type: "step", 
             style: varstyle(updown),
@@ -64,9 +70,30 @@ const UheppHistUIWithSyst = ({
   onEnvChange,
   onEnvStackChange,
   onReset,
+  onStackContentChange,
+  onStackItemRename,
+  onStackItemChangeColor,
+  onAddStackItem,
+  onDeleteStackItem,
+  onMoveUpStackItem,
+  onMoveDownStackItem,
+  onMoveUpStack,
+  onMoveDownStack,
+  onDeleteStack,
+  onAddStack,
+  onStackTypeChange,
+  onMoveUpRatioItem,
+  onMoveDownRatioItem,
+  onDeleteRatioItem,
+  onAddRatioItem,
+  onRatioItemTypeChange,
+  onRatioItemNumeratorChange,
+  onRatioItemDenominatorChange,
+  onRatioItemChangeColor,
   isVariationReady,
   variations,
   origStacks,
+  origRatio,
   envName,
   envId,
 }) => {
@@ -128,16 +155,22 @@ const UheppHistUIWithSyst = ({
 			<li className="nav-item">
 				<a className="nav-link" id="binning-tab" data-toggle="tab" href="#binning" role="tab" aria-controls="binning" aria-selected="true">Binning</a>
 			</li>
-			{ <li className="nav-item">
+			<li className="nav-item">
+				<a className="nav-link" id="stacks-tab" data-toggle="tab" href="#stacks" role="tab" aria-controls="stacks" aria-selected="false">Stacks</a>
+			</li>
+			<li className="nav-item">
+				<a className="nav-link" id="ratio-tab" data-toggle="tab" href="#ratio" role="tab" aria-controls="ratio" aria-selected="false">Ratio</a>
+			</li>
+			<li className="nav-item">
 				<a className="nav-link" id="variations-tab" data-toggle="tab" href="#variations" role="tab" aria-controls="variations" aria-selected="false">Variations</a>
-			</li> }
+			</li>
 			<li className="nav-item">
 				<a className="nav-link" id="reset-tab" data-toggle="tab" href="#reset" role="tab" aria-controls="reset" aria-selected="false">Reset</a>
 			</li>
 		</ul>
 
 		<div className="tab-content" id="view-options-content">
-			<div className="tab-pane show active p-3" id="info" role="tabpanel" aria-labelledby="binning-tab">
+			<div className="tab-pane show active p-3" id="info" role="tabpanel" aria-labelledby="info-tab">
 				<dl>
 					<dt>Author</dt>
 					<dd>{ uhepp.metadata.author || <i>None</i>}</dd>
@@ -211,6 +244,173 @@ const UheppHistUIWithSyst = ({
 					</div>
 				</form>
 			</div>
+			<div className="tab-pane p-3" id="stacks" role="tabpanel" aria-labelledby="stacks-tab">
+        <form>
+          { origStacks.map((stack, i) => (<div key={i}>
+            <h3 className="controls-head mt-2">
+              <span>Stack {i}</span>
+              <span>
+                  <button disabled={i == 0} className="btn btn-outline-primary mx-1" onClick={(e) => onMoveUpStack(e, i)}>
+                    <i className="fas fa-arrow-up"></i>
+                  </button>
+                  <button disabled={(i + 1) >= origStacks.length} className="btn btn-outline-primary mx-1" onClick={(e) => onMoveDownStack(e, i)}>
+                    <i className="fas fa-arrow-down"></i>
+                  </button>
+                <button className="btn btn-outline-danger ml-1" onClick={(e) => onDeleteStack(e, i)}>
+                  <i className="fas fa-trash"></i>
+                </button>
+              </span>
+            </h3>
+            <div className="from-group">
+              <label htmlFor={`t-${i}`}>Histogram type</label> 
+              <select id={`t-${i}`} value={stack.type} className="form-control" onChange={(e) => onStackTypeChange(e, i)}>
+                <option value="step">step</option>
+                <option value="stepfilled">stepfilled</option>
+                <option value="points">points</option>
+              </select>
+            </div>
+            { stack.content.map((content, j) =>  (
+            <div key={`${j}_${stack.content.length}`}>
+            <h5>Item {j}</h5>
+            <div className="form-row mb-2">
+              <div className="col-md-4">
+
+
+                <div className="input-group mb-1">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text">Label</span>
+                  </div>
+                  <input type="text" className="form-control" placeholder="label"
+                value={content.label} onChange={(e) => onStackItemRename(e, i, j)}/>
+                </div>
+
+                <div className="input-group my-1">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text">Color</span>
+                  </div>
+                  <input type="text" className="form-control" placeholder="color"
+                value={(content.style) ? content.style.color || "" : ""} onChange={(e) => onStackItemChangeColor(e, i, j)}/>
+                  {content.style && content.style.color && 
+                    <div className="input-group-append">
+                      <span className="input-group-text" style={{backgroundColor: content.style.color}}> </span>
+                    </div>
+                  }
+                </div>
+
+                <div className="my-1">
+                  <button className="btn btn-sm btn-outline-danger mr-1" onClick={(e) => onDeleteStackItem(e, i, j)}>
+                    <i className="fas fa-trash"></i>
+                  </button>
+                  <button disabled={j == 0} className="btn btn-sm btn-outline-primary mx-1" onClick={(e) => onMoveUpStackItem(e, i, j)}>
+                    <i className="fas fa-arrow-up"></i>
+                  </button>
+                  <button disabled={(j + 1) >= stack.content.length} className="btn btn-sm btn-outline-primary mx-1" onClick={(e) => onMoveDownStackItem(e, i, j)}>
+                    <i className="fas fa-arrow-down"></i>
+                  </button>
+                </div>
+
+              </div>
+              <div className="col-md-8">
+                  <select multiple size={8} value={content.yield} id={`p-${i}-${j}`} className="form-control"
+                  onChange={(e) => onStackContentChange(e, i, j)}>
+                    { Object.keys(uhepp.yields).map((name, i) => <option value={name} key={i}>{name}</option>) }
+                  </select>
+              </div>
+            </div>
+
+            </div>
+            ))}
+            <div className="my-1">
+              <button className="btn btn-sm btn-outline-primary" onClick={(e) => onAddStackItem(e, i)}>
+                <i className="fas fa-plus mr-1"></i>
+                Add item {stack.content.length} to stack
+              </button>
+            </div>
+
+
+          </div>))}
+          
+          <div className="my-3">
+            <button className="btn btn-outline-primary" onClick={(e) => onAddStack(e)}>
+              <i className="fas fa-plus mr-1"></i>
+              Add stack {origStacks.length}
+            </button>
+          </div>
+        </form>
+			</div>
+
+			<div className="tab-pane p-3" id="ratio" role="tabpanel" aria-labelledby="ratio-tab">
+        <form>
+          { origRatio.map((ratioitem, i) => (<div key={i}>
+            <h3 className="controls-head mt-2">
+              <span>Ratio item {i}</span>
+              <span>
+                  <button disabled={i == 0} className="btn btn-outline-primary mx-1" onClick={(e) => onMoveUpRatioItem(e, i)}>
+                    <i className="fas fa-arrow-up"></i>
+                  </button>
+                  <button disabled={(i + 1) >= origRatio.length} className="btn btn-outline-primary mx-1" onClick={(e) => onMoveDownRatioItem(e, i)}>
+                    <i className="fas fa-arrow-down"></i>
+                  </button>
+                <button className="btn btn-outline-danger ml-1" onClick={(e) => onDeleteRatioItem(e, i)}>
+                  <i className="fas fa-trash"></i>
+                </button>
+              </span>
+            </h3>
+
+            <div className="form-row mb-2">
+              <div className="col-md-4">
+                <div className="form-group mb-1">
+                  <label htmlFor={`t-${i}`}>Histogram type</label>
+                  <select id={`t-${i}`} value={ratioitem.type} className="form-control" onChange={(e) => onRatioItemTypeChange(e, i)}>
+                    <option value="step">step</option>
+                    <option value="points">points</option>
+                  </select>
+                </div>
+                <div className="input-group my-1">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text">Color</span>
+                  </div>
+                  <input type="text" className="form-control" placeholder="color" value={(ratioitem.style) ? ratioitem.style.color || "" : ""} onChange={(e) => onRatioItemChangeColor(e, i)}/>
+                  {ratioitem.style && ratioitem.style.color && 
+                    <div className="input-group-append">
+                      <span className="input-group-text" style={{backgroundColor: ratioitem.style.color}}> </span>
+                    </div>
+                  }
+                </div>
+              </div>
+
+              <div className="col-md-4">
+                <div className="form-group">
+                  <label htmlFor={`p-${i}`}>Numerator</label>
+                  <select multiple size={8} value={ratioitem.numerator} id={`p-${i}`} className="form-control"
+                  onChange={(e) => onRatioItemNumeratorChange(e, i)}>
+                    { Object.keys(uhepp.yields).map((name, i) => <option value={name} key={i}>{name}</option>) }
+                  </select>
+                </div>
+              </div>
+
+              <div className="col-md-4">
+                <div className="form-group">
+                  <label htmlFor={`p-${i}}`}>Denominator</label>
+                  <select multiple size={8} value={ratioitem.denominator} id={`p-${i}`} className="form-control"
+                  onChange={(e) => onRatioItemDenominatorChange(e, i)}>
+                    { Object.keys(uhepp.yields).map((name, i) => <option value={name} key={i}>{name}</option>) }
+                  </select>
+                </div>
+              </div>
+
+            </div>
+          </div>))}
+          
+          <div className="my-3">
+            <button className="btn btn-outline-primary" onClick={(e) => onAddRatioItem(e)}>
+              <i className="fas fa-plus mr-1"></i>
+              Add ratio item {origRatio.length}
+            </button>
+          </div>
+        </form>
+			</div>
+
 			<div className="tab-pane p-3" id="variations" role="tabpanel" aria-labelledby="variations-tab">
         { !isVariationReady && 
           <p>To use the variation feature, you must add variation data to
@@ -248,13 +448,19 @@ const UheppHistUIWithSyst = ({
   </>
 }
 
+const patchList = (iter, i, repl) => iter.map((v, j) => (i == j) ? repl : v)
+
 const UheppHistUI = ({width, height, uhepp}) => {
   const [envId, setEnvId] = useState(0)
   const [envName, setEnvName] = useState("NOMINAL")
+  const [stacks, setStacks] = useState(uhepp.stacks)
+  const [ratio, setRatio] = useState(uhepp.ratio)
 
   const reset = () => {
     setEnvId(0)
     setEnvName("NOMINAL")
+    setStacks(uhepp.stacks)
+    setRatio(uhepp.ratio)
   }
   const handleEnvelop = (e) => {
     let variationName = e.target.value
@@ -265,16 +471,169 @@ const UheppHistUI = ({width, height, uhepp}) => {
     setEnvId(stackId)
   }
 
-  const variations = variationList(uhepp)
-  const isVariationReady = noVariationUsed(uhepp) && (variations.length > 0)
+  const handleStackContentChange = (e, stackId, itemId) => {
+    const yields = Array.from(e.target.selectedOptions, option => option.value)
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": patchList(stacks[stackId].content, itemId, Object.assign({},
+      stacks[stackId].content[itemId], {"yield": yields}))})))
+  }
 
-  const main_names = uhepp.stacks.map(stack => stack.content.map(item => item.yields)).flat()
-  const num_names = uhepp.ratio.map(item => item.numerator).flat()
-  const den_names = uhepp.ratio.map(item => item.denominator).flat()
+  const handleStackItemRename = (e, stackId, itemId) => {
+    const label = e.target.value
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": patchList(stacks[stackId].content, itemId, Object.assign({},
+      stacks[stackId].content[itemId], {"label": label}))})))
+  }
+
+  const handleStackItemChangeColor = (e, stackId, itemId) => {
+    const color = e.target.value
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": patchList(stacks[stackId].content, itemId, Object.assign({},
+      stacks[stackId].content[itemId], {"style": 
+      Object.assign({}, stacks[stackId].content[itemId].style || {}, {"color":
+      color})}))})))
+  }
+
+  const handleAddStackItem = (e, stackId) => {
+    e.preventDefault()
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": [...stacks[stackId].content, {yield: [], label: "New item",
+      style: {}}]})))
+  }
+
+  const handleMoveUpStackItem = (e, stackId, item) => {
+    e.preventDefault()
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": [
+      ...stacks[stackId].content.filter((_, i) => i < (item-1)),
+      stacks[stackId].content[item],
+      stacks[stackId].content[item - 1],
+      ...stacks[stackId].content.filter((_, i) => i > item),
+      ]})))
+  }
+
+  const handleMoveDownStackItem = (e, stackId, item) => {
+    e.preventDefault()
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": [
+      ...stacks[stackId].content.filter((_, i) => i < item),
+      stacks[stackId].content[item + 1],
+      stacks[stackId].content[item ],
+      ...stacks[stackId].content.filter((_, i) => i > item + 1),
+      ]})))
+  }
+
+  const handleDeleteStackItem = (e, stackId, itemId) => {
+    e.preventDefault()
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId],
+      {"content": stacks[stackId].content.filter((_, j) => j != itemId)})))
+  }
+  
+  const handleDeleteStack = (e, stackId) => {
+    e.preventDefault()
+    setStacks(stacks.filter((_, i) => i != stackId))
+  }
+  
+  const handleAddStack = (e) => {
+    e.preventDefault()
+    setStacks([...stacks, {"type": "stepfilled", "error": "stat", content: []}])
+  }
+
+  const handleStackTypeChange = (e, stackId) => {
+    const type = e.target.value
+    setStacks(patchList(stacks, stackId, Object.assign({}, stacks[stackId], {"type": type})))
+  }
+
+  const handleMoveDownStack = (e, item) => {
+    e.preventDefault()
+    setStacks([
+      ...stacks.filter((_, i) => i < item),
+      stacks[item + 1],
+      stacks[item ],
+      ...stacks.filter((_, i) => i > item + 1),
+    ])
+  }
+  
+  const handleMoveUpStack = (e, item) => {
+    e.preventDefault()
+    setStacks([
+      ...stacks.filter((_, i) => i < item - 1),
+      stacks[item],
+      stacks[item - 1],
+      ...stacks.filter((_, i) => i > item),
+    ])
+  }
+  
+  
+  const handleDeleteRatioItem = (e, itemId) => {
+    e.preventDefault()
+    setRatio(ratio.filter((_, i) => i != ratioId))
+  }
+  
+  const handleAddRatioItem = (e) => {
+    e.preventDefault()
+    setRatio([...ratio, {
+      "type": "step",
+      "error": "stat",
+      numerator: [],
+      denominator: [],
+      style: {}
+    }])
+  }
+
+  const handleRatioItemTypeChange = (e, itemId) => {
+    const type = e.target.value
+    setRatio(patchList(ratio, itemId, Object.assign({}, ratio[itemId], {"type": type})))
+  }
+
+  const handleMoveDownRatioItem = (e, item) => {
+    e.preventDefault()
+    setRatio([
+      ...ratio.filter((_, i) => i < item),
+      ratio[item + 1],
+      ratio[item ],
+      ...ratio.filter((_, i) => i > item + 1),
+    ])
+  }
+  
+  const handleMoveUpRatioItem = (e, item) => {
+    e.preventDefault()
+    setRatio([
+      ...ratio.filter((_, i) => i < item - 1),
+      ratio[item],
+      ratio[item - 1],
+      ...ratio.filter((_, i) => i > item),
+    ])
+  }
+
+  const handleRatioItemChangeColor = (e, item) => {
+    const color = e.target.value
+    setRatio(patchList(ratio, item, Object.assign({}, ratio[item],
+      {"style": Object.assign({}, ratio[item].style, {color: color})})))
+  }
+
+  const handleRatioItemNumeratorChange = (e, itemId) => {
+    const yields = Array.from(e.target.selectedOptions, option => option.value)
+    setRatio(patchList(ratio, itemId, Object.assign({}, ratio[itemId], {"numerator": yields})))
+  }
+  
+  const handleRatioItemDenominatorChange = (e, itemId) => {
+    const yields = Array.from(e.target.selectedOptions, option => option.value)
+    setRatio(patchList(ratio, itemId, Object.assign({}, ratio[itemId], {"denominator": yields})))
+  }
+  
+  const mod_uhepp = Object.assign({}, uhepp, {"stacks": stacks, "ratio": ratio})
+
+  const variations = variationList(mod_uhepp)
+  const isVariationReady = noVariationUsed(mod_uhepp) && (variations.length > 0)
+
+  const main_names = mod_uhepp.stacks.map(stack => stack.content.map(item => item.yields)).flat()
+  const num_names = mod_uhepp.ratio.map(item => item.numerator).flat()
+  const den_names = mod_uhepp.ratio.map(item => item.denominator).flat()
   const all_names = [...main_names, ...num_names, ...den_names]
 
   const filter_var = (var_updown) => objfilter(var_updown, (value, key) => ((all_names.indexOf(key) != -1) || key.indexOf(envName) != -1))
-  const pruned = Object.assign({}, uhepp,
+  const pruned = Object.assign({}, mod_uhepp,
     {"yields": objmap(uhepp.yields, y => Object.assign({}, y, {
       "var_up": filter_var(y.var_up || {}), 
       "var_down": filter_var(y.var_down || {}), 
@@ -300,10 +659,33 @@ const UheppHistUI = ({width, height, uhepp}) => {
             uhepp={pruned_env}
             onEnvChange={e => handleEnvelop(e)}
             onEnvStackChange={e => handleEnvStack(e)}
+            onStackContentChange={(e, i, j) => handleStackContentChange(e, i, j)}
+            onStackItemRename={(e, i, j) => handleStackItemRename(e, i, j)}
+            onStackItemChangeColor={(e, i, j) => handleStackItemChangeColor(e, i, j)}
+            onAddStackItem={(e, i) => handleAddStackItem(e, i)}
+            onDeleteStackItem={(e, i, j) => handleDeleteStackItem(e, i, j)}
             onReset={() => reset()}
+            onMoveUpStackItem={(e, i, j) => handleMoveUpStackItem(e, i, j)}
+            onMoveDownStackItem={(e, i, j) => handleMoveDownStackItem(e, i, j)}
+            onMoveUpStack={(e, i) => handleMoveUpStack(e, i)}
+            onMoveDownStack={(e, i) => handleMoveDownStack(e, i)}
+            onDeleteStack={(e, i) => handleDeleteStack(e, i)}
+            onAddStack={(e) => handleAddStack(e)}
+            onStackTypeChange={(e, i) => handleStackTypeChange(e, i)}
+
+            onMoveUpRatioItem={(e, i) => handleMoveUpRatioItem(e, i)}
+            onMoveDownRatioItem={(e, i) => handleMoveDownRatioItem(e, i)}
+            onDeleteRatioItem={(e, i) => handleDeleteRatioItem(e, i)}
+            onAddRatioItem={(e) => handleAddRatioItem(e)}
+            onRatioItemTypeChange={(e, i) => handleRatioItemTypeChange(e, i)}
+            onRatioItemNumeratorChange={(e, i) => handleRatioItemNumeratorChange(e, i)}
+            onRatioItemDenominatorChange={(e, i) => handleRatioItemDenominatorChange(e, i)}
+            onRatioItemChangeColor={(e, i) => handleRatioItemChangeColor(e, i)}
+
             envName={envName}
             envId={envId}
             origStacks={pruned.stacks}
+            origRatio={pruned.ratio}
             isVariationReady={isVariationReady}
             variations={variations} />
 }
diff --git a/uhepp_org/uhepp_vault/templates/uhepp_vault/home.html b/uhepp_org/uhepp_vault/templates/uhepp_vault/home.html
index 253cc6d..0a28497 100644
--- a/uhepp_org/uhepp_vault/templates/uhepp_vault/home.html
+++ b/uhepp_org/uhepp_vault/templates/uhepp_vault/home.html
@@ -23,7 +23,7 @@ clear from the context which component is meant. The three parts are:</p>
 
   <p>The <a href="https://gitlab.cern.ch/fsauerbu/uhepp">Universal HEP plot</a>
   format defines a way to store the raw data and the visualization style in a
-  single. This allows for easy changes to the appearance, including colors,
+  single file. This allows for easy changes to the appearance, including colors,
   labels, or binning.</p>
   <p>The specification does not force a particular syntax. You can store the data
   in your favorite format that supports lists and maps, for example, YAML or
-- 
GitLab