diff --git a/uhepp-js/src/components/UheppHistUI.jsx b/uhepp-js/src/components/UheppHistUI.jsx index c4b8294016dcdba0bbd13ffe569c94c562908887..30825275be201efd0bfaf71d75198123b21e0491 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 253cc6d751c64ce1fac4a4e977bb3dc9eb406823..0a28497705a1b040d68e354a9445aae4196cc3b3 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