diff --git a/.gitignore b/.gitignore index 8b47a5d75bb2bb772116e5d16b85aa03c4e90d19..04bc8b15119d6e0ef53ce3f81cca25ed620aa25d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ client_secret .env -uhepp_org/uhepp_vault/static/uhepp-js/ +uhepp_org/uhepp_vault/static/react diff --git a/uhepp-js/src/common.scss b/uhepp-js/src/common.scss index ec3109d9c86bcd1858dd55caa983d50fe0133286..21bb873363368fe9bb4055d37f802c9aa0741c75 100644 --- a/uhepp-js/src/common.scss +++ b/uhepp-js/src/common.scss @@ -8,3 +8,12 @@ $theme-colors: ( a:hover { text-decoration: none; } + + +@include media-breakpoint-up(md) { + .uhepp-container { + width: 750px; + margin-left: auto; + margin-right: auto; + } +} diff --git a/uhepp-js/src/components/Uhepp.jsx b/uhepp-js/src/components/Uhepp.jsx new file mode 100644 index 0000000000000000000000000000000000000000..87bb0eb831b2a431015e4e8248e45ae7b5190250 --- /dev/null +++ b/uhepp-js/src/components/Uhepp.jsx @@ -0,0 +1,34 @@ +import React, { useState } from "react"; +import UheppHistUI from "./UheppHistUI.jsx"; + +const Error = (message) => ( + <div className="alert m-3 alert-danger"> + <h4 className="alert-heading">Rendering failed</h4> + <p>The interactive plot rendering failed with the following error message:</p> + <p style={{fontFamily: "monospace"}}>{message}</p> + <hr /> + <p>You can download the JSON file and try to fix the issue. Please note that + the web rendering is yet not fully supported.</p> + </div>) + + +const Uhepp = ({width, height, uhepp}) => { + if (!uhepp) { + return Error("Invalid uhepp data") + } + if (!uhepp.version) { + return Error("Missing uhepp version") + } + if (uhepp.version != "0.1") { + return Error(`Unsupported uhepp version: ${uhepp.version}`) + } + + if (uhepp.type == "histogram") { + return <UheppHistUI width={width} height={height} uhepp={uhepp} /> + } else { + return Error(`Unknown or missing plot type: ${uhepp.type}`) + } + +} + +export default Uhepp; diff --git a/uhepp-js/src/components/Graph.jsx b/uhepp-js/src/components/UheppHist.jsx similarity index 95% rename from uhepp-js/src/components/Graph.jsx rename to uhepp-js/src/components/UheppHist.jsx index 3ff7bf3b943a8bae29e707afb881e6f845c1e601..75632671bc5edec19e937a1851804180aeeb461b 100644 --- a/uhepp-js/src/components/Graph.jsx +++ b/uhepp-js/src/components/UheppHist.jsx @@ -13,18 +13,20 @@ import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html'; import { STATE } from 'mathjax-full/js/core/MathItem'; import { preprocessData, sumBase, sumStat, histogramify } from "../helpers/uhepp.js"; -const getMaxBin =(uhepp) => { +const getMaxBin = (uhepp) => { let max = 0; - const n_bins = uhepp.bins.edges.length - 1 + const edges = uhepp.bins.edges || uhepp.bins.rebin + const n_bins = edges.length - 1 for (let bin_i=0; bin_i < n_bins; bin_i++) { + const density_scale = uhepp.bins.density_width ? uhepp.bins.density_width / (edges[bin_i + 1] - edges[bin_i]) : 1 uhepp.stacks.forEach((stack, stack_index) => { let bottom = 0 stack.content.forEach((stack_item, si_i) => { - const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i) + const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i) * density_scale if (y_value > 0) { bottom += y_value; } }) const whole_stack = stack.content.map(si => si["yield"]).flat() - const stat = sumStat(uhepp.yields, whole_stack, bin_i) + const stat = sumStat(uhepp.yields, whole_stack, bin_i) * density_scale if (stat > 0) { bottom += stat; } max = Math.max(max, bottom) }) @@ -84,7 +86,11 @@ const Step = ({ } -const computeEquidistent = (edges) => { +const computeEquidistent = (edges, density) => { + if (density) { + return density + } + let width = false for (let i = 0; i < edges.length - 1; i++) { const this_width = edges[i + 1] - edges[i] @@ -261,7 +267,7 @@ const Ratio = ({ if (isFinite(rel_stat)) { objects.splice(0, 0, <rect key={`rect-uncert-${ri_i}-${bin_i}`} - x={xScale(edges[bin_i - 1])} + x={xScale(edges[bin_i])} width={xScale(edges[bin_i + 1]) - xScale(edges[bin_i])} y={ratioScale(1 + rel_stat)} height={ratioScale(1 - rel_stat) - ratioScale(1 + rel_stat)} @@ -289,13 +295,15 @@ const Histogram = ({ const n_bins = edges.length - 1 let objects = [] + // for (let bin_i=1; bin_i < n_bins; bin_i++) { uhepp.stacks.forEach((stack, stack_index) => { let bottom = Array(n_bins).fill(0) stack.content.forEach((stack_item, si_i) => { if (stack.type == "stepfilled") { for (let bin_i=0; bin_i < n_bins; bin_i++) { - const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i + 1) + const density_scale = uhepp.bins.density_width ? uhepp.bins.density_width / (edges[bin_i + 1] - edges[bin_i]) : 1 + const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i + 1) * density_scale objects.push(<Bar leftEdge={edges[bin_i]} rightEdge={edges[bin_i + 1]} bottom={bottom[bin_i]} @@ -315,7 +323,8 @@ const Histogram = ({ } } else if (stack.type == "step") { const bin_indices = Array(n_bins).fill(0).map((_, i) => i) - const y_values = bin_indices.map(i => sumBase(uhepp.yields, stack_item["yield"], i + 1)) + const density_scale = uhepp.bins.density_width ? uhepp.bins.density_width / (edges[bin_i + 1] - edges[bin_i]) : 1 + const y_values = bin_indices.map(i => sumBase(uhepp.yields, stack_item["yield"], i + 1)) * density_scale const [new_x, new_y] = histogramify(edges, y_values) @@ -338,8 +347,10 @@ const Histogram = ({ }) } else if (stack.type == "points") { for (let bin_i=0; bin_i < n_bins; bin_i++) { - const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i + 1) - const stat = sumStat(uhepp.yields, stack_item["yield"], bin_i + 1) + const density_scale = uhepp.bins.density_width ? uhepp.bins.density_width / (edges[bin_i + 1] - edges[bin_i]) : 1 + + const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i + 1) * density_scale + const stat = sumStat(uhepp.yields, stack_item["yield"], bin_i + 1) * density_scale const bin_center = (edges[bin_i + 1] + edges[bin_i]) / 2 const width = edges[bin_i + 1] - edges[bin_i] @@ -365,8 +376,10 @@ const Histogram = ({ if (stack.type == "stepfilled" || stack.type == "step") { for (let bin_i=0; bin_i < n_bins; bin_i++) { + const density_scale = uhepp.bins.density_width ? uhepp.bins.density_width / (edges[bin_i + 1] - edges[bin_i]) : 1 const whole_stack = stack.content.map(si => si["yield"]).flat() - const stat = sumStat(uhepp.yields, whole_stack, bin_i + 1) + + const stat = sumStat(uhepp.yields, whole_stack, bin_i + 1) * density_scale objects.push(<rect key={`rect-${stack_index}-uncert-${bin_i}`} x={xScale(edges[bin_i])} @@ -593,7 +606,7 @@ const UheppHist = ({width, height, uhepp}) => { const xScale = scaleLinear({ range: [0, xMax], - domain: extent(uhepp.bins.edges), + domain: extent(uhepp.bins.rebin || uhepp.bins.edges), }) const yScale = scaleLinear({ range: [yMax, 0], @@ -609,10 +622,10 @@ const UheppHist = ({width, height, uhepp}) => { - const equidistent = computeEquidistent(uhepp.bins.rebin || uhepp.bins.edges) + const equidistent = computeEquidistent(uhepp.bins.rebin || uhepp.bins.edges, uhepp.bins.density_width) return ( - <div style={{width: '100%', margin: '0 auto'}}> + <div className="uhepp-container"> <svg viewBox={`0 0 ${width} ${height}`} onMouseOut={() => setHighlightedBin(null)} > <style> diff --git a/uhepp-js/src/components/UheppHistUI.jsx b/uhepp-js/src/components/UheppHistUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fa50c09f9e1479a69f3e7eaaea57fde795e37d6c --- /dev/null +++ b/uhepp-js/src/components/UheppHistUI.jsx @@ -0,0 +1,118 @@ +import React, { useState } from "react"; +import UheppHist from "./UheppHist.jsx"; + +const UheppHistUI = ({width, height, uhepp}) => { + const [uhepp_data, setData] = useState(uhepp) + + const handleRebin = (e) => { + let values = Array.from(e.target.selectedOptions, option => parseFloat(option.value)) + setData(Object.assign({}, uhepp_data, + {bins: Object.assign({}, uhepp_data.bins, {rebin: values})} + )) + } + + const handleUnderflow = (e) => { + let value = e.target.checked + setData(Object.assign({}, uhepp_data, + {bins: Object.assign({}, uhepp_data.bins, {include_underflow: value})} + )) + } + const handleOverflow = (e) => { + let value = e.target.checked + setData(Object.assign({}, uhepp_data, + {bins: Object.assign({}, uhepp_data.bins, {include_overflow: value})} + )) + } + const handleDensityWidth = (e) => { + let value = parseFloat(e.target.value) + setData(Object.assign({}, uhepp_data, + {bins: Object.assign({}, uhepp_data.bins, {density_width: value})} + )) + } + + let rebin = uhepp_data.bins.rebin || uhepp_data.bins.edges + return <> + <UheppHist width={width} height={height} uhepp={uhepp_data} /> + + <ul className="nav nav-tabs" id="view-options" role="tablist"> + <li className="nav-item"> + <a className="nav-link active" id="info-tab" data-toggle="tab" href="#info" role="tab" aria-controls="info" aria-selected="true">Info</a> + </li> + <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"> + <a className="nav-link" id="other-tab" data-toggle="tab" href="#other" role="tab" aria-controls="other" aria-selected="false">Other</a> + </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" id="info" role="tabpanel" aria-labelledby="binning-tab"> + <dl> + <dt>Author</dt> + <dd>{ uhepp.metadata.author }</dd> + <dt>Data</dt> + <dd>{ uhepp.metadata.data }</dd> + <dt>Producer</dt> + <dd>{ uhepp.metadata.producer || <i>None</i>}</dd> + <dt>Code revision</dt> + <dd>{ uhepp.metadata.code_revision || <i>None</i>}</dd> + </dl> + </div> + <div className="tab-pane" id="binning" role="tabpanel" aria-labelledby="binning-tab"> + <form> + <div className="form-group"> + <label htmlFor="rebin">Bin edges</label> + <select multiple size={10} className="form-control" id="rebin" onChange={(e) => handleRebin(e)}> + {uhepp.bins.edges.map((v, i) => + <option value={v} key={i} selected={rebin.indexOf(v) >= 0}>{v}</option> + )} + </select> + </div> + + <div className="form-check"> + <input className="form-check-input" type="checkbox" + checked={uhepp_data.bins.include_underflow == true} id="checkUnderflow" + onChange={e => handleUnderflow(e)} /> + <label className="form-check-label" htmlFor="checkUnderflow"> + Include underflow events in first bin + </label> + </div> + + <div className="form-check"> + <input className="form-check-input" type="checkbox" + checked={uhepp_data.bins.include_overflow == true} id="checkOverflow" + onChange={e => handleOverflow(e)} /> + <label className="form-check-label" htmlFor="checkOverflow"> + Include overflow events in last bin + </label> + </div> + + <div className="form-group"> + <label htmlFor="densityWidth">Normalize yields to</label> + <div className="input-group"> + <input type="number" className="form-control" id="densityWidth" + onChange={e => handleDensityWidth(e)} + value={uhepp_data.bins.density_width || "0"} /> + <div className="input-group-append"> + <span className="input-group-text">{uhepp_data.variable.unit}</span> + </div> + </div> + </div> + </form> + </div> + <div className="tab-pane" id="other" role="tabpanel" aria-labelledby="other-tab"> + <h1>La di da</h1> + </div> + <div className="tab-pane" id="reset" role="tabpanel" aria-labelledby="reset-tab"> + <button className="btn btn-secondary m-1" type="button" onClick={() => setData(uhepp)}> + Reset view + </button> + </div> + </div> + </> +} +export default UheppHistUI; diff --git a/uhepp-js/src/index.js b/uhepp-js/src/index.js index 2ab49ab9ea3c7b79d44e58edb9c398c83a79bc13..110e05ddc350196a549b319705527e5c1bb94389 100644 --- a/uhepp-js/src/index.js +++ b/uhepp-js/src/index.js @@ -4,9 +4,7 @@ import "regenerator-runtime/runtime"; import './common.scss' import 'bootstrap' -import Header from './components/Header.jsx'; -import Graph from './components/Graph.jsx'; -import About from './components/About.jsx'; +import Uhepp from './components/Uhepp.jsx'; import React from "react"; import ReactDOM from "react-dom"; @@ -16,10 +14,5 @@ import { HashRouter as Router, Route, Link } from "react-router-dom"; export const fe = { React: React, ReactDOM: ReactDOM, - Graph: Graph, + Uhepp: Uhepp, }; - -// ReactDOM.render( -// <Graph width="600" height="500" uhepp={toy_data} />, -// document.getElementById('app-root') -// ); diff --git a/uhepp_org/uhepp_vault/templates/uhepp_vault/plot_detail.html b/uhepp_org/uhepp_vault/templates/uhepp_vault/plot_detail.html index 7c26e6746c4cb52aa7e284084a04bae5c9735345..ae87092175f7c8a96517a79fc9ce9e0fb017ac04 100644 --- a/uhepp_org/uhepp_vault/templates/uhepp_vault/plot_detail.html +++ b/uhepp_org/uhepp_vault/templates/uhepp_vault/plot_detail.html @@ -2,49 +2,104 @@ {% load pygmentify_tags %} {% block content %} + <a href="{% url 'uhepp_vault:collection-detail' plot.collection.id %}"> + back to collection + </a> <h1>{{ plot }}</h1> -<p><a href="{% url 'uhepp_vault:collection-detail' plot.collection.id %}"> -back to collection -</a></p> -<p>UUID: {{ plot.uuid }}</p> - -<div id="app-root"> -Loading... -</div> -{{ plot.uhepp|json_script:"plot-data" }} -<p> - <a class="btn btn-primary" href="{% url 'uhepp_vault:plot-download' plot.uuid %}">Download JSON</a> -</p> +<div class="d-flex"> +<p>UUID: {{ plot.uuid }}</p> +<div class="ml-auto btn-group btn-group-sm"> + <div class="btn-group btn-group-sm"> + <button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown"> + Direct download + </button> + <div class="dropdown-menu dropdown-menu-right p-4 shadow"> + <p>Download the uhepp data directy as a JSON file.</p> + <p> + <a class="btn btn-primary" href="{% url 'uhepp_vault:plot-download' plot.uuid %}">Download JSON</a> + </p> + </div> + </div> -<h2>API access</h2> -<p>Make sure you've set up an API access token.</p> -<h3>Pull plot</h3> -{% pygmentify %} -<pre class="python"> + <div class="btn-group btn-group-sm"> + <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown"> + API Access + </button> + <div class="dropdown-menu dropdown-menu-right p-4 shadow"> + <p class="alert alert-info m-2"> + Make sure you've set up an <a href="{% url 'uhepp_vault:token-list' %}">API access token</a>. + </p> + <h3>Pull plot</h3> + {% pygmentify %} + <pre class="python"> import uhepp plot = uhepp.pull("{{ plot.uuid }}") # Modify, save, render or show plot locally plot.show()</pre> -{% endpygmentify %} + {% endpygmentify %} -<h3>Push new plot to the same collection</h3> -{% pygmentify %} -<pre class="python"> + <h3>Push new plot to the same collection</h3> + {% pygmentify %} + <pre class="python"> import uhepp # Create a histogram or retrieve one from a location file hist = uhepp.from_json("local_file.json") hist.push({{ plot.collection.pk }})</pre> -{% endpygmentify %} + {% endpygmentify %} + </div> + </div> + + <div class="btn-group btn-group-sm"> + <button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown"> + CLI tool + </button> + <div class="dropdown-menu dropdown-menu-right p-4 shadow"> + <p class="alert alert-info m-2"> + Make sure you've set up an <a href="{% url 'uhepp_vault:token-list' %}">API access token</a>. + </p> + <h3>Pull plot</h3> + {% pygmentify %} + <pre class="console"> +$ uhepp pull {{ plot.uuid}}</pre> + {% endpygmentify %} + + <h3>Show plot</h3> + {% pygmentify %} + <pre class="console"> +$ uhepp show {{ plot.uuid}}</pre> + {% endpygmentify %} + + <h3>Push new plot to the same collection</h3> + {% pygmentify %} + <pre class="console"> +$ uhepp push {{ plot.collection.pk }} local_file.json</pre> + {% endpygmentify %} + </div> + </div> +</div> +</div> + +<div id="app-root"> + <div class="d-flex justify-content-center my-5"> + <div class="spinner-border text-primary" role="status"> + <span class="sr-only">Loading...</span> + </div> + </div> +</div> +{{ plot.uhepp|json_script:"plot-data" }} + + + {% endblock %} {% block loadscript %} <script> uhepp.fe.ReactDOM.render( - uhepp.fe.React.createElement(uhepp.fe.Graph, { + uhepp.fe.React.createElement(uhepp.fe.Uhepp, { width: "555", height: "400", uhepp: JSON.parse(document.getElementById('plot-data').textContent)