From 204d17fe99553320d4802985b204850e41ac8665 Mon Sep 17 00:00:00 2001 From: William Johnson Date: Thu, 4 Jan 2024 16:25:23 -0800 Subject: [PATCH 1/4] Fix exporting setMaxUndoRedoSteps to electron js --- target/electron/InterSpecAddOn.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/target/electron/InterSpecAddOn.cpp b/target/electron/InterSpecAddOn.cpp index bc0b62e9..78989670 100644 --- a/target/electron/InterSpecAddOn.cpp +++ b/target/electron/InterSpecAddOn.cpp @@ -595,6 +595,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) { exports.Set( "setTempDir", Napi::Function::New(env, InterSpecAddOn::setTempDir )); exports.Set( "setRequireSessionToken", Napi::Function::New(env, InterSpecAddOn::setRequireSessionToken )); + exports.Set( "setMaxUndoRedoSteps", Napi::Function::New(env, InterSpecAddOn::setMaxUndoRedoSteps )); exports.Set( "addPrimarySessionToken", Napi::Function::New(env, InterSpecAddOn::addPrimarySessionToken )); exports.Set( "addExternalSessionToken", Napi::Function::New(env, InterSpecAddOn::addExternalSessionToken )); From 40e8f781f567e826c88d869539d4a23973ab3522 Mon Sep 17 00:00:00 2001 From: William Johnson Date: Thu, 4 Jan 2024 21:25:47 -0800 Subject: [PATCH 2/4] Add log-y option for time chart. Option only applies to gammas, and to enable you have to click on the three dots menu in the upper right-hand corner of the time-chart, and then go to the "Opt" tab, and click "Log Y Scale". Only tested with a couple spectra; may need a little more adjusting/fine-tuning. Closes issue #26. --- InterSpec/D3TimeChart.h | 5 +++ InterSpec_resources/D3TimeChart.css | 2 +- InterSpec_resources/D3TimeChart.js | 68 +++++++++++++++++++++++++---- src/D3TimeChart.cpp | 62 ++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 10 deletions(-) diff --git a/InterSpec/D3TimeChart.h b/InterSpec/D3TimeChart.h index 0f25c01b..db05e799 100644 --- a/InterSpec/D3TimeChart.h +++ b/InterSpec/D3TimeChart.h @@ -152,6 +152,10 @@ class D3TimeChart : public Wt::WContainerWidget void setNeutronsHidden( const bool hide ); bool neutronsHidden() const; + void setGammaLogY( const bool logy ); + bool gammaLogY() const; + + void setXAxisRangeSamples( const int min_sample_num, const int max_sample_num ); /** Returns the current user-entered gamma energy range that should be summed to create this gross count chart. */ @@ -253,6 +257,7 @@ class D3TimeChart : public Wt::WContainerWidget bool m_showHorizontalLines; bool m_dontRebin; bool m_hideNeutrons; + bool m_gammaLogY; std::shared_ptr m_spec; std::vector m_detectors_to_display; diff --git a/InterSpec_resources/D3TimeChart.css b/InterSpec_resources/D3TimeChart.css index c051335b..15a18da5 100644 --- a/InterSpec_resources/D3TimeChart.css +++ b/InterSpec_resources/D3TimeChart.css @@ -220,7 +220,7 @@ } -.D3TimeDontRebin, .D3TimeHideNeutrons { +.D3TimeDontRebin, .D3TimeHideNeutrons, .D3TimeGammaLogY { margin-top: 5px; } diff --git a/InterSpec_resources/D3TimeChart.js b/InterSpec_resources/D3TimeChart.js index b7fe8ba1..7cb65d46 100644 --- a/InterSpec_resources/D3TimeChart.js +++ b/InterSpec_resources/D3TimeChart.js @@ -219,6 +219,7 @@ D3TimeChart = function (elem, options) { || (this.options.yAxisGammaNeutronRelMaxSf < 0.04) || (this.options.yAxisGammaNeutronRelMaxSf > 25) ) this.options.yAxisGammaNeutronRelMaxSf = 1; + if (typeof this.options.gammaLogY !== "boolean") this.options.gammaLogY = false; /* The dontRebin option makes it so when there are more time samples than pixels, instead of averaging multiple time samples together, instead the min and max counts from that interval are @@ -1634,10 +1635,17 @@ D3TimeChart.prototype.updateChart = function( scales, compressionIndex, options var yAxisLeft = d3.svg .axis() .scale(yScaleGamma) - .ticks(3) - .orient("left") - .tickFormat(d3.format(".1g")); + .orient("left"); + // Adjust which/how-many labels is shown + if( this.options.gammaLogY ) { + //yScaleGamma.nice(); //rounds down to next power of 10, and up to next power - its not bad; if we use this then we should remove scaling the domain by a factor of two in `this.getScales(...)`, for log display + yAxisLeft.tickFormat( yScaleGamma.tickFormat(1) ); + }else{ + yAxisLeft.ticks(3) + .tickFormat(d3.format(".1g")); + } + // update or create axis this.axisLeftG .attr("transform", "translate(" + this.margin.left + ",0)") @@ -2334,6 +2342,7 @@ D3TimeChart.prototype.getDomainsFromRaw = function (rawData) { return d[1]; }); + var yMinGamma = Number.MAX_SAFE_INTEGER; var yMaxGamma = Number.MIN_SAFE_INTEGER; var yMaxNeutron = Number.MIN_SAFE_INTEGER; @@ -2360,6 +2369,7 @@ D3TimeChart.prototype.getDomainsFromRaw = function (rawData) { var cps = dontRebin ? rawData.gammaCounts[i].maxCps[j] : (rawData.gammaCounts[i].counts[j] / dt); yMaxGamma = Math.max(yMaxGamma, cps ); + yMinGamma = Math.min(yMinGamma, cps ); } } @@ -2378,10 +2388,18 @@ D3TimeChart.prototype.getDomainsFromRaw = function (rawData) { } } + // This function gives a gamma y-range of zero to yMaxGamma, since this seems right for when you + // are viewing the whole time-chart (if you zoom-in, then the y-axis is no longer forced to zero, + // but to the actual minimum of data showing), but if we are using a log-y axis, we actually + // want to know what the data minimum value is - so we'll do kinda a hack and add an extra + // variable to the returned domains, `yGammaMin`, that we can use for this; this is the only + // place this variable is assigned. + return { x: [xMin, xMax], yGamma: [0, yMaxGamma], yNeutron: [0, yMaxNeutron], + yGammaMin: yMinGamma }; }; @@ -2453,12 +2471,34 @@ D3TimeChart.prototype.getScales = function (domains) { .domain(domains.x) .range([this.margin.left, this.state.width - this.margin.right]) : undefined; - var yScaleGamma = domains.yGamma - ? d3.scale - .linear() - .domain([domains.yGamma[0],yGammaMult*domains.yGamma[1]]) - .range([this.state.height - this.margin.bottom, this.margin.top]) - : undefined; + + var yScaleGamma = undefined; + if( domains.yGamma ) + { + if( this.options.gammaLogY ) + { + // If we are fully zoomed-out, the `this.getDomainsFromRaw()` function puts the gamma + // y-minimum to zero (which we dont want), but it also defines another variable, + // `yGammaMin`, that `this.getYDomainsInRange()` doesnt define, so we'l use it if available. + let lowery = domains.yGamma[0]; + let uppery = domains.yGamma[1]; + if (typeof domains.yGammaMin !== 'undefined') { + lowery = domains.yGammaMin; + } + + yScaleGamma = d3.scale + .log() + .domain([Math.max(0.5, 0.5*lowery),2*yGammaMult*uppery]) + .range([this.state.height - this.margin.bottom, this.margin.top]); + }else + { + yScaleGamma = d3.scale + .linear() + .domain([domains.yGamma[0],yGammaMult*domains.yGamma[1]]) + .range([this.state.height - this.margin.bottom, this.margin.top]); + } + }//if( domains.yGamma ) + var yScaleNeutron = domains.yNeutron ? d3.scale .linear() @@ -4001,6 +4041,16 @@ D3TimeChart.prototype.setNeutronsHidden = function (hide) { }; +D3TimeChart.prototype.setGammaLogY = function (logy) { + logy = !!logy; // make sure its a boolean + if( this.options.gammaLogY === logy ) //dont waste time if we dont need to + return; + + this.options.gammaLogY = logy; + if( this.state.data.raw ) + this.setData( this.state.data.raw ); +}; + /** Sets the displayed sample numbers. diff --git a/src/D3TimeChart.cpp b/src/D3TimeChart.cpp index f14f02d9..c7e53a5c 100644 --- a/src/D3TimeChart.cpp +++ b/src/D3TimeChart.cpp @@ -106,6 +106,7 @@ class D3TimeChartFilters : public WContainerWidget WPushButton *m_clearEnergyFilterBtn; WCheckBox *m_dontRebin; WCheckBox *m_hideNeutrons; + WCheckBox *m_gammaLogY; NativeFloatSpinBox *m_gammaNeutRelEmphasis; float m_gammaNeutRelEmphasisValue; // Tracking for undo/redo support WCheckBox* m_normalizeCb; @@ -130,6 +131,7 @@ class D3TimeChartFilters : public WContainerWidget m_clearEnergyFilterBtn( nullptr ), m_dontRebin( nullptr ), m_hideNeutrons( nullptr ), + m_gammaLogY( nullptr ), m_gammaNeutRelEmphasis( nullptr ), m_gammaNeutRelEmphasisValue( 1.0f ), m_normalizeCb(nullptr), @@ -358,6 +360,13 @@ class D3TimeChartFilters : public WContainerWidget m_hideNeutrons->unChecked().connect( this, &D3TimeChartFilters::hideNeutronsChanged ); + m_gammaLogY = new WCheckBox( "Log Y Scale", optContents ); + m_gammaLogY->addStyleClass( "D3TimeGammaLogY" ); + m_gammaLogY->setToolTip( "Make the y-axis log, for the gammas" ); + m_gammaLogY->checked().connect( this, &D3TimeChartFilters::gammaLogYChanged ); + m_gammaLogY->unChecked().connect( this, &D3TimeChartFilters::gammaLogYChanged ); + + WContainerWidget *sfDiv = new WContainerWidget(optContents); sfDiv->addStyleClass( "D3TimeYAxisRelScale" ); @@ -862,6 +871,34 @@ class D3TimeChartFilters : public WContainerWidget }//void hideNeutronsChanged() + void gammaLogYChanged() + { + const bool logy = m_gammaLogY->isChecked(); + m_parentChart->setGammaLogY( logy ); + + // Take care of undo/redo + UndoRedoManager *undoRedo = UndoRedoManager::instance(); + if( undoRedo && !undoRedo->isInUndoOrRedo() ) + { + auto toggleLogYCB = wApp->bind( boost::bind( &WCheckBox::setChecked, m_gammaLogY, !logy ) ); + auto unToggleLogYCB = wApp->bind( boost::bind( &WCheckBox::setChecked, m_gammaLogY, logy ) ); + auto callLogYChanged = wApp->bind( boost::bind( &D3TimeChartFilters::gammaLogYChanged, this ) ); + + auto undo = [toggleLogYCB, callLogYChanged](){ + toggleLogYCB(); + callLogYChanged(); + }; + + auto redo = [unToggleLogYCB, callLogYChanged](){ + unToggleLogYCB(); + callLogYChanged(); + }; + + undoRedo->addUndoRedoStep( undo, redo, "Toggle y-axis on time chart log/lin" ); + }//if( undoRedo && !undoRedo->isInUndoOrRedo() ) + }//void gammaLogYChanged() + + void handleGammaNeutRelEmphasisChanged() { float value = 1.0f; @@ -934,6 +971,11 @@ class D3TimeChartFilters : public WContainerWidget m_gammaNeutRelEmphasis->setHidden( !visible ); // Should we also reset m_gammaNeutRelEmphasis to 1.0 if we are hiding the option? } + + void setGammaLogY( const bool logy ) + { + m_gammaLogY->setChecked( logy ); + } };//class D3TimeChartFilters @@ -948,6 +990,7 @@ D3TimeChart::D3TimeChart( Wt::WContainerWidget *parent ) m_showHorizontalLines( false ), m_dontRebin( false ), m_hideNeutrons( false ), + m_gammaLogY( false ), m_spec( nullptr ), m_detectors_to_display(), m_highlights(), @@ -1031,6 +1074,7 @@ void D3TimeChart::defineJavaScript() options += ", gridy: " + jsbool(m_showHorizontalLines); options += ", chartLineWidth: 1.0"; //ToDo: Let this be specified in C++ options += ", dontRebin: " + jsbool(m_dontRebin); + options += ", gammaLogY: " + jsbool(m_gammaLogY); options += "}"; setJavaScriptMember( "chart", "new D3TimeChart(" + m_chart->jsRef() + "," + options + ");"); @@ -2348,6 +2392,24 @@ bool D3TimeChart::neutronsHidden() const return m_hideNeutrons; } + +void D3TimeChart::setGammaLogY( const bool logy ) +{ + m_gammaLogY = logy; + if( m_options ) + m_options->setGammaLogY( logy ); + + if( isRendered() ) + doJavaScript( m_jsgraph + ".setGammaLogY(" + jsbool(logy) + ");" ); +}//void setGammaLogY( const bool logy ) + + +bool D3TimeChart::gammaLogY() const +{ + return m_gammaLogY; +}//bool gammaLogY() const + + void D3TimeChart::setXAxisRangeSamples( const int min_sample_num, const int max_sample_num ) { doJavaScript( m_jsgraph + ".setXAxisZoomSamples(" From f91d7422791dd44831d5010ba56057c5a83cae73 Mon Sep 17 00:00:00 2001 From: William Johnson Date: Fri, 5 Jan 2024 13:17:17 -0800 Subject: [PATCH 3/4] Fix a few bugs in spectrum file export. Filtering detectors wasnt working correctly, in some instances. Peaks we're sometimes not included when summing spectra to single record/sample. --- InterSpec/ExportSpecFile.h | 1 + src/ExportSpecFile.cpp | 42 +++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/InterSpec/ExportSpecFile.h b/InterSpec/ExportSpecFile.h index 6fabda02..ccecfff7 100644 --- a/InterSpec/ExportSpecFile.h +++ b/InterSpec/ExportSpecFile.h @@ -168,6 +168,7 @@ class ExportSpecFileTool : public Wt::WContainerWidget void handleFormatChange(); void handleForePlusBackChanged(); void handleFilterDetectorCbChanged(); + void handleDetectorsToFilterChanged(); void handleSumToSingleRecordChanged(); void handleSumTypeToSingleRecordChanged(); diff --git a/src/ExportSpecFile.cpp b/src/ExportSpecFile.cpp index 7b78c9e4..93c42b14 100644 --- a/src/ExportSpecFile.cpp +++ b/src/ExportSpecFile.cpp @@ -1582,14 +1582,16 @@ vector ExportSpecFileTool::currentlySelectedDetectors() const } vector answer; - for( const auto w : m_detectorFilterCbs->children() ) + const vector &detector_cbs = m_detectorFilterCbs->children(); + for( const auto w : detector_cbs ) { WCheckBox *cb = dynamic_cast( w ); if( !cb || !cb->isChecked() ) continue; - const auto pos = label_to_orig.find( cb->text().toUTF8() ); + const string cb_label = cb->text().toUTF8(); + const auto pos = label_to_orig.find( cb_label ); assert( pos != end(label_to_orig) ); if( pos != end(label_to_orig) ) answer.push_back( pos->second ); @@ -1740,6 +1742,14 @@ void ExportSpecFileTool::refreshSampleAndDetectorOptions() cb->setChecked( prev_check[cb->text().toUTF8()] ); else cb->setChecked( true ); //Could check if spectrum is displayed, and if so if the det is displayed + + // Curiously, if we dont have these next two calls, then the checkboxes wont actually be + // registered and not-checked (after the user unchecks them) in + // `ExportSpecFileTool::currentlySelectedDetectors()`, if the user immediately clicks + // "Export" after un-checking a detector; `ExportSpecFileTool::handleDetectorsToFilterChanged()` + // doesnt even have to do anything, just any function to be called. + cb->checked().connect( this, &ExportSpecFileTool::handleDetectorsToFilterChanged ); + cb->unChecked().connect( this, &ExportSpecFileTool::handleDetectorsToFilterChanged ); }// if( (max_records <= 1) || (max_records == 2) ) @@ -2142,6 +2152,12 @@ void ExportSpecFileTool::handleFilterDetectorCbChanged() }//void handleFilterDetectorCbChanged() +void ExportSpecFileTool::handleDetectorsToFilterChanged() +{ + scheduleAddingUndoRedo(); +}//void handleDetectorsToFilterChanged(); + + void ExportSpecFileTool::handleSumToSingleRecordChanged() { scheduleAddingUndoRedo(); @@ -2246,7 +2262,8 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() const bool backToSingleRecord = (m_sumBackToSingleRecord->isVisible() && m_sumBackToSingleRecord->isChecked()); const bool secoToSingleRecord = (m_sumSecoToSingleRecord->isVisible() && m_sumSecoToSingleRecord->isChecked()); const bool filterDets = (m_filterDetector && m_filterDetector->isVisible() && m_filterDetector->isChecked()); - const bool sumDetectorsPerSample = ((max_records <= 2) && (detectors.size() > 1)); + const bool sumDetectorsPerSample = ( ((max_records <= 2) && (detectors.size() > 1)) + && !(use_disp_fore || use_disp_back || use_disp_seco) ); shared_ptr answer = make_shared(); @@ -2549,13 +2566,23 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() } }//if( sum_samples.size() == 1 ) + if( !single_meas ) { assert( !sum_samples.empty() ); + + shared_ptr>> peaks = answer->peaks(sum_samples); + shared_ptr m = answer->sum_measurements( sum_samples, detectors, nullptr ); m->set_sample_number( *begin(sum_samples) ); meas_to_add.push_back( m ); + answer->setPeaks( {}, sum_samples ); + if( peaks && peaks->size() ) + answer->setPeaks( *peaks, {m->sample_number()} ); + else + answer->setPeaks( {}, {m->sample_number()} ); + for( const int sample : sum_samples ) { for( const string &det : detectors ) @@ -2602,11 +2629,20 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() // Next call throws exception if invalid sample number, detector name, or cant find energy // binning to use. And returns nullptr if empty sample numbers or detector names. + shared_ptr>> peaks = answer->peaks(samples); + answer->setPeaks( {}, samples ); + shared_ptr sum_meas = answer->sum_measurements( samples, detectors, nullptr ); assert( sum_meas ); if( !sum_meas ) throw runtime_error( "Error summing records - perhaps empty sample numbers or detector names." ); + sum_meas->set_sample_number( 1 ); + if( peaks && peaks->size() ) + answer->setPeaks( *peaks, {1} ); + else + answer->setPeaks( {}, {1} ); + answer->remove_measurements( orig_meass ); answer->add_measurement( sum_meas, true ); }else if( sumDetectorsPerSample ) From 209979a6c95f3c2d09048037f7616afab8edd772 Mon Sep 17 00:00:00 2001 From: William Johnson Date: Fri, 5 Jan 2024 14:01:08 -0800 Subject: [PATCH 4/4] Fix summing-detectors-per-sample on file export. --- src/ExportSpecFile.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ExportSpecFile.cpp b/src/ExportSpecFile.cpp index 93c42b14..78562da4 100644 --- a/src/ExportSpecFile.cpp +++ b/src/ExportSpecFile.cpp @@ -2257,13 +2257,13 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() const vector detectors = currentlySelectedDetectors(); const bool backgroundSub = (m_backSubFore && m_backSubFore->isVisible() && m_backSubFore->isChecked()); - const bool sumAll = (m_sumAllToSingleRecord->isVisible() && m_sumAllToSingleRecord->isChecked()); + const bool sumAll = ((m_sumAllToSingleRecord->isVisible() && m_sumAllToSingleRecord->isChecked()) + || (max_records <= 2)); const bool foreToSingleRecord = (m_sumForeToSingleRecord->isVisible() && m_sumForeToSingleRecord->isChecked()); const bool backToSingleRecord = (m_sumBackToSingleRecord->isVisible() && m_sumBackToSingleRecord->isChecked()); const bool secoToSingleRecord = (m_sumSecoToSingleRecord->isVisible() && m_sumSecoToSingleRecord->isChecked()); const bool filterDets = (m_filterDetector && m_filterDetector->isVisible() && m_filterDetector->isChecked()); - const bool sumDetectorsPerSample = ( ((max_records <= 2) && (detectors.size() > 1)) - && !(use_disp_fore || use_disp_back || use_disp_seco) ); + const bool sumDetectorsPerSample = (sum_per_sample && ((max_records <= 2) && (detectors.size() > 1))); shared_ptr answer = make_shared(); @@ -2623,20 +2623,20 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() } - if( m_sumAllToSingleRecord->isVisible() && m_sumAllToSingleRecord->isChecked() ) + if( sumAll ) { const vector> orig_meass = answer->measurements(); // Next call throws exception if invalid sample number, detector name, or cant find energy // binning to use. And returns nullptr if empty sample numbers or detector names. - shared_ptr>> peaks = answer->peaks(samples); - answer->setPeaks( {}, samples ); - shared_ptr sum_meas = answer->sum_measurements( samples, detectors, nullptr ); assert( sum_meas ); if( !sum_meas ) throw runtime_error( "Error summing records - perhaps empty sample numbers or detector names." ); + shared_ptr>> peaks = answer->peaks(samples); + answer->setPeaks( {}, samples ); + sum_meas->set_sample_number( 1 ); if( peaks && peaks->size() ) answer->setPeaks( *peaks, {1} ); @@ -2645,7 +2645,7 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() answer->remove_measurements( orig_meass ); answer->add_measurement( sum_meas, true ); - }else if( sumDetectorsPerSample ) + }else if( sumDetectorsPerSample && (answer->detector_names().size() > 1) ) { set problem_samples; map>> sample_to_meas; @@ -2664,7 +2664,9 @@ std::shared_ptr ExportSpecFileTool::generateFileToSave() try { - shared_ptr m = answer->sum_measurements( {sample_num}, detectors, nullptr ); + // `detectors` is no longer valid, since we may have summed them together + const vector det_names = answer->detector_names(); + shared_ptr m = answer->sum_measurements( {sample_num}, det_names, nullptr ); if( !m ) throw runtime_error( "No gamma spectra found" );