From 5d1a4da0238d17879c3ed7cf5d8e3de5b3d76ca4 Mon Sep 17 00:00:00 2001 From: Frank Sauerburger <frank@sauerburger.com> Date: Mon, 28 Dec 2020 23:43:15 +0100 Subject: [PATCH] Modularize code and add ratio axes --- uhepp-js/src/components/Graph.jsx | 511 ++++++++++++++++++++++-------- 1 file changed, 376 insertions(+), 135 deletions(-) diff --git a/uhepp-js/src/components/Graph.jsx b/uhepp-js/src/components/Graph.jsx index 98c058e..758a1be 100644 --- a/uhepp-js/src/components/Graph.jsx +++ b/uhepp-js/src/components/Graph.jsx @@ -29,6 +29,119 @@ const getMaxBin =(uhepp) => { return max; } +const Badge = ({brand, label, subtext}) => ( + <> + <text y={0} x={0}> + { brand && + <tspan className="uhepp-brand">{brand}</tspan> + } + { label && + <tspan dx={3} className="uhepp-label">{label}</tspan> + } + </text> + { subtext && subtext.split("\n").map((s, i) => + <MixedText key={i} x={0} y={(i+1) * 16}>{s}</MixedText> + ) + } + </> +) + + +const computeEquidistent = (edges) => { + let width = false + for (let i = 0; i < edges.length - 1; i++) { + const this_width = edges[i + 1] - edges[i] + if (width === false || this_width == width) { + width = this_width + } else { + return false + } + } + return width +} + +const Bar = ({ + xScale, + yScale, + leftEdge, + rightEdge, + bottom, + y, + id, + style={}, + defaultColorsCycle, + highlighted=false, + onMouseOver=()=>null, + onMouseOut=()=>null, +}) => ( + <rect x={xScale(leftEdge)} + width={xScale(rightEdge) - xScale(leftEdge)} + y={yScale(bottom + y)} + height={yScale(bottom) - yScale(bottom + y)} + fill={style.color || defaultColorsCycle.get(id)} + opacity={highlighted ? 0.5 : 1.0} + onMouseOver={() => onMouseOver()} + onMouseOut={() => onMouseOut()} /> +) + +const Point = ({ + xScale, + yScale, + x, + y, + xErr=0, + yErr=0, + style={}, + highlighted=false, + onMouseOver=()=>null, + onMouseOut=()=>null, +}) => ( + <g onMouseOver={() => onMouseOver()} + onMouseOut={() => onMouseOut()}> + <circle cx={xScale(x)} + cy={yScale(y)} + fill={style.color || "#000"} + opacity={highlighted ? 0.8 : 1.0} + r="3" /> + <line y1={yScale(y)} + y2={yScale(y)} + stroke={style.color || "#000"} + strokeWidth="2" + strokeOpacity={highlighted ? 0.8 : 1.0} + x1={xScale(x - xErr)} + x2={xScale(x + xErr)} /> + <line y1={yScale(y + yErr)} + y2={yScale(y - yErr)} + strokeWidth="2" + stroke={style.color || "#000"} + strokeOpacity={highlighted ? 0.8 : 1.0} + x1={xScale(x)} + x2={xScale(x)} /> + </g> +) + + +class ColorCycle { + constructor(colors) { + this.colors = colors + this.index = 0 + this.cache = {} + } + + length() { + return this.colors.length + } + + get(id) { + if (!this.cache.hasOwnProperty(id)) { + const item = this.colors[this.index] + this.index = (this.index + 1) % this.colors.length + this.cache[id] = item + } + return this.cache[id] + } +} + // Child iterates over stacksitems = a list of rects // rects given freedom to highlight single bins or whole processes const Histogram = ({ @@ -36,6 +149,7 @@ const Histogram = ({ yScale, xScale, colorScale, + defaultColorsCycle, highlightedBin, onMouseOverBin, children @@ -51,55 +165,38 @@ const Histogram = ({ stack.content.forEach((stack_item, si_i) => { const y_value = sumBase(uhepp.yields, stack_item["yield"], bin_i) if (stack.type == "stepfilled") { - objects.push(<rect - key={`rect-${stack_index}-${si_i}-${bin_i}`} - x={xScale(edges[bin_i-1])} - width={xScale(edges[bin_i + 1]) - xScale(edges[bin_i])} - y={yScale(bottom + y_value)} - height={yScale(bottom) - yScale(bottom + y_value)} - fill={stack_item.style ? stack_item.style.color : '#1f77b4'} - opacity={highlightedBin == bin_i ? 0.5 : 1.0} - onMouseOver={() => onMouseOverBin(bin_i)} - />) - } else if (stack.type == "points") { const width = xScale(edges[bin_i]) - xScale(edges[bin_i - 1]) - const stat = sumStat(uhepp.yields, stack_item["yield"], bin_i) + objects.push(<Bar leftEdge={edges[bin_i - 1]} + rightEdge={edges[bin_i]} + bottom={bottom} + y={y_value} + xScale={xScale} + yScale={yScale} + style={stack_item.style} + highlighted={highlightedBin == bin_i} + defaultColorsCycle={defaultColorsCycle} + id={`${stack_index}-${si_i}`} + key={`key-${stack_index}-${si_i}-${bin_i}`} + onMouseOver={() => onMouseOverBin(bin_i)} + onMouseOverBin={() => onMouseOverBin(null)} />) - const stat_up = yScale(y_value + stat) - yScale(y_value) - const stat_down = yScale(y_value) - yScale(y_value - stat) - const gx = (xScale(edges[bin_i - 1]) + xScale(edges[bin_i])) / 2 - const gy = yScale(bottom + y_value) + } else if (stack.type == "points") { + const stat = sumStat(uhepp.yields, stack_item["yield"], bin_i) + const bin_center = (edges[bin_i - 1] + edges[bin_i]) / 2 + const width = edges[bin_i] - edges[bin_i - 1] if (stat && y_value) { - objects.push(<g transform={`translate(${gx}, ${gy})`} - key={`p-${stack_index}-${si_i}-${bin_i}`} - > - <circle - cx="0" - cy="0" - fill="#000" - opacity={highlightedBin == bin_i ? 0.8 : 1.0} - onMouseOver={() => onMouseOverBin(bin_i)} - r="3"/> - <line - y1="0" - y2="0" - stroke="#000" - strokeWidth="2" - strokeOpacity={highlightedBin == bin_i ? 0.8 : 1.0} - onMouseOver={() => onMouseOverBin(bin_i)} - x1={-width / 2} - x2={+width / 2} /> - <line - y1={-stat_down} - y2={stat_up} - strokeWidth="2" - stroke="#000" - strokeOpacity={highlightedBin == bin_i ? 0.8 : 1.0} - onMouseOver={() => onMouseOverBin(bin_i)} - x1="0" - x2="0" /> - </g>) + objects.push(<Point x={bin_center} + y={bottom + y_value} + xErr={width / 2} + yErr={stat} + xScale={xScale} + yScale={yScale} + key={`key-${stack_index}-${si_i}-${bin_i}`} + highlighted={highlightedBin == bin_i} + onMouseOver={() => onMouseOverBin(bin_i)} + style={stack_item.style} />) + } } if (y_value > 0) { @@ -165,7 +262,7 @@ const length_metrix = (str) => { return str.length + 0.2 * long_count - 0.5 * short_count } -const EmbeddedMathJax = ({src, posX, posY, props}) => { +const EmbeddedMathJax = ({src, posX, posY, align="left"}) => { let html = tex_html; let node = document.createElement("div"); const math = src.trim(); @@ -179,19 +276,96 @@ const EmbeddedMathJax = ({src, posX, posY, props}) => { node.innerHTML = outerHTML; let rendered_svg = node.getElementsByTagName("svg")[0]; const scale = 6 + const width = rendered_svg.width.baseVal.value * scale const transX = rendered_svg.width.baseVal.value * scale / rendered_svg.viewBox.baseVal.width const transY = rendered_svg.height.baseVal.value * scale / rendered_svg.viewBox.baseVal.height + let alignTransX = 0 + if (align == "center") { + alignTransX = -width / 2 + } else if (align == "right") { + alignTransX = -width + } + return <g - transform={`matrix(${transX} 0 0 ${transY} ${posX} ${posY})`} - dangerouslySetInnerHTML={{__html: rendered_svg.innerHTML}} { ...props}></g> + transform={`matrix(${transX} 0 0 ${transY} ${posX + alignTransX} ${posY})`} + dangerouslySetInnerHTML={{__html: rendered_svg.innerHTML}}></g> } -const MixedText = ({children, x=0, y=0}) => { +const MixedText = ({children, x=0, y=0, align="left"}) => { + if (children.join) { + children = children.join("") + } const embedded = `\\textsf{${children}}` const switched = embedded.replaceAll(/\$([^$]*)\$/g, "}$1\\textsf{") const cleared = switched.replaceAll("\\textsf{}", "") - return <EmbeddedMathJax posX={x} posY={y} src={cleared} /> + return <EmbeddedMathJax posX={x} posY={y} src={cleared} align={align} /> +} + +const Legend = ({ + post_uhepp, + setShowTotal, + defaultColorsCycle, + showTotal, + highlightedBin +}) => { + const lineSkip = 16; + const mathSkip = 120; + let i = 0 + let legend = [] + post_uhepp.stacks.map((stack, stack_index) => { + stack.content.map((stack_item, si_i) => { + if (stack.type == "points") { + legend.push(<Point x={10} + y={(i + 1) * lineSkip - 5} + xErr={10} + yErr={5} + xScale={(x) => x} + yScale={(y) => y} + key={`legend-${i}`} + onMouseOver={() => setShowTotal(true)} + onMouseOut={() => setShowTotal(false)} + style={stack_item.style} />) + } else { + legend.push(<Bar leftEdge={0} + rightEdge={20} + bottom={(i + 1) * lineSkip} + y={-10} + xScale={(x) => x} + yScale={(y) => y} + style={stack_item.style} + onMouseOver={() => setShowTotal(true)} + onMouseOut={() => setShowTotal(false)} + defaultColorsCycle={defaultColorsCycle} + id={`${stack_index}-${si_i}`} + key={`legend-${stack_index}-${si_i}`} />) + } + if (highlightedBin != null) { + const y_value = sumBase(post_uhepp.yields, stack_item["yield"], highlightedBin) + const stat = sumStat(post_uhepp.yields, stack_item["yield"], highlightedBin) + + const label = y_value.toFixed(1) + " ± " + stat.toFixed(1) + legend.push(<text fontSize={12} textAnchor="end" x={mathSkip} y={(i + 1) * lineSkip} + key={`legend-text-${i}`}>{label}</text>) + } else if (showTotal) { + const y_value = sumBase(post_uhepp.yields, stack_item["yield"]) + const stat = sumStat(post_uhepp.yields, stack_item["yield"]) + + const label = y_value.toFixed(1) + " ± " + stat.toFixed(1) + legend.push(<text fontSize={12} textAnchor="end" x={mathSkip} y={(i + 1) * lineSkip} + key={`legend-text-${i}`}>{label}</text>) + + } else { + const label = stack_item.label + legend.push( + <MixedText x={30} y={(i + 1) * lineSkip} + key={`legend-text-${i}`}>{label}</MixedText>) + } + i++; + }) + }) + + return legend } const UheppHist = ({width, height, uhepp}) => { @@ -199,6 +373,20 @@ const UheppHist = ({width, height, uhepp}) => { const [highlightedBin, setHighlightedBin] = useState(null); const [showTotal, setShowTotal] = useState(false); + const defaultColorsCycle = new ColorCycle([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + "#1f77b4", + ]) + const post_uhepp = Object.assign({}, uhepp, { yields: preprocessData({ yields: uhepp.yields, @@ -214,10 +402,18 @@ const UheppHist = ({width, height, uhepp}) => { bottom: 60, left: 80, right: 80, + sep: 3, } const xMax = width - margin.left - margin.right - const yMax = height - margin.top - margin.bottom + let yMax = height - margin.top - margin.bottom + let ratioMax = 0 + const hasRatio = (uhepp.ratio && uhepp.ratio.length) + if (hasRatio) { + const ratioFraction = (uhepp.layout && uhepp.layout.ratio_fraction) || 0.25 + yMax = (height - margin.top - margin.bottom - margin.sep) * (1 - ratioFraction) + ratioMax = (height - margin.top - margin.bottom - margin.sep) * (ratioFraction) + } const xScale = scaleLinear({ range: [0, xMax], @@ -225,81 +421,19 @@ const UheppHist = ({width, height, uhepp}) => { }) const yScale = scaleLinear({ range: [yMax, 0], - domain: [0, getMaxBin(post_uhepp) * 1.3], + domain: [0, getMaxBin(post_uhepp) * 1.5], + }) + const ratioScale = scaleLinear({ + range: [ratioMax, 0], + domain: [ + (uhepp.ratio_axis && uhepp.ratio_axis.min) || 0.5, + (uhepp.ratio_axis && uhepp.ratio_axis.max) || 1.5, + ], }) - let i = 0 - let legend = [] - post_uhepp.stacks.reverse().map(stack => { - stack.content.reverse().map(stack_item => { - if (stack.type == "points") { - let color = stack_item.style ? stack_item.style.color : '#000' - legend.push(<g key={`legend-${i}`} - onMouseOver={() => setShowTotal(true)} - onMouseOut={() => setShowTotal(false)} - > - <circle - cx={width * 0.5 + 10} - cy={25 + i * 20} - stroke={color} - r="3"/> - <line - y1={25 + i * 20} - y2={25 + i * 20} - stroke={color} - strokeWidth="2" - onMouseOver={() => setShowTotal(true)} - onMouseOut={() => setShowTotal(false)} - x1={width * 0.5} - x2={width * 0.5 + 20} /> - <line - y1={20 + i * 20} - y2={30 + i * 20} - strokeWidth="2" - stroke={color} - onMouseOver={() => setShowTotal(true)} - onMouseOut={() => setShowTotal(false)} - x1={width * 0.5} - x1={width * 0.5 + 10} - x2={width * 0.5 + 10} /> - </g>) - } else { - legend.push( - <rect x={width * 0.5} y={20 + i * 20} - key={`legend-${i}`} - onMouseOver={() => setShowTotal(true)} - onMouseOut={() => setShowTotal(false)} - height={10} width={20} - fill={stack_item.style ? stack_item.style.color : '#1f77b4'} - />) - } - if (highlightedBin != null) { - const y_value = sumBase(post_uhepp.yields, stack_item["yield"], highlightedBin) - const stat = sumStat(post_uhepp.yields, stack_item["yield"], highlightedBin) - - const label = y_value.toFixed(1) + " ± " + stat.toFixed(1) - legend.push(<text fontSize={12} textAnchor="end" x={width * 0.5 + 100} y={30 + i * 20} - key={`legend-text-${i}`}>{label}</text>) - } else if (showTotal) { - const y_value = sumBase(post_uhepp.yields, stack_item["yield"]) - const stat = sumStat(post_uhepp.yields, stack_item["yield"]) - - const label = y_value.toFixed(1) + " ± " + stat.toFixed(1) - legend.push(<text fontSize={12} textAnchor="end" x={width * 0.5 + 100} y={30 + i * 20} - key={`legend-text-${i}`}>{label}</text>) - } else { - const label = stack_item.label - legend.push( - <MixedText x={width * 0.5 + 30} y={30 + i * 20} - key={`legend-text-${i}`}>{label}</MixedText>) - } - i++; - }) - stack.content.reverse() - }) - post_uhepp.stacks.reverse() + const equidistent = computeEquidistent(uhepp.bins.rebin || uhepp.bins.edges) return ( <div style={{width: '100%', margin: '0 auto'}}> @@ -316,30 +450,36 @@ const UheppHist = ({width, height, uhepp}) => { </pattern> <Group top={margin.top} left={margin.left}> - - <text y={30} x={13} className="uhepp-brand">ATLAS</text> - <text y={30} x={75} className="uhepp-label">Internal</text> - <Histogram uhepp={post_uhepp} highlightedBin={highlightedBin} onMouseOverBin={binIndex => setHighlightedBin(binIndex)} stack_index={0} + defaultColorsCycle={defaultColorsCycle} xScale={xScale} yScale={yScale} /> - {legend} - + <Group top={26} left={13}> + <Badge brand={uhepp.badge.brand} label={uhepp.badge.label} subtext={uhepp.badge.subtext} /> + </Group> + + <Group top={5} left={xMax * 0.65}> + <Legend post_uhepp={post_uhepp} + setShowTotal={setShowTotal} + showTotal={showTotal} + defaultColorsCycle={defaultColorsCycle} + highlightedBin={highlightedBin} /> + </Group> + <AxisBottom scale={xScale} top={yMax} stroke={'#1b1a1e'} tickTextFill={'#1b1a1e'} tickTransform="translate(0, -8)" + tickComponent={hasRatio ? (props) => <></> : undefined} /> - <MixedText y={yMax * 1.08} x={xMax * 0.9}>{uhepp.variable.symbol}</MixedText> - <AxisBottom scale={xScale} top={yMax} @@ -349,16 +489,30 @@ const UheppHist = ({width, height, uhepp}) => { tickTransform="translate(0, -4)" tickComponent={(props) => <></>} /> - <AxisLeft + { !hasRatio && + <MixedText y={yMax + 30} x={xMax - 10} align="right"> + {uhepp.variable.name ? uhepp.variable.name + ' ' : ''} + {uhepp.variable.symbol} + {uhepp.variable.unit ? ' / ' + uhepp.variable.unit : ''} + </MixedText> + } + + <AxisLeft scale={yScale} top={0} left={0} - label={'Events'} stroke={'#1b1a1e'} tickTextFill={'#1b1a1e'} tickTransform="translate(8, 0)" /> - <AxisLeft + <g transform={`matrix(0 -1 1 0 ${-margin.left + 20} 10)`}> + <MixedText y={0} x={0} align="right"> + Events + { equidistent ? ' / ' + equidistent + (uhepp.variable.unit ? " " + uhepp.variable.unit : '') : ''} + </MixedText> + </g> + + <AxisLeft scale={yScale} top={0} left={0} @@ -368,7 +522,7 @@ const UheppHist = ({width, height, uhepp}) => { tickLength={4} tickComponent={(props) => <></>} /> - <AxisRight + <AxisRight scale={yScale} top={0} left={xMax} @@ -376,7 +530,7 @@ const UheppHist = ({width, height, uhepp}) => { tickComponent={(props) => <></>} tickTransform="translate(-8, 0)" /> - <AxisRight + <AxisRight scale={yScale} top={0} left={xMax} @@ -403,6 +557,93 @@ const UheppHist = ({width, height, uhepp}) => { tickTransform="translate(0, 4)" /> </Group> + + <Group top={margin.top + margin.sep + yMax} left={margin.left}> + <AxisBottom + scale={xScale} + top={ratioMax} + stroke={'#1b1a1e'} + tickTextFill={'#1b1a1e'} + tickTransform="translate(0, -8)" + /> + <AxisBottom + scale={xScale} + top={ratioMax} + numTicks={50} + stroke={'#1b1a1e'} + tickLength={4} + tickTransform="translate(0, -4)" + tickComponent={(props) => <></>} + /> + <MixedText y={ratioMax + 30} x={xMax - 10} align="right"> + {uhepp.variable.name ? uhepp.variable.name + ' ' : ''} + {uhepp.variable.symbol} + {uhepp.variable.unit ? ' / ' + uhepp.variable.unit : ''} + </MixedText> + + <AxisLeft + scale={ratioScale} + top={0} + left={0} + stroke={'#1b1a1e'} + tickTextFill={'#1b1a1e'} + numTicks={5} + tickTransform="translate(8, 0)" + /> + { uhepp.ratio_axis.label && + <g transform={`matrix(0 -1 1 0 ${-margin.left + 20} ${ratioMax / 2})`}> + <MixedText y={0} x={0} align="center"> + { uhepp.ratio_axis.label } + </MixedText> + </g> + } + + <AxisLeft + scale={ratioScale} + top={0} + left={0} + stroke={'#1b1a1e'} + numTicks={20} + tickTransform="translate(4, 0)" + tickLength={4} + tickComponent={(props) => <></>} + /> + <AxisRight + scale={ratioScale} + top={0} + left={xMax} + numTicks={5} + stroke={'#1b1a1e'} + tickComponent={(props) => <></>} + tickTransform="translate(-8, 0)" + /> + <AxisRight + scale={ratioScale} + top={0} + left={xMax} + stroke={'#1b1a1e'} + numTicks={20} + tickComponent={(props) => <></>} + tickLength={4} + tickTransform="translate(-4, 0)" + /> + <AxisTop + scale={xScale} + top={0} + stroke={'#1b1a1e'} + tickComponent={(props) => <></>} + tickTransform="translate(0, 8)" + /> + <AxisTop + scale={xScale} + top={0} + stroke={'#1b1a1e'} + numTicks={50} + tickComponent={(props) => <></>} + tickLength={4} + tickTransform="translate(0, 4)" + /> + </Group> </svg> </div> ) -- GitLab