From 59b71d01fd413e3e0bfb2db5776b4d83e81112f3 Mon Sep 17 00:00:00 2001 From: Paul Moeller Date: Tue, 3 Sep 2024 13:59:08 -0600 Subject: [PATCH] fix #7217 Impact-T input file parser (#7234) * fix #7217 added ImpactT parser - added ImpactT particle file input - display WRITE_BEAM output in beamline order - KaTeX parameter plot labels - reportPopup now in html - move dark mode to separate css file sirepo-dark.css - parameter plot y2 plot axis - parameter plot limits --- sirepo/package_data/static/css/lattice.css | 16 + .../static/css/{omega.css => sirepo-dark.css} | 95 +- sirepo/package_data/static/css/sirepo.css | 52 +- .../static/html/impactt-source.html | 4 +- .../static/html/impactt-visualization.html | 8 +- sirepo/package_data/static/html/lattice.html | 6 +- .../package_data/static/html/particle3d.html | 2 +- sirepo/package_data/static/html/plot2d.html | 92 +- .../package_data/static/html/vtk-display.html | 2 +- sirepo/package_data/static/js/activait.js | 2 +- sirepo/package_data/static/js/elegant.js | 3 + sirepo/package_data/static/js/impactt.js | 45 +- .../static/js/sirepo-components.js | 10 +- .../package_data/static/js/sirepo-lattice.js | 2 +- .../package_data/static/js/sirepo-plotting.js | 1216 ++++++----------- .../static/json/impactt-schema.json | 77 +- .../static/json/omega-schema.json | 2 +- .../static/json/schema-common.json | 8 +- .../template/impactt/parameters.py.jinja | 4 + .../template/opal/default-data.json | 1 - sirepo/sim_data/activait.py | 8 +- sirepo/sim_data/impactt.py | 18 +- sirepo/sim_data/opal.py | 2 + sirepo/template/activait.py | 6 +- sirepo/template/controls.py | 12 +- sirepo/template/impactt.py | 192 ++- sirepo/template/impactt_parser.py | 118 ++ sirepo/template/template_common.py | 8 - tests/karma/plotAxis_test.js | 2 +- tests/lib_data/opal_1/out.json | 1 - tests/lib_data/opal_2/out.json | 1 - tests/lib_data/opal_3/out.json | 1 - tests/lib_data/opal_4/out.json | 1 - tests/lib_data/opal_5/out.json | 1 - tests/lib_data/opal_6/out.json | 1 - 35 files changed, 1032 insertions(+), 987 deletions(-) rename sirepo/package_data/static/css/{omega.css => sirepo-dark.css} (70%) create mode 100644 sirepo/template/impactt_parser.py diff --git a/sirepo/package_data/static/css/lattice.css b/sirepo/package_data/static/css/lattice.css index 2f5f9b7b7a..bba2e8324c 100644 --- a/sirepo/package_data/static/css/lattice.css +++ b/sirepo/package_data/static/css/lattice.css @@ -134,3 +134,19 @@ div.sr-lattice-icon-disabled span { max-width: none; width: 25em; } + +.sr-lattice-watch { + stroke: black; + stroke-width: 0.01; +} + +.sr-selected-badge { + background-color: #fcf8e3; + color: #333; + border: 1px solid #777; +} + +.sr-lattice-marker > path { + stroke: black; + stroke-width: 1; +} diff --git a/sirepo/package_data/static/css/omega.css b/sirepo/package_data/static/css/sirepo-dark.css similarity index 70% rename from sirepo/package_data/static/css/omega.css rename to sirepo/package_data/static/css/sirepo-dark.css index 61749b6b34..94ff74cb34 100644 --- a/sirepo/package_data/static/css/omega.css +++ b/sirepo/package_data/static/css/sirepo-dark.css @@ -6,10 +6,14 @@ --sr-item-user-dark-mode: #46c246; --sr-link-dark-mode: #5da1e1; --sr-link-hover-dark-mode: #fff; - --sr-panel-bg-dark-mode: #333; + --sr-panel-heading-bg-dark-mode: #282c31; + --sr-panel-bg-dark-mode: #000; --sr-panel-border-dark-mode: #999; - --sr-panel-control-dark-mode: #444; + --sr-panel-control-dark-mode: #21252a; --sr-panel-text-dark-mode: #eee; + --sr-input-error-dark-mode: #430101; + --sr-table-border-dark-mode: #3d444db3; + --sr-body-color-dark-mode: #f0f6fc; } @media (prefers-color-scheme: dark) { :root { @@ -27,6 +31,7 @@ } body { + color: var(--sr-body-color-dark-mode); background-color: var(--sr-bg-dark-mode); } @@ -44,12 +49,6 @@ svg.sr-plot g.axis > path { stroke: var(--sr-panel-text-dark-mode); } - svg.sr-plot g.sr-plot-legend-item > text { - fill: var(--sr-link-dark-mode); - } - svg.sr-plot g.sr-plot-legend-item > text.focus-text { - fill: var(--sr-link-dark-mode); - } svg.sr-plot g.text-block > text#x-text { fill: var(--sr-panel-text-dark-mode) !important; } @@ -75,11 +74,18 @@ border-color: var(--sr-panel-border-dark-mode); border-bottom-color: transparent; } + app-header-right-sim-loaded .nav-tabs > li.sim-section > a:hover { + background-color: transparent; + } .btn-default { background-color: var(--sr-panel-control-dark-mode); color: var(--sr-item-active-dark-mode); } + .btn-default:hover { + background-color: var(--sr-item-focused-bg-dark-mode); + color: var(--sr-item-active-dark-mode); + } .dropdown-menu { background-color: var(--sr-panel-bg-dark-mode); @@ -140,12 +146,18 @@ .panel-info { border-color: var(--sr-panel-border-dark-mode); } + .modal-content { + border-color: var(--sr-panel-border-dark-mode); + } + .modal-backdrop.in { + opacity: 0.7; + } .panel-info > .panel-heading { border-color: var(--sr-panel-border-dark-mode); - background-color: var(--sr-panel-bg-dark-mode); + background-color: var(--sr-panel-heading-bg-dark-mode); } .bg-warning, .bg-info { - background-color: var(--sr-panel-bg-dark-mode); + background-color: var(--sr-panel-heading-bg-dark-mode); } button.close { color: var(--sr-panel-color); @@ -156,6 +168,10 @@ opacity: 1; } + .progress { + background-color: var(--sr-panel-heading-bg-dark-mode); + } + .report-window { fill: var(--sr-item-bg-dark-mode) !important; stroke: var(--sr-panel-border-dark-mode) !important; @@ -215,4 +231,63 @@ .table-hover > tbody > tr:hover { background-color: var(--sr-item-bg-dark-mode); } + + .sr-panel-loading .sr-panel-wait, .sr-panel-waiting .sr-panel-wait { + background-color: var(--sr-panel-heading-bg-dark-mode); + } + + .nav-tabs > li.active > a, + .nav-tabs > li.active > a:hover, + .nav-tabs > li.active > a:focus { + background-color: var(--sr-bg-dark-mode); + color: var(--sr-item-active-dark-mode); + } + + .nav > li > a:hover, + .nav > li > a:focus { + background-color: transparent; + } + + .toggle-group label.toggle-on { + background-color: var(--sr-panel-heading-bg-dark-mode); + } + .toggle.btn { + border-color: #ccc; + } + .toggle-group label.btn-default.toggle-off { + color: var(--sr-panel-text-dark-mode); + background-color: var(--sr-bg-dark-mode); + } + + .sr-lattice-watch { + stroke: white; + stroke-width: 0.01; + } + + .sr-screenshot { + background-color: var(--sr-bg-dark-mode); + } + + input.ng-invalid.ng-dirty { + background-color: var(--sr-input-error-dark-mode); + } + .btn-invalid { + background-color: var(--sr-input-error-dark-mode); + } + + .table > tbody > tr > td { + border-top-color: var(--sr-table-border-dark-mode); + } + .table > thead > tr > th { + border-bottom-color: var(--sr-table-border-dark-mode); + color: var(--sr-panel-color); + } + .table > tbody > tr.active > td { + background-color: var(--sr-item-active-dark-mode); + color: var(--sr-bg-dark-mode); + } + + .sr-lattice-marker > path { + stroke: var(--sr-panel-text-dark-mode); + } } diff --git a/sirepo/package_data/static/css/sirepo.css b/sirepo/package_data/static/css/sirepo.css index c8d64a2473..259833278f 100644 --- a/sirepo/package_data/static/css/sirepo.css +++ b/sirepo/package_data/static/css/sirepo.css @@ -388,7 +388,6 @@ .sr-plot .axis text.sr-svg-result { fill: #8a8a8a; font-size: 16px; - font-weight: bold; } .sr-plot text.sr-svg-label { @@ -420,14 +419,17 @@ .sr-plot text.z-axis-label, .sr-plot text.y-axis-label, .sr-plot text.x-axis-label, - .sr-plot text.frequency-label { + .sr-plot text.frequency-label, + .y-axis-label + { font-size: 14px; - font-weight: bold; text-anchor: middle; } + .sr-plot .katex { + font-size: 18px; + } .sr-plot .main-title { font-size: 18px; - font-weight: bold; text-anchor: middle; } .sr-plot .sub-title { @@ -564,37 +566,6 @@ .sr-plot .x-base, .sr-plot .y-base { font-size: 13px; } - .sr-plot .popup-report { - display: none; - } - .sr-plot .popup-group { - fill: white; - } - .sr-plot .popup-group .report-window { - fill: rgba(255, 255, 255, 0.90); - stroke: blue; - } - .sr-plot .popup-group .report-window-title-bar { - fill: rgba(221, 237, 246, 0.9); - stroke: none; - cursor: move; - } - - .sr-plot .popup-group .report-window-title-icon { - fill: #222; - cursor: pointer; - user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - } - .sr-plot .popup-group .report-window-close { - } - .sr-plot .popup-group .report-window-copy { - font-family: "Glyphicons Halflings"; - fill: #337ab7; - } - .sr-plot .plot-visibility { cursor: pointer; @@ -624,9 +595,10 @@ @media (max-width: 767px) { div.sr-plot .z-axis-label, div.sr-plot .y-axis-label, - div.sr-plot .x-axis-label { + div.sr-plot .x-axis-label, + .y-axis-label + { font-size: 12px; - font-weight: bold; text-anchor: middle; } div.sr-plot .main-title { @@ -635,6 +607,9 @@ div.sr-plot .focus-text { font-size: 12px; } + .sr-plot .katex { + font-size: 16px; + } } @@ -1044,9 +1019,6 @@ label .katex { font-size: 16px; font-weight: normal; } -.latex-title .katex { - font-size: 18px; -} .sr-disabled-item { background-color: lightgray !important; diff --git a/sirepo/package_data/static/html/impactt-source.html b/sirepo/package_data/static/html/impactt-source.html index fa8baf8014..767f2bc241 100644 --- a/sirepo/package_data/static/html/impactt-source.html +++ b/sirepo/package_data/static/html/impactt-source.html @@ -1,9 +1,9 @@
-
+
-
+
diff --git a/sirepo/package_data/static/html/impactt-visualization.html b/sirepo/package_data/static/html/impactt-visualization.html index 42d9d0e384..0e296592bf 100644 --- a/sirepo/package_data/static/html/impactt-visualization.html +++ b/sirepo/package_data/static/html/impactt-visualization.html @@ -8,11 +8,13 @@
-
+
-
-
+
+
+
+
diff --git a/sirepo/package_data/static/html/lattice.html b/sirepo/package_data/static/html/lattice.html index 7c3f57a907..874f3e3957 100644 --- a/sirepo/package_data/static/html/lattice.html +++ b/sirepo/package_data/static/html/lattice.html @@ -26,7 +26,7 @@ {{ ::item.title }} - {{ ::item.title }} + {{ ::item.title }} {{ ::item.title }} @@ -60,8 +60,8 @@ - - + + {{ markerUnits }}
diff --git a/sirepo/package_data/static/html/particle3d.html b/sirepo/package_data/static/html/particle3d.html index f48434c2f3..c306272b4e 100644 --- a/sirepo/package_data/static/html/particle3d.html +++ b/sirepo/package_data/static/html/particle3d.html @@ -1,7 +1,7 @@
-
+
diff --git a/sirepo/package_data/static/html/plot2d.html b/sirepo/package_data/static/html/plot2d.html index c1548060c5..d2614ee3f0 100644 --- a/sirepo/package_data/static/html/plot2d.html +++ b/sirepo/package_data/static/html/plot2d.html @@ -1,46 +1,54 @@
-
-
- - - - - - - - - - - - - +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
+
+
diff --git a/sirepo/package_data/static/html/vtk-display.html b/sirepo/package_data/static/html/vtk-display.html index 24e572d111..1a8fbfe9d5 100644 --- a/sirepo/package_data/static/html/vtk-display.html +++ b/sirepo/package_data/static/html/vtk-display.html @@ -10,7 +10,7 @@
-
+
Click and drag to rotate. Control-click an object to select. Double-click to reset camera
diff --git a/sirepo/package_data/static/js/activait.js b/sirepo/package_data/static/js/activait.js index 0cf24f7fbc..b9b8c1d1ae 100644 --- a/sirepo/package_data/static/js/activait.js +++ b/sirepo/package_data/static/js/activait.js @@ -563,7 +563,7 @@ SIREPO.app.directive('analysisActions', function(appState, panelState, activaitS
-
+
diff --git a/sirepo/package_data/static/js/elegant.js b/sirepo/package_data/static/js/elegant.js index 2b622b7e94..e2e201d8d7 100644 --- a/sirepo/package_data/static/js/elegant.js +++ b/sirepo/package_data/static/js/elegant.js @@ -570,6 +570,9 @@ SIREPO.app.controller('VisualizationController', function(appState, elegantServi panelState.toggleHidden(modelKey); } } + if (outputFile.reportType != 'heatmap') { + m.aspectRatio = 4.0 / 7; + } appState.setModelDefaults(m, 'elementAnimation'); m.valueList = { x: info.plottableColumns, diff --git a/sirepo/package_data/static/js/impactt.js b/sirepo/package_data/static/js/impactt.js index 0182961de6..fd5d1eca23 100644 --- a/sirepo/package_data/static/js/impactt.js +++ b/sirepo/package_data/static/js/impactt.js @@ -1,26 +1,18 @@ 'use strict'; -var srlog = SIREPO.srlog; -var srdbg = SIREPO.srdbg; - SIREPO.app.config(function() { SIREPO.PLOTTING_SUMMED_LINEOUTS = true; SIREPO.SINGLE_FRAME_ANIMATION = ['statAnimation']; SIREPO.appFieldEditors += ``; SIREPO.lattice = { elementColor: { - MULTIPOLE: 'yellow', - QUADRUPOLE: 'red', - DIPOLE: 'lightgreen', - SOLENOID: 'red', - DRIFT: 'grey', }, elementPic: { drift: ['DRIFT', 'EMFIELD_CARTESIAN', 'EMFIELD_CYLINDRICAL', 'WAKEFIELD'], lens: ['ROTATIONALLY_SYMMETRIC_TO_3D'], magnet: ['QUADRUPOLE', 'DIPOLE'], solenoid: ['SOLENOID', 'SOLRF'], - watch: ['WRITE_BEAM', 'WRITE_SLICE_INFO',], + watch: ['WRITE_BEAM', 'WRITE_SLICE_INFO'], zeroLength: [ 'CHANGE_TIMESTEP', 'OFFSET_BEAM', @@ -136,7 +128,7 @@ SIREPO.app.directive('appHeader', function(appState, panelState) {
@@ -158,8 +150,8 @@ SIREPO.viewLogic('wakefieldView', function(appState, panelState, $scope) { return; } panelState.showFields('WAKEFIELD', [ - ['gap', 'period', 'iris_radius'], m.method == 'analytical', - ['filename'], m.method == 'from_file', + ['gap', 'period', 'iris_radius'], m.method === 'analytical', + ['filename'], m.method === 'from_file', ]); } @@ -168,3 +160,32 @@ SIREPO.viewLogic('wakefieldView', function(appState, panelState, $scope) { ['WAKEFIELD.method'], updateFields, ]; }); + +SIREPO.viewLogic('beamView', function(appState, panelState, $scope) { + + function updateFields() { + panelState.showFields('beam', [ + ['Bmass', 'Bcharge'], appState.models.beam.particle === 'other', + ]); + } + + $scope.whenSelected = updateFields; + $scope.watchFields = [ + ['beam.particle'], updateFields, + ]; + +}); + +SIREPO.viewLogic('distributionView', function(appState, panelState, $scope) { + + function updateFields() { + panelState.showField('distribution', 'filename', appState.models.distribution.Flagdist === "16"); + // the other distribution fields may also apply even when "from file" is selected + } + + $scope.whenSelected = updateFields; + $scope.watchFields = [ + ['distribution.Flagdist'], updateFields, + ]; + +}); diff --git a/sirepo/package_data/static/js/sirepo-components.js b/sirepo/package_data/static/js/sirepo-components.js index 6e800e2940..8551f698ff 100644 --- a/sirepo/package_data/static/js/sirepo-components.js +++ b/sirepo/package_data/static/js/sirepo-components.js @@ -1946,6 +1946,9 @@ SIREPO.app.directive('textWithMath', function(appState, mathRendering, utilities controller: function($scope) { $scope.appState = appState; $scope.getHTML = function() { + if (! $scope.textWithMath) { + return ''; + } return $sce.trustAsHtml(mathRendering.mathAsHTML( utilities.interpolateString($scope.textWithMath, $scope) )); @@ -4978,7 +4981,7 @@ SIREPO.app.service('plotToPNG', function() { html2canvas(el, { scale: outputHeight / $(el).height(), backgroundColor: '#ffffff', - ignoreElements: (element) => element.matches("path.pointer.axis") + ignoreElements: (element) => element.matches("path.pointer.axis") || element.matches('text.glyphicon') }).then(canvas => { canvas.toBlob(function(blob) { saveAs(blob, fileName); @@ -5048,7 +5051,10 @@ SIREPO.app.service('mathRendering', function() { return encodeHTML(text); } var parts = []; - + //if (! options) { + // options = {}; + //} + //options.output = 'html'; var i = text.search(RE); while (i != -1) { if (i > 0) { diff --git a/sirepo/package_data/static/js/sirepo-lattice.js b/sirepo/package_data/static/js/sirepo-lattice.js index de373dc1e1..a72a8b5e5f 100644 --- a/sirepo/package_data/static/js/sirepo-lattice.js +++ b/sirepo/package_data/static/js/sirepo-lattice.js @@ -2959,7 +2959,7 @@ SIREPO.app.directive('varEditor', function(appState, latticeService, requestSend
- +
diff --git a/sirepo/package_data/static/js/sirepo-plotting.js b/sirepo/package_data/static/js/sirepo-plotting.js index fbb6ca65b1..45b0b07bef 100644 --- a/sirepo/package_data/static/js/sirepo-plotting.js +++ b/sirepo/package_data/static/js/sirepo-plotting.js @@ -709,28 +709,6 @@ SIREPO.app.factory('plotting', function(appState, frameCache, panelState, utilit saveAs(new Blob([res], {type: "text/csv;charset=utf-8"}), self.csvFilename(fileName)); }, - // returns an array of substrings of str that fit in the given width. The provided d3Text selection - // must be part of the document so its size can be calculated - fitSplit: function(str, d3Text, width) { - if (!str || str.length === 0) { - return []; - } - var splits = utilities.wordSplits(str).reverse(); - var split; - for (var i = 0; i < splits.length; ++i) { - var s = splits[i]; - var w = d3Text.text(s).node().getBBox().width; - if (w <= width) { - split = s; - break; - } - } - if (!split) { - return []; - } - return $.merge([split], self.fitSplit(str.substring(split.length), d3Text, width)); - }, - formatValue: function(v, formatter, ordinateFormatter) { var fmt = formatter ? formatter : d3.format('.3f'); var ordfmt = ordinateFormatter ? ordinateFormatter : d3.format('.3e'); @@ -1226,8 +1204,8 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState $scope.axes = { x: layoutService.plotAxis($scope.margin, 'x', 'bottom', refresh), y: layoutService.plotAxis($scope.margin, 'y', 'left', refresh), + y2: layoutService.plotAxis($scope.margin, 'y2', 'right', refresh), }; - function init() { $scope.select('svg.sr-plot').attr('height', plotting.initialHeight($scope)); $.each($scope.axes, function(dim, axis) { @@ -1244,11 +1222,24 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState resetZoom(); } + function recalcAxes() { + $.each($scope.axes, function(dim, axis) { + axis.updateLabelAndTicks({ + width: $scope.width, + height: $scope.height, + scaleFunction: dim == 'x' ? null: scaleFunction, + }, $scope.select); + axis.grid.ticks(axis.tickCount); + $scope.select('.' + dim + '.axis.grid').call(axis.grid); + }); + } + function refresh() { if (! $scope.axes.x.domain) { return; } if (layoutService.plotAxis.allowUpdates) { + recalcAxes(); var elementWidth = parseInt($scope.select().style('width')); if (isNaN(elementWidth)) { return; @@ -1259,16 +1250,20 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState .attr('height', $scope.height + $scope.margin.top + $scope.margin.bottom); $scope.axes.x.scale.range([0, $scope.width]); $scope.axes.y.scale.range([$scope.height, 0]); + if ($scope.axes.y2) { + $scope.axes.y2.scale.range([$scope.height, 0]); + } $scope.axes.x.grid.tickSize(-$scope.height); $scope.axes.y.grid.tickSize(-$scope.width); + recalcAxes(); } var isFullSize = plotting.trimDomain($scope.axes.x.scale, $scope.axes.x.domain); - if (isFullSize) { - $scope.setYDomain(); - } - else if ($scope.recalculateYDomain && ! $scope.isZoomXY) { + if ($scope.recalculateYDomain && ! $scope.isZoomXY) { $scope.recalculateYDomain(); } + else if (isFullSize) { + $scope.axes.y.scale.domain($scope.axes.y.domain).nice(); + } $scope.select($scope.zoomContainer) .classed('mouse-zoom', isFullSize) .classed('mouse-move', ! isFullSize && $scope.isZoomXY) @@ -1276,15 +1271,6 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState .classed('mouse-move-ew', ! isFullSize && ! ($scope.isZoomXY || $scope.isZoomY)); resetZoom(); $scope.select($scope.zoomContainer).call(zoom); - $.each($scope.axes, function(dim, axis) { - axis.updateLabelAndTicks({ - width: $scope.width, - height: $scope.height, - scaleFunction: dim == 'y' ? scaleFunction : null, - }, $scope.select); - axis.grid.ticks(axis.tickCount); - $scope.select('.' + dim + '.axis.grid').call(axis.grid); - }); if ($scope.wantColorbar) { colorbar.barlength($scope.height) .origin([0, 0]); @@ -1312,8 +1298,6 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState $scope.destroy = function() { zoom.on('zoom', null); $($scope.element).find($scope.zoomContainer).off(); - // not part of all plots, just parameterPlot - $($scope.element).find('.sr-plot-legend-item text').off(); }; $scope.resize = function() { @@ -1323,12 +1307,6 @@ SIREPO.app.service('plot2dService', function(appState, layoutService, panelState refresh(); }; - if (! $scope.setYDomain) { - $scope.setYDomain = function() { - $scope.axes.y.scale.domain($scope.axes.y.domain).nice(); - }; - } - $scope.updatePlot = function(json) { $scope.dataCleared = false; $.each($scope.axes, (dim, axis) => { @@ -1654,7 +1632,7 @@ SIREPO.app.service('focusPointService', function(plotting) { }); -SIREPO.app.service('layoutService', function(panelState, plotting, utilities) { +SIREPO.app.service('layoutService', function(mathRendering, panelState, plotting, utilities) { var svc = this; svc.formatUnits = (units, isFixed) => { @@ -1906,6 +1884,10 @@ SIREPO.app.service('layoutService', function(panelState, plotting, utilities) { return calcFormat(tickValues.length, unit, base); } + function setLabel(label, select) { + select(`.${dimension}-axis-label`).html(mathRendering.mathAsHTML(label)); + } + function useFloatFormat(v) { v = valuePrecision(v); return v >= -2 && v <= 3; @@ -1948,7 +1930,7 @@ SIREPO.app.service('layoutService', function(panelState, plotting, utilities) { self.updateLabel = (label, select) => { self.parseLabelAndUnits(label); - select(`.${dimension}-axis-label`).text(label); + setLabel(label, select); }; self.updateLabelAndTicks = function(canvasSize, select, cssPrefix) { @@ -1960,15 +1942,19 @@ SIREPO.app.service('layoutService', function(panelState, plotting, utilities) { if (self.units) { unit = formatPrefix(0); formatInfo = calcTicks(calcFormat(MAX_TICKS, unit), canvasSize, fontSize); - select('.' + dimension + '-axis-label').text( + setLabel( self.label + (formatInfo.base ? (' - ' + baseLabel()) : '') - + ' ' + svc.formatUnits(formatInfo.unit.symbol + self.units)); + + ' ' + svc.formatUnits(formatInfo.unit.symbol + self.units), + select, + ); } else { formatInfo = calcTicks(calcFormat(MAX_TICKS), canvasSize, fontSize); if (self.label) { - select('.' + dimension + '-axis-label').text( - self.label + (formatInfo.base ? (' - ' + baseLabel()) : '')); + setLabel( + self.label + (formatInfo.base ? (' - ' + baseLabel()) : ''), + select, + ); } } if (! self.noBaseFormat) { @@ -1999,10 +1985,10 @@ SIREPO.app.directive('columnForAspectRatio', function(appState) { if (appState.isLoaded()) { var ratio = parseFloat(appState.applicationState()[$scope.modelName].aspectRatio); if (ratio <= 0.5) { - return 'col-md-12 col-xl-8'; + return 'col-md-12 col-xl-8 col-xxl-5'; } } - return 'col-md-6 col-xl-4'; + return 'col-md-6 col-xl-4 col-xxl-2'; }; } }; @@ -2280,284 +2266,144 @@ SIREPO.app.directive('focusCircle', function(focusPointService, plotting) { }; }); + SIREPO.app.directive('popupReport', function(focusPointService, plotting) { return { restrict: 'A', scope: { + modelName: '@', focusPoints: '=', + plots: '=', }, template: ` - - - - - - - × - -  - - - - - - - - - - - - - - - +
+
+
+ +
+
+
+
= {{ pointText(0, true) }} {{ focusPoints[0].config.xAxis.units }}
+
+
+ = {{ pointText($index) }} {{ p._units }} +
+
+
+
`, controller: function($scope, $element) { - if (! $scope.focusPoints) { - // popupReport only applies if focusPoints are defined on the plot - return; - } - plotting.setupSelector($scope, $element); - - var borderWidth = 1; - var didDragToNewPositon = false; - var moveEventDetected = false; - var popupMargin = 4; - var textMargin = 8; - var titleBarHeight = 24; - var dgElement; - var group; - var plotScope; + let didDragToNewPositon = false; - function closePopup() { - focusPointService.hideFocusPoint(plotScope, true); - } + // prevent memory leak? + $scope.focusPoints.allowClone = true; - function copyToClipboard() { - $scope.select('.report-window-copy') - .transition() - .delay(0) - .duration(100) - .style('fill', 'white') - .transition() - .style('fill', null); - plotScope.copyToClipboard(); - } - - function currentXform() { - var xform = { - tx: NaN, - ty: NaN - }; - var reportTransform = group.attr('transform'); - if (reportTransform) { - var xlateIndex = reportTransform.indexOf('translate('); - if (xlateIndex >= 0) { - var tmp = reportTransform.substring('translate('.length); - var coords = tmp.substring(0, tmp.indexOf(')')); - var delimiter = coords.indexOf(',') >= 0 ? ',' : ' '; - xform.tx = parseFloat(coords.substring(0, coords.indexOf(delimiter))); - xform.ty = parseFloat(coords.substring(coords.indexOf(delimiter) + 1)); - } + function adjustBounds(bound, dim) { + if (bound < 1) { + return 1; + } + const p = popup(); + const v = p[dim](); + const pv = p.parent()[dim](); + if (bound > pv - v - 2) { + return pv - v - 2; } - return xform; + return bound; } function hidePopup() { didDragToNewPositon = false; - $scope.select().style('display', 'none'); + popup().css({ + display: 'none', + }); } - function init() { - $scope.focusPoints.allowClone = false; - group = $scope.select('.popup-group'); - dgElement = angular.element(group.select('g').node()); - group.select('.report-window-close') - .on('click', closePopup); - group.select('.report-window-copy') - .on('click', copyToClipboard); - } - - function movePopup() { - // move in response to arrow keys - but if user dragged the window we assume they don't - // want it to track the focus point + function moveToFocusPoint() { if (didDragToNewPositon) { - refreshText(); + return; } - else { - // just use the first focus point - var mouseCoords = focusPointService.dataCoordsToMouseCoords( - $scope.$parent.modelName, $scope.focusPoints[0]); - if (mouseCoords) { - var xf = currentXform(); - showPopup({mouseX: mouseCoords.x, mouseY: xf.ty}, true); - } + const m = focusPointService.dataCoordsToMouseCoords( + $scope.modelName, $scope.focusPoints[0]); + if (m) { + const p = popup(); + setPosition(m.x, p.offset().top - p.parent().offset().top); } } - function popupTitleSize() { - return { - width: popupWindowSize().width - 2 * borderWidth, - height: titleBarHeight - }; + function plotScope() { + let p = $scope.$parent; + while (! p.broadcastEvent) { + p = p.$parent; + } + return p; } - function popupWindowSize() { - var bbox = group.select('.text-block').node().getBBox(); - var maxWidth = parseFloat($scope.select().attr('width')) - 2 * popupMargin; - var maxHeight = parseFloat($scope.select().attr('height')) - 2 * popupMargin; - return { - width: Math.min(maxWidth, bbox.width + 2 * textMargin), - height: Math.min(maxHeight, titleBarHeight + bbox.height + 2 * textMargin) - }; + function popup() { + return $($element).find('.sr-popup-report'); } - function refreshText() { - // format data - var maxWidth = selectAttr('width') - 2 * popupMargin - 2 * textMargin; - - // all focus points share the same x value - var xText = plotScope.formatFocusPointData($scope.focusPoints[0]).xText; - var hNode = $scope.select('.hidden-txt-layout'); - var tNode = group.select('#x-text') - .text(xText) - .style('fill', '#000000') - .attr('y', popupTitleSize().height + textMargin) - .attr('dy', '1em'); - var tSize = tNode.node().getBBox(); - var txtY = tSize.y + tSize.height; - $scope.focusPoints.forEach(function(fp, fpIndex) { - var color = fp.config.color; - var fmtText = plotScope.formatFocusPointData(fp); - var fits = plotting.fitSplit(fmtText.yText, hNode, maxWidth); - var yGrp = group.select('#y-text-' + fpIndex); - yGrp.selectAll('text').remove(); - yGrp.selectAll('circle').remove(); - fits.forEach(function(str) { - yGrp.append('circle') - .attr('r', '6') - .style('stroke', color) - .style('fill', color) - .attr('cx', 13) - .attr('cy', txtY + 8); - tNode = yGrp.append('text') - .text(str) - .attr('class', 'focus-text-popup') - .attr('x', 15) - .attr('dx', '0.5em') - .attr('y', txtY) - .attr('dy', '1em'); - txtY += tNode.node().getBBox().height; - }); + function setRelativePosition(left, top) { + const p = popup(); + const po = p.parent().offset(); + setPosition(left - po.left, top - po.top); + } - tNode = group.select('#fwhm-text-' + fpIndex) - .text(fmtText.fwhmText) - .style('fill', color) - .attr('y', txtY) - .attr('dy', '1em'); - if (fmtText.yText) { - txtY += (tNode.node().getBBox().height); - } + function setPosition(left, top) { + popup().css({ + left: adjustBounds(left, 'width') + 'px', + top: adjustBounds(top, 'height') + 'px', + display: 'block', }); - hNode.text(''); - refreshWindow(); } - function refreshWindow() { - var size = popupWindowSize(); - $scope.select('.report-window') - .attr('width', size.width) - .attr('height', size.height); - var tSize = popupTitleSize(); - $scope.select('.report-window-title-bar') - .attr('width', tSize.width) - .attr('height', tSize.height); - $scope.select('.report-window-close') - .attr('x', size.width); - } + $scope.closePopup = () => { + focusPointService.hideFocusPoint(plotScope(), true); + }; - function selectAttr(name) { - return parseFloat($scope.select().attr(name)); - } + $scope.copyToClipboard = () => { + plotScope().copyToClipboard(); + }; - function setInfoVisible(pIndex, isVisible) { - // don't completely hide for now, so it's clear the data exists - var textAlpha = isVisible ? 1.0 : 0.4; - group.select('#x-text-' + pIndex).style('opacity', textAlpha); - group.select('#y-text-' + pIndex).style('opacity', textAlpha); - group.select('#fwhm-text-' + pIndex).style('opacity', textAlpha); - } + $scope.dragDone = ($data, $event) => { + didDragToNewPositon = true; + const o = popup().offset(); + setRelativePosition(o.left, o.top); + }; - function showPopup(geometry, isReposition) { - $scope.select().style('display', 'block'); - refreshText(); - if (didDragToNewPositon && ! isReposition) { - return; + $scope.opacity = (index) => { + if (! $scope.plots[index]._isVisible) { + return 0.4; } - // set position and size - var newX = Math.max(popupMargin, geometry.mouseX); - var newY = Math.max(popupMargin, geometry.mouseY); - var rptWindow = group.select('.report-window'); - var tbw = parseFloat(rptWindow.attr('width')); - var tbh = parseFloat(rptWindow.attr('height')); - - newX = Math.min(selectAttr('width') - tbw - popupMargin, newX); - newY = Math.min(selectAttr('height') - tbh - popupMargin, newY); - group.attr('transform', 'translate(' + newX + ',' + newY + ')'); - group.select('.report-window-title-bar').attr('width', tbw - 2 * borderWidth); - } + return 1.0; + }; - $scope.dragDone = function($data, $event) { - didDragToNewPositon = true; - var xf = currentXform(); - if (moveEventDetected) { - showPopup({mouseX: xf.tx + $event.tx, mouseY: xf.ty + $event.ty}, true); + $scope.pointText = (index, xValue) => { + if ($scope.focusPoints[index]) { + return plotting.formatValue($scope.focusPoints[index].data[xValue ? 'x' : 'y']); } - moveEventDetected = false; }; - init(); - - $scope.$on('sr-plotEvent', function(event, args) { - if (! group.node()) { - // special handler for Internet Explorer which can't resolve group - return; - } + $scope.$on('sr-plotEvent', (event, args) => { if (args.name == 'showFocusPointInfo') { - if (args.geometry) { - showPopup(args.geometry); + if (args.geometry && ! didDragToNewPositon) { + setPosition(args.geometry.mouseX, args.geometry.mouseY); } + $scope.$applyAsync(); } else if (args.name == 'hideFocusPointInfo') { hidePopup(); } else if (args.name == 'moveFocusPointInfo') { - movePopup(); - } - else if (args.name == 'setInfoVisible') { - setInfoVisible(args.index, args.isVisible); + moveToFocusPoint(); + $scope.$applyAsync(); } }); - $scope.$on('sr-plotLinked', function(event) { - plotScope = event.targetScope; - }); - - $scope.$on('$destroy', function() { - group.select('.report-window-close') - .on('click', null); - group.select('.report-window-copy') - .on('click', null); - }); - - // ngDraggable interprets even clicks as starting a drag event - we don't want to do transforms later - // unless we really moved it - $scope.$on('draggable:move', function(event, obj) { - // all popups will hear this event, so confine logic to this one - if (obj.element[0] == dgElement[0]) { - moveEventDetected = true; - } - }); }, }; }); @@ -2678,7 +2524,6 @@ SIREPO.app.directive('plot2d', function(focusPointService, plotting, plot2dServi return { restrict: 'A', scope: { - reportId: '<', modelName: '@', }, templateUrl: '/static/html/plot2d.html' + SIREPO.SOURCE_CACHE_KEY, @@ -2752,7 +2597,6 @@ SIREPO.app.directive('plot3d', function(appState, focusPointService, layoutServi return { restrict: 'A', scope: { - reportId: '<', modelName: '@', }, templateUrl: '/static/html/plot3d.html' + SIREPO.SOURCE_CACHE_KEY, @@ -3182,8 +3026,7 @@ SIREPO.app.directive('plot3d', function(appState, focusPointService, layoutServi var domain = plotting.ensureDomain([zmin, zmax], scaleFunction); axes.bottomY.scale.domain(domain).nice(); axes.rightX.scale.domain([domain[1], domain[0]]).nice(); - plotting.initImage({ min: zmin, max: zmax }, heatmap, cacheCanvas, imageData, $scope.modelName); - $scope.resize(); + plotting.initImage({ min: zmin, max: zmax }, heatmap, cacheCanvas, imageData, $scope.modelName, json.threshold); $scope.resize(); }; @@ -3292,7 +3135,6 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util let canvas, ctx, amrLine, heatmap, mouseClickPoint, mouseMovePoint, pointer, zoom; let globalMin = 0.0; let globalMax = 1.0; - let threshold = null; let cacheCanvas, imageData; let colorbar, hideColorBar; const overlaySelector = 'svg.sr-plot g.sr-overlay-data-group'; @@ -3464,7 +3306,7 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util return selector ? e.select(selector) : e; } - function setColorScale() { + function setColorScale(threshold) { var plotMin = globalMin != null ? globalMin : plotting.min2d(heatmap); var plotMax = globalMax != null ? globalMax : plotting.max2d(heatmap); if (plotMin == plotMax) { @@ -3479,7 +3321,7 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util cacheCanvas, imageData, $scope.modelName, - threshold + threshold, ); colorbar.scale(colorScale); } @@ -3605,7 +3447,6 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util heatmap = plotting.safeHeatmap(appState.clone(json.z_matrix).reverse()); globalMin = json.global_min; globalMax = json.global_max; - threshold = json.threshold; select('.main-title').text(json.title); select('.sub-title').text(json.subtitle); let c = false; @@ -3627,7 +3468,7 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util imageData = ctx.getImageData(0, 0, cacheCanvas.width, cacheCanvas.height); select('.z-axis-label').text(json.z_label); select('.frequency-label').text(json.frequency_title); - setColorScale(); + setColorScale(json.threshold); hideColorBar = json.hideColorBar || false; var amrLines = []; @@ -3642,7 +3483,6 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util } select('.line-amr-grid').datum(amrLines); $scope.resize(); - $scope.resize(); }; $scope.resize = function() { @@ -3668,200 +3508,196 @@ SIREPO.app.directive('heatmap', function(appState, layoutService, plotting, util }; }); + +SIREPO.app.directive('colorCircle', function() { + return { + resize: 'A', + scope: { + color: '
+ `, + controller: function($scope) { + $scope.bgcolor = $scope.dashed + ? `linear-gradient(90deg, ${$scope.color} 38%, transparent 38%, transparent 62%, ${$scope.color} 62%)` + : $scope.color; + }, + }; +}); + +SIREPO.app.directive('plotLegend', function(mathRendering) { + return { + restrict: 'A', + scope: { + plots: '<', + togglePlot: '&', + dynamicYLabel: '<', + }, + template: ` +
+
+
+ +
+ +
+
+
+ `, + controller: function($scope) { + function hasSameUnits(units) { + for (const p of $scope.plots) { + if (p._units !== units) { + return false; + } + } + return true; + } + + $scope.click = (index) => { + $scope.togglePlot({ pIndex: index }); + }; + + $scope.label = (p) => { + if ($scope.dynamicYLabel && hasSameUnits(p._units)) { + return p._label; + } + return p.label; + }; + + $scope.opacity = (p) => { + if (p._isVisible) { + for (const p2 of $scope.plots) { + if (p.label !== p2.label && p2._isVisible) { + return 1.0; + } + } + return 0.4; + } + return 1.0; + }; + }, + }; +}); + SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layoutService, mathRendering, plotting, plot2dService, utilities) { return { restrict: 'A', scope: { - reportId: '<', modelName: '@', }, templateUrl: '/static/html/plot2d.html' + SIREPO.SOURCE_CACHE_KEY, controller: function($scope, $element) { - let childPlots = {}; - let dynamicYLabel = false; - let includeForDomain = []; - let plotVisibility = {}; - let scaleFunction; - let selectedPlotLabels = []; + const yMargin = 23; + let scaleFunction, y2_axis; - // for built-in d3 symbols - the units are *pixels squared* - var symbolSize = 144.0; - var legendSymbolSize = 48.0; - - $scope.reportId = SIREPO.UTILS.randomId(); - $scope.domPadding = { - x: 0, - y: 0 - }; + $scope.dynamicYLabel = false; $scope.focusPoints = []; $scope.focusStrategy = 'closest'; - $scope.latexTitle = ''; - $scope.wantLegend = true; + $scope.plots = null; + $scope.reportId = SIREPO.UTILS.randomId(); function build2dPointsForPlot(plotIndex) { var pts = []; - var xPoints = $scope.axes.y.plots[plotIndex].x_points || $scope.axes.x.points; + var xPoints = $scope.axes.x.points; for (var ptIndex = 0; ptIndex < xPoints.length; ++ptIndex) { pts.push([ xPoints[ptIndex], - $scope.axes.y.plots[plotIndex].points[ptIndex] + $scope.plots[plotIndex].points[ptIndex] ]); } return pts; } - function buildSymbols(d3Selection, size, type) { - var symbols = []; - $scope.axes.y.plots - .map(function (plot) { - return plot.symbol; - }) - .forEach(function (s) { - if (! s) { - return; - } - var symId = s + '-' + type; - if (symbols.indexOf(s) >= 0) { - return; - } - symbols.push(s); - d3Selection.append('symbol') - .attr('id', symId) - .attr('overflow', 'visible') - .append('path') - .attr('d', d3.svg.symbol().size(size).type(s)); - }); - } - function canToggle(pIndex) { - if (includeForDomain.length === 1 && includeForDomain[0] === pIndex) { - return false; - } - - function intSort(a, b) { - return parseInt(a) - parseInt(b); - } - - var dp = appState.clone(includeForDomain); - dp.sort(intSort); - if (childPlots[pIndex]) { - var cp = appState.clone(childPlots[pIndex]); - cp.push(parseInt(pIndex)); - cp.sort(intSort); - if (angular.equals(cp, dp)) { - return false; + for (const [idx, p] of $scope.plots.entries()) { + if (idx != pIndex && p._isVisible) { + return true; } } - return true; - } - - function createLegend() { - const plots = $scope.axes.y.plots; - var legend = $scope.select('.sr-plot-legend'); - legend.selectAll('.sr-plot-legend-item').remove(); - if (plots.length == 1) { - return 0; - } - var itemWidth; - var count = 0; - - buildSymbols(legend, legendSymbolSize, 'legend'); - - plots.forEach(function(plot, i) { - if (! plot.label) { - return; - } - var item = legend.append('g').attr('class', 'sr-plot-legend-item').attr('data-sr-index', i); - item.append('text') - .attr('class', 'focus-text-popup glyphicon plot-visibility') - .attr('x', 8) - .attr('y', 17 + count * 20) - .text(vIconText(true)) - .on('click', function() { - togglePlot(i); - $scope.$applyAsync(); - }); - itemWidth = item.node().getBBox().width; - if (plot.symbol) { - item.append('use') - .attr('xlink:href', '#' + plot.symbol + '-legend') - .attr('x', 24 + itemWidth) - .attr('y', 10 + count * 20) - .attr('fill', plot.color) - .attr('class', 'scatter-point line-color') - .style('stroke', 'black') - .style('stroke-width', 0.5) - .style('fill', plot.color); - } - else { - item.append('circle') - .attr('r', 7) - .attr('cx', 24 + itemWidth) - .attr('cy', 10 + count * 20) - .style('stroke', plot.color) - .style('fill', plot.color); - } - itemWidth = item.node().getBBox().width; - item.append('text') - .attr('class', 'focus-text') - .attr('x', 12 + itemWidth) - .attr('y', 16 + count * 20) - .text(plot.label); - count++; - }); - return count; + return false; } function getPlotLabels() { - return $scope.axes.y.plots.map(plot => plot.label); + return $scope.plots.map(plot => plot.label); } - function includeDomain(pIndex, doInclude) { - var domainIndex = includeForDomain.indexOf(pIndex); - if (! doInclude) { - if (domainIndex >= 0) { - includeForDomain.splice(domainIndex, 1); - } - } - else { - if (domainIndex < 0) { - includeForDomain.push(pIndex); - } - } - if (childPlots[pIndex]) { - childPlots[pIndex].forEach(function (cIndex) { - includeDomain(cIndex, doInclude); - }); - } + function isFixedDomain() { + var m = appState.models[$scope.modelName]; + return m && (m.plotRangeType == 'fixed' || m.plotRangeType == 'fit'); } function isPlotVisible(pIndex) { - return parseFloat(plotPath(pIndex).style('opacity')) == 1; + return $scope.plots[pIndex]._isVisible; } - function modulateRGBA(start, end, steps, reverse) { - if (! start[3]) { - start.push(1.0); + function normalizeInput(json) { + $scope.aspectRatio = plotting.getAspectRatio($scope.modelName, json, 4.0 / 7); + $scope.dynamicYLabel = json.dynamicYLabel || false; + // data may contain 2 plots (y1, y2) or multiple plots (plots) + json.plots = json.plots || [ + { + points: json.points[0], + label: json.y1_title, + color: '#1f77b4', + }, + { + points: json.points[1], + label: json.y2_title, + color: '#ff7f0e', + }, + ]; + if (json.plots[0].x_points) { + $scope.noOverlay = true; + } + if (json.plots.length == 1 && ! json.y_label) { + json.y_label = json.plots[0].label; + } + $scope.axes.x.points = json.x_points + || plotting.linearlySpacedArray(json.x_range[0], json.x_range[1], json.x_range[2] || json.points.length); + if (angular.isArray($scope.axes.x.points[0])) { + throw new Error('expecting a single array for x values: ' + $scope.modelName); } - if (! end[3]) { - end.push(1.0); + //TODO(pjm): onRefresh indicates a beamline overlay, needs improvement + if ($scope.onRefresh && json.x_range[1] > 0) { + // beamline overlay always starts at position 0 + json.x_range[0] = 0; } - var s = reverse ? end : start; - var e = reverse ? start : end; - if (steps <= 1) { - return [e]; + $scope.margin.top = json.title + ? 50 + : $scope.onRefresh + ? 65 + : 20; + let hasY2Axis = false; + json.plots.forEach(function(plot, ip) { + const lu = layoutService.parseLabelAndUnits(plot.label); + plot._units = lu.units; + plot._label = lu.label; + plot._yaxis = appState.applicationState()[$scope.modelName][plot.dim + 'Position'] || 'left'; + if (plot._yaxis == 'right') { + hasY2Axis = true; + plot.dashes = '5 5'; + } + }); + if (hasY2Axis) { + const ydoms = calcYDomains(json.plots); + json.y_range = ydoms[0]; + json.y2_range = ydoms[1]; + $scope.axes.y2 = y2_axis; + $($element).find('.y2.axis').show(); + $($element).find('.y2-axis-label').show(); } - var rgbaSteps = []; - for (var i = 0; i < steps; ++i) { - var c = []; - for (var j = 0; j < 4; ++j) { - var startComp = s[j]; - var endComp = e[j]; - c.push(startComp + i * (endComp - startComp) / (steps - 1)); + else { + delete json.y2_range; + if ($scope.axes.y2) { + delete $scope.axes.y2; + $($element).find('.y2.axis').hide(); + $($element).find('.y2-axis-label').hide(); + $scope.margin.right = yMargin; } - rgbaSteps.push(c); } - return rgbaSteps; } function plotPath(pIndex) { @@ -3869,10 +3705,6 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo return d3.selectAll(selectAll(sel)[0]); } - function rgbaToCSS(rgba) { - return 'rgba(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ',' + rgba[3] + ')'; - } - function selectAll(selector) { var e = d3.select($scope.element); return selector ? e.selectAll(selector) : e; @@ -3880,21 +3712,13 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo function setPlotVisible(pIndex, isVisible) { // disable last toggle - meaningless to show no plots - if (! canToggle(pIndex)) { + if (! isVisible && ! canToggle(pIndex)) { return; } - ([pIndex].concat(childPlots[pIndex] || [])).forEach(function (i) { - plotPath(i).style('opacity', isVisible ? 1.0 : 0.0); - vIcon(i).text(vIconText(isVisible)); - }); - - if ($scope.axes.y.plots && $scope.axes.y.plots[pIndex]) { - includeDomain(pIndex, isVisible); - includeForDomain.forEach(function (ip) { - vIcon(ip).style('fill', canToggle(ip) ? null : '#aaaaaa'); - }); + $scope.plots[pIndex]._isVisible = isVisible; + plotPath(pIndex).style('opacity', isVisible ? 1.0 : 0.0); + if ($scope.plots && $scope.plots[pIndex]) { $scope.recalculateYDomain(); - $scope.resize(); } $scope.broadcastEvent({ name: 'setInfoVisible', @@ -3904,15 +3728,98 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo }); } + function setupPlots(json) { + const viewport = $scope.select('.plot-viewport'); + viewport.selectAll('.line').remove(); + viewport.selectAll('g.param-plot').remove(); + json.plots.forEach(function(plot, ip) { + let strokeWidth = 2.0; + if (plot.style === 'scatter') { + let clusterInfo; + let circleRadius = plot.circleRadius || 2; + if (json.clusters) { + clusterInfo = json.clusters; + $scope.clusterInfo = clusterInfo; + clusterInfo.scale = clusterInfo.count > 10 + ? d3.scale.category20() + : d3.scale.category10(); + circleRadius = plot.circleRadius || 4; + } + viewport.append('g') + .attr('class', 'param-plot') + .attr('data-sr-index', ip) + .selectAll('.scatter-point') + .data(plot.points) + .enter() + .append('circle') + .attr('r', circleRadius) + .style('fill', function (d, j) { + return clusterInfo ? clusterInfo.scale(clusterInfo.group[j]) : plot.color; + }) + .attr('class', 'scatter-point line-color'); + } + else { + const p = viewport.append('path') + .attr('class', 'param-plot line line-color') + .attr('data-sr-index', ip) + .style('stroke', plot.color) + .style('stroke-width', strokeWidth) + .datum(plot.points); + if (plot.dashes) { + p.style('stroke-dasharray', (plot.dashes)); + } + } + // must create extra focus points here since we don't know how many to make + const name = $scope.modelName + '-fp-' + ip; + if (! $scope.focusPoints[ip]) { + $scope.focusPoints[ip] = focusPointService.setupFocusPoint( + $scope.axes.x, + // will be reset below + $scope.axes.y, + false, + name, + ); + } + $scope.focusPoints[ip].config.yAxis = $scope.axes[plot._yaxis === 'left' ? 'y' : 'y2']; + + }); + return json.plots; + } + function togglePlot(pIndex) { setPlotVisible(pIndex, ! isPlotVisible(pIndex)); - updateYLabel(); - plotVisibility[pIndex] = ! plotVisibility[pIndex]; + updateYLabels(); + $scope.resize(); + } + + function updateAxes(json) { + const xdom = [json.x_range[0], json.x_range[1]]; + if (! appState.deepEquals(xdom, $scope.axes.x.domain)) { + $scope.axes.x.domain = xdom; + $scope.axes.x.scale.domain(xdom); + } + + function setDomain(dim, range) { + if (range) { + $scope.axes[dim].domain = plotting.ensureDomain([range[0], range[1]], plotting.scaleFunction($scope.modelName)); + $scope.axes[dim].scale.domain($scope.axes[dim].domain).nice(); + } + } + setDomain('y', json.y_range); + setDomain('y2', json.y2_range); } - function updateYLabel() { + function updateYLabels() { // combine labels from all selected plots, use common units if possible - if (! dynamicYLabel) { + if (! $scope.dynamicYLabel) { + return; + } + updateYLabel($scope.axes.y, 'left'); + updateYLabel($scope.axes.y2, 'right'); + } + + function updateYLabel(yaxis, orientation) { + if (! yaxis) { return; } function addUnits(labels, units) { @@ -3930,18 +3837,14 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo hasCommonUnits = false; } }); - if (hasCommonUnits) { - const plotLabels = getPlotLabels(); - for (let i in $scope.axes.y.plots) { - vIconLabel(i).text(plotLabels[i].replace(/\[.*?\]/, '')); - } - } return hasCommonUnits ? layoutService.formatUnits(units[0], isFixedUnits) : ''; } const maxLabelSize = 45; - const labels = getPlotLabels().filter((l, idx) => isPlotVisible(idx)); + const labels = getPlotLabels().filter((l, idx) => { + return isPlotVisible(idx) && $scope.plots[idx]._yaxis === orientation; + }); if (! labels.length) { return; } @@ -3957,42 +3860,14 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo yLabel += ' ' + layoutService.formatUnits(units[idx], true); } }); - if (yLabel.length > maxLabelSize) { + // strip out any KaTeX formatting before computing label length + if (yLabel.replace(/\$|\{|\s+/g, '').length > maxLabelSize) { yLabel = yUnits; } else if (yUnits) { yLabel += ' ' + yUnits; } - $scope.axes.y.updateLabel(yLabel, $scope.select); - $scope.resize(); - } - - function vIcon(pIndex) { - return $scope.select('.sr-plot-legend .sr-plot-legend-item[data-sr-index=\'' + pIndex + '\'] .plot-visibility'); - } - - function vIconLabel(pIndex) { - return $scope.select('.sr-plot-legend .sr-plot-legend-item[data-sr-index=\'' + pIndex + '\'] .focus-text'); - } - - function vIconText(isVisible) { - // e067 == checked box, e157 == empty box - return isVisible ? '\ue067' : '\ue157'; - } - - // get the broadest domain from the visible plots - function visibleDomain() { - var ydomMin = utilities.arrayMin( - includeForDomain.map(function(index) { - return utilities.arrayMin($scope.axes.y.plots[index].points); - }) - ); - var ydomMax = utilities.arrayMax( - includeForDomain.map(function(index) { - return utilities.arrayMax($scope.axes.y.plots[index].points); - }) - ); - return plotting.ensureDomain([ydomMin, ydomMax], scaleFunction); + yaxis.updateLabel(yLabel, $scope.select); } $scope.formatFocusPointData = function(fp) { @@ -4014,17 +3889,21 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo $scope.init = function() { plot2dService.init2dPlot($scope, { - margin: {top: 50, right: 23, bottom: 50, left: 75} + margin: {top: 50, right: yMargin, bottom: 20, left: yMargin} }); + y2_axis = $scope.axes.y2; + delete $scope.axes.y2; // override graphLine to work with multiple point sets $scope.plotGraphLine = function(plotIndex) { - var xPoints = (($scope.axes.y.plots || [])[plotIndex] || {}).x_points || $scope.axes.x.points; + const p = ($scope.plots || [])[plotIndex] || {}; + const xPoints = p.x_points || $scope.axes.x.points; + const yaxis = p._yaxis == 'right' ? $scope.axes.y2 : $scope.axes.y; return d3.svg.line() .x(function(d, i) { return $scope.axes.x.scale(xPoints[i]); }) .y(function(d) { - return $scope.axes.y.scale(scaleFunction ? scaleFunction(d) : d); + return yaxis.scale(scaleFunction ? scaleFunction(d) : d); }); }; $scope.graphLine = d3.svg.line() @@ -4041,262 +3920,86 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo //TODO(pjm): plot may be loaded with { state: 'canceled' }? return; } - $scope.firstRefresh = true; - //TODO(pjm): move first part into normalizeInput() - childPlots = {}; - includeForDomain.length = 0; - if (json.aspectRatio) { - // only use aspectRatio from server for parameterPlot for now, not from model like heatplots - $scope.aspectRatio = json.aspectRatio; - } - dynamicYLabel = json.dynamicYLabel || false; - // data may contain 2 plots (y1, y2) or multiple plots (plots) - var plots = json.plots || [ - { - points: json.points[0], - label: json.y1_title, - color: '#1f77b4', - }, - { - points: json.points[1], - label: json.y2_title, - color: '#ff7f0e', - }, - ]; - if (plots[0].x_points) { - $scope.noOverlay = true; - } - if (plots.length == 1 && ! json.y_label) { - json.y_label = plots[0].label; - } - $scope.axes.x.points = json.x_points - || plotting.linearlySpacedArray(json.x_range[0], json.x_range[1], json.x_range[2] || json.points.length); - if (angular.isArray($scope.axes.x.points[0])) { - throw new Error('expecting a single array for x values: ' + $scope.modelName); - } - var xdom = [json.x_range[0], json.x_range[1]]; - //TODO(pjm): onRefresh indicates a beamline overlay, needs improvement - if ($scope.onRefresh && xdom[1] > 0) { - // beamline overlay always starts at position 0 - xdom[0] = 0; - } - - if (! appState.deepEquals(xdom, $scope.axes.x.domain)) { - $scope.axes.x.domain = xdom; - $scope.axes.x.scale.domain(xdom); - } - scaleFunction = plotting.scaleFunction($scope.modelName); - $scope.axes.y.domain = plotting.ensureDomain([json.y_range[0], json.y_range[1]], scaleFunction); - $scope.axes.y.scale.domain($scope.axes.y.domain).nice(); - - var viewport = $scope.select('.plot-viewport'); - viewport.selectAll('.line').remove(); - viewport.selectAll('g.param-plot').remove(); - - $scope.hasSymbols = false; - - $scope.axes.y.plots = plots; - const legendCount = createLegend(); - - buildSymbols(viewport, symbolSize, 'data'); - - plots.forEach(function(plot, ip) { - var color = plotting.colorsFromHexString(plot.color, 1.0); - - // specifically meant for historical data - each data point's color gets - // modulated by the amount specified - var endColor = plot.colorModulation || color; - var reverseMod = (plot.modDirection || 0) < 0; - var strokeWidth = plot._parent ? 0.75 : 2.0; - var sym; - if (plot.symbol) { - $scope.hasSymbols = true; + normalizeInput(json); + updateAxes(json); + const oldPlots = $scope.plots; + $scope.plots = setupPlots(json); + $scope.updatePlot(json); + $scope.plots.forEach((plot, ip) => setPlotVisible(ip, true)); + $scope.plots.forEach(function(plot, ip) { + if (oldPlots && oldPlots[ip] && oldPlots[ip].label === plot.label && ! oldPlots[ip]._isVisible) { + setPlotVisible(ip, false); } - if (plot.style === 'scatter') { - var clusterInfo; - var circleRadius = 2; - if (json.clusters) { - clusterInfo = json.clusters; - $scope.clusterInfo = clusterInfo; - clusterInfo.scale = clusterInfo.count > 10 - ? d3.scale.category20() - : d3.scale.category10(); - circleRadius = 4; - } - if (plot.symbol) { - var pointColorMod = modulateRGBA(color, endColor, plot.points.length, reverseMod); - sym = d3.svg.symbol().size(symbolSize).type(plot.symbol); - viewport.append('g') - .attr('class', 'param-plot') - .attr('data-sr-index', ip) - .selectAll('.scatter-point') - .data(plot.points) - .enter() - .append('use') - .attr('xlink:href', '#' + plot.symbol + '-data') - .attr('class', 'scatter-point line-color') - .style('fill', function (d, j) { - return rgbaToCSS(pointColorMod[j]); - }) - .style('opacity', (d, j) => { - if (d === null) { - return 0; - } - return 100; - }) - .style('stroke', 'black') - .style('stroke-width', 0.5); + }); + updateYLabels(); + if (! $scope.noOverlay) { + for (var fpIndex = 0; fpIndex < $scope.focusPoints.length; ++fpIndex) { + if (fpIndex < $scope.plots.length) { + $scope.focusPoints[fpIndex].config.color = $scope.plots[fpIndex].color; + focusPointService.loadFocusPoint($scope.focusPoints[fpIndex], build2dPointsForPlot(fpIndex), false, $scope); } else { - viewport.append('g') - .attr('class', 'param-plot') - .attr('data-sr-index', ip) - .selectAll('.scatter-point') - .data(plot.points) - .enter() - .append('circle') - .attr('r', circleRadius) - .style('fill', function (d, j) { - return clusterInfo ? clusterInfo.scale(clusterInfo.group[j]) : plot.color; - }) - .attr('class', 'scatter-point line-color'); - } - } - else { - var plotColorMod = modulateRGBA(color, endColor, plots.length, reverseMod); - var p = viewport.append('path') - .attr('class', 'param-plot line line-color') - .attr('data-sr-index', ip) - .style('stroke', rgbaToCSS(plotColorMod[ip])) - .style('stroke-width', strokeWidth) - .datum(plot.points); - if (plot.dashes) { - p.style('stroke-dasharray', (plot.dashes)); - } - if (plot.symbol) { - viewport.append('g') - .attr('data-sr-index', ip) - .attr('data-color', rgbaToCSS(plotColorMod[ip])) - .attr('class', 'param-plot').selectAll('.data-point') - .data(plot.points) - .enter() - .append('use') - .attr('xlink:href', '#' + plot.symbol + '-data') - .attr('class', 'data-point line-color') - .style('fill', rgbaToCSS(plotColorMod[ip])) - .style('stroke', 'black') - .style('stroke-width', 0.5); + focusPointService.loadFocusPoint($scope.focusPoints[fpIndex], [], false, $scope); } } - if (plot._parent) { - var parent = plots.filter(function (p, j) { - return j !== ip && p.label === plot._parent; - })[0]; - if (parent) { - var pIndex = plots.indexOf(parent); - var cp = childPlots[pIndex] || []; - cp.push(ip); - childPlots[pIndex] = cp; - } - } - // must create extra focus points here since we don't know how many to make - var name = $scope.modelName + '-fp-' + ip; - if (! $scope.focusPoints[ip]) { - $scope.focusPoints[ip] = focusPointService.setupFocusPoint($scope.axes.x, $scope.axes.y, false, name); - } - }); - - for (var fpIndex = 0; fpIndex < $scope.focusPoints.length; ++fpIndex) { - if (fpIndex < plots.length) { - $scope.focusPoints[fpIndex].config.color = plots[fpIndex].color; - focusPointService.loadFocusPoint($scope.focusPoints[fpIndex], build2dPointsForPlot(fpIndex), false, $scope); - } - else { - focusPointService.loadFocusPoint($scope.focusPoints[fpIndex], [], false, $scope); - } - } - - $($element).find('.latex-title').eq(0).html(mathRendering.mathAsHTML(json.latex_label, {displayMode: true})); - - //TODO(pjm): onRefresh indicates an embedded header, needs improvement - $scope.margin.top = json.title - ? 50 - : $scope.onRefresh - ? 65 - : 20; - $scope.margin.bottom = 50 + 20 * legendCount; - $scope.updatePlot(json); - - if (! appState.deepEquals(getPlotLabels(), selectedPlotLabels)) { - plotVisibility = {}; - selectedPlotLabels = getPlotLabels(); } - // initially set all states visible - plots.forEach(function(plot, ip) { - includeDomain(ip, true); - setPlotVisible(ip, true); - }); - // hide previously hidden plots - plots.forEach(function(plot, ip) { - if (! plotVisibility.hasOwnProperty(ip)) { - plotVisibility[ip] = true; - } - if (! plotVisibility[ip]) { - setPlotVisible(ip, false); - } - }); - updateYLabel(); + $scope.resize(); }; - $scope.recalculateYDomain = function() { - var ydom; - var xdom = $scope.axes.x.scale.domain(); - var xPoints = $scope.axes.x.points; - var plots = $scope.axes.y.plots; - for (var i = 0; i < xPoints.length; i++) { - var x = xPoints[i]; - if (x > xdom[1] || x < xdom[0]) { + function calcYDomains(plots, xdom) { + // calculate left and right y axis domains for plots (assumed all visible) + const ydom = [null, null]; + const xPoints = $scope.axes.x.points; + + for (let i = 0; i < xPoints.length; i++) { + const x = xPoints[i]; + if (xdom && (x > xdom[1] || x < xdom[0])) { continue; } - for (var d in includeForDomain) { - var j = includeForDomain[d]; - var y = plots[j].points[i]; - if (ydom) { - if (y < ydom[0]) { - ydom[0] = y; + for (const p of plots) { + const y = p.points[i]; + const ia = p._yaxis == 'left' ? 0 : 1; + if (ydom[ia]) { + if (y < ydom[ia][0]) { + ydom[ia][0] = y; } - else if (y > ydom[1]) { - ydom[1] = y; + else if (y > ydom[ia][1]) { + ydom[ia][1] = y; } } else { - ydom = [y, y]; + ydom[ia] = [y, y]; } } } - if (ydom) { - plotting.scaleYDomain($scope.axes.y.scale, ydom, scaleFunction, ydom[0] > 0 && $scope.axes.y.domain[0] == 0); - } - }; - - $scope.refresh = function() { - // need to wait for the screen dimensions to be set, then calculate the padding once - if ($scope.firstRefresh) { - $scope.firstRefresh = false; - if ($scope.hasSymbols) { - for (var dim in $scope.domPadding) { - $scope.domPadding[dim] = Math.abs($scope.axes[dim].scale.invert(Math.sqrt(symbolSize)) - - $scope.axes[dim].scale.invert(0)); + ['left', 'right'].forEach((v, i) => { + if (ydom[i]) { + const limit = appState.applicationState()[$scope.modelName][`${v}Limit`]; + if (limit && ydom[i][1] > limit) { + ydom[i][1] = limit; } } - const xdom = $scope.axes.x.domain; - $scope.setYDomain(); - $scope.padXDomain(); - if (! appState.deepEquals(xdom, $scope.axes.x.domain)) { - $scope.axes.x.scale.domain($scope.axes.x.domain); - } + }); + + return ydom; + } + + $scope.recalculateYDomain = function() { + if (isFixedDomain()) { + //TODO(pjm): I don't think a fixed domain works in conjunction with scaleFunction + $scope.axes.y.scale.domain($scope.axes.y.domain).nice(); + return; } + const ydoms = calcYDomains($scope.plots.filter(p => p._isVisible), $scope.axes.x.scale.domain()); + if (ydoms[0]) { + plotting.scaleYDomain($scope.axes.y.scale, ydoms[0], scaleFunction, ydoms[0][0] > 0 && $scope.axes.y.domain[0] == 0); + } + if (ydoms[1]) { + plotting.scaleYDomain($scope.axes.y2.scale, ydoms[1], scaleFunction, ydoms[1][0] > 0 && $scope.axes.y2.domain[0] == 0); + } + }; + $scope.refresh = function() { $scope.select('.plot-viewport').selectAll('.line') .each(function (d) { var ip = parseInt(d3.select(this).attr('data-sr-index')); @@ -4311,14 +4014,8 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo if (! pt) { return; } - if ($scope.axes.y.plots[ip].symbol) { - pt.attr('x', $scope.plotGraphLine(ip).x()) - .attr('y', $scope.plotGraphLine(ip).y()); - } - else { - pt.attr('cx', $scope.plotGraphLine(ip).x()) - .attr('cy', $scope.plotGraphLine(ip).y()); - } + pt.attr('cx', $scope.plotGraphLine(ip).x()) + .attr('cy', $scope.plotGraphLine(ip).y()); }); }); @@ -4330,15 +4027,19 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo } }; + $scope.togglePlot = (pIndex) => { + togglePlot(pIndex); + }; + $scope.$on(SIREPO.PLOTTING_CSV_EVENT, ()=> { const points = [ $scope.axes.x.points, ]; - $scope.axes.y.plots.forEach((plot)=> { + $scope.plots.forEach((plot)=> { points.push(plot.points); }); let res = $scope.axes.x.label; - for (const p of $scope.axes.y.plots) { + for (const p of $scope.plots) { res += ',' + p.label; } res += '\n'; @@ -4358,18 +4059,7 @@ SIREPO.app.directive('parameterPlot', function(appState, focusPointService, layo // user interaction $scope.padXDomain = function() { var xdom = $scope.axes.x.domain; - $scope.axes.x.domain = [xdom[0] - $scope.domPadding.x, xdom[1] + $scope.domPadding.x]; - }; - - $scope.setYDomain = function() { - var model = appState.models[$scope.modelName]; - if (model && (model.plotRangeType == 'fixed' || model.plotRangeType == 'fit')) { - $scope.axes.y.scale.domain($scope.axes.y.domain).nice(); - } - else { - var vd = visibleDomain(); - $scope.axes.y.scale.domain([vd[0] - $scope.domPadding.y, vd[1] + $scope.domPadding.y]).nice(); - } + $scope.axes.x.domain = [xdom[0], xdom[1]]; }; }, link: function link(scope, element) { diff --git a/sirepo/package_data/static/json/impactt-schema.json b/sirepo/package_data/static/json/impactt-schema.json index 086b2fee42..44e1cae457 100644 --- a/sirepo/package_data/static/json/impactt-schema.json +++ b/sirepo/package_data/static/json/impactt-schema.json @@ -55,7 +55,7 @@ "histogramBins", "reportIndex" ], - "statAnimation": ["x", "y1", "y2", "y3"] + "statAnimation": ["x", "y1", "y2", "y3", "y4", "y5"] }, "enum": { "BeamParticle": [ @@ -69,14 +69,15 @@ ["3", "no output"] ], "DistType": [ - ["1", "Uniform"], + ["27", "CylcoldZSob"], + ["16", "From File"], ["2", "Gauss3"], - ["3", "Waterbag"], - ["4", "Semigauss"], ["5", "KV3d"], ["10", "ParobGauss"], ["15", "SemicircGauss"], - ["27", "CylcoldZSob"] + ["4", "Semigauss"], + ["1", "Uniform"], + ["3", "Waterbag"] ], "PhaseSpaceCoordinate": [ ["x", "x"], @@ -181,7 +182,7 @@ "Np": ["Macroparticle count", "Integer", 1000, "Np: Number of macroparticles to track"], "Bcurr": ["Current [A]", "RPNValue", 1.0], "Bkenergy": ["Initial energy [eV]", "RPNValue", 1.0, "Bkenergy: Initial beam pseudo-kinetic energy in eV"], - "Bmas": ["Mass [eV]", "RPNValue", 510998.95], + "Bmass": ["Mass [eV]", "RPNValue", 510998.95], "Bcharge": ["Charge", "RPNValue", -1.0, "Bcharge: Particle charge in units of proton charge"], "Bfreq": ["Reference frequency [Hz]", "RPNValue", 2856000000.0], "Tini": ["Initial reference time [sec]", "RPNValue", 0.0], @@ -209,13 +210,14 @@ "zscale": ["Z scale", "RPNValue", 1.0], "pzscale": ["Z pscale", "RPNValue", 1.0], "zmu1": ["Z mu1 [m]", "RPNValue", 0.0], - "zmu2": ["Z mu2", "RPNValue", 19.569511835591836] + "zmu2": ["Z mu2", "RPNValue", 19.569511835591836], + "filename": ["Filename", "InputFile", "", "Particle datafiles with the format:
nptot
nx, px, y, py, z, pz"] }, "elementAnimation": { "x": ["X Value", "PhaseSpaceCoordinate", "x"], "y": ["Y Value", "PhaseSpaceCoordinate", "px"], "plotType": ["Plot Type", "PlotType", "heatmap"], - "histogramBins": ["Histogram Bins", "Integer", 200], + "histogramBins": ["Histogram Bins", "Integer", 60], "colorMap": ["Color Map", "ColorMap", "viridis"], "aspectRatio": ["Aspect Ratio", "AspectRatio", "1"], "notes": ["Notes", "Text", ""], @@ -252,7 +254,17 @@ "y1": ["Vertical Value to Plot", "Stat", "sigma_x"], "y2": ["Vertical Value to Plot", "Stat", "sigma_y"], "y3": ["Vertical Value to Plot", "Stat", "sigma_z"], + "y4": ["Vertical Value to Plot", "Stat", "none"], + "y5": ["Vertical Value to Plot", "Stat", "none"], + "y1Position": ["Vertical Postion", "AxisPosition", "left"], + "y2Position": ["Vertical Position", "AxisPosition", "left"], + "y3Position": ["Vertical Position", "AxisPosition", "left"], + "y4Position": ["Vertical Position", "AxisPosition", "right"], + "y5Position": ["Vertical Position", "AxisPosition", "right"], + "leftLimit": ["Left Axis Limit", "Float", 0], + "rightLimit": ["Right Axis Limit", "Float", 0], "includeLattice": ["Show Lattice Overlay", "Boolean", "0"], + "aspectRatio": ["Aspect Ratio", "AspectRatio", "0.5625"], "notes": ["Notes", "Text", ""] }, "_ELEMENT": { @@ -385,6 +397,8 @@ "title": "Beam", "basic": [ "particle", + "Bmass", + "Bcharge", "Np", "Bcurr", "Bkenergy", @@ -397,6 +411,7 @@ "title": "Particle Distribution", "basic": [ "Flagdist", + "filename", [ ["X", [ "sigx", @@ -484,18 +499,40 @@ "statAnimation": { "title": "Beam Variables", "advanced": [ - [ - ["Horizontal", [ - "x" - ]], - ["Vertical", [ - "y1", - "y2", - "y3" - ]] - ], - "includeLattice", - "notes" + ["Main", [ + [ + ["Horizontal", [ + "x" + ]], + ["Vertical", [ + "y1", + "y2", + "y3", + "y4", + "y5" + ]], + ["Position", [ + "y1Position", + "y2Position", + "y3Position", + "y4Position", + "y5Position" + ]] + ], + "includeLattice", + "aspectRatio", + "notes" + ]], + ["Limits", [ + [ + ["Left", [ + "leftLimit" + ]], + ["Right", [ + "rightLimit" + ]] + ] + ]] ] }, "CHANGE_TIMESTEP": { diff --git a/sirepo/package_data/static/json/omega-schema.json b/sirepo/package_data/static/json/omega-schema.json index 19d1cca2f5..b626ff987c 100644 --- a/sirepo/package_data/static/json/omega-schema.json +++ b/sirepo/package_data/static/json/omega-schema.json @@ -5,7 +5,7 @@ "omega.js" ], "css": [ - "omega.css" + "sirepo-dark.css" ] } }, diff --git a/sirepo/package_data/static/json/schema-common.json b/sirepo/package_data/static/json/schema-common.json index c46e126bc9..bb669aa186 100644 --- a/sirepo/package_data/static/json/schema-common.json +++ b/sirepo/package_data/static/json/schema-common.json @@ -277,7 +277,13 @@ ["1", "1 x 1"], ["0.75", "4 x 3"], ["0.5625", "16 x 9"], - ["0.5", "2 x 1"] + ["0.5", "2 x 1"], + ["0.333333", "3 x 1"], + ["0.25", "4 x 1"] + ], + "AxisPosition": [ + ["left", "Left"], + ["right", "Right"] ], "Boolean": [ ["0", "No"], diff --git a/sirepo/package_data/template/impactt/parameters.py.jinja b/sirepo/package_data/template/impactt/parameters.py.jinja index 2bf6ae2583..80f9fc1acc 100644 --- a/sirepo/package_data/template/impactt/parameters.py.jinja +++ b/sirepo/package_data/template/impactt/parameters.py.jinja @@ -36,6 +36,10 @@ I.lattice.extend([ {% if numProcs > 1 %} I.numprocs = {{ numProcs }} {% endif %} + +{% if distributionFilename %} +shutil.copy("{{distributionFilename}}", "partcl.data") +{% endif %} I.run() for k in I.output["particles"]: diff --git a/sirepo/package_data/template/opal/default-data.json b/sirepo/package_data/template/opal/default-data.json index c602a13500..6963c57b30 100644 --- a/sirepo/package_data/template/opal/default-data.json +++ b/sirepo/package_data/template/opal/default-data.json @@ -250,7 +250,6 @@ ], "elements": [], "plotAnimation": { - "aspectRatio": "1", "notes": "", "x": "SPOS", "y1": "#varepsilon x", diff --git a/sirepo/sim_data/activait.py b/sirepo/sim_data/activait.py index 434abf10e7..a8d8af0a74 100644 --- a/sirepo/sim_data/activait.py +++ b/sirepo/sim_data/activait.py @@ -61,13 +61,15 @@ def _compute_model(cls, analysis_model, *args, **kwargs): return super(SimData, cls)._compute_model(analysis_model, *args, **kwargs) @classmethod - def _compute_job_fields(cls, data, r, compute_model): + def _compute_job_fields(cls, data, report, compute_model): res = [ "columnInfo.header", "dataFile.file", "dataFile.inputsScaler", ] - if "fileColumnReport" in r: + if "analysisReport" in report: + res.append(report) + if "fileColumnReport" in report: d = data.models.dataFile if d.appMode == "classification": # no outputsScaler for classification @@ -78,7 +80,7 @@ def _compute_job_fields(cls, data, r, compute_model): # the columns will be unchanged when switching between input/output return res return res + ["columnInfo.inputOutput"] - if "partitionColumnReport" in r: + if "partitionColumnReport" in report: res.append("partition") return res diff --git a/sirepo/sim_data/impactt.py b/sirepo/sim_data/impactt.py index 32adce5b6d..731ed53bf5 100644 --- a/sirepo/sim_data/impactt.py +++ b/sirepo/sim_data/impactt.py @@ -25,14 +25,30 @@ def fixup_old_data(cls, data, qcall, **kwargs): ), dm.setdefault("rpnVariables", []) + @classmethod + def get_distribution_file(cls, data): + if ( + data.models.distribution.Flagdist == "16" + and data.models.distribution.filename + ): + return cls.lib_file_name_with_model_field( + "distribution", "filename", data.models.distribution.filename + ) + return None + @classmethod def _compute_job_fields(cls, data, *args, **kwargs): return [data.report] @classmethod def _lib_file_basenames(cls, data): + res = [] + df = cls.get_distribution_file(data) + if df: + res.append(df) return ( - sirepo.template.lattice.LatticeUtil(data, cls.schema()) + res + + sirepo.template.lattice.LatticeUtil(data, cls.schema()) .iterate_models(sirepo.template.lattice.InputFileIterator(cls)) .result ) diff --git a/sirepo/sim_data/opal.py b/sirepo/sim_data/opal.py index 8c8d51cf0f..acae6b3bd1 100644 --- a/sirepo/sim_data/opal.py +++ b/sirepo/sim_data/opal.py @@ -50,6 +50,8 @@ def fixup_old_data(cls, data, qcall, **kwargs): elif i == 4: m.x = "z" m.y = "pz" + if "aspectRatio" in dm.plotAnimation: + del dm.plotAnimation["aspectRatio"] for bl in dm.beamlines: cls.update_model_defaults(bl, "beamline") cls._remove_deprecated_items(dm) diff --git a/sirepo/template/activait.py b/sirepo/template/activait.py index 467954033e..ebfee1a9c9 100644 --- a/sirepo/template/activait.py +++ b/sirepo/template/activait.py @@ -1201,14 +1201,12 @@ def _get_fit_report(report, x_vals, y_vals): PKDict( points=fit_y_min.tolist(), x_points=fit_x.tolist(), - label="confidence", - _parent="confidence", + label="confidence 1", ), PKDict( points=fit_y_max.tolist(), x_points=fit_x.tolist(), - label="", - _parent="confidence", + label="confidence 2", ), ] return param_vals, param_sigmas, plots diff --git a/sirepo/template/controls.py b/sirepo/template/controls.py index 7e8e80842b..58489dd8b7 100644 --- a/sirepo/template/controls.py +++ b/sirepo/template/controls.py @@ -93,20 +93,18 @@ def _y_range(points): x_range=[points.s[0], points.s[-1]], plots=[ PKDict( - style="scatter", field="x", points=points.x, label="x [m]", - symbol="triangle-up", color="#1f77b4", + circleRadius=10, ), PKDict( - style="scatter", field="y", points=points.y, label="y [m]", - symbol="triangle-down", color="#ff7f0e", + circleRadius=10, ), ], y_range=_y_range(points), @@ -117,23 +115,21 @@ def _y_range(points): res.plots.insert( 0, PKDict( - style="scatter", field="log_y", points=points.log_y, label="y [m]", - symbol="diamond", color="#d62728", + circleRadius=10, ), ) res.plots.insert( 0, PKDict( - style="scatter", field="log_x", points=points.log_x, label="x [m]", - symbol="square", color="#2ca02c", + circleRadius=10, ), ) return res diff --git a/sirepo/template/impactt.py b/sirepo/template/impactt.py index 49bdd4e681..323ce74026 100644 --- a/sirepo/template/impactt.py +++ b/sirepo/template/impactt.py @@ -3,6 +3,7 @@ :copyright: Copyright (c) 2024 RadiaSoft LLC. All Rights Reserved. :license: http://www.apache.org/licenses/LICENSE-2.0.html """ +from pmd_beamphysics.labels import mathlabel from pykern import pkio from pykern.pkcollections import PKDict from pykern.pkdebug import pkdc, pkdp @@ -14,6 +15,7 @@ import impact.parsers import numpy import pmd_beamphysics +import re import sirepo.mpi import sirepo.sim_data import sirepo.template.lattice @@ -21,7 +23,6 @@ _SIM_DATA, SIM_TYPE, SCHEMA = sirepo.sim_data.template_globals() -_ARCHIVE_FILE = "impact.h5" _MAX_OUTPUT_ID = 100 _PLOT_TITLE = PKDict( { @@ -43,7 +44,6 @@ "WRITE_SLICE_INFO", ] ) -_STAT_RETRIES = 5 _TIME_AND_ENERGY_FILE_NO = 18 @@ -60,7 +60,7 @@ def background_percent_complete(report, run_dir, is_running): d = impact.parsers.load_many_fort( str(run_dir), types=[_TIME_AND_ENERGY_FILE_NO] ) - if "mean_z" in d and len(d["mean_z"] > 1): + if "mean_z" in d and len(d["mean_z"] > 10): return PKDict( frameCount=len(d["mean_z"]), percentComplete=d["mean_z"][-1] * 100.0 / stop_z, @@ -79,16 +79,24 @@ def background_percent_complete(report, run_dir, is_running): ) -def bunch_plot(model, run_dir, frame_index, filename): - p = pmd_beamphysics.ParticleGroup(str(run_dir.join(filename))) +def bunch_plot(model, frame_index, particle_group): + def _label(name): + if name == "delta_z": + return "z -〈z〉" + if name == "energy": + return "E" + return name + return template_common.heatmap( - values=[p[model.x], p[model.y]], + values=[particle_group[model.x], particle_group[model.y]], model=model, plot_fields=PKDict( - x_label=f"{model.x} [{p.units(model.x)}]", - y_label=f"{model.y} [{p.units(model.y)}]", + x_label=f"{_label(model.x)} [{particle_group.units(model.x)}]", + y_label=f"{_label(model.y)} [{particle_group.units(model.y)}]", title=_PLOT_TITLE.get(f"{model.x}-{model.y}", f"{model.x} - {model.y}"), + threshold=[1e-20, 1e20], ), + # weights=particle_group.weight, ) @@ -105,31 +113,19 @@ def sim_frame(frame_args): # elementAnimations return bunch_plot( frame_args, - frame_args.run_dir, frame_args.frameIndex, - _file_name_for_element_animation(frame_args), + pmd_beamphysics.ParticleGroup( + str(frame_args.run_dir.join(_file_name_for_element_animation(frame_args))) + ), ) -def sim_frame_statAnimation(frame_args): - I = impact.Impact( - use_temp_dir=False, - workdir=str(frame_args.run_dir), - ) - for _ in range(_STAT_RETRIES): - try: - I.load_input(I._workdir + "/ImpactT.in") - I.load_output() - except ValueError as err: - time.sleep(1) - if "stats" not in I.output: - I.load_input(I._workdir + "/ImpactT.in") - I.load_output() +def stat_animation(I, frame_args): stats = I.output["stats"] plots = PKDict() if frame_args.x == "none": frame_args.x = "mean_z" - for f in ("x", "y1", "y2", "y3"): + for f in ("x", "y1", "y2", "y3", "y4", "y5"): if frame_args[f] == "none": continue units = I.units(frame_args[f]) @@ -163,7 +159,7 @@ def sim_frame_statAnimation(frame_args): else: p = stats[frame_args[f]] plots[f] = PKDict( - label=f"{frame_args[f]}{units}", + label=f"{_plot_label(frame_args[f])}{units}", dim=f, points=p.tolist(), ) @@ -180,6 +176,18 @@ def sim_frame_statAnimation(frame_args): ) +def sim_frame_statAnimation(frame_args): + # TODO(pjm): monkey patch to avoid shape errors when loading during updates + impact.parsers.load_many_fort = _patched_load_many_fort + I = impact.Impact( + use_temp_dir=False, + workdir=str(frame_args.run_dir), + ) + I.load_input(I._workdir + "/ImpactT.in") + I.load_output() + return stat_animation(I, frame_args) + + def write_parameters(data, run_dir, is_parallel): pkio.write_text( run_dir.join(template_common.PARAMETERS_PYTHON_FILE), @@ -224,10 +232,11 @@ def _format_field(code_var, name, field, field_type, value): # TODO(pjm): handle wakefield filename return "" value = f'prep_input_file("{_SIM_DATA.lib_file_name_with_model_field(name, field, value)}")' - elif field_type in ("Integer", "Float"): - pass elif field_type == "RPNValue": - value = code_var.eval_var_with_assert(value) + # TODO(pjm): eval RPNValue + value = float(value) + elif field_type == "Integer": + pass else: value = f'"{value}"' return f"{field}={value},\n" @@ -237,16 +246,21 @@ def _generate_header(data): dm = data.models res = PKDict() for m in ("beam", "distribution", "simulationSettings"): + s = SCHEMA.model[m] for k in dm[m]: - res[k] = dm[m][k] - if dm.beam.particle == "electron": - pass - elif dm.beam.particle == "proton": - pass + if s[k][1] == "RPNValue": + res[k] = float(dm[m][k]) + else: + res[k] = dm[m][k] + if dm.beam.particle in ("electron", "proton"): + res["Bmass"], res["Bcharge"] = SCHEMA.constants.particleMassAndCharge[ + dm.beam.particle + ] else: if dm.beam.particle != "other": raise AssertionError(f"Invalid particle type: {dm.beam.particle}") del res["particle"] + del res["filename"] return res @@ -286,8 +300,8 @@ def _generate_parameters_file(data): res, v = template_common.generate_parameters_file(data) util = sirepo.template.lattice.LatticeUtil(data, SCHEMA) v.lattice = _generate_lattice(util, util.select_beamline().id, []) - v.archiveFile = _ARCHIVE_FILE v.numProcs = sirepo.mpi.cfg().cores + v.distributionFilename = _SIM_DATA.get_distribution_file(data) v.impactHeader = _generate_header(data) return res + template_common.render_jinja( SIM_TYPE, @@ -306,27 +320,101 @@ def _next_output_id(output_ids): return i -def _output_info(data, run_dir): +def output_info(data): res = [] for idx, n in enumerate(_output_names(data)): - fn = f"{n}.h5" - if run_dir.join(fn).exists(): - res.append( - PKDict( - modelKey=f"elementAnimation{idx}", - reportIndex=idx, - report="elementAnimation", - name=n, - filename=fn, - frameCount=1, - ) + res.append( + PKDict( + modelKey=f"elementAnimation{idx}", + reportIndex=idx, + report="elementAnimation", + name=n, + frameCount=1, ) + ) return res -def _output_names(data): - res = ["initial_particles", "final_particles"] - for el in data.models.elements: - if el.type == "WRITE_BEAM": - res.append(el.name) +def _output_info(data, run_dir): + res = [] + for r in output_info(data): + fn = f"{r.name}.h5" + if run_dir.join(fn).exists(): + r.filename = fn + res.append(r) return res + + +# This method is copied, modified and monkey patched from the impact.parsers module. +# The method can be called while files are still being written, so the size is adjusted +# if later files have a longer length. +# +def _patched_load_many_fort(path, types=impact.parsers.FORT_STAT_TYPES, verbose=False): + """ + Loads a large dict with data from many fort files. + Checks that keys do not conflict. + + Default types are for typical statistical information along the simulation path. + + """ + fortfiles = impact.parsers.fort_files(path) + alldat = {} + size = None + for f in fortfiles: + file_type = impact.parsers.fort_type(f, verbose=False) + if file_type not in types: + continue + + dat = impact.parsers.load_fort(f, type=file_type, verbose=verbose) + for k in dat: + if isinstance(dat[k], dict): + alldat[k] = dat[k] + continue + if size is None: + size = len(dat[k]) + elif len(dat[k]) > size: + dat[k] = dat[k][:size] + if k not in alldat: + alldat[k] = dat[k] + + elif numpy.allclose(alldat[k], dat[k], atol=1e-20): + # If the difference between alldat-dat < 1e-20, + # move on to next key without error. + # https://numpy.org/devdocs/reference/generated/numpy.isclose.html#numpy.isclose + pass + + else: + # Data is not close enough to ignore differences. + # Check that this data is the same as what's already in there + assert numpy.all(alldat[k] == dat[k]), "Conflicting data for key:" + k + + return alldat + + +def _plot_label(field): + l = mathlabel(field) + if re.search(r"mathrm|None", l): + return field + return l + + +def _watches_in_beamline_order(data, beamline_id, result): + util = sirepo.template.lattice.LatticeUtil(data, SCHEMA) + beamline = util.id_map[abs(beamline_id)] + for item_id in beamline["items"]: + item = util.id_map[abs(item_id)] + if "type" in item: + if item.type == "WRITE_BEAM": + if item.name not in result: + result.append(item.name) + else: + _watches_in_beamline_order(data, item_id, result) + return result + + +def _output_names(data): + return _watches_in_beamline_order( + data, + data.models.simulation.visualizationBeamlineId, + ["initial_particles", "final_particles"], + ) diff --git a/sirepo/template/impactt_parser.py b/sirepo/template/impactt_parser.py new file mode 100644 index 0000000000..879e941175 --- /dev/null +++ b/sirepo/template/impactt_parser.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +"""ImpactT parser. + +:copyright: Copyright (c) 2024 RadiaSoft LLC. All Rights Reserved. +:license: http://www.apache.org/licenses/LICENSE-2.0.html +""" +from impact.parsers import parse_header, ix_lattice, parse_lattice +from pykern.pkcollections import PKDict +from pykern.pkdebug import pkdc, pkdlog, pkdp +import re +import sirepo.sim_data + + +class ImpactTParser(object): + + _IGNORE_FIELDS = set( + ["Npcol", "Nprow", "Nbunch", "Flagmap", "Flagbc", "Rstartflg", "Flagsbstp"] + ) + _IGNORE_MODELS = set(["COMMENT"]) + _IGNORE_MODEL_FIELDS = set(["description", "original", "type", "s", "zedge"]) + + def parse_file(self, lattice_text): + from sirepo import simulation_db + + self.sim_data = sirepo.sim_data.get_class("impactt") + self.schema = self.sim_data.schema() + self.data = simulation_db.default_data(self.sim_data.sim_type()) + self.next_id = 0 + return self._import_impactt(self._parse_impactt(lattice_text)) + + def _elements_and_positions(self, lattice): + elements = [] + positions = [] + + for el in lattice: + n = el["type"].upper() + if n in self._IGNORE_MODELS: + continue + if n not in self.schema.model: + pkdlog("unhandled model: {}", el["type"]) + continue + m = self.schema.model[n] + element = self.sim_data.model_defaults(n).pkupdate( + type=n, + ) + positions.append(el["zedge"] if "zedge" in el else el["s"]) + for k, v in el.items(): + f = "l" if k == "L" else k + if f in m: + if self._is_enum(n, f): + element[f] = str(v) + else: + element[f] = v + elif k in self._IGNORE_MODEL_FIELDS: + continue + else: + pkdlog("unhandled model field {}: {} = {}", n, k, v) + elements.append(element) + return elements, positions + + def _import_elements(self, lattice): + elements, positions = self._elements_and_positions(lattice) + for e in sorted(zip(positions, elements)): + self.data.models.beamlines[0].positions.append( + PKDict( + elemedge=e[0], + ) + ) + e[1]._id = self._next_id() + self.data.models.elements.append(e[1]) + self.data.models.beamlines[0]["items"].append(e[1]._id) + self.data.models.elements = sorted( + self.data.models.elements, key=lambda e: (e.type, e.name.lower()) + ) + + def _import_header(self, header): + for k, v in header.items(): + if k in self._IGNORE_FIELDS: + continue + matched = False + for m in ("beam", "distribution", "simulationSettings"): + f = re.sub(r"\(.*\)", "", k) + if f in self.data.models[m]: + if self._is_enum(m, f): + self.data.models[m][f] = str(v) + else: + self.data.models[m][f] = v + matched = True + break + if not matched: + pkdlog("unhandled header value {}: {}", k, v) + + def _import_impactt(self, parsed): + self.data.models.beamlines = [ + self.sim_data.model_defaults("beamline").pkupdate( + id=self._next_id(), + items=[], + positions=[], + name="BL1", + ) + ] + self._import_header(parsed.header) + self._import_elements(parsed.lattice) + return self.data + + def _is_enum(self, model_name, field): + return self.schema.model[model_name][field][1] in self.schema.enum + + def _next_id(self): + self.next_id += 1 + return self.next_id + + def _parse_impactt(self, lattice_text): + lines = lattice_text.split("\n") + return PKDict( + header=parse_header(lines), + lattice=parse_lattice(lines[ix_lattice(lines) :]), + ) diff --git a/sirepo/template/template_common.py b/sirepo/template/template_common.py index c45f440889..613a1142cb 100644 --- a/sirepo/template/template_common.py +++ b/sirepo/template/template_common.py @@ -373,14 +373,6 @@ def compute_plot_color_and_range(plots, plot_colors=None, fixed_y_range=None): y_range[1] = vmax else: y_range = [vmin, vmax] - # color child plots the same as parent - for child in [p for p in plots if "_parent" in p]: - parent = next( - (pr for pr in plots if "label" in pr and pr["label"] == child["_parent"]), - None, - ) - if parent: - child["color"] = parent["color"] if "color" in parent else "#000000" return y_range diff --git a/tests/karma/plotAxis_test.js b/tests/karma/plotAxis_test.js index 695775945f..1828bd2cce 100644 --- a/tests/karma/plotAxis_test.js +++ b/tests/karma/plotAxis_test.js @@ -12,7 +12,7 @@ describe('plotting: plotAxis', function() { } function selectStub() { var res = function() {}; - res.text = function(v) { + res.html = res.text = function(v) { formattedBase = v; }; return res; diff --git a/tests/lib_data/opal_1/out.json b/tests/lib_data/opal_1/out.json index fbfcd729eb..d561c62923 100644 --- a/tests/lib_data/opal_1/out.json +++ b/tests/lib_data/opal_1/out.json @@ -411,7 +411,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS", diff --git a/tests/lib_data/opal_2/out.json b/tests/lib_data/opal_2/out.json index 7e5b4ed9fb..7ef61a1eab 100644 --- a/tests/lib_data/opal_2/out.json +++ b/tests/lib_data/opal_2/out.json @@ -480,7 +480,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS", diff --git a/tests/lib_data/opal_3/out.json b/tests/lib_data/opal_3/out.json index 9b09ecc2cf..a27d1c35e6 100644 --- a/tests/lib_data/opal_3/out.json +++ b/tests/lib_data/opal_3/out.json @@ -560,7 +560,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS", diff --git a/tests/lib_data/opal_4/out.json b/tests/lib_data/opal_4/out.json index 906083932c..57c137dde6 100644 --- a/tests/lib_data/opal_4/out.json +++ b/tests/lib_data/opal_4/out.json @@ -560,7 +560,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS", diff --git a/tests/lib_data/opal_5/out.json b/tests/lib_data/opal_5/out.json index d67239ab92..e6223793d7 100644 --- a/tests/lib_data/opal_5/out.json +++ b/tests/lib_data/opal_5/out.json @@ -651,7 +651,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS", diff --git a/tests/lib_data/opal_6/out.json b/tests/lib_data/opal_6/out.json index 2a2658c4c9..e3083b0311 100644 --- a/tests/lib_data/opal_6/out.json +++ b/tests/lib_data/opal_6/out.json @@ -334,7 +334,6 @@ "y3": "rms s" }, "plotAnimation": { - "aspectRatio": "1", "includeLattice": "0", "notes": "", "x": "SPOS",