1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.viewers
;
12 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNull
;
13 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.nullToEmptyString
;
15 import java
.math
.BigDecimal
;
16 import java
.text
.Format
;
17 import java
.util
.ArrayList
;
18 import java
.util
.HashSet
;
19 import java
.util
.List
;
21 import java
.util
.concurrent
.TimeUnit
;
22 import java
.util
.function
.ToDoubleFunction
;
24 import org
.eclipse
.jdt
.annotation
.NonNull
;
25 import org
.eclipse
.jdt
.annotation
.Nullable
;
26 import org
.eclipse
.swt
.SWT
;
27 import org
.eclipse
.swt
.events
.SelectionEvent
;
28 import org
.eclipse
.swt
.events
.SelectionListener
;
29 import org
.eclipse
.swt
.graphics
.Color
;
30 import org
.eclipse
.swt
.graphics
.Font
;
31 import org
.eclipse
.swt
.graphics
.GC
;
32 import org
.eclipse
.swt
.graphics
.Image
;
33 import org
.eclipse
.swt
.graphics
.Point
;
34 import org
.eclipse
.swt
.graphics
.Rectangle
;
35 import org
.eclipse
.swt
.widgets
.Composite
;
36 import org
.eclipse
.swt
.widgets
.Control
;
37 import org
.eclipse
.swt
.widgets
.Display
;
38 import org
.eclipse
.swt
.widgets
.Event
;
39 import org
.eclipse
.swt
.widgets
.Listener
;
40 import org
.eclipse
.swt
.widgets
.ToolBar
;
41 import org
.eclipse
.swt
.widgets
.ToolItem
;
42 import org
.eclipse
.tracecompass
.common
.core
.format
.DecimalUnitFormat
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiChartModel
;
45 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiResultTable
;
46 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
.LamiTableEntry
;
47 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.format
.LamiDecimalUnitFormat
;
48 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.format
.LamiTimeStampFormat
;
49 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.ui
.signals
.LamiSelectionUpdateSignal
;
50 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalHandler
;
51 import org
.eclipse
.tracecompass
.tmf
.ui
.viewers
.TmfViewer
;
52 import org
.eclipse
.ui
.ISharedImages
;
53 import org
.eclipse
.ui
.PlatformUI
;
54 import org
.swtchart
.Chart
;
55 import org
.swtchart
.ITitle
;
57 import com
.google
.common
.collect
.ImmutableList
;
60 * Abstract XYChart Viewer for LAMI views.
62 * @author Michael Jeanson
65 public abstract class LamiXYChartViewer
extends TmfViewer
implements ILamiViewer
{
67 /** Ellipsis character */
68 protected static final String ELLIPSIS
= "…"; //$NON-NLS-1$
71 * String representing unknown values. Can be present even in numerical
74 protected static final String UNKNOWN
= "?"; //$NON-NLS-1$
76 /** Zero long value */
77 protected static final long ZERO_LONG
= 0L;
78 /** Zero double value */
79 protected static final double ZERO_DOUBLE
= 0.0;
82 * Function to use to map Strings read from the data table to doubles for
83 * use in SWTChart series.
85 protected static final ToDoubleFunction
<@Nullable String
> DOUBLE_MAPPER
= str
-> {
86 if (str
== null || str
.equals(UNKNOWN
)) {
89 return Double
.parseDouble(str
);
93 * List of standard colors
95 protected static final List
<@NonNull Color
> COLORS
= ImmutableList
.of(
96 new Color(Display
.getDefault(), 72, 120, 207),
97 new Color(Display
.getDefault(), 106, 204, 101),
98 new Color(Display
.getDefault(), 214, 95, 95),
99 new Color(Display
.getDefault(), 180, 124, 199),
100 new Color(Display
.getDefault(), 196, 173, 102),
101 new Color(Display
.getDefault(), 119, 190, 219)
105 * List of "light" colors (when unselected)
107 protected static final List
<@NonNull Color
> LIGHT_COLORS
= ImmutableList
.of(
108 new Color(Display
.getDefault(), 173, 195, 233),
109 new Color(Display
.getDefault(), 199, 236, 197),
110 new Color(Display
.getDefault(), 240, 196, 196),
111 new Color(Display
.getDefault(), 231, 213, 237),
112 new Color(Display
.getDefault(), 231, 222, 194),
113 new Color(Display
.getDefault(), 220, 238, 246)
117 * Time stamp formatter for intervals in the days range.
119 protected static final LamiTimeStampFormat DAYS_FORMATTER
= new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$
122 * Time stamp formatter for intervals in the hours range.
124 protected static final LamiTimeStampFormat HOURS_FORMATTER
= new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$
127 * Time stamp formatter for intervals in the minutes range.
129 protected static final LamiTimeStampFormat MINUTES_FORMATTER
= new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$
132 * Time stamp formatter for intervals in the seconds range.
134 protected static final LamiTimeStampFormat SECONDS_FORMATTER
= new LamiTimeStampFormat("ss"); //$NON-NLS-1$
137 * Time stamp formatter for intervals in the milliseconds range.
139 protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER
= new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$
142 * Decimal formatter to display nanoseconds as seconds.
144 protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER
= new LamiDecimalUnitFormat(0.000000001);
147 * Default decimal formatter.
149 protected static final DecimalUnitFormat DECIMAL_FORMATTER
= new LamiDecimalUnitFormat();
151 /** Symbol for seconds (used in the custom ns -> s conversion) */
152 private static final String SECONDS_SYMBOL
= "s"; //$NON-NLS-1$
154 /** Symbol for nanoseconds (used in the custom ns -> s conversion) */
155 private static final String NANOSECONDS_SYMBOL
= "ns"; //$NON-NLS-1$
157 /** Maximum amount of digits that can be represented into a double */
158 private static final int BIG_DECIMAL_DIVISION_SCALE
= 22;
160 private final Listener fResizeListener
= event
-> {
161 /* Refresh the titles to fit the current chart size */
162 refreshDisplayTitles();
164 /* Refresh the Axis labels to fit the current chart size */
165 refreshDisplayLabels();
168 private final LamiResultTable fResultTable
;
169 private final LamiChartModel fChartModel
;
171 private final Chart fChart
;
173 private final String fChartTitle
;
174 private final String fXTitle
;
175 private final String fYTitle
;
177 private boolean fSelected
;
178 private Set
<Integer
> fSelection
;
180 private final ToolBar fToolBar
;
183 * Creates a Viewer instance based on SWTChart.
186 * The parent composite to draw in.
188 * The result table containing the data from which to build the
191 * The information about the chart to build
193 public LamiXYChartViewer(Composite parent
, LamiResultTable resultTable
, LamiChartModel chartModel
) {
197 fResultTable
= resultTable
;
198 fChartModel
= chartModel
;
199 fSelection
= new HashSet
<>();
201 fChart
= new Chart(parent
, SWT
.NONE
);
202 fChart
.addListener(SWT
.Resize
, fResizeListener
);
204 /* Set Chart title */
205 fChartTitle
= fResultTable
.getTableClass().getTableTitle();
207 /* Set X axis title */
208 if (fChartModel
.getXSeriesColumns().size() == 1) {
210 * There is only 1 series in the chart, we will use its name as the
211 * Y axis (and hide the legend).
213 String seriesName
= getChartModel().getXSeriesColumns().get(0);
214 // The time duration formatter converts ns to s on the axis
215 if (NANOSECONDS_SYMBOL
.equals(getXAxisAspects().get(0).getUnits())) {
216 seriesName
= getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
218 fXTitle
= seriesName
;
221 * There are multiple series in the chart, if they all share the same
222 * units, display that.
224 long nbDiffAspectsUnits
= getXAxisAspects().stream()
225 .map(aspect
-> aspect
.getUnits())
229 long nbDiffAspectName
= getXAxisAspects().stream()
230 .map(aspect
-> aspect
.getName())
234 String xBaseTitle
= Messages
.LamiViewer_DefaultValueName
;
235 if (nbDiffAspectName
== 1) {
236 xBaseTitle
= getXAxisAspects().get(0).getName();
239 String units
= getXAxisAspects().get(0).getUnits();
240 if (nbDiffAspectsUnits
== 1 && units
!= null) {
241 /* All aspects use the same unit type */
243 // The time duration formatter converts ns to s on the axis
244 if (NANOSECONDS_SYMBOL
.equals(units
)) {
245 units
= SECONDS_SYMBOL
;
247 fXTitle
= xBaseTitle
+ " (" + units
+ ')'; //$NON-NLS-1$
249 /* Various unit types, just say "Value" */
250 fXTitle
= nullToEmptyString(xBaseTitle
);
254 /* Set Y axis title */
255 if (fChartModel
.getYSeriesColumns().size() == 1) {
257 * There is only 1 series in the chart, we will use its name as the
258 * Y axis (and hide the legend).
260 String seriesName
= getChartModel().getYSeriesColumns().get(0);
261 // The time duration formatter converts ns to s on the axis
262 if (NANOSECONDS_SYMBOL
.equals(getYAxisAspects().get(0).getUnits())) {
263 seriesName
= getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL
+ ')'; //$NON-NLS-1$
265 fYTitle
= seriesName
;
266 fChart
.getLegend().setVisible(false);
269 * There are multiple series in the chart, if they all share the same
270 * units, display that.
272 long nbDiffAspectsUnits
= getYAxisAspects().stream()
273 .map(aspect
-> aspect
.getUnits())
277 long nbDiffAspectName
= getYAxisAspects().stream()
278 .map(aspect
-> aspect
.getName())
282 String yBaseTitle
= Messages
.LamiViewer_DefaultValueName
;
283 if (nbDiffAspectName
== 1) {
284 yBaseTitle
= getYAxisAspects().get(0).getName();
287 String units
= getYAxisAspects().get(0).getUnits();
288 if (nbDiffAspectsUnits
== 1 && units
!= null) {
289 /* All aspects use the same unit type */
291 // The time duration formatter converts ns to s on the axis
292 if (NANOSECONDS_SYMBOL
.equals(units
)) {
293 units
= SECONDS_SYMBOL
;
295 fYTitle
= yBaseTitle
+ " (" + units
+ ')'; //$NON-NLS-1$
297 /* Various unit types, don't display any units */
298 fYTitle
= nullToEmptyString(yBaseTitle
);
301 /* Put legend at the bottom */
302 fChart
.getLegend().setPosition(SWT
.BOTTOM
);
305 /* Set all titles and labels font color to black */
306 fChart
.getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
307 fChart
.getAxisSet().getXAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
308 fChart
.getAxisSet().getYAxis(0).getTitle().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
309 fChart
.getAxisSet().getXAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
310 fChart
.getAxisSet().getYAxis(0).getTick().setForeground(Display
.getDefault().getSystemColor(SWT
.COLOR_BLACK
));
312 /* Set X label 90 degrees */
313 fChart
.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90);
315 /* Refresh the titles to fit the current chart size */
316 refreshDisplayTitles();
318 fToolBar
= createChartToolBar();
320 fChart
.addDisposeListener(e
-> {
321 /* Dispose resources of this class */
322 LamiXYChartViewer
.super.dispose();
327 * Util method to check if a list of aspects are all continuous.
330 * The list of aspects to check.
331 * @return true is all aspects are continuous, otherwise false.
333 protected static boolean areAspectsContinuous(List
<LamiTableEntryAspect
> axisAspects
) {
334 return axisAspects
.stream().allMatch(aspect
-> aspect
.isContinuous());
338 * Util method to check if a list of aspects are all time stamps.
341 * The list of aspects to check.
342 * @return true is all aspects are time stamps, otherwise false.
344 protected static boolean areAspectsTimeStamp(List
<LamiTableEntryAspect
> axisAspects
) {
345 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeStamp());
349 * Util method to check if a list of aspects are all time durations.
352 * The list of aspects to check.
353 * @return true is all aspects are time durations, otherwise false.
355 protected static boolean areAspectsTimeDuration(List
<LamiTableEntryAspect
> axisAspects
) {
356 return axisAspects
.stream().allMatch(aspect
-> aspect
.isTimeDuration());
360 * Util method that will return a formatter based on the aspects linked to an axis
362 * If all aspects are time stamps, return a timestamp formatter tuned to the interval.
363 * If all aspects are time durations, return the nanoseconds to seconds formatter.
364 * Otherwise, return the generic decimal formatter.
367 * The list of aspects of the axis.
369 * The list of entries of the chart.
370 * @param internalRange
371 * The internal range for value transformation
372 * @param externalRange
373 * The external range for value transformation
374 * @return a formatter for the axis.
376 protected static Format
getContinuousAxisFormatter(List
<LamiTableEntryAspect
> axisAspects
, List
<LamiTableEntry
> entries
, @Nullable LamiGraphRange internalRange
, @Nullable LamiGraphRange externalRange
) {
378 Format formatter
= DECIMAL_FORMATTER
;
380 if (areAspectsTimeStamp(axisAspects
)) {
381 /* Set a TimeStamp formatter depending on the duration between the first and last value */
382 BigDecimal max
= new BigDecimal(Long
.MIN_VALUE
);
383 BigDecimal min
= new BigDecimal(Long
.MAX_VALUE
);
385 for (LamiTableEntry entry
: entries
) {
386 for (LamiTableEntryAspect aspect
: axisAspects
) {
387 @Nullable Number number
= aspect
.resolveNumber(entry
);
388 if (number
!= null) {
389 BigDecimal current
= new BigDecimal(number
.toString());
390 max
= current
.max(max
);
391 min
= current
.min(min
);
396 long duration
= max
.subtract(min
).longValue();
397 if (duration
> TimeUnit
.DAYS
.toNanos(1)) {
398 formatter
= DAYS_FORMATTER
;
399 } else if (duration
> TimeUnit
.HOURS
.toNanos(1)) {
400 formatter
= HOURS_FORMATTER
;
401 } else if (duration
> TimeUnit
.MINUTES
.toNanos(1)) {
402 formatter
= MINUTES_FORMATTER
;
403 } else if (duration
> TimeUnit
.SECONDS
.toNanos(15)) {
404 formatter
= SECONDS_FORMATTER
;
406 formatter
= MILLISECONDS_FORMATTER
;
408 ((LamiTimeStampFormat
) formatter
).setInternalRange(internalRange
);
409 ((LamiTimeStampFormat
) formatter
).setExternalRange(externalRange
);
411 } else if (areAspectsTimeDuration(axisAspects
)) {
412 /* Set the time duration formatter. */
413 formatter
= NANO_TO_SECS_FORMATTER
;
414 ((LamiDecimalUnitFormat
) formatter
).setInternalRange(internalRange
);
415 ((LamiDecimalUnitFormat
) formatter
).setExternalRange(externalRange
);
419 * For other numeric aspects, use the default lami decimal unit
422 formatter
= DECIMAL_FORMATTER
;
423 ((LamiDecimalUnitFormat
) formatter
).setInternalRange(internalRange
);
424 ((LamiDecimalUnitFormat
) formatter
).setExternalRange(externalRange
);
431 * Get the chart result table.
433 * @return The chart result table.
435 protected LamiResultTable
getResultTable() {
440 * Get the chart model.
442 * @return The chart model.
444 protected LamiChartModel
getChartModel() {
449 * Get the chart object.
450 * @return The chart object.
452 protected Chart
getChart() {
457 * @return the toolBar
459 public ToolBar
getToolBar() {
464 * Is a selection made in the chart.
466 * @return true if there is a selection.
468 protected boolean isSelected() {
473 * Set the selection index.
475 * @param selection the index to select.
477 protected void setSelection(Set
<Integer
> selection
) {
478 fSelection
= selection
;
479 fSelected
= !selection
.isEmpty();
483 * Unset the chart selection.
485 protected void unsetSelection() {
491 * Get the current selection index.
493 * @return the current selection index.
495 protected Set
<Integer
> getSelection() {
500 public @Nullable Control
getControl() {
501 return fChart
.getParent();
505 public void refresh() {
506 Display
.getDefault().asyncExec(() -> {
507 if (!fChart
.isDisposed()) {
514 public void dispose() {
516 /* The control's DisposeListener will call super.dispose() */
520 * Get a list of all the aspect of the Y axis.
522 * @return The aspects for the Y axis
524 protected List
<LamiTableEntryAspect
> getYAxisAspects() {
526 List
<LamiTableEntryAspect
> yAxisAspects
= new ArrayList
<>();
528 for (String colName
: getChartModel().getYSeriesColumns()) {
529 yAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
536 * Get a list of all the aspect of the X axis.
538 * @return The aspects for the X axis
540 protected List
<LamiTableEntryAspect
> getXAxisAspects() {
542 List
<LamiTableEntryAspect
> xAxisAspects
= new ArrayList
<>();
544 for (String colName
: getChartModel().getXSeriesColumns()) {
545 xAxisAspects
.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName
)));
552 * Set the ITitle object text to a substring of canonicalTitle that when
553 * rendered in the chart will fit maxPixelLength.
555 private void refreshDisplayTitle(ITitle title
, String canonicalTitle
, int maxPixelLength
) {
556 if (title
.isVisible()) {
558 String newTitle
= canonicalTitle
;
560 /* Get the title font */
561 Font font
= title
.getFont();
563 GC gc
= new GC(fParent
);
566 /* Get the length and height of the canonical title in pixels */
567 Point pixels
= gc
.stringExtent(canonicalTitle
);
570 * If the title is too long, generate a shortened version based on the
571 * average character width of the current font.
573 if (pixels
.x
> maxPixelLength
) {
574 int charwidth
= gc
.getFontMetrics().getAverageCharWidth();
578 int strLen
= ((maxPixelLength
/ charwidth
) - minimum
);
580 if (strLen
> minimum
) {
581 newTitle
= canonicalTitle
.substring(0, strLen
) + ELLIPSIS
;
587 title
.setText(newTitle
);
595 * Refresh the Chart, XAxis and YAxis titles to fit the current
598 private void refreshDisplayTitles() {
599 Rectangle chartRect
= fChart
.getClientArea();
600 Rectangle plotRect
= fChart
.getPlotArea().getClientArea();
602 ITitle chartTitle
= checkNotNull(fChart
.getTitle());
603 refreshDisplayTitle(chartTitle
, fChartTitle
, chartRect
.width
);
605 ITitle xTitle
= checkNotNull(fChart
.getAxisSet().getXAxis(0).getTitle());
606 refreshDisplayTitle(xTitle
, fXTitle
, plotRect
.width
);
608 ITitle yTitle
= checkNotNull(fChart
.getAxisSet().getYAxis(0).getTitle());
609 refreshDisplayTitle(yTitle
, fYTitle
, plotRect
.height
);
613 * Get the aspect with the given name
616 * The list of aspects to search into
618 * The name of the aspect we are looking for
619 * @return The corresponding aspect
621 protected static @Nullable LamiTableEntryAspect
getAspectFromName(List
<LamiTableEntryAspect
> aspects
, String aspectName
) {
622 for (LamiTableEntryAspect lamiTableEntryAspect
: aspects
) {
624 if (lamiTableEntryAspect
.getLabel().equals(aspectName
)) {
625 return lamiTableEntryAspect
;
633 * Refresh the axis labels to fit the current chart size.
635 protected abstract void refreshDisplayLabels();
640 protected void redraw() {
645 * Signal handler for selection update.
648 * The selection update signal
651 public void updateSelection(LamiSelectionUpdateSignal signal
) {
652 if (getResultTable().hashCode() != signal
.getSignalHash() || equals(signal
.getSource())) {
653 /* The signal is not for us */
656 setSelection(signal
.getEntryIndex());
662 * Create a tool bar on top right of the chart. Contained actions:
664 * <li>Dispose the current viewer, also known as "Close the chart"</li>
667 * This tool bar should only appear when the mouse enters the composite.
669 * @return the tool bar
671 protected ToolBar
createChartToolBar() {
672 Image removeImage
= PlatformUI
.getWorkbench().getSharedImages().getImage(ISharedImages
.IMG_ELCL_REMOVE
);
673 ToolBar toolBar
= new ToolBar(getChart(), SWT
.HORIZONTAL
);
676 toolBar
.moveAbove(null);
677 toolBar
.setVisible(false);
682 ToolItem closeButton
= new ToolItem(toolBar
, SWT
.PUSH
);
683 closeButton
.setImage(removeImage
);
684 closeButton
.setToolTipText(Messages
.LamiXYChartViewer_CloseChartToolTip
);
685 closeButton
.addSelectionListener(new SelectionListener() {
687 public void widgetSelected(@Nullable SelectionEvent e
) {
688 Composite parent
= getParent();
694 public void widgetDefaultSelected(@Nullable SelectionEvent e
) {
699 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));
701 /* Visibility toggle filter */
702 Listener toolBarVisibilityToggleListener
= e
-> {
703 if (e
.widget
instanceof Control
) {
704 Control control
= (Control
) e
.widget
;
705 Point display
= control
.toDisplay(e
.x
, e
.y
);
706 Point location
= getChart().getParent().toControl(display
);
709 * Only set to visible if we are at the right location, in the
712 boolean visible
= getChart().getBounds().contains(location
) &&
713 control
.getShell().equals(getChart().getShell());
714 getToolBar().setVisible(visible
);
718 /* Filter to make sure we hide the toolbar if we exit the window */
719 Listener hideToolBarListener
= (e
-> getToolBar().setVisible(false));
722 * Add the filters to the main Display, and remove them when we dispose
725 Display display
= getChart().getDisplay();
726 display
.addFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
727 display
.addFilter(SWT
.MouseExit
, hideToolBarListener
);
729 getChart().addDisposeListener(e
-> {
730 display
.removeFilter(SWT
.MouseEnter
, toolBarVisibilityToggleListener
);
731 display
.removeFilter(SWT
.MouseExit
, hideToolBarListener
);
734 /* Reposition the tool bar on resize */
735 getChart().addListener(SWT
.Resize
, new Listener() {
737 public void handleEvent(@Nullable Event event
) {
738 toolBar
.setLocation(new Point(getChart().getSize().x
- toolBar
.getSize().x
, 0));
746 * Get a {@link LamiGraphRange} that covers all data points in the result
749 * The returned range will be the minimum and maximum of the resolved values
750 * of the passed aspects for all result entries. If <code>clampToZero</code>
751 * is true, a positive minimum value will be clamped down to zero.
754 * The aspects that the range will represent
756 * If true, a positive minimum value will be clamped down to zero
759 protected LamiGraphRange
getRange(List
<LamiTableEntryAspect
> aspects
, boolean clampToZero
) {
760 /* Find the minimum and maximum values */
761 BigDecimal min
= new BigDecimal(Long
.MAX_VALUE
);
762 BigDecimal max
= new BigDecimal(Long
.MIN_VALUE
);
763 for (LamiTableEntryAspect lamiTableEntryAspect
: aspects
) {
764 for (LamiTableEntry entry
: getResultTable().getEntries()) {
765 @Nullable Number number
= lamiTableEntryAspect
.resolveNumber(entry
);
766 if (number
!= null) {
767 BigDecimal current
= new BigDecimal(number
.toString());
768 min
= current
.min(min
);
769 max
= current
.max(max
);
775 min
.min(BigDecimal
.ZERO
);
778 /* Do not allow a range with a zero delta default to 1 */
779 if (max
.equals(min
)) {
780 max
= min
.add(BigDecimal
.ONE
);
783 return new LamiGraphRange(checkNotNull(min
), checkNotNull(max
));
787 * Transform an external value into an internal value. Since SWTChart only
788 * support Double and Lami can pass Long values, loss of precision might
789 * happen. To minimize this, transform the raw values to an internal
790 * representation based on a linear transformation.
792 * The internal value =
794 * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) +
798 * The number to transform
799 * @param internalRange
800 * The internal range definition to be used
801 * @param externalRange
802 * The external range definition to be used
803 * @return the transformed value in Double comprised inside the internal
806 protected static double getInternalDoubleValue(Number number
, LamiGraphRange internalRange
, LamiGraphRange externalRange
) {
807 BigDecimal value
= new BigDecimal(number
.toString());
809 if (externalRange
.getDelta().compareTo(BigDecimal
.ZERO
) == 0) {
810 return internalRange
.getMinimum().doubleValue();
813 BigDecimal internalValue
= value
814 .subtract(externalRange
.getMinimum())
815 .multiply(internalRange
.getDelta())
816 .divide(externalRange
.getDelta(), BIG_DECIMAL_DIVISION_SCALE
, BigDecimal
.ROUND_DOWN
)
817 .add(internalRange
.getMinimum());
819 return internalValue
.doubleValue();