diff --git a/Modules/Chart/documentation/mitkChart.dox b/Modules/Chart/documentation/mitkChart.dox index 64bd9eb9e2..625eb17181 100644 --- a/Modules/Chart/documentation/mitkChart.dox +++ b/Modules/Chart/documentation/mitkChart.dox @@ -1,169 +1,188 @@ /** \page ChartModule The MITK Chart Module \tableofcontents \section ChartModule_brief Description The MITK chart module is able to show different types of charts in a widget with customizable labels. Four types of charts are supported: line, bar, spline and pie. \imageMacro{images\barChartTemperature.png,"Example bar chart",6} \imageMacro{images\lineChartTemperature.png,"Example line chart",6} \imageMacro{images\splineChartTemperature.png,"Example spline chart",6} \imageMacro{images\pieChartExample.png,"Example pie chart",6} \subsection Chart_Technical Technical background -The module uses the java script library C3js (http://www.c3js.org) to display the chart in a QWebEngineView (that renders html/js content). For examples, please visit http://c3js.org/examples.html. +The module uses the java script library C3js to display the chart in a QWebEngineView (that renders html/js content). + +For examples, please visit http://c3js.org/examples.html. \subsection Chart_GUI GUI \note Be sure that the dependency to the Chart Module is resolved in the CMakeLists.txt of the plugin: MODULE_DEPENDS MitkChart. Open the ui file of the plugin. Then add a widget (we always use the name chartWidget in the following) at the desired position. Change the class of the widget to a user-defined widget (right click → user defined classes, see http://doc.qt.io/qt-5/designer-using-custom-widgets.html). Set "QWidget" as base class. Set "QmitkChartWidget" as class name and "QmitkChartWidget.h" as include file. \imageMacro{images\userDefinedWidget.png,"User defined widget",10} \subsection Chart_data Data The most important functionality is to add data to the chartWidget. Either one dimensional or two dimensional data can be used. One-dimensional data has the same interval between values on the x-axis (and no x-axis values) while the two-dimensional data has arbitrary x-axis difference values (and given x-axis values). An example for one-dimensional data is the temperature for each month of a year: std::vector temperatureHD = {1.8, 3.1, 6.3, 10.2, 14.5, 17.4, 19.4, 18.9, 15.5, 11.2, 5.8, 2.6}. The first entry corresponds (implicitly) to the temperature in january, two second to the temperature in february the last entry to the temperature in december. Thus, the x values have same intervals. The given temperature values are defined as y-axis values. An example for two-dimensional data is the people living in a city in different years: std::map peopleHeidelberg={{1975, 129368 }, { 1985, 134724 },{ 1990, 136796 },{ 2010, 147312 }}. Thus, the x-values are given as their intervals are different (10 years, 5 years, 10 years). Each x value is connected to an y-value that represents the amount of people (1975 → 129368, 1985 → 134724, ...). -Data is added by calling chartWidget->AddData1D(temperatureHD) or chartWidget->AddData2D(peopleHeidelberg). +Data is added by calling chartWidget->AddData1D(temperatureHD, "Heidelberg") or chartWidget->AddData2D(peopleHeidelberg, "Heidelberg"), where the second argument is just a label for the data entry. + +As the data labels are used as identifier, they have to be unique. This is checked. If non-unique entries are entered, they are made unique by adding numbers. Example: +\code{.cpp} +chartWidget->AddData1D(temperatureHD, "Heidelberg") +chartWidget->AddData1D(temperatureOslo, "Heidelberg") +\endcode + +will result in the labels "Heidelberg" and "Heidelberg0", whereas "Heidelberg0" refers to temperatureOslo. \note All data can be cleared by calling chartWidget->Clear(). \imageMacro{images\2DDataExample.png,"2D data example: Heidelberg has fewer entries and their x-range (years) is smaller than Freiburg",8} \subsection Chart_type Chart type The default chart type is bar. To use a different type, you have to change it. See \ref ChartModule_brief for the available chart types. Call e.g. chartWidget->SetChartType(QmitkChartWidget::ChartType::line) for changing the chart type. Note that it is not directly displayed. A convenience function if you want to change the chart type and display the result directly is to use chartWidget->SetChartTypeAndReload(QmitkChartWidget::ChartType::line). bar chart, line chart and spline chart are somewhat similar. Pie charts are handled differently. The input data sum is regarded as 100%. The values of all data entries are summed. Example: \code{.cpp} -chartWidget->AddData1D({5}) -chartWidget->AddData1D({2}) -chartWidget->AddData1D({3}) +chartWidget->AddData1D({5}, "entry1"); +chartWidget->AddData1D({2}, "entry2"); +chartWidget->AddData1D({3}, "entry3"); \endcode The pie chart has then entries of 50%, 20% and 30%. Calling chartWidget->AddData1D({5,2,3}) leads to a pie chart with one class having 100%. Calling \code{.cpp} -chartWidget->AddData1D({2,2,1}) -chartWidget->AddData1D({1,1}) -chartWidget->AddData1D({3}) +chartWidget->AddData1D({2,2,1}, "entry1"); +chartWidget->AddData1D({1,1}, "entry1"); +chartWidget->AddData1D({3}, "entry1"); \endcode leads to the first result again (50%, 20% and 30%) as entries are summed. \note pie charts differ significantly from the other chart types. Be aware of the differences. \subsection Chart_labels Labels Three labels can be set to custom strings. These are -Data labels provide the name for data legend entries ("Heidelberg" for our 1D data \ref{Chart_data} as it is the average temperature in Heidelberg). An example x-Axis and y-axis label would be month and temperature, respectively. - -If not set, the data labels are set to data1, data2 and so on. The x and y-axis labels are empty strings by default. +Data labels provide the name for data legend entries ("Heidelberg" for our 1D data \ref{Chart_data} as it is the average temperature in Heidelberg) and are given by the second argument of AddData1D and AddData2D. An example x-Axis and y-axis label would be month and temperature, respectively. -Call chartWidget->setDataLabels({"Heidelberg"}) to set the labels for the different data entries. Order is defined by order of calling AddData1D() and AddData2D(). +The data label argument is mandatory. The x and y-axis labels are empty strings by default. -chartWidget->setXAxisLabel("month") and chartWidget->setYAxisLabel("temperature") ensures the labeling of x- and y-Axis in the chart. No labels are defined as default. +chartWidget->SetXAxisLabel("month") and chartWidget->SetYAxisLabel("temperature") ensures the labeling of x- and y-Axis in the chart. No labels are defined as default. \subsection Chart_show Displaying the chart Finally, the chart is displayed by calling chartWidget->Show(bool). If the optional parameter is set to true, a subchart is shown additionally. That's useful for ensuring the overview if the user wants to zoom in. \subsection Chart_changingData changing the data -If you want to add more data, just call chartWidget->AddData1D() or chartWidget->AddData2D() as often as desired. Then, call chartWidget->Show(). The range of x- and y-axis is adjusted automatically. +If you want to add more data, just call chartWidget->AddData1D(data, label) or chartWidget->AddData2D(data, label) as often as desired. Then, call chartWidget->Show(). The range of x- and y-axis is adjusted automatically. + +\subsection Chart_dataAttributes Changing visualization attributes of data + +The following attributes of an data entry can be changed: + + + +this is done by referencing the data entry by its label (e.g. "Heidelberg" above): chartWidget->SetColor("data1", "green"), chartWidget->SetColor("data1", "#FF4500"), chartWidget->SetLineStyle("data1", LineStyle::dashed). + +Colors are chosen automatically be C3js if not given. However, if some data entries have chosen colors and some have not, same colors may appear for different data entries. For color selection, the following reference is helpful: https://www.w3schools.com/cssref/css_colors.asp + +Line style only can be set if ChartType is defined as QmitkChartWidget::ChartType::line. It is ignored otherwise. Also, if a non-existing label is given, the command is ignored. The default linestyle is LineStyle::solid. \section Chart_example Example \subsection Chart_exampleBarChart Bar chart To create and visualize a bar chart with two data sets, x/y-axis labels and data labels, the following code is used: \code{.cpp} std::vector temperatureHD = {1.8, 3.1, 6.3, 10.2, 14.5, 17.4, 19.4, 18.9, 15.5, 11.2, 5.8, 2.6}; std::vector temperatureOslo = {-4.3, -4, -0.2, 4.6, 10.8, 15.2, 16.4, 15.2, 10.8, 6.4, 0.7, -2.8}; -chartWidget->AddData1D(temperatureHD); -chartWidget->AddData1D(temperatureOslo); +chartWidget->AddData1D(temperatureHD, "Heidelberg"); +chartWidget->AddData1D(temperatureOslo, "Oslo"); chartWidget->SetChartType(QmitkChartWidget::ChartType::bar); -chartWidget->SetDataLabels({ "Heidelberg", "Oslo" }); chartWidget->SetXAxisLabel("month"); chartWidget->SetYAxisLabel("temperature"); chartWidget->Show(); \endcode The order when AddData1D() is called influences the colors of the bars and the order of the shown data. The line chartWidget->SetChartType(QmitkChartWidget::ChartType::bar) is superfluos (bar is the default type) and only for completeness. After Show() is called, the chart is visualized. The chart type can be changed to spline and directly showed: \code{.cpp} chartWidget->SetChartTypeAndReload(QmitkChartWidget::ChartType::spline); \endcode the equivalent code is: \code{.cpp} chartWidget->SetChartType(QmitkChartWidget::ChartType::spline); chartWidget->Show(); \endcode The temperature of another city can be added: \code{.cpp} std::vector temperatureRome = {8.1, 8.7, 8.7, 11.6, 18.8, 22.8, 25.4, 25.7, 21.4, 17.6, 12.6, 8.9}; -chartWidget->AddData1D(temperatureRome); -chartWidget->SetDataLabels({ "Heidelberg", "Oslo", "Rome" }); +chartWidget->AddData1D(temperatureRome, "Rome"); chartWidget->Show(true); \endcode If SetDataLabels() is not called, the label data2 is automatically created. As Show(true) is used, a subchart is shown. \subsection Chart_examplePieChart Pie chart A pie chart (the same as \ref ChartModule_brief) can be generated with the following code: \code{.cpp} -chartWidget->AddData1D({5}); -chartWidget->AddData1D({3}); -chartWidget->AddData1D({2}); +chartWidget->AddData1D({5}, "Heidelberg"); +chartWidget->AddData1D({3}, "Oslo"); +chartWidget->AddData1D({2}, "New York"); chartWidget->SetChartType(QmitkChartWidget::ChartType::pie); -chartWidget->SetDataLabels({ "Heidelberg", "Oslo", "New York" }); chartWidget->Show(); \endcode \subsection Chart_example2DData Chart with 2D data A line chart with two-dimensional data is the following example: \code{.cpp} std::map peopleHD= { {1975, 129368 }, { 1985, 134724 },{ 1990, 136796 },{ 2010, 147312 } }; std::map peopleFreiburg = { { 1969, 165960 },{ 1973, 174997 },{ 1982, 178545 },{ 2001, 208294 },{ 2015, 222203 } }; - chartWidget->AddData2D(peopleHD); - chartWidget->AddData2D(peopleFreiburg); + chartWidget->AddData2D(peopleHD, "Heidelberg"); + chartWidget->AddData2D(peopleFreiburg, "Freiburg"); chartWidget->SetChartType(QmitkChartWidget::ChartType::line); - chartWidget->SetDataLabels({ "Heidelberg", "Freiburg" }); chartWidget->SetXAxisLabel("year"); chartWidget->SetYAxisLabel("people"); chartWidget->Show(); \endcode Hence, 2D data is having the following assignment: year → people. In the vector peopleHD, four values are defined, in the vector peopleFreiburg, five values are defined. The defined years are different for Heidelberg (1975-2010) than for Freiburg (1969-2015). \subsection Chart_imageStatistics image statistics plugin Another example of the use of QmitkChartWidget can be found in the image statistics plugin (org.mitk.gui.qt.measurementtoolbox\QmitkImageStatisticsView). The chartWidget is named m_Controls->m_JSHistogram there. */ diff --git a/Modules/Chart/include/QmitkChartWidget.h b/Modules/Chart/include/QmitkChartWidget.h index 570182c9c5..46ce9c86c2 100644 --- a/Modules/Chart/include/QmitkChartWidget.h +++ b/Modules/Chart/include/QmitkChartWidget.h @@ -1,152 +1,173 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef QmitkC3jsWidget_h #define QmitkC3jsWidget_h #include #include #include /*! \brief QmitkChartWidget is a widget to display various charts based on the javascript chart library C3js. Currently, bar charts, line charts and pie charts are supported. * \details Data is added via AddData1D() or AddData2D().\n * There can be multiple charts (of the same type) created by calling AddData1D or AddData2D multiple times.\n\n * The following chart types are supported: * * line chart: http://c3js.org/samples/simple_multiple.html * * bar chart: http://c3js.org/samples/chart_bar.html * * spline chart: http://c3js.org/samples/chart_spline.html * * pie chart: http://c3js.org/samples/chart_pie.html * \n Technical details: The javascript code is embedded in a QWebEngineView. The actual js code is implemented in resource\Chart.js. * \sa http://c3js.org for further information about the used javaScript library. * \warning Pie is significantly different than the other types. Here, the data given by AddData1D is summed. Each entry represents a different category. * \ingroup Modules/Chart */ class MITKCHART_EXPORT QmitkChartWidget : public QWidget { Q_OBJECT public: /*! * \brief enum of diagram types. Supported are bar, line, spline (a smoothed line) and pie. */ enum class ChartType { bar, /*!< bar chart, see http://c3js.org/samples/chart_bar.html */ line, /*!< line chart, see http://c3js.org/samples/simple_multiple.html */ spline, /*!< spline chart (smoothed line chart), see http://c3js.org/samples/chart_spline.html */ pie /*!< pie chart, see http://c3js.org/samples/chart_pie.html*/ }; enum class ChartStyle { defaultstyle, darkstyle }; + enum class LineStyle { + solid, + dashed + }; /*! * \brief enum of legend position. Supported are bottom, right, inset. * See http://c3js.org/reference.html#legend-position */ enum class LegendPosition { bottom, right, inset }; explicit QmitkChartWidget(ChartType type=ChartType::bar, QWidget* parent = nullptr); virtual ~QmitkChartWidget(); /*! * \brief Adds 1D data to the widget * \details internally, the list is converted to a map with increasing integers keys starting at 0. + * \param label the name of the data that is also used as identifier. * \note the data can be cleared with ClearDiagram() + * \note If the label name already exists, the name is replaced with a unique one by concatenating numbers to it. */ - void AddData1D(const std::vector& data1D); + void AddData1D(const std::vector& data1D, const std::string& label); /*! * \brief Adds 2D data to the widget. Call repeatedly for displaying multiple charts. * \details each entry represents a data point: key: value --> x-value: y-value. + * \param label the name of the data that is also used as identifier. * \note the data can be cleared with ClearDiagram() + * \note If the label name already exists, the name is replaced with a unique one by concatenating numbers to it. */ - void AddData2D(const std::map& data2D); + void AddData2D(const std::map& data2D, const std::string& label); /*! - * \brief Sets the data labels as legend - * \details Sets a label for each data entry (i.e. line). Default is data+#number + * \brief sets the color of one data entry (identifier is previously assigned label) + * \details the color name can be "red" or a hex number (#FF0000). + * Either define all data entries with a color or none. If a mixed approach is used, different data entries could have the same color. + * If an unknown label is given, nothing happens. + * \sa https://www.w3schools.com/cssref/css_colors.asp */ - void SetDataLabels(const std::vector& labels); + void SetColor(const std::string& label, const std::string& colorName); + + /*! + * \brief sets the line style of one data entry (identifier is previously assigned label) + * \details two line styles are possible: LineStyle::solid and LineStyle::dashed. + * The default linestyle is solid. + * If an unknown label is given, nothing happens. + * \warning only sets the line style if the current chart type is ChartType::line. However, the line style remains also if the chart changes (e.g. new chart type) + */ + void SetLineStyle(const std::string& label, LineStyle style); + std::vector GetDataLabels() const; void SetXAxisLabel(const std::string& label); std::string GetXAxisLabel() const; void SetYAxisLabel(const std::string& label); std::string GetYAxisLabel() const; void SetDiagramTitle(const std::string &title); std::string GetDiagramTitle() const; /*! * \brief sets the diagram type * \note to also display the changes, call ChangeDiagramTypeAndReload() * \sa DiagramType for available types */ void SetChartType(ChartType type); ChartType GetChartType() const; void SetLegendPosition(LegendPosition position); LegendPosition GetLegendPosition() const; /*! * \brief Reloads the chart and changes the chart type */ void SetChartTypeAndReload(ChartType type); /*! * \brief Displays the diagram in the widget * \param showSubChart if a subchart is displayed inside the widget or not. * \details see http://c3js.org/samples/options_subchart.html */ void Show(bool showSubChart=false); /*! * \brief Clears all data inside and resets the widget. */ void Clear(); /*! * \brief Changes the theme of the widget. */ void SetTheme(ChartStyle themeEnabled); /*! * \brief Reloads the chart in the widget * \details reloading may be needed to display added data in an existing chart * \param showSubChart if a subchart is displayed inside the widget or not. */ void Reload(bool showSubChart); private: class Impl; Impl* m_Impl; public slots: void OnLoadFinished(bool isLoadSuccessfull); signals: void PageSuccessfullyLoaded(); }; #endif diff --git a/Modules/Chart/include/QmitkChartxyData.h b/Modules/Chart/include/QmitkChartxyData.h index a04716073b..ad6911e5a7 100644 --- a/Modules/Chart/include/QmitkChartxyData.h +++ b/Modules/Chart/include/QmitkChartxyData.h @@ -1,62 +1,80 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef QmitkC3xyData_h #define QmitkC3xyData_h #include /** /brief This class holds the actual data for the chart generation with C3. * data can be loaded in constructor directly or with SetData * It is derived from QObject, because we need Q_PROPERTIES to send Data via QWebChannel to JavaScript. */ class QmitkChartxyData : public QObject { Q_OBJECT Q_PROPERTY(QList m_YData READ GetYData WRITE SetYData NOTIFY SignalYDataChanged); Q_PROPERTY(QList m_XData READ GetXData WRITE SetXData NOTIFY SignalXDataChanged); + Q_PROPERTY(QVariant m_Color READ GetColor WRITE SetColor NOTIFY SignalColorChanged); + Q_PROPERTY(QVariant m_Label READ GetLabel WRITE SetLabel NOTIFY SignalLabelChanged); + Q_PROPERTY(QVariant m_LineStyleName READ GetLineStyle WRITE SetLineStyle NOTIFY SignalLineStyleChanged); public: - explicit QmitkChartxyData(const QList& data); //Constructor for Data1D (y=1,5,6,...) - explicit QmitkChartxyData(const QMap& data); //Constructor for Data2D (x:y=1:2, 2:6, 3:7) + explicit QmitkChartxyData(const QList& data, const QVariant& label); //Constructor for Data1D (y=1,5,6,...) + explicit QmitkChartxyData(const QMap& data, const QVariant& label); //Constructor for Data2D (x:y=1:2, 2:6, 3:7) - void SetData(const QMap& data); + void SetData(const QMap& data, const QVariant& label); Q_INVOKABLE QList GetYData() const { return m_YData; }; Q_INVOKABLE void SetYData(const QList& yData) { m_YData =yData; }; Q_INVOKABLE QList GetXData() const { return m_XData; }; Q_INVOKABLE void SetXData(const QList& xData) { m_XData =xData; }; + Q_INVOKABLE QVariant GetLabel() const { return m_Label; }; + Q_INVOKABLE void SetLabel(const QVariant& label) { m_Label = label; }; + + Q_INVOKABLE QVariant GetColor() const { return m_Color; }; + Q_INVOKABLE void SetColor(const QVariant& color) { m_Color = color; }; + + Q_INVOKABLE QVariant GetLineStyle() const { return m_LineStyleName; }; + Q_INVOKABLE void SetLineStyle(const QVariant& lineStyle) { m_LineStyleName = lineStyle; }; + /** * \brief Clears the Data. * * This function clears the data. */ void ClearData(); signals: void SignalYDataChanged(const QList yData); void SignalXDataChanged(const QList xData); + void SignalColorChanged(const QVariant color); + void SignalLabelChanged(const QVariant label); + void SignalLineStyleChanged(const QVariant lineStyle); private: QList m_YData; QList m_XData; + QVariant m_Label; + QVariant m_Color; + QVariant m_LineStyleName; }; #endif //QmitkC3xyData_h \ No newline at end of file diff --git a/Modules/Chart/resource/Chart.js b/Modules/Chart/resource/Chart.js index c18f6f40b2..c034669124 100644 --- a/Modules/Chart/resource/Chart.js +++ b/Modules/Chart/resource/Chart.js @@ -1,224 +1,237 @@ //Based on C3.js (http://c3js.org). See Website for examples! document.body.style.backgroundColor = 'rgb(240, 240, 240)'; var minHeight = 255; var chart; GenerateChart('bar', false, false) var chartData; var xValues=[]; var yValues=[]; var xs = {}; +var dataColors = {}; +var lineStyle = {}; //Is executed when js is loaded first. //Extracts relevant information from chartData in variables window.onload = function() { new QWebChannel(qt.webChannelTransport, function(channel) { chartData = channel.objects.chartData; var count = 0; for(var propertyName in channel.objects) { if (propertyName != 'chartData'){ var xDataTemp = channel.objects[propertyName].m_XData var yDataTemp = channel.objects[propertyName].m_YData var dataLabelsTemp; //add label to x array xDataTemp.unshift('x'+count.toString()) if (count < chartData.m_dataLabels.length){ dataLabelsTemp = chartData.m_dataLabels[count] } else { dataLabelsTemp = "data" + count.toString() } xs[dataLabelsTemp] = 'x'+count.toString() xDataTemp.push(null); //append null value, to make sure the last tick on x-axis is displayed correctly yDataTemp.unshift(dataLabelsTemp) yDataTemp.push(null); //append null value, to make sure the last tick on y-axis is displayed correctly xValues[count] = xDataTemp yValues[count] = yDataTemp + dataColors[dataLabelsTemp] = channel.objects[propertyName].m_Color + + if (channel.objects[propertyName].m_LineStyleName=="solid"){ + lineStyle[dataLabelsTemp]= '' + } + else { + lineStyle[dataLabelsTemp]= [{'style':'dashed'}] + } + count++; } } setupChart(chartData) }); } //This is necessary to resize the chart, after the size of the parent changed window.onresize = function () { var size = window.innerHeight-(window.innerHeight/100*10); //subtract 5% of height to hide vertical scrool bar if (size < minHeight) { size = minHeight; } chart.resize({ height: size, }); } function ReloadChart(showSubchart) { chartData.m_ShowSubchart = showSubchart; setupChart(chartData); } function setupChart(chartData) { window.onresize(); GenerateChart(chartData.m_DiagramTypeName, chartData.m_ShowSubchart, chartData.m_UsePercentageInPieChart, chartData.m_xAxisLabel, chartData.m_yAxisLabel, chartData.m_diagramTitle, chartData.m_LegendPosition) chart.unload(); //unload data before loading new data //for multiple xy line chart, see http://c3js.org/samples/simple_xy_multiple.html var columns = []; for (var i in xValues){ columns.push(xValues[i]) } for (var i in yValues){ columns.push(yValues[i]) } chart.load({ xs: xs, columns: columns }); } //Transformation between different chart types //takes the name of the chart type as a parameter //called by QmitkC3jsWidget function transformView(TransformTo) { chart.transform(TransformTo); }; function changeTheme(color) { if (color == 'dark') { link = document.getElementsByTagName("link")[0]; link.href = "Chart_dark.css"; } else { link = document.getElementsByTagName("link")[0]; link.href = "Chart.css"; } }; //Here, the chart magic takes place. C3js is called //chartType: either bar, line or pie //showSubchart: see http://c3js.org/samples/options_subchart.html //usePercentageInPieChart: percentage labels (only for pie chart) function GenerateChart(chartType, showSubchart, usePercentageInPieChart, xAxisLabel, yAxisLabel, title, legendPosition) { //adaption for bar ratio indepenend of amount of data points //otherwise, bars could be covered. var barRatio; try { barRatio = 0.8*Math.exp(-0.015*xValues[0].length); } catch (err){ barRatio=0.42 } var formatCharacter; if (usePercentageInPieChart==true){ formatCharacter = '%' } else{ formatCharacter = 's' } chart = c3.generate({ title:{ text: title, position: 'top-center' }, data: { xs: {}, //use first "column" as x axis values columns: [], //initialize empty. Data will be loaded in function setupChart(chartData) type: chartType, selection: { enabled: false, multiple: false, - } + }, + colors: dataColors, + regions: lineStyle, }, legend: { position: legendPosition, show: true }, grid: { y: { lines: [{value:0}] //Draws a horizontal line at y=0 } }, bar: { width: { ratio: barRatio } }, pie:{ label: { format: function (value, ratio, id) { if (usePercentageInPieChart==true){ return d3.format('%') (ratio); } else{ return value; } } } }, zoom: { enabled: true, }, subchart: { show: showSubchart //Shows a subchart that shows the region the primary chart is zoomed in to by overlay. }, axis: { x: { tick: { multiline: false, fit: false, //to make more x labels appear on zoom centered: true, format: d3.format(".1f"), }, label: { text: xAxisLabel, position: 'outer-center' } }, y: { tick: { format: d3.format(formatCharacter) }, label: { text: yAxisLabel, position: 'outer-middle' } } }, tooltip: { format: { title: function (x) { return xAxisLabel + ': ' + d3.format(".2f")(x)} }, }, //Style data points in linegraph point: { r: 0.2, focus: { expand: { r: 4 } } }, }); } diff --git a/Modules/Chart/src/QmitkChartWidget.cpp b/Modules/Chart/src/QmitkChartWidget.cpp index 11a1bcb8c9..46b7c948ef 100644 --- a/Modules/Chart/src/QmitkChartWidget.cpp +++ b/Modules/Chart/src/QmitkChartWidget.cpp @@ -1,381 +1,434 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include #include #include #include #include #include #include "mitkExceptionMacro.h" class QmitkChartWidget::Impl final { public: explicit Impl(QWidget* parent); ~Impl(); Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; - void AddData1D(const std::vector& data1D); + void AddData1D(const std::vector& data1D, const std::string& label); - void AddData2D(const std::map& data2D); + void AddData2D(const std::map& data2D, const std::string& label); void ClearData2D(); void SetDataLabels(const std::vector& labels); std::vector GetDataLabels() const; + void SetColor(const std::string& label, const std::string& colorName); + void SetLineStyle(const std::string& label, LineStyle style); + void SetXAxisLabel(const std::string& label); std::string GetXAxisLabel() const; void SetYAxisLabel(const std::string& label); std::string GetYAxisLabel() const; void SetDiagramTitle(const std::string &title); std::string GetDiagramTitle() const; void SetLegendPosition(LegendPosition position); LegendPosition GetLegendPosition() const; std::string GetLegendPositionAsString() const; void SetDiagramType(QmitkChartWidget::ChartType diagramType); ChartType GetDiagramType() const; std::string GetDiagramTypeAsString() const; void ClearJavaScriptChart(); - void initializeJavaScriptChart(); - void callJavaScriptFuntion(const QString& command); + void InitializeJavaScriptChart(); + void CallJavaScriptFuntion(const QString& command); QmitkChartData* GetC3Data() const; std::vector* GetC3xyData() const; private: + void AddLabelIfNotAlreadyDefined(QList& labelList, const std::string& label) const; + QmitkChartxyData* GetElementByLabel(const std::vector* c3xyData, const std::string& label) const; + QWebChannel* m_WebChannel; QWebEngineView* m_WebEngineView; QmitkChartData * m_C3Data; std::vector * m_C3xyData; std::map m_DiagramTypeToName; std::map m_LegendPositionToName; + std::map m_LineStyleToName; }; QmitkChartWidget::Impl::Impl(QWidget* parent) : m_WebChannel(new QWebChannel(parent)), m_WebEngineView(new QWebEngineView(parent)) { //disable context menu for QWebEngineView m_WebEngineView->setContextMenuPolicy(Qt::NoContextMenu); //Set the webengineview to an initial empty page. The actual chart will be loaded once the data is calculated. m_WebEngineView->setUrl(QUrl(QStringLiteral("qrc:///C3js/empty.html"))); m_WebEngineView->page()->setWebChannel(m_WebChannel); connect(m_WebEngineView, SIGNAL(loadFinished(bool)), parent, SLOT(OnLoadFinished(bool))); auto layout = new QGridLayout(parent); layout->setMargin(0); layout->addWidget(m_WebEngineView); parent->setLayout(layout); m_DiagramTypeToName.emplace(ChartType::bar, "bar"); m_DiagramTypeToName.emplace(ChartType::line, "line"); m_DiagramTypeToName.emplace(ChartType::spline, "spline"); m_DiagramTypeToName.emplace(ChartType::pie, "pie"); m_LegendPositionToName.emplace(LegendPosition::bottom, "bottom"); m_LegendPositionToName.emplace(LegendPosition::right, "right"); m_LegendPositionToName.emplace(LegendPosition::inset, "inset"); + m_LineStyleToName.emplace(LineStyle::solid, "solid"); + m_LineStyleToName.emplace(LineStyle::dashed, "dashed"); + m_C3Data = new QmitkChartData(); m_C3xyData = new std::vector(); } QmitkChartWidget::Impl::~Impl() { delete m_C3Data; delete m_C3xyData; } -void QmitkChartWidget::Impl::SetDataLabels(const std::vector& labels) + +void QmitkChartWidget::Impl::AddLabelIfNotAlreadyDefined(QList& labelList, const std::string& label) const { - QList variantList; - for (const auto& label : labels) { - variantList.append(QString::fromStdString(label)); + QString currentLabel = QString::fromStdString(label); + int counter = 0; + while (labelList.contains(currentLabel)) + { + currentLabel = QString::fromStdString(label + std::to_string(counter)); + counter++; } - GetC3Data()->SetDataLabels(variantList); + labelList.append(currentLabel); } -void QmitkChartWidget::Impl::AddData1D(const std::vector& data1D) { +void QmitkChartWidget::Impl::AddData1D(const std::vector& data1D, const std::string& label) { QList data1DConverted; for (const auto& aValue : data1D) { data1DConverted.append(aValue); } - GetC3xyData()->push_back(new QmitkChartxyData(data1DConverted)); + GetC3xyData()->push_back(new QmitkChartxyData(data1DConverted, QVariant(QString::fromStdString(label)))); + auto definedLabels = GetC3Data()->GetDataLabels(); + AddLabelIfNotAlreadyDefined(definedLabels, label); + GetC3Data()->SetDataLabels(definedLabels); } -void QmitkChartWidget::Impl::AddData2D(const std::map& data2D) { +void QmitkChartWidget::Impl::AddData2D(const std::map& data2D, const std::string& label) { QMap data2DConverted; for (const auto& aValue : data2D) { data2DConverted.insert(aValue.first, aValue.second); } - GetC3xyData()->push_back(new QmitkChartxyData(data2DConverted)); + GetC3xyData()->push_back(new QmitkChartxyData(data2DConverted, QVariant(QString::fromStdString(label)))); + auto definedLabels = GetC3Data()->GetDataLabels(); + AddLabelIfNotAlreadyDefined(definedLabels, label); + GetC3Data()->SetDataLabels(definedLabels); } std::vector QmitkChartWidget::Impl::GetDataLabels() const { auto dataLabels = GetC3Data()->GetDataLabels(); std::vector dataLabelsAsStringVector; for (const auto& label : dataLabels) { dataLabelsAsStringVector.push_back(label.toString().toStdString()); } return dataLabelsAsStringVector; } +void QmitkChartWidget::Impl::SetColor(const std::string& label, const std::string& colorName) +{ + auto element = GetElementByLabel(GetC3xyData(), label); + if (element) { + element->SetColor(QVariant(QString::fromStdString(colorName))); + } +} + +void QmitkChartWidget::Impl::SetLineStyle(const std::string& label, LineStyle style) { + auto element = GetElementByLabel(GetC3xyData(), label); + if (element) { + const std::string lineStyleName(m_LineStyleToName.at(style)); + element->SetLineStyle(QVariant(QString::fromStdString(lineStyleName))); + } +} + +QmitkChartxyData* QmitkChartWidget::Impl::GetElementByLabel(const std::vector* c3xyData, const std::string& label) const +{ + for (auto element = c3xyData->begin(); element != c3xyData->end(); ++element) { + if ((*element)->GetLabel().toString().toStdString() == label) { + return *element; + } + } + MITK_WARN << "label " << label << " not found in QmitkChartWidget"; + return nullptr; +} + void QmitkChartWidget::Impl::SetXAxisLabel(const std::string& label) { GetC3Data()->SetXAxisLabel(QString::fromStdString(label)); } std::string QmitkChartWidget::Impl::GetXAxisLabel() const { return GetC3Data()->GetXAxisLabel().toString().toStdString(); } void QmitkChartWidget::Impl::SetYAxisLabel(const std::string& label) { GetC3Data()->SetYAxisLabel(QString::fromStdString(label)); } std::string QmitkChartWidget::Impl::GetYAxisLabel() const { return GetC3Data()->GetYAxisLabel().toString().toStdString(); } void QmitkChartWidget::Impl::SetDiagramTitle(const std::string& title) { GetC3Data()->SetDiagramTitle(QString::fromStdString(title)); } std::string QmitkChartWidget::Impl::GetDiagramTitle() const { return GetC3Data()->GetDiagramTitle().toString().toStdString(); } void QmitkChartWidget::Impl::SetLegendPosition(QmitkChartWidget::LegendPosition legendPosition) { const std::string legendPositionName(m_LegendPositionToName.at(legendPosition)); GetC3Data()->SetLegendPosition(QString::fromStdString(legendPositionName)); } QmitkChartWidget::LegendPosition QmitkChartWidget::Impl::GetLegendPosition() const { for (const auto& aLegendPosition : m_LegendPositionToName) { if (aLegendPosition.second == GetLegendPositionAsString()) { return aLegendPosition.first; } } mitkThrow() << "can't find legend position!"; } std::string QmitkChartWidget::Impl::GetLegendPositionAsString() const { return GetC3Data()->GetLegendPosition().toString().toStdString(); } void QmitkChartWidget::Impl::SetDiagramType(QmitkChartWidget::ChartType diagramType) { const std::string diagramTypeName(m_DiagramTypeToName.at(diagramType)); GetC3Data()->SetDiagramType(QString::fromStdString(diagramTypeName)); } QmitkChartWidget::ChartType QmitkChartWidget::Impl::GetDiagramType() const { for (const auto& aDiagramType : m_DiagramTypeToName) { if (aDiagramType.second == GetDiagramTypeAsString()){ return aDiagramType.first; } } mitkThrow() << "can't find diagram type!"; } std::string QmitkChartWidget::Impl::GetDiagramTypeAsString() const { return GetC3Data()->GetDiagramType().toString().toStdString(); } QmitkChartData* QmitkChartWidget::Impl::GetC3Data() const { return m_C3Data; } std::vector* QmitkChartWidget::Impl::GetC3xyData() const { return m_C3xyData; } void QmitkChartWidget::Impl::ClearData2D() { GetC3xyData()->clear(); } QmitkChartWidget::QmitkChartWidget(ChartType type, QWidget* parent) : QWidget(parent), m_Impl(new Impl(this)) { SetChartType(type); } -void QmitkChartWidget::Impl::callJavaScriptFuntion(const QString& command) +void QmitkChartWidget::Impl::CallJavaScriptFuntion(const QString& command) { m_WebEngineView->page()->runJavaScript(command); } void QmitkChartWidget::Impl::ClearJavaScriptChart() { m_WebEngineView->setUrl(QUrl(QStringLiteral("qrc:///C3js/empty.html"))); } -void QmitkChartWidget::Impl::initializeJavaScriptChart() +void QmitkChartWidget::Impl::InitializeJavaScriptChart() { m_WebChannel->registerObject(QStringLiteral("chartData"), m_C3Data); unsigned count = 0; for (auto& xyData : *m_C3xyData) { QString variableName = "xyData" + QString::number(count); m_WebChannel->registerObject(variableName, xyData); count++; } m_WebEngineView->load(QUrl(QStringLiteral("qrc:///C3js/QmitkChartWidget.html"))); } QmitkChartWidget::~QmitkChartWidget() { delete m_Impl; } -void QmitkChartWidget::AddData2D(const std::map& data2D) +void QmitkChartWidget::AddData2D(const std::map& data2D, const std::string& label) { - m_Impl->AddData2D(data2D); + m_Impl->AddData2D(data2D, label); } -void QmitkChartWidget::AddData1D(const std::vector& data1D) +void QmitkChartWidget::SetColor(const std::string& label, const std::string& colorName) { - m_Impl->AddData1D(data1D); + m_Impl->SetColor(label, colorName); } -void QmitkChartWidget::SetDataLabels(const std::vector& labels) +void QmitkChartWidget::SetLineStyle(const std::string& label, LineStyle style) { - m_Impl->SetDataLabels(labels); + if (m_Impl->GetDiagramType() == ChartType::line) { + m_Impl->SetLineStyle(label, style); +} +} + +void QmitkChartWidget::AddData1D(const std::vector& data1D, const std::string& label) +{ + m_Impl->AddData1D(data1D, label); } std::vector QmitkChartWidget::GetDataLabels() const { return m_Impl->GetDataLabels(); } void QmitkChartWidget::SetXAxisLabel(const std::string & label) { m_Impl->SetXAxisLabel(label); } std::string QmitkChartWidget::GetXAxisLabel() const { return m_Impl->GetXAxisLabel(); } void QmitkChartWidget::SetYAxisLabel(const std::string & label) { m_Impl->SetYAxisLabel(label); } std::string QmitkChartWidget::GetYAxisLabel() const { return m_Impl->GetYAxisLabel(); } void QmitkChartWidget::SetDiagramTitle(const std::string & title) { m_Impl->SetDiagramTitle(title); } std::string QmitkChartWidget::GetDiagramTitle() const { return m_Impl->GetDiagramTitle(); } void QmitkChartWidget::SetChartType(ChartType type) { m_Impl->SetDiagramType(type); } QmitkChartWidget::ChartType QmitkChartWidget::GetChartType() const { return m_Impl->GetDiagramType(); } void QmitkChartWidget::SetLegendPosition(LegendPosition position) { m_Impl->SetLegendPosition(position); } QmitkChartWidget::LegendPosition QmitkChartWidget::GetLegendPosition() const { return m_Impl->GetLegendPosition(); } void QmitkChartWidget::Show(bool showSubChart) { this->m_Impl->GetC3Data()->SetAppearance(m_Impl->GetC3Data()->GetDiagramType(), showSubChart, m_Impl->GetC3Data()->GetDiagramType()== QVariant("pie")); - - m_Impl->initializeJavaScriptChart(); + m_Impl->InitializeJavaScriptChart(); } void QmitkChartWidget::Clear() { m_Impl->ClearData2D(); m_Impl->GetC3xyData()->clear(); m_Impl->ClearJavaScriptChart(); } void QmitkChartWidget::OnLoadFinished(bool isLoadSuccessfull) { if(isLoadSuccessfull) { emit PageSuccessfullyLoaded(); } } void QmitkChartWidget::SetChartTypeAndReload(ChartType type) { SetChartType(type); auto diagramTypeName = m_Impl->GetDiagramTypeAsString(); const QString command = QString::fromStdString("transformView('" + diagramTypeName + "')"); - m_Impl->callJavaScriptFuntion(command); + m_Impl->CallJavaScriptFuntion(command); } void QmitkChartWidget::SetTheme(ChartStyle themeEnabled) { QString command; if (themeEnabled == ChartStyle::darkstyle) { command = QString("changeTheme('dark')"); } else { command = QString("changeTheme('default')"); } - m_Impl->callJavaScriptFuntion(command); + m_Impl->CallJavaScriptFuntion(command); } void QmitkChartWidget::Reload(bool showSubChart) { QString subChartString; if (showSubChart) { subChartString = "true"; } else { subChartString = "false"; } const QString command = QString("ReloadChart(" + subChartString + ")"); - m_Impl->callJavaScriptFuntion(command); + m_Impl->CallJavaScriptFuntion(command); } diff --git a/Modules/Chart/src/QmitkChartxyData.cpp b/Modules/Chart/src/QmitkChartxyData.cpp index 3f1d17e2ba..8a4cf7b6bc 100644 --- a/Modules/Chart/src/QmitkChartxyData.cpp +++ b/Modules/Chart/src/QmitkChartxyData.cpp @@ -1,48 +1,49 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include -QmitkChartxyData::QmitkChartxyData(const QMap& data) { - SetData(data); +QmitkChartxyData::QmitkChartxyData(const QMap& data, const QVariant& label) : m_Color(""), m_LineStyleName("solid") { + SetData(data, label); } -QmitkChartxyData::QmitkChartxyData(const QList& data) { +QmitkChartxyData::QmitkChartxyData(const QList& data, const QVariant& label) : m_Color(""), m_LineStyleName("solid") { QMap augmentedData; unsigned int count = 0; //just augment the 1D data for (const auto& ele : data) { augmentedData[count] = ele; count++; } - SetData(augmentedData); + SetData(augmentedData, label); } -void QmitkChartxyData::SetData(const QMap& data) +void QmitkChartxyData::SetData(const QMap& data, const QVariant& label) { for (const auto& entry : data.toStdMap()) { m_XData.push_back(entry.first); m_YData.push_back(entry.second); } + m_Label = label; } void QmitkChartxyData::ClearData() { this->m_YData.clear(); this->m_XData.clear(); } diff --git a/Plugins/org.mitk.gui.qt.measurementtoolbox/src/internal/QmitkImageStatisticsView.cpp b/Plugins/org.mitk.gui.qt.measurementtoolbox/src/internal/QmitkImageStatisticsView.cpp index 4b71a93d32..7f737848bd 100644 --- a/Plugins/org.mitk.gui.qt.measurementtoolbox/src/internal/QmitkImageStatisticsView.cpp +++ b/Plugins/org.mitk.gui.qt.measurementtoolbox/src/internal/QmitkImageStatisticsView.cpp @@ -1,1334 +1,1331 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "QmitkImageStatisticsView.h" // Qt includes #include #include #include // berry includes #include // mitk includes #include "mitkNodePredicateDataType.h" #include "mitkNodePredicateOr.h" #include "mitkPlanarFigureInteractor.h" #include "mitkImageTimeSelector.h" #include #include // itk includes #include "itksys/SystemTools.hxx" #include #include "itkImageRegionConstIteratorWithIndex.h" #include //blueberry includes #include #include const std::string QmitkImageStatisticsView::VIEW_ID = "org.mitk.views.imagestatistics"; const int QmitkImageStatisticsView::STAT_TABLE_BASE_HEIGHT = 180; QmitkImageStatisticsView::QmitkImageStatisticsView(QObject* /*parent*/, const char* /*name*/) : m_Controls( nullptr ), m_TimeStepperAdapter( nullptr ), m_SelectedImage( nullptr ), m_SelectedImageMask( nullptr ), m_SelectedPlanarFigure( nullptr ), m_ImageObserverTag( -1 ), m_ImageMaskObserverTag( -1 ), m_PlanarFigureObserverTag( -1 ), m_TimeObserverTag( -1 ), m_CurrentStatisticsValid( false ), m_StatisticsUpdatePending( false ), m_DataNodeSelectionChanged ( false ), m_Visible(false) { this->m_CalculationThread = new QmitkImageStatisticsCalculationThread; } QmitkImageStatisticsView::~QmitkImageStatisticsView() { if ( m_SelectedImage != nullptr ) m_SelectedImage->RemoveObserver( m_ImageObserverTag ); if ( m_SelectedImageMask != nullptr ) m_SelectedImageMask->RemoveObserver( m_ImageMaskObserverTag ); if ( m_SelectedPlanarFigure != nullptr ) m_SelectedPlanarFigure->RemoveObserver( m_PlanarFigureObserverTag ); while(this->m_CalculationThread->isRunning()) // wait until thread has finished { itksys::SystemTools::Delay(100); } delete this->m_CalculationThread; } void QmitkImageStatisticsView::CreateQtPartControl(QWidget *parent) { if (m_Controls == nullptr) { m_Controls = new Ui::QmitkImageStatisticsViewControls; m_Controls->setupUi(parent); CreateConnections(); m_Controls->m_ErrorMessageLabel->hide(); m_Controls->m_StatisticsWidgetStack->setCurrentIndex(0); m_Controls->m_BinSizeFrame->setEnabled(false); } } void QmitkImageStatisticsView::OnPageSuccessfullyLoaded() { berry::IPreferencesService* prefService = berry::WorkbenchPlugin::GetDefault()->GetPreferencesService(); m_StylePref = prefService->GetSystemPreferences()->Node(berry::QtPreferences::QT_STYLES_NODE); QString styleName = m_StylePref->Get(berry::QtPreferences::QT_STYLE_NAME, ""); if (styleName == ":/org.blueberry.ui.qt/darkstyle.qss") { this->m_Controls->m_JSHistogram->SetTheme(QmitkChartWidget::ChartStyle::darkstyle); } else { this->m_Controls->m_JSHistogram->SetTheme(QmitkChartWidget::ChartStyle::defaultstyle); } } void QmitkImageStatisticsView::CreateConnections() { if ( m_Controls ) { connect( (QObject*)(this->m_Controls->m_ButtonCopyHistogramToClipboard), SIGNAL(clicked()),(QObject*) this, SLOT(OnClipboardHistogramButtonClicked()) ); connect( (QObject*)(this->m_Controls->m_ButtonCopyStatisticsToClipboard), SIGNAL(clicked()),(QObject*) this, SLOT(OnClipboardStatisticsButtonClicked()) ); connect( (QObject*)(this->m_Controls->m_IgnoreZerosCheckbox), SIGNAL(clicked()),(QObject*) this, SLOT(OnIgnoreZerosCheckboxClicked()) ); connect( (QObject*) this->m_CalculationThread, SIGNAL(finished()),this, SLOT( OnThreadedStatisticsCalculationEnds()),Qt::QueuedConnection); connect( (QObject*) this, SIGNAL(StatisticsUpdate()),this, SLOT( RequestStatisticsUpdate()), Qt::QueuedConnection); connect( (QObject*) this->m_Controls->m_StatisticsTable, SIGNAL(cellDoubleClicked(int,int)),this, SLOT( JumpToCoordinates(int,int)) ); connect((QObject*)(this->m_Controls->m_barRadioButton), SIGNAL(clicked()), (QObject*)(this), SLOT(OnBarRadioButtonSelected())); connect((QObject*)(this->m_Controls->m_lineRadioButton), SIGNAL(clicked()), (QObject*)(this), SLOT(OnLineRadioButtonSelected())); connect( (QObject*) (this->m_Controls->m_HistogramBinSizeSpinbox), SIGNAL(editingFinished()), this, SLOT(OnHistogramBinSizeBoxValueChanged())); connect((QObject*)(this->m_Controls->m_UseDefaultBinSizeBox), SIGNAL(clicked()), (QObject*) this, SLOT(OnDefaultBinSizeBoxChanged())); connect((QObject*)(this->m_Controls->m_ShowSubchartCheckBox), SIGNAL(clicked()), (QObject*) this, SLOT(OnShowSubchartBoxChanged())); connect((QObject*)(this->m_Controls->m_JSHistogram), SIGNAL(PageSuccessfullyLoaded()), (QObject*) this, SLOT(OnPageSuccessfullyLoaded())); } } void QmitkImageStatisticsView::OnDefaultBinSizeBoxChanged() { m_Controls->m_BinSizeFrame->setEnabled(!m_Controls->m_UseDefaultBinSizeBox->isChecked()); if (m_CalculationThread != nullptr){ m_Controls->m_HistogramBinSizeSpinbox->setValue(m_CalculationThread->GetHistogramBinSize()); m_CalculationThread->SetUseDefaultNBins(m_Controls->m_UseDefaultBinSizeBox->isChecked()); } this->UpdateStatistics(); } void QmitkImageStatisticsView::OnShowSubchartBoxChanged() { bool showSubchart = this->m_Controls->m_ShowSubchartCheckBox->isChecked(); this->m_Controls->m_JSHistogram->Reload(showSubchart); } void QmitkImageStatisticsView::OnBarRadioButtonSelected() { this->m_Controls->m_JSHistogram->SetChartTypeAndReload(QmitkChartWidget::ChartType::bar); } void QmitkImageStatisticsView::OnLineRadioButtonSelected() { this->m_Controls->m_JSHistogram->SetChartTypeAndReload(QmitkChartWidget::ChartType::line); } void QmitkImageStatisticsView::PartClosed(const berry::IWorkbenchPartReference::Pointer& ) { } void QmitkImageStatisticsView::OnTimeChanged(const itk::EventObject& e) { if (this->m_SelectedDataNodes.isEmpty() || this->m_SelectedImage == nullptr) return; const mitk::SliceNavigationController::GeometryTimeEvent* timeEvent = dynamic_cast(&e); assert(timeEvent != nullptr); int timestep = timeEvent->GetPos(); if (this->m_SelectedImage->GetTimeSteps() > 1) { for (int x = 0; x < this->m_Controls->m_StatisticsTable->columnCount(); x++) { for (int y = 0; y < this->m_Controls->m_StatisticsTable->rowCount(); y++) { QTableWidgetItem* item = this->m_Controls->m_StatisticsTable->item(y, x); if (item == nullptr) break; if (x == timestep) { item->setBackgroundColor(Qt::yellow); } else { if (y % 2 == 0) item->setBackground(this->m_Controls->m_StatisticsTable->palette().base()); else item->setBackground(this->m_Controls->m_StatisticsTable->palette().alternateBase()); } } } this->m_Controls->m_StatisticsTable->viewport()->update(); } if ((this->m_SelectedImage->GetTimeSteps() == 1 && timestep == 0) || this->m_SelectedImage->GetTimeSteps() > 1) { // display histogram for selected timestep this->m_Controls->m_JSHistogram->Clear(); QmitkImageStatisticsCalculationThread::HistogramType::ConstPointer histogram = (QmitkImageStatisticsCalculationThread::HistogramType::ConstPointer)this->m_CalculationThread->GetTimeStepHistogram(timestep); if (histogram.IsNotNull()) { bool closedFigure = this->m_CalculationThread->GetStatisticsUpdateSuccessFlag(); - if ( closedFigure ) + if (closedFigure) { - this->m_Controls->m_JSHistogram->AddData2D(ConvertHistogramToMap(histogram) ); - if (this->m_Controls->m_lineRadioButton->isChecked()) - { - this->m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::line); + this->m_Controls->m_JSHistogram->AddData2D(ConvertHistogramToMap(histogram), m_Controls->m_SelectedFeatureImageLabel->text().toStdString()); + if (this->m_Controls->m_lineRadioButton->isChecked()) + { + this->m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::line); + } + else + { + this->m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::bar); + } + this->m_Controls->m_JSHistogram->SetXAxisLabel("Grey value"); + this->m_Controls->m_JSHistogram->SetYAxisLabel("Frequency"); + this->m_Controls->m_JSHistogram->Show(this->m_Controls->m_ShowSubchartCheckBox->isChecked()); } - else - { - this->m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::bar); } - this->m_Controls->m_JSHistogram->SetDataLabels({ m_Controls->m_SelectedFeatureImageLabel->text().toStdString() }); - this->m_Controls->m_JSHistogram->SetXAxisLabel("Grey value"); - this->m_Controls->m_JSHistogram->SetYAxisLabel("Frequency"); - this->m_Controls->m_JSHistogram->Show(this->m_Controls->m_ShowSubchartCheckBox->isChecked()); - } -} } } void QmitkImageStatisticsView::JumpToCoordinates(int row ,int col) { if(m_SelectedDataNodes.isEmpty()) { MITK_WARN("QmitkImageStatisticsView") << "No data node selected for statistics calculation." ; return; } mitk::Point3D world; if (row==5 && !m_WorldMinList.empty()) world = m_WorldMinList[col]; else if (row==4 && !m_WorldMaxList.empty()) world = m_WorldMaxList[col]; else return; mitk::IRenderWindowPart* part = this->GetRenderWindowPart(); if (part) { part->GetQmitkRenderWindow("axial")->GetSliceNavigationController()->SelectSliceByPoint(world); part->GetQmitkRenderWindow("sagittal")->GetSliceNavigationController()->SelectSliceByPoint(world); part->GetQmitkRenderWindow("coronal")->GetSliceNavigationController()->SelectSliceByPoint(world); mitk::SliceNavigationController::GeometryTimeEvent timeEvent(this->m_SelectedImage->GetTimeGeometry(), col); part->GetQmitkRenderWindow("axial")->GetSliceNavigationController()->SetGeometryTime(timeEvent); } } void QmitkImageStatisticsView::OnIgnoreZerosCheckboxClicked() { emit StatisticsUpdate(); } void QmitkImageStatisticsView::OnClipboardHistogramButtonClicked() { if ( m_CurrentStatisticsValid && !( m_SelectedPlanarFigure != nullptr)) { const unsigned int t = this->GetRenderWindowPart()->GetTimeNavigationController()->GetTime()->GetPos(); typedef mitk::ImageStatisticsCalculator::HistogramType HistogramType; const HistogramType *histogram = this->m_CalculationThread->GetTimeStepHistogram(t).GetPointer(); QString clipboard( "Measurement \t Frequency\n" ); for ( HistogramType::ConstIterator it = histogram->Begin(); it != histogram->End(); ++it ) { if( m_Controls->m_HistogramBinSizeSpinbox->value() == 1.0) { clipboard = clipboard.append( "%L1 \t %L2\n" ) .arg( it.GetMeasurementVector()[0], 0, 'f', 0 ) .arg( it.GetFrequency() ); } else { clipboard = clipboard.append( "%L1 \t %L2\n" ) .arg( it.GetMeasurementVector()[0], 0, 'f', 2 ) .arg( it.GetFrequency() ); } } QApplication::clipboard()->setText( clipboard, QClipboard::Clipboard ); } // If a (non-closed) PlanarFigure is selected, display a line profile widget else if ( m_CurrentStatisticsValid && (m_SelectedPlanarFigure != nullptr )) { /*auto intensity = m_Controls->m_JSHistogram->GetFrequency(); auto pixel = m_Controls->m_JSHistogram->GetMeasurement(); QString clipboard( "Pixel \t Intensity\n" ); auto j = pixel.begin(); for (auto i = intensity.begin(); i < intensity.end(); i++) { assert(j != pixel.end()); clipboard = clipboard.append( "%L1 \t %L2\n" ) .arg( (*j).toString()) .arg( (*i).toString()); j++; } QApplication::clipboard()->setText( clipboard, QClipboard::Clipboard ); */ } else { QApplication::clipboard()->clear(); } } void QmitkImageStatisticsView::OnClipboardStatisticsButtonClicked() { QLocale tempLocal; QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); if ( m_CurrentStatisticsValid && !( m_SelectedPlanarFigure != nullptr)) { const std::vector &statistics = this->m_CalculationThread->GetStatisticsData(); // Set time borders for for loop ;) unsigned int startT, endT; if(this->m_Controls->m_CheckBox4dCompleteTable->checkState()==Qt::CheckState::Unchecked) { startT = this->GetRenderWindowPart()->GetTimeNavigationController()->GetTime()-> GetPos(); endT = startT+1; } else { startT = 0; endT = statistics.size(); } QVector< QVector > statisticsTable; QStringList headline; // Create Headline headline << " " << "Mean" << "Median" << "StdDev" << "RMS" << "Max" << "Min" << "NumberOfVoxels" << "Skewness" << "Kurtosis" << "Uniformity" << "Entropy" << "MPP" << "UPP" << "V [mm³]"; for(int i=0;i row; row.append(headline.at(i)); statisticsTable.append(row); } // Fill Table for(unsigned int t=startT;tGetMean()) << QString::number(statistics[t]->GetMedian()) << QString::number(statistics[t]->GetStd()) << QString::number(statistics[t]->GetRMS()) << QString::number(statistics[t]->GetMax()) << QString::number(statistics[t]->GetMin()) << QString::number(statistics[t]->GetN()) << QString::number(statistics[t]->GetSkewness()) << QString::number(statistics[t]->GetKurtosis()) << QString::number(statistics[t]->GetUniformity()) << QString::number(statistics[t]->GetEntropy()) << QString::number(statistics[t]->GetMPP()) << QString::number(statistics[t]->GetUPP()) << QString::number(m_Controls->m_StatisticsTable->item(7, 0)->data(Qt::DisplayRole).toDouble()); for(int z=0;zsetText(clipboard, QClipboard::Clipboard); } else { QApplication::clipboard()->clear(); } QLocale::setDefault(tempLocal); } void QmitkImageStatisticsView::OnSelectionChanged( berry::IWorkbenchPart::Pointer /*part*/, const QList &nodes ) { if (this->m_Visible) { this->SelectionChanged( nodes ); } else { this->m_DataNodeSelectionChanged = true; } } void QmitkImageStatisticsView::SelectionChanged(const QList &selectedNodes) { //Clear Histogram if data node is deselected m_Controls->m_JSHistogram->Clear(); if( this->m_StatisticsUpdatePending ) { this->m_DataNodeSelectionChanged = true; return; // not ready for new data now! } if (selectedNodes.size() == this->m_SelectedDataNodes.size()) { int i = 0; for (; i < selectedNodes.size(); ++i) { if (selectedNodes.at(i) != this->m_SelectedDataNodes.at(i)) { break; } } // node selection did not change if (i == selectedNodes.size()) return; } //reset the feature image and image mask field m_Controls->m_SelectedFeatureImageLabel->setText("None"); m_Controls->m_SelectedMaskLabel->setText("None"); this->ReinitData(); if (selectedNodes.isEmpty()) { m_Controls->m_lineRadioButton->setEnabled(true); m_Controls->m_barRadioButton->setEnabled(true); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(true); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(true); m_Controls->m_UseDefaultBinSizeBox->setEnabled(true); m_Controls->m_InfoLabel->setText(""); m_Controls->groupBox->setEnabled(false); m_Controls->groupBox_3->setEnabled(false); } else { m_Controls->groupBox->setEnabled(true); m_Controls->groupBox_3->setEnabled(true); m_Controls->m_barRadioButton->setChecked(true); } if(selectedNodes.size() == 1 || selectedNodes.size() == 2) { bool isBinary = false; selectedNodes.value(0)->GetBoolProperty("binary",isBinary); mitk::NodePredicateDataType::Pointer isLabelSet = mitk::NodePredicateDataType::New("LabelSetImage"); isBinary |= isLabelSet->CheckNode(selectedNodes.value(0)); if(isBinary) { m_Controls->m_lineRadioButton->setEnabled(true); m_Controls->m_barRadioButton->setEnabled(true); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(true); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(true); m_Controls->m_UseDefaultBinSizeBox->setEnabled(true); m_Controls->m_InfoLabel->setText(""); } for (int i= 0; i< selectedNodes.size(); ++i) { this->m_SelectedDataNodes.push_back(selectedNodes.at(i)); } this->m_DataNodeSelectionChanged = false; this->m_Controls->m_ErrorMessageLabel->setText( "" ); this->m_Controls->m_ErrorMessageLabel->hide(); emit StatisticsUpdate(); } else { this->m_DataNodeSelectionChanged = false; } } void QmitkImageStatisticsView::ReinitData() { while( this->m_CalculationThread->isRunning()) // wait until thread has finished { itksys::SystemTools::Delay(100); } if(this->m_SelectedImage != nullptr) { this->m_SelectedImage->RemoveObserver( this->m_ImageObserverTag); this->m_SelectedImage = nullptr; } if(this->m_SelectedImageMask != nullptr) { this->m_SelectedImageMask->RemoveObserver( this->m_ImageMaskObserverTag); this->m_SelectedImageMask = nullptr; } if(this->m_SelectedPlanarFigure != nullptr) { this->m_SelectedPlanarFigure->RemoveObserver( this->m_PlanarFigureObserverTag); this->m_SelectedPlanarFigure = nullptr; } this->m_SelectedDataNodes.clear(); this->m_StatisticsUpdatePending = false; m_Controls->m_ErrorMessageLabel->setText( "" ); m_Controls->m_ErrorMessageLabel->hide(); this->InvalidateStatisticsTableView(); m_Controls->m_StatisticsWidgetStack->setCurrentIndex( 0 ); } void QmitkImageStatisticsView::OnThreadedStatisticsCalculationEnds() { std::stringstream message; message << ""; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->hide(); this->WriteStatisticsToGUI(); } void QmitkImageStatisticsView::UpdateStatistics() { mitk::IRenderWindowPart* renderPart = this->GetRenderWindowPart(); if ( renderPart == nullptr ) { this->m_StatisticsUpdatePending = false; return; } m_WorldMinList.clear(); m_WorldMaxList.clear(); // classify selected nodes mitk::NodePredicateDataType::Pointer isImage = mitk::NodePredicateDataType::New("Image"); mitk::NodePredicateDataType::Pointer isLabelSet = mitk::NodePredicateDataType::New("LabelSetImage"); mitk::NodePredicateOr::Pointer imagePredicate = mitk::NodePredicateOr::New(isImage, isLabelSet); std::string maskName; std::string maskType; std::string featureImageName; unsigned int maskDimension = 0; // reset data from last run ITKCommandType::Pointer changeListener = ITKCommandType::New(); changeListener->SetCallbackFunction( this, &QmitkImageStatisticsView::SelectedDataModified ); mitk::DataNode::Pointer planarFigureNode; for( int i= 0 ; i < this->m_SelectedDataNodes.size(); ++i) { mitk::PlanarFigure::Pointer planarFig = dynamic_cast(this->m_SelectedDataNodes.at(i)->GetData()); if( imagePredicate->CheckNode(this->m_SelectedDataNodes.at(i)) ) { bool isMask = false; this->m_SelectedDataNodes.at(i)->GetPropertyValue("binary", isMask); isMask |= isLabelSet->CheckNode(this->m_SelectedDataNodes.at(i)); if( this->m_SelectedImageMask == nullptr && isMask) { this->m_SelectedImageMask = dynamic_cast(this->m_SelectedDataNodes.at(i)->GetData()); this->m_ImageMaskObserverTag = this->m_SelectedImageMask->AddObserver(itk::ModifiedEvent(), changeListener); maskName = this->m_SelectedDataNodes.at(i)->GetName(); maskType = m_SelectedImageMask->GetNameOfClass(); maskDimension = 3; } else if( !isMask ) { if(this->m_SelectedImage == nullptr) { this->m_SelectedImage = static_cast(this->m_SelectedDataNodes.at(i)->GetData()); this->m_ImageObserverTag = this->m_SelectedImage->AddObserver(itk::ModifiedEvent(), changeListener); } featureImageName = this->m_SelectedDataNodes.at(i)->GetName(); } } else if (planarFig.IsNotNull()) { if(this->m_SelectedPlanarFigure == nullptr) { this->m_SelectedPlanarFigure = planarFig; this->m_PlanarFigureObserverTag = this->m_SelectedPlanarFigure->AddObserver(mitk::EndInteractionPlanarFigureEvent(), changeListener); maskName = this->m_SelectedDataNodes.at(i)->GetName(); maskType = this->m_SelectedPlanarFigure->GetNameOfClass(); maskDimension = 2; planarFigureNode = m_SelectedDataNodes.at(i); } } else { std::stringstream message; message << "" << "Invalid data node type!" << ""; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); } } if(maskName == "") { maskName = "None"; maskType = ""; maskDimension = 0; } if(featureImageName == "") { featureImageName = "None"; } if (m_SelectedPlanarFigure != nullptr && m_SelectedImage == nullptr) { mitk::DataStorage::SetOfObjects::ConstPointer parentSet = this->GetDataStorage()->GetSources(planarFigureNode); for (unsigned int i=0; iSize(); i++) { mitk::DataNode::Pointer node = parentSet->ElementAt(i); if( imagePredicate->CheckNode(node) ) { bool isMask = false; node->GetPropertyValue("binary", isMask); isMask |= isLabelSet->CheckNode(node); if( !isMask ) { if(this->m_SelectedImage == nullptr) { this->m_SelectedImage = static_cast(node->GetData()); this->m_ImageObserverTag = this->m_SelectedImage->AddObserver(itk::ModifiedEvent(), changeListener); } } } } } unsigned int timeStep = renderPart->GetTimeNavigationController()->GetTime()->GetPos(); if ( m_SelectedImage != nullptr && m_SelectedImage->IsInitialized()) { // Check if a the selected image is a multi-channel image. If yes, statistics // cannot be calculated currently. if ( m_SelectedImage->GetPixelType().GetNumberOfComponents() > 1 ) { std::stringstream message; message << "Multi-component images not supported."; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); this->InvalidateStatisticsTableView(); m_Controls->m_StatisticsWidgetStack->setCurrentIndex( 0 ); m_CurrentStatisticsValid = false; this->m_StatisticsUpdatePending = false; m_Controls->m_lineRadioButton->setEnabled(true); m_Controls->m_barRadioButton->setEnabled(true); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(true); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(true); m_Controls->m_UseDefaultBinSizeBox->setEnabled(true); m_Controls->m_InfoLabel->setText(""); return; } std::stringstream maskLabel; maskLabel << maskName; if ( maskDimension > 0 ) { maskLabel << " [" << maskDimension << "D " << maskType << "]"; } m_Controls->m_SelectedMaskLabel->setText( maskLabel.str().c_str() ); m_Controls->m_SelectedFeatureImageLabel->setText(featureImageName.c_str()); // check time step validity if(m_SelectedImage->GetDimension() <= 3 && timeStep > m_SelectedImage->GetDimension(3)-1) { timeStep = m_SelectedImage->GetDimension(3)-1; } // Add the used mask time step to the mask label so the user knows which mask time step was used // if the image time step is bigger than the total number of mask time steps (see // ImageStatisticsCalculator::ExtractImageAndMask) if (m_SelectedImageMask != nullptr) { unsigned int maskTimeStep = timeStep; if (maskTimeStep >= m_SelectedImageMask->GetTimeSteps()) { maskTimeStep = m_SelectedImageMask->GetTimeSteps() - 1; } m_Controls->m_SelectedMaskLabel->setText(m_Controls->m_SelectedMaskLabel->text() + QString(" (t=") + QString::number(maskTimeStep) + QString(")")); } // check if the segmentation mask is empty if (m_SelectedImageMask != NULL) { typedef itk::Image ItkImageType; typedef itk::ImageRegionConstIteratorWithIndex< ItkImageType > IteratorType; ItkImageType::Pointer itkImage; mitk::CastToItkImage( m_SelectedImageMask, itkImage ); bool empty = true; IteratorType it( itkImage, itkImage->GetLargestPossibleRegion() ); while ( !it.IsAtEnd() ) { ItkImageType::ValueType val = it.Get(); if ( val != 0 ) { empty = false; break; } ++it; } if ( empty ) { std::stringstream message; message << "Empty segmentation mask selected..."; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); return; } } //// initialize thread and trigger it this->m_CalculationThread->SetIgnoreZeroValueVoxel( m_Controls->m_IgnoreZerosCheckbox->isChecked() ); this->m_CalculationThread->Initialize( m_SelectedImage, m_SelectedImageMask, m_SelectedPlanarFigure ); this->m_CalculationThread->SetTimeStep( timeStep ); std::stringstream message; message << "Calculating statistics..."; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); try { // Compute statistics // this->m_CalculationThread->SetUseDefaultBinSize(m_Controls->m_UseDefaultBinSizeBox->isChecked()); this->m_CalculationThread->start(); } catch ( const mitk::Exception& e) { std::stringstream message; message << "" << e.GetDescription() << ""; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); this->m_StatisticsUpdatePending = false; } catch ( const std::runtime_error &e ) { // In case of exception, print error message on GUI std::stringstream message; message << "" << e.what() << ""; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); this->m_StatisticsUpdatePending = false; } catch ( const std::exception &e ) { MITK_ERROR << "Caught exception: " << e.what(); // In case of exception, print error message on GUI std::stringstream message; message << "Error! Unequal Dimensions of Image and Segmentation. No recompute possible "; m_Controls->m_ErrorMessageLabel->setText( message.str().c_str() ); m_Controls->m_ErrorMessageLabel->show(); this->m_StatisticsUpdatePending = false; } } else { this->m_StatisticsUpdatePending = false; } } void QmitkImageStatisticsView::SelectedDataModified() { if( !m_StatisticsUpdatePending ) { emit StatisticsUpdate(); } } void QmitkImageStatisticsView::NodeRemoved(const mitk::DataNode *node) { while(this->m_CalculationThread->isRunning()) // wait until thread has finished { itksys::SystemTools::Delay(100); } if (node->GetData() == m_SelectedImage) { m_SelectedImage = nullptr; } } void QmitkImageStatisticsView::RequestStatisticsUpdate() { if ( !m_StatisticsUpdatePending ) { if(this->m_DataNodeSelectionChanged) { this->SelectionChanged(this->GetCurrentSelection()); } else { this->m_StatisticsUpdatePending = true; this->UpdateStatistics(); } } if (this->GetRenderWindowPart()) this->GetRenderWindowPart()->RequestUpdate(); } void QmitkImageStatisticsView::OnHistogramBinSizeBoxValueChanged() { if (m_Controls->m_HistogramBinSizeSpinbox->value() != m_HistogramBinSize) { m_HistogramBinSize = m_Controls->m_HistogramBinSizeSpinbox->value(); this->m_CalculationThread->SetHistogramBinSize(m_Controls->m_HistogramBinSizeSpinbox->value()); this->UpdateStatistics(); } } void QmitkImageStatisticsView::WriteStatisticsToGUI() { m_Controls->m_JSHistogram->Clear(); //Disconnect OnLineRadioButtonSelected() to prevent reloading chart when radiobutton is checked programmatically disconnect((QObject*)(this->m_Controls->m_JSHistogram), SIGNAL(PageSuccessfullyLoaded()), 0, 0); connect((QObject*)(this->m_Controls->m_JSHistogram), SIGNAL(PageSuccessfullyLoaded()), (QObject*) this, SLOT(OnPageSuccessfullyLoaded())); m_Controls->m_lineRadioButton->setEnabled(true); m_Controls->m_barRadioButton->setEnabled(true); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(true); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(true); m_Controls->m_InfoLabel->setText(""); if(m_DataNodeSelectionChanged) { this->m_StatisticsUpdatePending = false; this->RequestStatisticsUpdate(); return; // stop visualization of results and calculate statistics of new selection } if ( this->m_CalculationThread->GetStatisticsUpdateSuccessFlag()) { if ( this->m_CalculationThread->GetStatisticsChangedFlag() ) { // Do not show any error messages m_Controls->m_ErrorMessageLabel->hide(); m_CurrentStatisticsValid = true; } if (m_SelectedImage != nullptr) { //all statistics are now computed also on planar figures (lines, paths...)! // If a (non-closed) PlanarFigure is selected, display a line profile widget if (m_SelectedPlanarFigure != nullptr) { // Check if the (closed) planar figure is out of bounds and so no image mask could be calculated--> Intensity Profile can not be calculated bool outOfBounds = false; if ( m_SelectedPlanarFigure->IsClosed() && m_SelectedImageMask == nullptr) { outOfBounds = true; const QString message("Planar figure is on a rotated image plane or outside the image bounds."); m_Controls->m_InfoLabel->setText(message); } // check whether PlanarFigure is initialized const mitk::PlaneGeometry *planarFigurePlaneGeometry = m_SelectedPlanarFigure->GetPlaneGeometry(); if ( !(planarFigurePlaneGeometry == nullptr || outOfBounds)) { unsigned int timeStep = this->GetRenderWindowPart()->GetTimeNavigationController()->GetTime()->GetPos(); mitk::Image::Pointer image; if (this->m_CalculationThread->GetStatisticsImage()->GetDimension() == 4) { mitk::ImageTimeSelector::Pointer timeSelector = mitk::ImageTimeSelector::New(); timeSelector->SetInput(this->m_CalculationThread->GetStatisticsImage()); timeSelector->SetTimeNr(timeStep); timeSelector->Update(); image = timeSelector->GetOutput(); } else { image = this->m_CalculationThread->GetStatisticsImage(); } mitk::IntensityProfile::ConstPointer intensityProfile = (mitk::IntensityProfile::ConstPointer)mitk::ComputeIntensityProfile(image, m_SelectedPlanarFigure); auto intensityProfileList = ConvertIntensityProfileToVector(intensityProfile); m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::line); - m_Controls->m_JSHistogram->AddData1D(intensityProfileList); - m_Controls->m_JSHistogram->SetDataLabels({ "Intensity profile " + m_Controls->m_SelectedMaskLabel->text().toStdString() }); + m_Controls->m_JSHistogram->AddData1D(intensityProfileList, "Intensity profile " + m_Controls->m_SelectedMaskLabel->text().toStdString()); m_Controls->m_JSHistogram->SetXAxisLabel("Distance"); m_Controls->m_JSHistogram->SetYAxisLabel("Intensity"); m_Controls->m_JSHistogram->Show(m_Controls->m_ShowSubchartCheckBox->isChecked()); m_Controls->m_lineRadioButton->setChecked(true); m_Controls->m_lineRadioButton->setEnabled(false); m_Controls->m_barRadioButton->setEnabled(false); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(false); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(false); m_Controls->m_UseDefaultBinSizeBox->setEnabled(false); //Reconnect OnLineRadioButtonSelected() connect((QObject*)(this->m_Controls->m_JSHistogram), SIGNAL(PageSuccessfullyLoaded()), (QObject*) this, SLOT(OnLineRadioButtonSelected())); auto statisticsVector = this->m_CalculationThread->GetStatisticsData(); //only one entry (current timestep) this->FillLinearProfileStatisticsTableView(statisticsVector.front().GetPointer(), this->m_CalculationThread->GetStatisticsImage()); QString message("Only linegraph available for an intensity profile!"); if (this->m_CalculationThread->GetStatisticsImage()->GetDimension() == 4) { message += "Only current timestep displayed!"; } message += ""; m_Controls->m_InfoLabel->setText(message); m_CurrentStatisticsValid = true; } else { // Clear statistics, histogram, and GUI this->InvalidateStatisticsTableView(); m_Controls->m_StatisticsWidgetStack->setCurrentIndex( 0 ); m_CurrentStatisticsValid = false; m_Controls->m_ErrorMessageLabel->hide(); m_Controls->m_SelectedMaskLabel->setText( "None" ); this->m_StatisticsUpdatePending = false; m_Controls->m_lineRadioButton->setEnabled(true); m_Controls->m_barRadioButton->setEnabled(true); m_Controls->m_HistogramBinSizeSpinbox->setEnabled(true); m_Controls->m_HistogramBinSizeCaptionLabel->setEnabled(true); if (!outOfBounds) m_Controls->m_InfoLabel->setText(""); return; } } else { m_Controls->m_StatisticsWidgetStack->setCurrentIndex(0); m_Controls->m_HistogramBinSizeSpinbox->setValue(this->m_CalculationThread->GetHistogramBinSize()); auto histogram = this->m_CalculationThread->GetTimeStepHistogram(this->m_CalculationThread->GetTimeStep()).GetPointer(); - m_Controls->m_JSHistogram->AddData2D(ConvertHistogramToMap(histogram)); + m_Controls->m_JSHistogram->AddData2D(ConvertHistogramToMap(histogram), m_Controls->m_SelectedFeatureImageLabel->text().toStdString()); m_Controls->m_JSHistogram->SetChartType(QmitkChartWidget::ChartType::bar); - this->m_Controls->m_JSHistogram->SetDataLabels({ m_Controls->m_SelectedFeatureImageLabel->text().toStdString() }); this->m_Controls->m_JSHistogram->SetXAxisLabel("Gray value"); this->m_Controls->m_JSHistogram->SetYAxisLabel("Frequency"); m_Controls->m_UseDefaultBinSizeBox->setEnabled(true); m_Controls->m_JSHistogram->Show(this->m_Controls->m_ShowSubchartCheckBox->isChecked()); this->FillStatisticsTableView(this->m_CalculationThread->GetStatisticsData(), this->m_CalculationThread->GetStatisticsImage()); } m_CurrentStatisticsValid = true; } } else { m_Controls->m_SelectedMaskLabel->setText("None"); m_Controls->m_ErrorMessageLabel->setText(m_CalculationThread->GetLastErrorMessage().c_str()); m_Controls->m_ErrorMessageLabel->show(); // Clear statistics and histogram this->InvalidateStatisticsTableView(); m_Controls->m_StatisticsWidgetStack->setCurrentIndex(0); m_CurrentStatisticsValid = false; } berry::IPreferencesService* prefService = berry::WorkbenchPlugin::GetDefault()->GetPreferencesService(); m_StylePref = prefService->GetSystemPreferences()->Node(berry::QtPreferences::QT_STYLES_NODE); this->m_StatisticsUpdatePending = false; } void QmitkImageStatisticsView::FillStatisticsTableView( const std::vector &statistics, const mitk::Image *image ) { this->m_Controls->m_StatisticsTable->setColumnCount(image->GetTimeSteps()); this->m_Controls->m_StatisticsTable->horizontalHeader()->setVisible(image->GetTimeSteps() > 1); // Set Checkbox for complete copy of statistic table if(image->GetTimeSteps()>1) { this->m_Controls->m_CheckBox4dCompleteTable->setEnabled(true); } else { this->m_Controls->m_CheckBox4dCompleteTable->setEnabled(false); this->m_Controls->m_CheckBox4dCompleteTable->setChecked(false); } for (unsigned int t = 0; t < image->GetTimeSteps(); t++) { this->m_Controls->m_StatisticsTable->setHorizontalHeaderItem(t, new QTableWidgetItem(QString::number(t))); if (statistics.at(t)->GetMaxIndex().size()==3) { mitk::Point3D index, max, min; index[0] = statistics.at(t)->GetMaxIndex()[0]; index[1] = statistics.at(t)->GetMaxIndex()[1]; index[2] = statistics.at(t)->GetMaxIndex()[2]; m_SelectedImage->GetGeometry()->IndexToWorld(index, max); this->m_WorldMaxList.push_back(max); index[0] = statistics.at(t)->GetMinIndex()[0]; index[1] = statistics.at(t)->GetMinIndex()[1]; index[2] = statistics.at(t)->GetMinIndex()[2]; m_SelectedImage->GetGeometry()->IndexToWorld(index, min); this->m_WorldMinList.push_back(min); } auto statisticsVector = AssembleStatisticsIntoVector(statistics.at(t).GetPointer(), image); unsigned int count = 0; for (const auto& entry : statisticsVector) { auto item = new QTableWidgetItem(entry); this->m_Controls->m_StatisticsTable->setItem(count, t, item); count++; } } this->m_Controls->m_StatisticsTable->resizeColumnsToContents(); int height = STAT_TABLE_BASE_HEIGHT; if (this->m_Controls->m_StatisticsTable->horizontalHeader()->isVisible()) height += this->m_Controls->m_StatisticsTable->horizontalHeader()->height(); if (this->m_Controls->m_StatisticsTable->horizontalScrollBar()->isVisible()) height += this->m_Controls->m_StatisticsTable->horizontalScrollBar()->height(); this->m_Controls->m_StatisticsTable->setMinimumHeight(height); // make sure the current timestep's column is highlighted (and the correct histogram is displayed) unsigned int t = this->GetRenderWindowPart()->GetTimeNavigationController()->GetTime()-> GetPos(); mitk::SliceNavigationController::GeometryTimeEvent timeEvent(this->m_SelectedImage->GetTimeGeometry(), t); this->OnTimeChanged(timeEvent); t = std::min(image->GetTimeSteps() - 1, t); // See bug 18340 /*QString hotspotMean; hotspotMean.append(QString("%1").arg(s[t].GetHotspotStatistics().GetMean(), 0, 'f', decimals)); hotspotMean += " ("; for (int i=0; im_Controls->m_StatisticsTable->setItem( 7, t, new QTableWidgetItem( hotspotMean ) ); QString hotspotMax; hotspotMax.append(QString("%1").arg(s[t].GetHotspotStatistics().GetMax(), 0, 'f', decimals)); hotspotMax += " ("; for (int i=0; im_Controls->m_StatisticsTable->setItem( 8, t, new QTableWidgetItem( hotspotMax ) ); QString hotspotMin; hotspotMin.append(QString("%1").arg(s[t].GetHotspotStatistics().GetMin(), 0, 'f', decimals)); hotspotMin += " ("; for (int i=0; im_Controls->m_StatisticsTable->setItem( 9, t, new QTableWidgetItem( hotspotMin ) );*/ } std::vector QmitkImageStatisticsView::AssembleStatisticsIntoVector(mitk::ImageStatisticsCalculator::StatisticsContainer::ConstPointer statistics, mitk::Image::ConstPointer image, bool noVolumeDefined) const { std::vector result; unsigned int decimals = 2; //statistics of higher order should have 5 decimal places because they used to be very small unsigned int decimalsHigherOrderStatistics = 5; if (image->GetPixelType().GetComponentType() == itk::ImageIOBase::DOUBLE || image->GetPixelType().GetComponentType() == itk::ImageIOBase::FLOAT) { decimals = 5; } result.push_back(GetFormattedString(statistics->GetMean(), decimals)); result.push_back(GetFormattedString(statistics->GetMedian(), decimals)); result.push_back(GetFormattedString(statistics->GetStd(), decimals)); result.push_back(GetFormattedString(statistics->GetRMS(), decimals)); result.push_back(GetFormattedString(statistics->GetMax(), decimals) + " " + GetFormattedIndex(statistics->GetMaxIndex())); result.push_back(GetFormattedString(statistics->GetMin(), decimals) + " " + GetFormattedIndex(statistics->GetMinIndex())); //to prevent large negative values of empty image statistics if (statistics->GetN() != std::numeric_limits::min()) { result.push_back(GetFormattedString(statistics->GetN(), 0)); const mitk::BaseGeometry *geometry = image->GetGeometry(); if (geometry != NULL && !noVolumeDefined) { const mitk::Vector3D &spacing = image->GetGeometry()->GetSpacing(); double volume = spacing[0] * spacing[1] * spacing[2] * static_cast(statistics->GetN()); result.push_back(GetFormattedString(volume, decimals)); } else { result.push_back("NA"); } } else { result.push_back("NA"); result.push_back("NA"); } result.push_back(GetFormattedString(statistics->GetSkewness(), decimalsHigherOrderStatistics)); result.push_back(GetFormattedString(statistics->GetKurtosis(), decimalsHigherOrderStatistics)); result.push_back(GetFormattedString(statistics->GetUniformity(), decimalsHigherOrderStatistics)); result.push_back(GetFormattedString(statistics->GetEntropy(), decimalsHigherOrderStatistics)); result.push_back(GetFormattedString(statistics->GetMPP(), decimals)); result.push_back(GetFormattedString(statistics->GetUPP(), decimalsHigherOrderStatistics)); return result; } void QmitkImageStatisticsView::FillLinearProfileStatisticsTableView(mitk::ImageStatisticsCalculator::StatisticsContainer::ConstPointer statistics, const mitk::Image *image) { this->m_Controls->m_StatisticsTable->setColumnCount(1); this->m_Controls->m_StatisticsTable->horizontalHeader()->setVisible(false); m_PlanarFigureStatistics = this->AssembleStatisticsIntoVector(statistics, image, true); for (unsigned int i = 0; i< m_PlanarFigureStatistics.size(); i++) { this->m_Controls->m_StatisticsTable->setItem( i, 0, new QTableWidgetItem(m_PlanarFigureStatistics[i] )); } this->m_Controls->m_StatisticsTable->resizeColumnsToContents(); int height = STAT_TABLE_BASE_HEIGHT; if (this->m_Controls->m_StatisticsTable->horizontalHeader()->isVisible()) height += this->m_Controls->m_StatisticsTable->horizontalHeader()->height(); if (this->m_Controls->m_StatisticsTable->horizontalScrollBar()->isVisible()) height += this->m_Controls->m_StatisticsTable->horizontalScrollBar()->height(); this->m_Controls->m_StatisticsTable->setMinimumHeight(height); } void QmitkImageStatisticsView::InvalidateStatisticsTableView() { this->m_Controls->m_StatisticsTable->horizontalHeader()->setVisible(false); this->m_Controls->m_StatisticsTable->setColumnCount(1); for ( int i = 0; i < this->m_Controls->m_StatisticsTable->rowCount(); ++i ) { { this->m_Controls->m_StatisticsTable->setItem( i, 0, new QTableWidgetItem( "NA" ) ); } } this->m_Controls->m_StatisticsTable->setMinimumHeight(STAT_TABLE_BASE_HEIGHT); } void QmitkImageStatisticsView::Activated() { } void QmitkImageStatisticsView::Deactivated() { } void QmitkImageStatisticsView::Visible() { m_Visible = true; mitk::IRenderWindowPart* renderWindow = GetRenderWindowPart(); if (renderWindow) { itk::ReceptorMemberCommand::Pointer cmdTimeEvent = itk::ReceptorMemberCommand::New(); cmdTimeEvent->SetCallbackFunction(this, &QmitkImageStatisticsView::OnTimeChanged); // It is sufficient to add the observer to the axial render window since the GeometryTimeEvent // is always triggered by all views. m_TimeObserverTag = renderWindow->GetQmitkRenderWindow("axial")-> GetSliceNavigationController()-> AddObserver(mitk::SliceNavigationController::GeometryTimeEvent(nullptr, 0), cmdTimeEvent); } if (m_DataNodeSelectionChanged) { if (this->IsCurrentSelectionValid()) { this->SelectionChanged(this->GetCurrentSelection()); } else { this->SelectionChanged(this->GetDataManagerSelection()); } m_DataNodeSelectionChanged = false; } } void QmitkImageStatisticsView::Hidden() { m_Visible = false; // The slice navigation controller observer is removed here instead of in the destructor. // If it was called in the destructor, the application would freeze because the view's // destructor gets called after the render windows have been destructed. if ( m_TimeObserverTag != 0 ) { mitk::IRenderWindowPart* renderWindow = GetRenderWindowPart(); if (renderWindow) { renderWindow->GetQmitkRenderWindow("axial")->GetSliceNavigationController()-> RemoveObserver( m_TimeObserverTag ); } m_TimeObserverTag = 0; } } void QmitkImageStatisticsView::SetFocus() { } std::map QmitkImageStatisticsView::ConvertHistogramToMap(itk::Statistics::Histogram::ConstPointer histogram) const { std::map histogramMap; auto endIt = histogram->End(); auto it = histogram->Begin(); // generating Lists of measurement and frequencies for (; it != endIt; ++it) { double frequency = it.GetFrequency(); double measurement = it.GetMeasurementVector()[0]; histogramMap.emplace(measurement, frequency); } return histogramMap; } std::vector QmitkImageStatisticsView::ConvertIntensityProfileToVector(mitk::IntensityProfile::ConstPointer intensityProfile) const { std::vector intensityProfileList; auto end = intensityProfile->End(); for (auto it = intensityProfile->Begin(); it != end; ++it) { intensityProfileList.push_back(it.GetMeasurementVector()[0]); } return intensityProfileList; } QString QmitkImageStatisticsView::GetFormattedString(double value, unsigned int decimals) const { typedef mitk::ImageStatisticsCalculator::StatisticsContainer::RealType RealType; RealType maxVal = std::numeric_limits::max(); if (value == maxVal) { return QString("NA"); } else { return QString("%1").arg(value, 0, 'f', decimals); } } QString QmitkImageStatisticsView::GetFormattedIndex(const vnl_vector& vector) const { if (vector.empty()) { return QString(); } QString formattedIndex("("); for (const auto& entry : vector) { formattedIndex += QString::number(entry); formattedIndex += ","; } formattedIndex.chop(1); formattedIndex += ")"; return formattedIndex; }